Skip to content

Instantly share code, notes, and snippets.

@mpj
Created October 3, 2012 09:11
Show Gist options
  • Save mpj/3825942 to your computer and use it in GitHub Desktop.
Save mpj/3825942 to your computer and use it in GitHub Desktop.
Reactive Hlepers
require([ 'scripts/lib/responsive/context#ResponsiveContext' ],
function(ResponsiveContext) {
exports.bindSetter = function(target, targetSetter, source, sourceGetter) {
var ctx = new ResponsiveContext();
ctx.onInvalidate(function() {
exports.bindSetter(target, targetSetter, source, sourceGetter);
});
ctx.run(function () {
var result = source[sourceGetter]();
target[targetSetter](result);
});
};
});
(function () {
/**
A responsive context, inspired by Meteor.
A responsive context is intended as a replacement for
events as a tool for invalidating databindings. Instead of
dispatching events that the dependent code listens to, the
dependent code instantiates a context and runs code inside of
it. The code then checks if it is running inside a context,
stores a reference to that context, and calls invalidate() on
it once it is stale. The dependent code will hear about this
since it has added callbacks to the context with onInvalidate().
@constructor
*/
var ResponsiveContext = function () {
this.id = next_context_id++;
this._callbacks = [];
this._invalidated = false;
};
/**
Array of contexts that are pending invalidation.
Private, Static
*/
var pending_invalidate = [];
/**
A counter to generate unique IDs for ResponsiveContexts.
Private, Static
*/
var next_context_id = 1;
/**
Flush is called to actually do the invalidation
(i.e. call the onInvalidate callbacks) of the
contexts that reside in pending_invalidate.
Private, Static
*/
var flush = function() {
// It's possible that more contexts
// are invalidated and added to pending_invalidate
// while flush is running, so we need to run this
// until it's empty.
while (pending_invalidate.length) {
// Move the contexts pending invalidation
// to a separate array and assign an empty array
// to pending invalidate so that more contexts
// can be invalidated while flush is working.
var contexts = pending_invalidate;
pending_invalidate = [];
for(var i=0;i<contexts.length;i++) {
var callbacks = contexts[i]._callbacks;
for(var k=0;k<callbacks.length;k++) {
callbacks[k]();
}
}
}
};
/**
A reference to the current context.
This is to make the the context available to the
code that runs within the context, so that it can
call invalidate() once the result becomes stale.
Public, static
*/
ResponsiveContext.current = null;
ResponsiveContext.prototype = {
/**
Run a function within the context. This sets
ResponsiveContext.current so that the executed
code can access it, and call invalidate() on
it once whatever it returns has become stale.
@param {Function} f The function to run. No arguments
will be provided.
@returns {Object} Whatever f returns.
*/
run: function (f) {
// Store the previous context so that
// contexts can be created and executed within
// contexts.
var previous = ResponsiveContext.current;
ResponsiveContext.current = this;
try { return f(); }
finally {
ResponsiveContext.current = previous;
}
},
/**
Invalidates the context.
Called from code executing inside "run",
which tells the context that it has become stale
and that the result needs to be fetched again.
@returns void.
*/
invalidate: function () {
if (!this._invalidated) {
this._invalidated = true;
var isFlushing = !!pending_invalidate.length;
if (!isFlushing) {
setTimeout(flush, 0);
// We don't want invalidate to block execution,
// so we defer the flush using setTimeout.
}
pending_invalidate.push(this);
}
},
/**
Attach a callback that listens for whenever
the context is invalidated.
@param {Function} f Callback that will be
executed when context is invalidated. The
context will be provided as only argument.
@returns void.
*/
onInvalidate: function (f) {
if (this._invalidated)
f(this);
else
this._callbacks.push(f);
}
};
exports.ResponsiveContext = ResponsiveContext;
})();
require([ 'scripts/lib/responsive/context#ResponsiveContext' ],
function(ResponsiveContext) {
(function() {
/**
Responsive, in-memory, key-value store for use with
ResponsiveContext. Loosely inspired by the
Session store in Meteor.
@constructor
*/
function ResponsiveStore() {
this._data = {};
this.dependent_contexts = {};
}
/**
Retrieves a value from the store.
@param {string} key The key for which to retrieve a value.
@returns {Object} The value for the key.
*/
ResponsiveStore.prototype.get = function(key) {
if (ResponsiveContext.current) {
// Make sure we have somewhere to store
// dependent contexts.
if (!this.dependent_contexts[key]) {
this.dependent_contexts[key] = {};
}
// Shorthand
var context = ResponsiveContext.current;
var dependents = this.dependent_contexts[key];
if (!dependents[context.id]) {
// Ok, we are inside a context, but the context is
// not yet assigned as dependent on the key. Let's do it!
dependents[context.id] = context;
// Whenever this context is invalidated,
// it's no longer dependent, so delete it
// from the dependent contexts.
context.onInvalidate(function () {
delete dependents[context.id];
});
}
}
return this._data[key];
};
/**
Stores a value into the store on a given key.
@param {string} key The key on which to store the value.
@param {Object} value The value to store.
@returns void
*/
ResponsiveStore.prototype.set = function(key, value) {
var old_value = this._data[key];
if (value === old_value) {
return;
}
this._data[key] = value;
var dependents = this.dependent_contexts[key]; // Shorthand
if(!dependents) {
// No dependents. This will happen if set is called
// before any gets have gotten in and attached their
// contexts.
return;
}
// Invalidate all dependent contexts.
for (var ctxId in dependents) {
dependents[ctxId].invalidate();
}
};
exports.ResponsiveStore = ResponsiveStore;
})();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment