From 0e7057ac1f6786a7ffe8ea4679827369a07cdd7e Mon Sep 17 00:00:00 2001 From: dsc Date: Tue, 24 Apr 2012 18:05:54 -0700 Subject: [PATCH] Vastly improves model cache. --- lib/base/base-mixin.co | 6 +- lib/base/base-model.co | 3 + lib/base/model-cache.co | 224 +++++++++++++++++++++++++++++++++------ lib/dashboard/dashboard-view.co | 3 +- lib/dataset/data-view.co | 3 +- lib/dataset/dataset-model.co | 6 +- lib/dataset/datasource-model.co | 5 +- lib/dataset/metric-model.co | 6 +- lib/graph/graph-model.co | 50 +++------ www/modules.yaml | 2 +- 10 files changed, 221 insertions(+), 87 deletions(-) diff --git a/lib/base/base-mixin.co b/lib/base/base-mixin.co index fd10c59..1dc1a2b 100644 --- a/lib/base/base-mixin.co +++ b/lib/base/base-mixin.co @@ -41,7 +41,7 @@ BaseBackboneMixin = exports.BaseBackboneMixin = /** * Triggers the 'ready' event if it has not yet been triggered. - * Subsequent listeners added to this event will be auto-triggered. + * Subsequent listeners added on this event will be auto-triggered. * @returns {this} */ triggerReady: -> @@ -51,7 +51,9 @@ BaseBackboneMixin = exports.BaseBackboneMixin = this /** - * Resets the 'ready' event to its non-triggered state, firing the + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} */ resetReady: -> return this unless @ready diff --git a/lib/base/base-model.co b/lib/base/base-model.co index f26b8b3..499fb7d 100644 --- a/lib/base/base-model.co +++ b/lib/base/base-model.co @@ -131,6 +131,9 @@ BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{ Backbone.Collection ... @trigger 'create', this + getIds: -> + @models.map -> it.id or it.get('id') or it.cid + ### Serialization diff --git a/lib/base/model-cache.co b/lib/base/model-cache.co index 5bd459d..f3ef08c 100644 --- a/lib/base/model-cache.co +++ b/lib/base/model-cache.co @@ -1,40 +1,196 @@ +_ = require 'underscore' +Seq = require 'seq' -function ModelCache (ModelClass, ModelListClass) - ModelClass import do - CACHE : new ModelListClass - ready : false +{ReadyEmitter} = require 'kraken/util/event' + + +# TODO: Bubble events to decorated emitters +# TODO: Automatically create a cache for any class that extends BaseModel +/** + * @class Caches models and provides static lookups by ID. + */ +class exports.ModelCache extends ReadyEmitter + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + readyEventName : 'cache-ready' + + /** + * Default options. + * @private + * @constant + * @type Object + */ + DEFAULT_OPTIONS: + ready : true + cache : null + create : null + ModelType : null + + /** + * @private + * @type Object + */ + options : null + + /** + * Type we're caching (presumably extending `Backbone.Model`), used to create new + * instances unless a `create` function was provided in options. + * @private + * @type Class + */ + ModelType : null + + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + cache : null + + + + /** + * @constructor + * @param {Class} [ModelType] Type of cached object (presumably extending + * `Backbone.Model`), used to create new instances unless `options.create` + * is provided. + * @param {Object} [options] Options: + * @param {Boolean} [options.ready=true] Starting `ready` state. If false, + * the cache will queue lookup calls until `triggerReady()` is called. + * @param {Class} [options.cache=new Backbone.Collection] + * The backing data-structure for the cache. If omitted, we'll use a new + * `Backbone.Collection`, but really, anything with a `get(id)` method for + * model lookup will work here. + * @param {Function} [options.create] A function called when a new Model + * object is needed, being passed the new model ID. + * @param {Class} [options.ModelType] Type of cached object + * (presumably extending `Backbone.Model`), used to create new instances + * unless `options.create` is provided. + */ + (ModelType, options) -> + unless _.isFunction ModelType + [options, ModelType] = [ModelType or {}, null] + @options = {...@DEFAULT_OPTIONS, ...options} - register: (model) -> - # console.log "ModelCache(#{@CACHE}).register(#{model.id or model.get('id')})", model - if @CACHE.contains model - @CACHE.remove model, {+silent} - @CACHE.add model - model + @cache = @options.cache or new Backbone.Collection - get: (id) -> - @CACHE.get id + @ModelType = ModelType or @options.ModelType + @createModel = that if @options.create - lookup: (id, cb, cxt=this) -> - # console.log "ModelCache(#{@CACHE}).lookup(#id, #{typeof cb})" - unless @ready - @on 'cache-ready', ~> - @off 'cache-ready', arguments.callee - @lookup id, cb, cxt - return - - if @CACHE.get id - cb.call cxt, null, that - else - Cls = this - @register new Cls {id} - .on 'ready', -> cb.call cxt, null, it - this - - # Bind the ModelCache methods to the class - for m of <[ register get lookup ]> - ModelClass[m] .= bind ModelClass - - ModelClass + @ready = !!@options.ready + @decorate @ModelType if @ModelType + + + /** + * Called when a new Model object is needed, being passed the new model ID. + * Uses the supplied `ModelType`; overriden by `options.create` if provided. + * + * @param {String} id The model ID to create. + * @returns {Model} Created model. + */ + createModel: (id) -> + new @ModelType {id} + + /** + * Registers a model with the cache. If a model by this ID already exists + * in the cache, it will be removed and this one will take its place. + * + * Fires an `add` event. + * + * @param {Model} model The model. + * @returns {Model} The model. + */ + register: (model) -> + # console.log "ModelCache(#{@CACHE}).register(#{model.id or model.get('id')})", model + if @cache.contains model + @cache.remove model, {+silent} + @cache.add model + @trigger 'add', this, model + model + + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + get: (id) -> + @cache.get id + + /** + * Asynchronously look up any number of models, requesting them from the + * server if not already known to the cache. + * + * @param {String|Array} ids List of model IDs to lookup. + * @param {Function} cb Callback of the form `(err, models)`, + * where `err` will be null on success and `models` will be an Array + * of model objects. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + lookupAll: (ids, cb, cxt=this) -> + ids = [ids] unless _.isArray ids + # console.log "ModelCache(#{@cache}).lookup([#ids], #{typeof cb})" + + unless @ready + @on 'cache-ready', ~> + @off 'cache-ready', arguments.callee + @lookupAll ids, cb, cxt + return this + + Seq ids + .parMap_ (next, id) ~> + return next.ok(that) if @cache.get id + @register @createModel id + .on 'ready', next + .load() + .unflatten() + .seq (models) -> + cb.call cxt, null, models + .catch (err) -> + cb.call cxt, err + this + + /** + * Looks up a model, requesting it from the server if it is not already + * known to the cache. + * + * @param {String|Array} id Model ID to lookup. + * @param {Function} cb Callback of the form `(err, model)`, + * where `err` will be null on success and `model` will be the + * model object. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + lookup: (id, cb, cxt=this) -> + @lookupAll [id], (err, models) -> + if err then cb.call cxt, err + else cb.call cxt, null, models[0] + + /** + * Decorate an object with the cache methods: + * - register + * - get + * - lookup + * - lookupAll + * + * This is automatically called on `ModelType` if supplied. + * + * @param {Object} obj Object to decorate. + * @returns {obj} The supplied object. + */ + decorate: (obj) -> + obj.__cache__ = this + # Bind the ModelCache methods to the class + for m of <[ register get lookup lookupAll ]> + obj[m] = @[m].bind this + obj + + toString: -> + "#{@..displayName or @..name}(cache=#{@cache})" -module.exports = exports = ModelCache diff --git a/lib/dashboard/dashboard-view.co b/lib/dashboard/dashboard-view.co index fe66048..74ec90f 100644 --- a/lib/dashboard/dashboard-view.co +++ b/lib/dashboard/dashboard-view.co @@ -63,9 +63,8 @@ DashboardView = exports.DashboardView = BaseView.extend do # {{{ .seq next_phase.ok .seq_ (next) ~> console.log "#this.ready!" - @ready = true @attachGraphs() - @trigger 'ready', this + @triggerReady() attachGraphs: -> graphs_el = @$ '#graphs' diff --git a/lib/dataset/data-view.co b/lib/dataset/data-view.co index f07549e..9fc9f3d 100644 --- a/lib/dataset/data-view.co +++ b/lib/dataset/data-view.co @@ -59,10 +59,9 @@ DataView = exports.DataView = BaseView.extend do # {{{ # $.getJSON '/datasources/all', (@data) ~> # _.each @data, @canonicalizeDataSource, this # @model.sources.reset _.map @data, -> it - @ready = true @unwait() # @render() - @trigger 'ready', this + @triggerReady() /** * Transform the `columns` field to ensure an Array of {label, type} objects. diff --git a/lib/dataset/dataset-model.co b/lib/dataset/dataset-model.co index 5d1e863..e0722b4 100644 --- a/lib/dataset/dataset-model.co +++ b/lib/dataset/dataset-model.co @@ -47,7 +47,8 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ load: (opts={}) -> - return this if @ready and not opts.force + @resetReady() if opts.force + return this if @ready @wait() @trigger 'load', this Seq _.unique @metrics.pluck 'source_id' @@ -57,9 +58,8 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ @sources.add source next.ok source .seq ~> - @ready = true @unwait() # terminates the `load` wait - @trigger 'ready', this + @triggerReady() this diff --git a/lib/dataset/datasource-model.co b/lib/dataset/datasource-model.co index cb3bfb4..7b824eb 100644 --- a/lib/dataset/datasource-model.co +++ b/lib/dataset/datasource-model.co @@ -115,9 +115,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ onLoadSuccess: (@data) -> console.log "#this.onLoadSuccess #{@data}" @trigger 'load-success', this - return if @ready - @ready = true - @trigger 'ready', this + @triggerReady() onLoadError: (jqXHR, txtStatus, err) -> @_errorLoading = true @@ -169,6 +167,7 @@ DataSource import do ready : false register: (model) -> + return model unless model # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model if @CACHE.contains model @CACHE.remove model, {+silent} diff --git a/lib/dataset/metric-model.co b/lib/dataset/metric-model.co index ffef7a9..b1f7cd2 100644 --- a/lib/dataset/metric-model.co +++ b/lib/dataset/metric-model.co @@ -63,16 +63,14 @@ Metric = exports.Metric = BaseModel.extend do # {{{ this onSourceReady: (err, source) -> - console.log "#this.onSourceReady", arguments + # console.log "#this.onSourceReady", arguments @unwait() if err console.error "#this Error loading DataSource! #err" else @source = source @updateId() - unless @ready - @ready = true - @trigger 'ready', this + @triggerReady() this diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index d0cfa09..d778892 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -1,11 +1,11 @@ -Seq = require "seq" +Seq = require 'seq' -_ = require 'kraken/util/underscore' -Cascade = require 'kraken/util/cascade' +{ _, Cascade, +} = require 'kraken/util' +{ BaseModel, BaseList, ModelCache, +} = require 'kraken/base' { ChartType, } = require 'kraken/chart' -{ BaseModel, BaseList, -} = require 'kraken/base' { DataSet } = require 'kraken/dataset' @@ -76,6 +76,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ constructor: function Graph (attributes={}, opts) # @on 'ready', ~> console.log "(#this via Graph).ready!" attributes.options or= {} + attributes.slug or= attributes.id if attributes.id? @optionCascade = new Cascade attributes.options BaseModel.call this, attributes, opts @@ -99,7 +100,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{ load: (opts={}) -> - return this if @ready and not opts.force + return this if (@loading or @ready) and not opts.force + @loading = true @wait() @trigger 'load', this Seq() @@ -116,6 +118,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{ success : @unwaitAnd (model, res) ~> # console.log "#{this}.fetch() --> success!", res @dataset.set @get('data') + @trigger 'change:data', this, @dataset, 'data' + @trigger 'change', this, @dataset, 'data' next.ok res .seq_ (next) ~> next.ok @get('parents') @@ -129,9 +133,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{ @unwait() next.ok() .seq ~> - @ready = true - @trigger 'ready', this + @loading = false @unwait() # terminates the `load` wait + @triggerReady() this @@ -323,32 +327,6 @@ GraphList = exports.GraphList = BaseList.extend do # {{{ # }}} -/* * * * Visualization Cache for parent-lookup * * * {{{ */ - -Graph import do - CACHE : new GraphList - - register: (model) -> - # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model - if @CACHE.contains model - @CACHE.remove model, {+silent} - @CACHE.add model - model - - get: (id) -> - @CACHE.get id - - lookup: (id, cb) -> - # console.log "#{@CACHE}.lookup(#id, #{typeof cb})" - if @CACHE.get id - cb null, that - else - Cls = this - @register new Cls { id, slug:id } - .on 'ready', -> cb null, it - - - -_.bindAll Graph, 'register', 'get', 'lookup' +### Graph Cache for parent-lookup +new ModelCache Graph -/* }}} */ diff --git a/www/modules.yaml b/www/modules.yaml index c553b9f..37f1b82 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -42,6 +42,7 @@ dev: - js: - kraken: - util: + - op - underscore: - array - object @@ -52,7 +53,6 @@ dev: - ready-emitter - waiting-emitter - index - - op - backbone - parser - cascade -- 1.7.0.4