Vastly improves model cache.
authordsc <dsc@wikimedia.org>
Wed, 25 Apr 2012 01:05:54 +0000 (18:05 -0700)
committerdsc <dsc@wikimedia.org>
Wed, 25 Apr 2012 01:07:39 +0000 (18:07 -0700)
docs/todo.md
lib/base/base-mixin.co
lib/base/base-model.co
lib/base/model-cache.co
lib/dashboard/dashboard-view.co
lib/dataset/data-view.co
lib/dataset/dataset-model.co
lib/dataset/datasource-model.co
lib/dataset/metric-model.co
lib/graph/graph-model.co
www/modules.yaml

index 3f51a7e..0d2651a 100644 (file)
@@ -3,6 +3,7 @@
 ### general
 - Library for undo/redo
 - LocalStorage for unsaved changes, so accidental refresh (etc) doesn't lose changes
+- Wrap `Backbone.extend()` to fire `subclass` event on parent class
 
 
 ### Dashboard
index fd10c59..1dc1a2b 100644 (file)
@@ -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
index f26b8b3..499fb7d 100644 (file)
@@ -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
     
index 5bd459d..f3ef08c 100644 (file)
+_   = 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<Backbone.Model>
+     */
+    ModelType : null
+    
+    /**
+     * Collection holding the cached Models.
+     * @private
+     * @type Backbone.Collection
+     */
+    cache : null
+    
+    
+    
+    /**
+     * @constructor
+     * @param {Class<Backbone.Model>} [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<Backbone.Collection>} [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<Backbone.Model>} [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<String>} 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<String>} 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
index fe66048..74ec90f 100644 (file)
@@ -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'
index f07549e..9fc9f3d 100644 (file)
@@ -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.
index 5d1e863..e0722b4 100644 (file)
@@ -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
     
     
index cb3bfb4..7b824eb 100644 (file)
@@ -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}
index ffef7a9..b1f7cd2 100644 (file)
@@ -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
     
     
index d0cfa09..d778892 100644 (file)
@@ -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
 
-/* }}} */
index c553b9f..37f1b82 100644 (file)
@@ -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