diff --git a/README.md b/README.md new file mode 100644 index 0000000..92663d0 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Glaze +The database-agnostic caching layer for Mongoose. + +## How and Why +Glaze was made to work seamlessly with Mongoose in providing a layer of caching on top of the already fast MongoDB. The concept of Glaze is very simple: a mongoose model has relational data is that either changing to quickly or not best stored to be queued and calculated through only MongoDB. Glaze can theoretically store any data in key-value sense, but was created with realtime(ish) counts, indexes, and joins in mind that can take advantage of the speed of caching. + +Glaze is most like a cached data layer; you call for that layer to computed on top of the current Mongoose model and the data is filled in using the provided cache. That's why I named it Glaze. + +Glaze is database-agnostic in how it interacts with the caching layer and its own mechanisms. Although Glaze was originally only for Redis caching, Glaze has been reformatted to work with any database that can meet the Glaze Interface Format. See the 'Glaze Interface Format' section for more details as to what is supported and what can be supported by viewers like you. + +## Install +Just use NPM 'npm install glaze' in your project directory, use 'npm install glaze -g' if you want to use it anywhere. + +## Glazing +To use Glaze, you really only have to migrate your relational Mongoose code into the Glaze.Cache initializer. This is very simple to do: + + + + +## Glaze Interface Format + +### Current Built-in GIFs +- Redis + +### How to GIF +Borrowing from Go (Golang), GIF is any object that meets the following method requirements: + +- interface#init: +> (options object) +< returns undefined +This function is called on the initialization of an interface. This is where connection or client of the database should be established and in some way attached to the interface object. + +- interface#write: +> (model Mongoose.model, key string, value any, next function) +< next(err error, result any) +This function will write to the database. Glaze assumes a key-value store will be used as most caching databases are, but the function itself can shoe-horn into any database. Glaze won't mind a little customization. + +- interface#read: +> (key string, next function) +< next(err error, result any) +This function will read from the database. Glaze assumes a key-value store will be used as most caching databases are, but the function itself can shoe-horn into any database. Glaze won't mind a little customization. You can return 'false' for err and Glaze will calculate the value, add it to the cache, and reread it automatically. + +And that is GIF. You can make your own interface and merge it into your own Glaze fork. Send a pull-request and get it added maybe? + diff --git a/lib/cache.js b/lib/cache.js new file mode 100644 index 0000000..c9f44e6 --- /dev/null +++ b/lib/cache.js @@ -0,0 +1,129 @@ +// Generated by CoffeeScript 1.6.3 +(function() { + var async, cacheKey, db, _; + + _ = require('underscore'); + + async = require('async'); + + db = {}; + + module.exports = function(gif) { + var Cache; + db = gif; + Cache = (function() { + function Cache(model, attributes, options) { + var a; + this.model = model; + this.attributes = attributes != null ? attributes : {}; + if (!(this instanceof Cache)) { + return new Cache(this.model, this.attributes, this.redit); + } + db.init(options); + a = _(this.attributes); + this.keys = a.keys; + this.calc = a.values; + this.attach(); + } + + Cache.prototype.attach = function() { + var action, operations, _i, _len, _results; + operations = ['cache', 'cast']; + _results = []; + for (_i = 0, _len = operations.length; _i < _len; _i++) { + action = operations[_i]; + _results.push(this.model.method(action, this[action]())); + } + return _results; + }; + + Cache.prototype.cache = function() { + var cache; + cache = this; + return function(attributes, next) { + var model; + if (attributes == null) { + attributes = cache.keys; + } + model = this; + if (_(attributes).isNull() === true) { + attributes = cache.attributes; + } + if (typeof attributes === typeof String) { + attributes = [attributes]; + } + if (attributes.length === 0) { + return next(model, null); + } + return async.map(attributes, cache.write, function(err, changes) { + if (err) { + throw err; + } + if (next) { + return model.cast(next); + } + }); + }; + }; + + Cache.prototype.write = function(attribute, done) { + var key, model, next, redit; + redit = this.redis; + model = this.model; + key = cacheKey(model, attribute); + next = function(value) { + return db.write(model, key, value, done); + }; + return this.attributes[attribute].call(this.model, next); + }; + + Cache.prototype.cast = function() { + var cache; + cache = this; + return function(next) { + var model; + model = this; + if (cache.keys.length === 0) { + return next(model, null); + } + return async.map(cache.keys, cache.read, function(err, changes) { + if (err) { + throw err; + } + if (next) { + return next(model, changes); + } + }); + }; + }; + + Cache.prototype.read = function(attribute, done) { + var cache, ckey, model, next, redit; + cache = this; + redit = this.redis; + model = this.model; + ckey = cacheKey(model, attribute); + next = function(err, result) { + model[attribute] = result; + return done(err, result); + }; + return db.read(key, function(err, result) { + if (err === false) { + return cache.write(attribute, next); + } else { + return next(err, result); + } + }); + }; + + return Cache; + + })(); + return Cache; + }; + + cacheKey = function(model, attribute) { + return model.modelName + ':' + model._id + ':' + attribute; + }; + +}).call(this); diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..378b7f3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,10 @@ +// Generated by CoffeeScript 1.6.3 +(function() { + module.exports = { + Cache: require('./cache'), + Redis: function() { + return require('./redis'); + } + }; + +}).call(this); diff --git a/lib/redis.js b/lib/redis.js new file mode 100644 index 0000000..ce2a834 --- /dev/null +++ b/lib/redis.js @@ -0,0 +1,46 @@ +// Generated by CoffeeScript 1.6.3 +(function() { + var expireTime, redis; + + redis = require(redis); + + module.exports = Interface; + + Interface.init = function(options) { + return this.client = redis.createClient(options); + }; + + Interface.write = function(model, key, value, next) { + var client; + client = this.client; + return client.set([key, value], function(err, result) { + if (err) { + redis.print(err, result); + } + next(err, result); + return client.expire(key, expireTime(model, function(err) { + if (err) { + return redis.print(err, result); + } + })); + }); + }; + + Interface.read = function(key, next) { + return client.get(key, function(err, result) { + if (err) { + redis.print(err, result); + } + if (err) { + return next(false); + } else { + return next(err, result); + } + }); + }; + + expireTime = function(model) { + return model.get('expire' || 1000 * 30); + }; + +}).call(this); diff --git a/src/cache.coffee b/src/cache.coffee new file mode 100644 index 0000000..840ed4d --- /dev/null +++ b/src/cache.coffee @@ -0,0 +1,109 @@ +# cache.coffee +# init: chris andrejewski 6/8/2013 + +_ = require 'underscore' +async = require 'async' +## redis = require 'redis' +db = {} + +module.exports = (gif) -> + db = gif + + class Cache + constructor: (@model, @attributes = {}, options) -> + if (!(this instanceof Cache)) + return new Cache(@model, @attributes, @redit); + + db.init options + + a = _ @attributes + @keys = a.keys + @calc = a.values + + this.attach() + + attach: -> + operations = ['cache','cast'] + for action in operations + @model.method action, this[action]() + + # OPERATIONS + + # writes the cache, calls the cast by default + cache: -> + cache = this + return (attributes = cache.keys, next) -> + model = this + + if _(attributes).isNull() is true + attributes = cache.attributes + + if typeof attributes is typeof String + attributes = [attributes] + + if attributes.length is 0 + return next model, null + + # async-ly write cache changes + async.map attributes, cache.write, (err, changes) -> + throw err if err + # finally + if next + model.cast next + + # does the actual writing + write: (attribute, done) -> + redit = @redis + model = @model + + key = cacheKey model, attribute + + next = (value) -> + db.write(model, key, value, done) + + # actually calculate the value + @attributes[attribute].call @model, next + + # reads values from the cache and updates the model instance + cast: -> + cache = this + return (next) -> + model = this + + if cache.keys.length is 0 + return next model, null + + # async-ly write cache changes + async.map cache.keys, cache.read, (err, changes) -> + throw err if err + # finally + if next + next model, changes + + # does the actual reading + read: (attribute, done) -> + + cache = this + redit = @redis + model = @model + + ckey = cacheKey model, attribute + + next = (err, result) -> + model[attribute] = result + done err, result + + # get value + db.read key, (err, result) -> + if err is false + cache.write attribute, next + else + next err, result + + return Cache + + +cacheKey = (model, attribute) -> + model.modelName+':'+model._id+':'+attribute + + diff --git a/src/index.coffee b/src/index.coffee new file mode 100644 index 0000000..d872219 --- /dev/null +++ b/src/index.coffee @@ -0,0 +1,11 @@ +# index.coffee +# init: chris andrejewski 6/8/2013 + +module.exports = + Cache : require './cache' + Redis : -> + require './redis' + + + + diff --git a/src/redis.coffee b/src/redis.coffee new file mode 100644 index 0000000..a2fab53 --- /dev/null +++ b/src/redis.coffee @@ -0,0 +1,34 @@ +# redis.coffee +# init: chris andrejewski 6/8/2013 + +redis = require redis; + +# Interface +module.exports = Interface + +Interface.init = (options) -> + this.client = redis.createClient(options) + +Interface.write = (model, key, value, next) -> + client = this.client + client.set [key, value], (err, result) -> + (redis.print err, result) if err + next err, result + + client.expire key, expireTime model, (err) -> + (redis.print err, result) if err + +Interface.read = (key, next) -> + client.get key, (err, result) -> + (redis.print err, result) if err + # assume err == DNE; must cache now + if err + next false + else + next err, result + +# Helpers + +expireTime = (model) -> + model.get 'expire' || 1000*30; # ms*sec*min*hr*day :: 30secs +