$waitPromise should handle gracefully non promise.

--- src/fibers-utils.coffee
+++ src/fibers-utils.coffee
@@ -59,6 +59,9 @@

 # Waits for a promise to be fulfilled or broken.
 $waitPromise = (promise) ->
+  # If it is not a promise, just forwards it.
+  return promise unless $isPromise promise
+
   fiber = $fiber.current

   promise.then(


closes #83

closes #83

--- src/main/coffeescript/view/OperationView.coffee
+++ src/main/coffeescript/view/OperationView.coffee
@@ -104,7 +104,7 @@
         complete: (data) =>
           @showCompleteStatus(data)

-      obj.contentType = "application/json" if (obj.type.toLowerCase() == "put" or obj.type.toLowerCase() == "patch")
+      obj.contentType = "application/json" if (obj.type.toLowerCase() == "post" or obj.type.toLowerCase() == "put" or obj.type.toLowerCase() == "patch")
       obj.contentType = false if isFileUpload
       jQuery.ajax(obj)
       false


remove debug code from spec

remove debug code from spec

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -202,7 +202,6 @@
         keydown('y')

       it "is in linewise motion", ->
-        console.log("CK", vimState.getRegister('"').type)
         expect(vimState.getRegister('"').type).toEqual "linewise"

       it "saves the lines to the default register", ->


Fixed regexes

Fixed regexes

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -7,7 +7,7 @@
         \x20 # space
       | \n # newline
       | [#{controls}] # delims
-      | /([^\s]|\\/)([^/\s]|\\/)*?/[gmi]? # regex
+      | /([^\s]|\\/)([^/]|\\/)*?/[gmi]? # regex
       | "(?:[^"\\]|\\.)*" # strings
       | \\[^\s][^\s#{controls}]* # char
       | [^#{controls}\\"\s]+ # normal tokens


Reverted to making origin report whole function because of currying, to be figured out

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -1595,7 +1595,7 @@
           call.tea = op.tea =
             if body and body.tea
               new Constrained (join body.tea.constraints, deferredConstraints),
-                withOrigin (typeFn paramTypeVars..., body.tea?.type), op
+                withOrigin (typeFn paramTypeVars..., body.tea?.type), call
             else if explicitType
               (copyOrigin (freshInstance ctx, explicitType), compiledType)
           # """λ#{paramNames.length}(function (#{listOf paramNames}) {


Fix vertical bar compilation

Fix vertical bar compilation

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -2726,6 +2726,7 @@
     # .replace(/\√/g, 'sqrt_')
     # .replace(/\./g, 'dot_')
     .replace(/\&/g, 'and_')
+    .replace(/\|/g, 'or_')
     .replace(/\?/g, 'p_')


Add Windows backslash to regex path

Add Windows backslash to regex path

--- docpad.coffee
+++ docpad.coffee
@@ -138,7 +138,7 @@

       tutorials: ->
         @getCollection('html')
-          .findAll({relativeOutPath:  /^tutorials\/.+?\/index\.html$/},[{order: -1}])
+          .findAll({relativeOutPath:  /^tutorials(\/|\\).+?(\/|\\)index\.html$/},[{order: -1}])
           .on 'add', (model) ->
             model.set({addToTitle: 'Knot.x Tutorials'})
             model.setMetaDefaults({layout: "post"})


Update adjustURL for new dispatcher signature.

--- src/chaplin/dispatcher.coffee
+++ src/chaplin/dispatcher.coffee
@@ -120,7 +120,7 @@
     return if controller.redirected

     # Adjust the URL.
-    @adjustURL params, options
+    @adjustURL route, params, options

     # We're done! Spread the word!
     @publishEvent 'dispatcher:dispatch', @currentController,
@@ -180,17 +180,12 @@
     next beforeActions.shift()

   # Change the URL to the new controller using the router.
-  adjustURL: (params, options) ->
-    return unless options.path?
-
-    url = options.path +
-      if options.queryString then "?#{options.queryString}" else ""
+  adjustURL: (route, params, options) ->
+    return unless route.path?

     # Tell the router to actually change the current URL.
+    url = route.path + if route.query then "?#{route.query}" else ""
     @publishEvent '!router:changeURL', url, options if options.changeURL
-
-    # Save the URL.
-    @url = url

   # Disposal
   # --------


Add test for syntax macro, retrieve after macro expansion and fix toplevelexprssion compilation by adding a new line

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -685,7 +685,9 @@
   if isTranslated expanded
     expanded
   else
-    expressionCompile ctx, expanded
+    compiled = expressionCompile ctx, expanded
+    retrieve call, expanded
+    compiled

 isTranslated = (result) ->
   (isSimpleTranslated result) or (Array.isArray result) and (isSimpleTranslated result[0])
@@ -4789,7 +4791,7 @@
     [expression] = _terms ast
     {js} = compileCtxAstToJs topLevelExpression, ctx, expression
     (finalizeTypes ctx, expression)
-    js: library + immutable + (listOfLines map lookupJs, (reverse modules)) + js
+    js: library + immutable + (listOfLines map lookupJs, (reverse modules)) + '\n' + js
     ast: ast
     errors: checkTypes ctx

@@ -4943,7 +4945,7 @@
   types: ctx._scope()
   subs: ctx.substitution.fails
   ast: ast
-  compiled: library + immutable + compiledDefinitions.js + compiledExpression.js
+  compiled: library + immutable + compiledDefinitions.js + '\n' + compiledExpression.js

 typeEnumaration = (ctx) ->
   values mapMap _type, ctx._scope()
@@ -6146,6 +6148,13 @@
   """
   'g', 4

+  'syntax macro'
+  """
+  id (syntax [x]
+    x)
+  """
+  '(id 42)', 42
+
   # TODO: support matching with the same name
   #       to implement this we need the iife to take as arguments all variables
   #       with the same names, since JavaScript shadows it too strongly and


first pass at sticky top/bottom method for supporting decision board

Former-commit-id: 6a3164193668e67109b2b32c96bd3360701d2ac4 [formerly c237641b0e068f037a1b6648676c0cecb81c0041] [formerly bb6be8fe5eda1c39395a6d8dcb0e4ebd8b605436 [formerly 8cf3f6143eb799b3a5f189f64e1641021609bd01]] [formerly 67949f889fc80a1224058be52c4dbf43a8587940 [formerly d16ce6dd646508397df64f3fcd78087ba4f6e171] [formerly a2912f4d1b4d09861840762536fb6395e1197393 [formerly a2912f4d1b4d09861840762536fb6395e1197393 [formerly c6af257408a73704af2dd2268a932dde3617ae44]]]]
Former-commit-id: 3f89a459b48530e8b48699b1d9367a2feab8e81d [formerly 368b5385d9fdd0d6af1aa5453b9628ff9754d181] [formerly 86b87f28c9ef2f9de520213359ac5ea3938ec037 [formerly 62e1402c23bfb7c46e77ff9aa77bc1072a62fe91]]
Former-commit-id: 33233dbae511f10929ec0f4f07c710029866de17 [formerly 82df6283b5cdab8fc53b51fa8c0ca33c8b2c91e1]
Former-commit-id: 2c21e708f9b87e3c7ed88d1ce92e94fa2fe45bc0
Former-commit-id: b521947dcc7afcc09a9335185be8833f1377cd87

--- @client/assets/javascripts/vendor_extensions/jquery_extensions.js.coffee
+++ @client/assets/javascripts/vendor_extensions/jquery_extensions.js.coffee
@@ -1,4 +1,77 @@
 do ($) ->
+
+  $.fn.stickyTopBottom = (options = {}) ->
+
+
+    ## ##############
+    #initialization
+    _.defaults options,
+      container: $('body')
+      top_offset: 0
+      bottom_offset: 0
+
+    $el = $(this)
+
+    container_top = options.container.offset().top
+    element_top = $el.offset().top
+
+    current_translate = 0
+
+    viewport_height = $(window).height()
+    $(window).on 'resize', ->
+      viewport_height = $(window).height()
+
+    last_viewport_top = document.documentElement.scrollTop || document.body.scrollTop
+
+    setTop = (translation) ->
+      #this will overwrite existing transform...
+      $el.css 'transform', "translate(0, #{current_translate}px)"
+
+    ## #################
+    # The meat: scroll handler
+    #
+    # When moving up or element is shorter than viewport:
+    #    if scrolled above top of element, position top of element to top of viewport
+    #      (stick to top)
+    # When moving down:
+    #    if scrolled past bottom of element, position bottom of element at bottom of viewport
+    #      (stick to bottom)
+
+    $(window).scroll (event) ->
+      viewport_top = document.documentElement.scrollTop || document.body.scrollTop
+      viewport_bottom = viewport_top + viewport_height
+      effective_viewport_top = viewport_top + options.top_offset
+      effective_viewport_bottom = viewport_bottom - options.bottom_offset
+
+      # warning: checking height causes bad performance
+      element_height = $el.height()
+
+      scrolling_up = viewport_top < last_viewport_top
+      taller_than_viewport = element_height > viewport_height
+
+      if scrolling_up
+        if effective_viewport_top < container_top # if we're scrolled past container top
+          current_translate = 0
+          setTop current_translate
+        else if effective_viewport_top < element_top + current_translate
+          current_translate = effective_viewport_top - element_top
+          setTop current_translate
+
+      else if !taller_than_viewport
+        console.log !taller_than_viewport, effective_viewport_top, element_top, current_translate, effective_viewport_top > element_top + current_translate
+        if effective_viewport_top > element_top + current_translate
+          current_translate = effective_viewport_top - element_top
+          setTop current_translate
+      else # scrolling down
+        container_bottom = container_top + options.container.height() #warning: checking height causes bad performance
+        if effective_viewport_bottom > container_bottom #scrolled past container bottom
+          current_translate = container_bottom - (element_top + element_height)
+          setTop current_translate
+        else if effective_viewport_bottom > element_top + element_height + current_translate
+          current_translate = effective_viewport_bottom - (element_top + element_height)
+          setTop current_translate
+
+      last_viewport_top = viewport_top

   $.fn.ensureInView = (options = {}) ->


move cursors after an insert

move cursors after an insert

--- src/editor.coffee
+++ src/editor.coffee
@@ -29,6 +29,7 @@
     @id = _.uniqueId(TandemEditor.ID_PREFIX)
     @iframeContainer = document.getElementById(@iframeContainer) if _.isString(@iframeContainer)
     @destructors = []
+    @cursors = {}
     this.reset(true)
     this.enable() if @options.enabled

@@ -48,9 +49,18 @@
     inner.style['background-color'] = nameNode.style['background-color'] = color
     cursor.appendChild(nameNode)
     cursor.appendChild(inner)
+    @cursors[userId] = {
+      index: index
+      name: name
+      color: color
+    }
     Tandem.Utils.insertExternal(new Tandem.Position(this, index), cursor)
     line = @doc.findLine(cursor)
     line.rebuild() if line?
+
+  moveCursor: (userId, index) ->
+    if @cursors[userId]
+      this.setCursor(userId, index, @cursors[userId].name, @cursors[userId].color)

   clearCursors: ->
     _.each(@doc.root.querySelectorAll('.cursor'), (cursor) ->
@@ -135,9 +145,11 @@
     offset = 0      # Tracks how many characters inserted to correctly offset new text
     oldDelta = @doc.toDelta()
     retains = []
+    cursors = {}
     _.each(delta.deltas, (delta) =>
       if JetDelta.isInsert(delta)
         @doc.insertText(index + offset, delta.text)
+        cursors[delta.attributes['author']] = index + offset + delta.text.length if delta.attributes['author']?
         retains.push(new JetRetain(index + offset, index + offset + delta.text.length, delta.attributes))
         offset += delta.getLength()
       else if JetDelta.isRetain(delta)
@@ -163,6 +175,9 @@
       )
     )
     @doc.forceTrailingNewline()
+    _.each(cursors, (index, userId) =>
+      this.moveCursor(userId, index)
+    )
     newDelta = @doc.toDelta()
     composed = JetSync.compose(oldDelta, delta)
     composed.compact()


Add Engine.stop()

Add Engine.stop()

--- src/engine.coffee
+++ src/engine.coffee
@@ -7,6 +7,7 @@
   defaultFPS: 24

   running: false
+  stopped: false

   # Internal: Queue of callbacks to invoke once the engine starts
   _queue: []
@@ -59,8 +60,13 @@

   # Internal: Repeatedly draw the current scene by requesting animation frames
   run: ->
-    requestAnimFrame(Kona.Engine.run, Kona.Canvas.elem)
+    return if @stopped
+    requestAnimFrame(Kona.Engine.run.bind(Kona.Engine), Kona.Canvas.elem)
     Kona.Scenes.drawCurrent()
+
+
+  stop: ->
+    @stopped = true


possibly need to call calcbuffer in overlay

--- src/coffee/bokeh_views.coffee
+++ src/coffee/bokeh_views.coffee
@@ -34,7 +34,7 @@
 class GridPlotContainerView extends Continuum.DeferredView
   tagName : 'div'
   className:"gridplot_container"
-
+
   default_options : { scale:1.0}
   initialize : (options) ->
     super(_.defaults(options, @default_options))
@@ -62,7 +62,7 @@

   render : ->
     super()
-
+
     row_heights =  @model.layout_heights()
     col_widths =  @model.layout_widths()
     y_coords = [0]
@@ -118,7 +118,7 @@
     @eventSink.active = true
     @bind_events()

-  bind_events : () ->
+  bind_events : () ->
     @eventSink.on("clear_active_tool", () =>
       @eventSink.trigger("#{@eventSink.active}:deactivated")
       @eventSink.active = true)
@@ -204,7 +204,7 @@
     @$el.append($("""

-
+

           <canvas class='main_can'></canvas>
@@ -228,7 +228,7 @@
     @bind_tools()
     @bind_overlays()
     return this
-
+
   render : () ->
     super()
     @$el.attr("width", @options.scale * @mget('outerwidth'))
@@ -265,12 +265,12 @@
     for own key, view of @renderers
       @$el.append(view.$el)
     @render_end()
-
+
   render_deferred_components: (force) ->
     super(force)
     all_views = _.flatten(_.map([@tools, @axes, @renderers, @overlays], _.values))
     if _.any(all_views, (v) -> v._dirty)
-      @ctx.clearRect(0,0,  @mget('width'), @mget('height'))
+      @ctx.clearRect(0,0,  @mget('width'), @mget('height'))
       for v in all_views
         v._dirty = true
         v.render_deferred_components(true)
@@ -421,7 +421,7 @@
     op_scale = @plot_view.options.scale
     ypos = (real_y) ->
       (op_scale * (HEIGHT - ((real_y - min_y)*y_scale)))
-
+
     [first_tick, last_tick] = ticks.auto_bounds(
       data_range.get('start'), data_range.get('end'), interval)

@@ -520,10 +520,10 @@
       value_pos = (x) =>
         vp =  (@mget('width') - x)
         return vp
-
+
       for i in [0..heights.length]
         @plot_view.ctx.fillRect(0, left_points[i], value_pos(heights[i]), thickness)
-
+
     @plot_view.ctx.stroke()
     return null

@@ -616,7 +616,7 @@
     @bind_overlays()
     @$el.addClass('table_wrap')
     return this
-
+
   render : () ->
     super()
     o_w = @mget('outerwidth')
@@ -634,7 +634,7 @@
     for own key, view of @renderers
       @$el.append(view.$el)
     @render_end()
-
+
   render_deferred_components: (force) ->
     super(force)
     all_views = _.flatten(_.map([@tools, @axes, @renderers, @overlays], _.values))
@@ -657,7 +657,7 @@
     yfield = @model.get('yfield')
     @dataX = (x[xfield] for x in data)
     @dataY = (y[yfield] for y in data)
-
+
     #@screeny = new Float32Array(screeny)
     #@screenx = new Float32Array(screenx)
     #@screenx = screenx
@@ -704,7 +704,7 @@
         #skip data sources which are not selecting'
         @render_end()
         return null
-
+
     data = @model.get_ref('data_source').get('data')
     a = new Date()
     @calc_buffer(data)
@@ -769,9 +769,12 @@
       sel_idxs = renderer.get_ref('data_source').get('selected')
       ds = renderer.get_ref('data_source')
       data = ds.get('data')
+      #hugo - there must be a better way
+      if (not rendererview.screenx) or (not rendererview.screeny)
+        rendererview.calc_buffer(data)
       fcolor = @mget('foreground_color')
       rvm = rendererview.model
-
+
       fcolor = rvm.get('foreground_color')
       unselected_color = @mget('unselected_color')
       color_field = rvm.get('color_field')
@@ -782,11 +785,11 @@
       last_color_field = fcolor
       @plot_view.ctx.strokeStyle = fcolor
       @plot_view.ctx.fillStyle = fcolor
-
+
       last_color_field = false
       ctx = @plotview.ctx
       for idx in [0..data.length]
-
+
         if idx in sel_idxs
           if color_field
             comp_color = color_mapper.map_screen(idx)
@@ -795,7 +798,7 @@
           else
             ctx.strokeStyle = fcolor
             ctx.fillStyle = fcolor
-
+
         else
           ctx.fillStyle = unselected_color
           ctx.strokeStyle = unselected_color


Revert "feat(perPage): enable option also for standard export"

This reverts commit a465c05fb0e8465e353aab7602dd79104110dfe3.

--- src/coffee/orderexport.coffee
+++ src/coffee/orderexport.coffee
@@ -103,7 +103,6 @@
       .expand('lineItems[*].supplyChannel')
       .expand('discountCodes[*].discountCode')
       .expand('customerGroup')
-      .perPage(@_exportOptions.perPage)
       .last("#{@_exportOptions.fetchHours}h")
       .fetch()
     .then (result) =>


avoid multiple upload on image crop

avoid multiple upload on image crop

--- BackofficeBundle/Resources/public/coffee/mediatheque/SuperboxView.coffee
+++ BackofficeBundle/Resources/public/coffee/mediatheque/SuperboxView.coffee
@@ -150,7 +150,6 @@
           200: (response) ->
             superboxViewParam['jcrop_api'].destroy()
             currentView.refreshImages()
-            currentView.addEventOnCropForm()
     return

   addEventOnOverrideForm: ->
@@ -164,7 +163,6 @@
         statusCode:
           200: (response) ->
             currentView.refreshImages()
-            currentView.addEventOnOverrideForm()
     return

   addEventOnMetaForm: ->
@@ -185,7 +183,8 @@

   refreshImages: ->
     format = $('#media_crop_format').val()
-    $('.media_crop_' + format).attr 'src', $('.media_crop_' + format).attr('src') + '?' + Math.random()
+    newSrc = $('.media_crop_' + format).attr('src').split('?')[0] + '?' + Math.random()
+    $('.media_crop_' + format).attr 'src', newSrc
     $(".media-override-format-form").hide()
     $('#image-loader').hide()
     @showPreview(format)


--- app/assets/javascripts/rglossa/components/centre/main_area.jsx.coffee
+++ app/assets/javascripts/rglossa/components/centre/main_area.jsx.coffee
@@ -168,6 +168,14 @@
       state.sortBy = 'headword_len'
     firstQueryString = state.searchQueries[0].query
     return unless firstQueryString and firstQueryString isnt '""'
+    searchQueries = if corpusNs.getLanguage(@props.corpus, 'zh')
+      # If the tone number is missing, add a pattern that matches all tones
+      state.searchQueries.map (query) ->
+        newQuery = $.extend({}, query)
+        newQuery.query = query.query.replace(/\bphon="([^0-9\"]+)"/g, 'phon="$1[1-4]?"')
+        newQuery
+    else
+      state.searchQueries

     {store, statechart, corpus} = @props

@@ -179,7 +187,7 @@
       method: 'POST'
       data: JSON.stringify
         corpus_short_name: corpus.short_name
-        queries: state.searchQueries
+        queries: searchQueries
         metadata_value_ids: state.selectedMetadataIds
         max_hits: state.maxHits
         sortBy: state.sortBy


Added google analytics tracking for order data

--- app/assets/javascripts/sprangular/controllers/checkout/confirm.coffee
+++ app/assets/javascripts/sprangular/controllers/checkout/confirm.coffee
@@ -6,5 +6,23 @@
     $scope.processing = true

     Checkout.complete()
-      .success -> $location.path('/checkout/complete')
       .error   -> $location.path('/checkout')
+      .success ->
+        order = $scope.order
+        ga "ecommerce:addTransaction",
+          id:       order.number
+          revenue:  order.total
+          shipping: order.shipTotal
+          tax:      order.taxTotal
+
+        for item in order.items
+          ga "ecommerce:addItem",
+            id:       order.number
+            name:     item.variant.name
+            sku:      item.variant.sku
+            price:    item.price
+            quantity: item.quantity
+
+        ga "ecommerce:send"
+
+        $location.path('/checkout/complete')


remove cache of dataTable when edit and delete data

--- BackofficeBundle/Resources/public/coffee/table/TableviewAction.coffee
+++ BackofficeBundle/Resources/public/coffee/table/TableviewAction.coffee
@@ -21,6 +21,7 @@

   clickDelete: (event) ->
     event.preventDefault()
+    options = @options
     smartConfirm(
       'fa-trash-o',
       'Delete this element',
@@ -33,6 +34,7 @@
           url: params.url
           method: 'DELETE'
         params.row.hide()
+        options.table.fnSettings().clearCache = true
     )

   clickEdit: (event) ->
@@ -58,4 +60,5 @@
       success: (response) ->
         viewClass = appConfigurationView.getConfiguration(viewContext.options.entityType, 'editEntity')
         new viewClass(viewContext.addOption(html: response, domContainer: $('#content')))
+        viewContext.options.table.fnSettings().clearCache = true
 )


cleanup

cleanup

--- public/js/controllers.coffee
+++ public/js/controllers.coffee
@@ -66,14 +66,9 @@
   $scope.markerClicked = ->

   # Markers should be added after map is loaded
-  # $scope.onMapIdle = ->
   $scope.onMapReady = =>
-    console.log "onMapReady"
     $scope.$watch "results", ->
       if $scope.results and $scope.results.length and not $scope.mapLoaded
-        console.log "map results changed!"
-
-
         $scope.mapLoaded = true

         for m in $scope.markers


Minor spelling.

Minor spelling.

--- src/helpers.spec.coffee
+++ src/helpers.spec.coffee
@@ -30,7 +30,7 @@
     #   console.log args...
     #   emit.call collection, args...

-    # Binds helpers to this collections.
+    # Binds helpers to this collection.
     {$set, $sum, $val} = do ->
       helpers = {}
       helpers[name] = fn.bind collection for name, fn of $nonBindedHelpers


Added href creation in href helper

Added href creation in href helper

--- src/helpers/href.coffee
+++ src/helpers/href.coffee
@@ -9,5 +9,8 @@
 class Neck.Helper.href

   constructor: (opts)->
+    # Add href for styling purposes
+    opts.el.attr 'href', '#'
+
     opts.el.on 'click', (e)->
       new HrefHelper _.extend opts, e: e


Fix invalid audio panning values

Fix invalid audio panning values

--- app/lib/AudioPlayer.coffee
+++ app/lib/AudioPlayer.coffee
@@ -62,7 +62,9 @@
     sup = @camera.worldToSurface pos
     svp = @camera.surfaceViewport
     pan = Math.max -1, Math.min 1, ((sup.x - svp.x) - svp.width / 2) / svp.width
+    pan = 0 if _.isNaN pan
     dst = @camera.distanceRatioTo pos
+    dst = 0.8 if _.isNaN dst
     vol = Math.min 1, options.volume / Math.pow (dst + 0.2), 2
     volume: options.volume, delay: options.delay, pan: pan


Extract function to load an example page

--- apps/examples/index.coffee
+++ apps/examples/index.coffee
@@ -37,6 +37,25 @@
   #{require '../../components/block_v2/queries/block.coffee'}
 """

+getExamplePage = (page = 1, user) ->
+  offset = (page - 1) * DEFAULT_ARGS.per
+  examplesPage = rest(examples, offset).slice(0, DEFAULT_ARGS.per)
+
+  Promise.all(
+    map examplesPage, (example) ->
+      send =
+        query: exampleChannelQuery
+        user: user
+        variables:
+          ids: first(example.channel_ids, 2)
+      graphQL(send)
+  )
+  .then (results) ->
+    each results, (result, i) ->
+      examplesPage[i].channels = result.channels
+    examplesPage
+
+
 app.get '/api/examples/:id', (req, res, next) ->
   example = find(examples, (example) -> example.id is req.params.id)
   send =
@@ -50,41 +69,12 @@
     res.json example

 app.get '/api/examples', (req, res, next) ->
-  page = req.query.page or 1
-  offset = (page - 1) * DEFAULT_ARGS.per
-  examplesPage = rest(examples, offset).slice(0, DEFAULT_ARGS.per)
-
-  Promise.all(
-    map examplesPage, (example) ->
-      send =
-        query: exampleChannelQuery
-        user: req.user
-        variables:
-          ids: first(example.channel_ids, 2)
-      graphQL(send)
-  )
-  .then (results) ->
-    each results, (result, i) ->
-      examplesPage[i].channels = result.channels
-
+  getExamplePage(req.query.page, req.user).then (examplesPage) ->
     res.json examplesPage

 app.get '/examples', (req, res, next) ->
-  Promise.all(
-    map first(examples, DEFAULT_ARGS.per), (example) ->
-      send =
-        query: exampleChannelQuery
-        user: req.user
-        variables:
-          ids: first(example.channel_ids, 2)
-
-      graphQL(send)
-  )
-  .then (results) ->
-    each results, (result, i) ->
-      examples[i].channels = result.channels
-
-    res.locals.examples = examples
+  getExamplePage(1, req.user).then (examplesPage) ->
+    res.locals.examples = examplesPage
     res.locals.totalExamples = examples.length
     res.locals.totalPages = Math.ceil(examples.length / DEFAULT_ARGS.per)
     res.render 'index'


Fix for latest coffee-script-redux compiler

--- src/instrumenter.coffee
+++ src/instrumenter.coffee
@@ -84,7 +84,7 @@
         # CoffeeScriptRedux compiler
         estraverse.traverse program,
             leave: (node, parent) ->
-                if node.offset? and (node.raw? or node.value?)
+                if node.loc? and node.range? and (node.raw? or node.value?)
                     value =
                         if node.raw?
                             node.raw
@@ -96,10 +96,10 @@
                     # calculate start line & column
                     node.loc =
                         start:
-                            line: node.line
-                            column: calculateColumn program.raw, node.offset
+                            line: node.loc.start.line
+                            column: calculateColumn program.raw, node.range[0]
                         end:
-                            line: node.line
+                            line: node.loc.start.line
                             column: 0
                     node.loc.end.column = node.loc.start.column + value.length
                     lines = value.split /(?:\r\n|[\r\n])/g


code clearing

code clearing

--- app/parser.coffee
+++ app/parser.coffee
@@ -24,14 +24,14 @@
     results = $.find selector
     return false if not results.length
     return results.eq(0).text() or false if single
-    if list
-      result = (Parser.textTree(results[index].children) for index in [0..results.length - 1])
-      # flatten the tree
-      result = helpers.flatten result
+
+    result = helpers.flatten if list
+      (Parser.textTree(results[index].children) for index in [0..results.length - 1])
     else
-      result = (results.eq(index).text() or false for index in [0..results.length - 1])
-      # trim non-strings
-    result.filter((leaf) -> typeof leaf is 'string')
+      (results.eq(index).text() or false for index in [0..results.length - 1])
+
+    # trim non-strings
+    leaf for leaf in result when typeof leaf is 'string'

   string  : ($, {selector, single, list}) ->
     text = Parser.text $, selector, single, list
@@ -42,6 +42,7 @@
     found   = false
     result  = {}
     needs   = Object.keys rules if needs and needs is true
+
     Object.keys(rules).forEach (key) ->
       res = Parser.parse $, rules[key]
       return if res is false
@@ -66,6 +67,7 @@
       return false

     result
+
   obj     : ($, obj)  ->
     log "object"
     selection = $.find obj.selector


minor default attach empty object rather than null

--- src/access/database.coffee
+++ src/access/database.coffee
@@ -86,7 +86,7 @@
             # made sure to fetch keys for whatever ActiveRecord type they wish
             # to work with. If they HAVEN'T, attach() is going to throw at us
             try
-                result = gw.attach(data)
+                result = gw.attach(data ? { })
             catch e
                 return cb(e)


Avoid passing closures to browser when showing dialog.

It's resource-costy.

--- atom/renderer/lib/inspector.coffee
+++ atom/renderer/lib/inspector.coffee
@@ -51,8 +51,8 @@
 showFileChooserDialog = (callback) ->
   remote = require 'remote'
   dialog = remote.require 'dialog'
-  dialog.showOpenDialog remote.getCurrentWindow(), null, (files) ->
-    callback pathToHtml5FileObject(files[0]) if files?
+  files = dialog.showOpenDialog remote.getCurrentWindow(), null
+  callback pathToHtml5FileObject files[0] if files?

 pathToHtml5FileObject = (path) ->
   fs = require 'fs'


update rudimentary docs

update rudimentary docs

--- src/experiments/recycle.coffee
+++ src/experiments/recycle.coffee
@@ -39,8 +39,8 @@
 Pipestream Events v2
 ====================

-d         := { sigil,          key, value, ... }    # implicit global namespace
-          := { sigil, prefix,  key, value, ... }    # explicit namespace
+d         := { sigil,          key, value, ..., $, }    # implicit global namespace
+          := { sigil, prefix,  key, value, ..., $, }    # explicit namespace

 # `d.sigil` indicates 'regionality':

@@ -57,6 +57,8 @@
 key       := non-empty text         # typename

 value     := any                    # payload
+
+$         := pod                    # system-level attributes, to be copied from old to new events

 ###


Fix bug that sometimes destroyed leaderboards on server restart There was a race condition on loading, depending how long it took to read the data from redis. Need to wait for @loaded event before reading from hubot brain. Also changed to use the new style of accessing the brain (get and set methods) but preserved backward compatibility https://github.com/canadianveggie/hubot-leaderboard/issues/1

--- src/leaderboard.coffee
+++ src/leaderboard.coffee
@@ -10,32 +10,44 @@
 # Commands:
 #   hubot <user> climbs <event> leaderboard - Add a victory for the user
 #   hubot set <event> leaderboard <user> <score> - Set the score for a user
-#   hubot show <event> leaderboard - Returns the event leaderboard
-#   hubot reset <event> leaderboard - Clears the event leaderboard
-#   hubot list leaderboards - Returns a list of leaderboards
+#   hubot show <event> leaderboard - Returns the event leaderboard
+#   hubot reset <event> leaderboard - Clears the event leaderboard
+#   hubot list leaderboards - Returns a list of leaderboards
 #
 # Author:
 #   canadianveggie

 class Leaderboards
   constructor: (@robot) ->
-    @robot.brain.data.leaderboards = {}
+    this.reload()
+    @robot.brain.on 'loaded', =>
+      this.reload()
+
+  reload: ->
+    @leaderboards = @robot.brain.get('leaderboards') || @robot.brain.data.leaderboards || {}
+
+  save: ->
+    @robot.brain.set('leaderboards', @leaderboards)
+    @robot.brain.save()

   initialize: (event, user) ->
-    @robot.brain.data.leaderboards[event.toLowerCase()] ?= {}
-    @robot.brain.data.leaderboards[event.toLowerCase()][user.toLowerCase()] ?= {user: user, score: 0}
+    @leaderboards[event.toLowerCase()] ?= {}
+    @leaderboards[event.toLowerCase()][user.toLowerCase()] ?= {user: user, score: 0}

   increment: (event, user, i) ->
     this.initialize event, user
     i ?= 1
-    @robot.brain.data.leaderboards[event.toLowerCase()][user.toLowerCase()].score += i
+    @leaderboards[event.toLowerCase()][user.toLowerCase()].score += i
+    this.save()

   set: (event, user, val) ->
     this.initialize event, user
-    @robot.brain.data.leaderboards[event.toLowerCase()][user.toLowerCase()].score = val
+    @leaderboards[event.toLowerCase()][user.toLowerCase()].score = val
+    this.save()

   delete: (event) ->
-    delete @robot.brain.data.leaderboards[event.toLowerCase()]
+    delete @leaderboards[event.toLowerCase()]
+    this.save()

   get: (event) ->
     sortBy = (key, a, b, r) ->
@@ -44,7 +56,7 @@
       return +1*r if a[key] < b[key]
       return 0

-    leaderboard_values = (v for k,v of @robot.brain.data.leaderboards[event.toLowerCase()] or {})
+    leaderboard_values = (v for k,v of @leaderboards[event.toLowerCase()] or {})

     leaderboard_values.sort (a,b) ->
       sortBy('score', a, b, true) or
@@ -52,7 +64,7 @@
     return leaderboard_values

   list: ->
-    return (key for key of @robot.brain.data.leaderboards)
+    return (key for key of @leaderboards)

 module.exports = (robot) ->
   leaderboards = new Leaderboards robot


Extend Subscriber from GitDiffView

Extend Subscriber from GitDiffView

--- src/packages/git-diff/lib/git-diff-view.coffee
+++ src/packages/git-diff/lib/git-diff-view.coffee
@@ -1,4 +1,5 @@
 _ = require 'underscore'
+Subscriber = require 'subscriber'

 module.exports =
 class GitDiffView
@@ -9,16 +10,19 @@
     @gutter = @editor.gutter
     @diffs = {}

-    @editor.on 'editor:path-changed', => @subscribeToBuffer()
-    @editor.on 'editor:display-updated', => @renderDiffs()
-    git.on 'statuses-changed', =>
+    @subscribe @editor, 'editor:path-changed', => @subscribeToBuffer()
+    @subscribe @editor, 'editor:display-updated', => @renderDiffs()
+    @subscribe git, 'statuses-changed', =>
       @diffs = {}
       @scheduleUpdate()
-    git.on 'status-changed', (path) =>
+    @subscribe git, 'status-changed', (path) =>
       delete @diffs[path]
       @scheduleUpdate() if path is @editor.getPath()

     @subscribeToBuffer()
+
+  beforeRemove: ->
+    @unsubscribe()

   subscribeToBuffer: ->
     if @buffer?
@@ -64,3 +68,5 @@
         for row in [newStart...newStart + newLines]
           linesHighlighted += @gutter.find(".line-number[lineNumber=#{row - 1}]").addClass('git-line-modified').length
     @gutter.hasGitLineDiffs = linesHighlighted > 0
+
+_.extend GitDiffView.prototype, Subscriber


removed log [#BOOK-559]

removed log [#BOOK-559]

--- src/core/javascripts/services/login.js.coffee
+++ src/core/javascripts/services/login.js.coffee
@@ -116,7 +116,6 @@
         , (err) =>
           deferred.reject(err)
       , (err) =>
-        console.log(err)
         deferred.reject(err)
       deferred.promise


Client: Activity: Deleted unnecessary accountId from addparticipants method arguments

--- client/activity/lib/components/channelparticipantavatars/index.coffee
+++ client/activity/lib/components/channelparticipantavatars/index.coffee
@@ -276,7 +276,7 @@
     options =
       userId            : participant.get '_id'
       channelId         : @props.channelThread.get 'channelId'
-      accountIds        : [ whoami().socialApiId, participant.get 'socialApiId' ]
+      accountIds        : [ participant.get 'socialApiId' ]

     { channel } = ActivityFlux.actions
     channel.addParticipants options


Hide the panel when it looses focues

Hide the panel when it looses focues

--- client/app/Applications/Chat.kdapplication/Views/mainchatpanel.coffee
+++ client/app/Applications/Chat.kdapplication/Views/mainchatpanel.coffee
@@ -2,7 +2,7 @@

   constructor:->
     super
-      cssClass : 'main-chat-panel visible'
+      cssClass : 'main-chat-panel'

     @registerSingleton "chatPanel", @, yes

@@ -10,6 +10,11 @@
     @conversationList = new ChatConversationListView
     @conversationListController = new ChatConversationListController
       view : @conversationList
+
+    @on 'PanelVisibilityChanged', (visible)=>
+      if visible
+        @getSingleton('windowController').addLayer @
+        @once 'ReceivedClickElsewhere', @bound 'hide'

   createConversation:(data)->
     # Data includes chatChannel and the conversation
@@ -19,6 +24,7 @@
     @addSubView @header
     @addSubView @conversationList
     @conversationListController.loadItems()
+    @show()

   show:->
     @setClass 'visible'


remove console.log in source

remove console.log in source

--- src/jquery.anim8.coffee
+++ src/jquery.anim8.coffee
@@ -114,8 +114,6 @@
             settings.offsetY = parseInt $this.data 'offset-y' if $this.data('offset-y')?
             settings.loop = parseInt $this.data 'loop' if $this.data('loop')?

-            console.log settings
-
             return $.error("No animation index") unless settings.index?
             return $.error("No animation sheet") unless settings.sheet?


In immutable mode, don’t flag associations as loaded unless they are included in the response

--- src/interfaces/json_api.coffee
+++ src/interfaces/json_api.coffee
@@ -376,7 +376,7 @@
     resource.__assignFields(attributes)

     resource.__links = _.extend(resource.links(), data['links'])
-    resource.klass().reflectOnAllAssociations().each (reflection) ->
+    resource.klass().reflectOnAllAssociations().each (reflection) =>
       association = resource.association(reflection.name)

       if(relationshipLinks = data['relationships']?[s.underscored(reflection.name)]?['links'])?
@@ -395,6 +395,8 @@
       relationshipEmpty =
         if _.isObject(relationship = data['relationships']?[s.underscored(reflection.name)]?['data'])
           _.keys(relationship).length == 0
+        else if @resourceLibrary.immutable
+          _.isNull(relationship) || _.isEmpty(relationship) || _.has(attributes, reflection.name)
         else if relationship?
           relationship.length == 0
         else


Search completion; tweak example text.

Search completion; tweak example text.

--- background_scripts/settings.coffee
+++ background_scripts/settings.coffee
@@ -92,7 +92,9 @@
     # put in an example search engine
     searchEngines: [
       "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
-      "# Examples:"
+      ""
+      "# More examples:"
+      "#"
       "# g: http://www.google.com/search?q=%s Google"
       "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
       "# y: http://www.youtube.com/results?search_query=%s Youtube"


Documentation on locks, tests working.

Documentation on locks, tests working.

--- src/lib/strata_.coffee
+++ src/lib/strata_.coffee
@@ -307,6 +307,7 @@
     @head.next      = @head
     @head.previous  = @head
     @nextAddress    = 0
+    @length         = 1024

   # Pages are identified by an integer page address. The page address is a number
   # that is incremented as new pages are created. A page file has a file name that
@@ -373,7 +374,6 @@

   #
   rewriteBranches: (page, _) ->
-    link unlink page
     filename = @filename page.address, ".new"
     record = [ page.penultimate, page.next?.address, page.addresses ]
     json = JSON.stringify(record) + "\n"
@@ -385,37 +385,381 @@

   #
   readBranches: (page, _) ->
-    link unlink page
     filename = @filename page.address
     json = fs.readFile filename, "utf8", _
     [ penultimate, next, addresses ] = JSON.parse json
     extend page, { penultimate, next, addresses }

-  # After creating a database object, the client will either open the existing
+  # ### Page Caching
+  #
+  # We keep a map of page addresses to cached page objects.
+  #
+  # We also maintain a most-recently used linked list using the page objets as
+  # list nodes. When we want to cull the cache, we can remove the pages at the
+  # end of the list, since they are the least recently used.
+  #
+  # TODO Some way of judging popularity. A decent index?
+  #
+  # It is important that the page objects are unique, that we do not represent a
+  # page file with more than one page object, because the page objects house the
+  # locking mechanisms.
+
+  # Link tier to the head of the MRU list.
+  link: (entry) ->
+    next = @head.next
+    entry.next = next
+    next.previous = entry
+    @head.next = entry
+    entry.previous = @head
+    entry
+
+  # Unlnk a tier from the MRU list.
+  unlink: (entry) ->
+    { next, previous } = entry
+    next.previous = previous
+    previous.next = next
+    entry
+
+  # ### Leaf Tier Files
+  #
+  # A leaf tier maintains an array of file positions called a positions array. A
+  # file position in the positions array references a record in the leaf tier
+  # file by its file position. The positions in the positions array are sorted
+  # according to the b-tree collation of the referenced records.
+  #
+  # A new leaf page is given the next unused page number.
+  #
+  # The in memory representation of the leaf page includes a flag to indicate
+  # that the page is leaf page, the address of the leaf page, the page address
+  # of the next leaf page, and a cache that maps record file positions to
+  # records that have been loaded from the file.
+
+  #
+  allocateLeaves: (positions, right) ->
+    address = @nextAddress++
+    @cache[address] = @link
+      leaf: true
+      address: address
+      positions: positions
+      cache: {}
+      right: right
+
+  # #### Appends for Durability
+  #
+  # A leaf tier file contains JSON objects, one object on each line. The objects
+  # represent record insertions and deletions, so that the leaf tier file is
+  # essentially a log. Each time we write to the log, we open and close the
+  # file, so that the operating system will flush our writes to disk. This gives
+  # us durability.
+
+  # Append an object to the leaf tier file as a single line of JSON.
+  #
+  # We call the append method to both update an existing leaf page file, as
+  # well as to create a replacment leaf page file that will be relinked to
+  # replace the existing leaf page file. The caller determines which file should
+  # be written, so it opens and closes the file descriptor.
+  #
+  # The file descriptor must be open for for append.
+
+  #
+  _writeJSON: (fd, page, object, _) ->
+    # TODO Move this point somewhere. Since we need to fsync anyway, it we just
+    # to open the file and and close the file when we append a JSON object to
+    # it. Because no file handles are kept open, the b-tree object can simply be
+    # reaped by the garbage collection.
+    page.position or= fs.fstat(fd, _).size
+
+    # Calcuate a buffer length. Take note of the current page position.
+    json            = JSON.stringify object
+    position        = page.positon
+    length          = Buffer.byteLength(json, "utf8")
+    buffer          = new Buffer(length + 1)
+    offset          = 0
+
+    # Write JSON and newline.
+    buffer.write json
+    buffer[length] = 0x0A
+
+    # Write may be interrupted by a signal, so we keep track of how many bytes
+    # are actually written and write the difference if we come up short.
+    while offset != buffer.length
+      count = buffer.length - offset
+      written = fs.write fd, buffer, offset, count, page.position, _
+      page.position += written
+      offset += written
+
+    # Return the file position of the appended JSON object.
+    position
+
+  # #### Leaf Tier File Records
+  #
+  # There are three types of objects in a leaf tier file, *insert objects*,
+  # *delete objects*, and *address array objects*.
+  #
+  # An insert object contains a *record* and the index in the address array
+  # where the record's address would be inserted to preserve the sort order of
+  # the address array.
+  #
+  # Beginning with an empty position array and reading from the start of the
+  # file, the leaf tier is reconstituted by replaying the inserts and deletes
+  # described by the insert and delete objects.
+  #
+  # The JSON objects stored in the leaf array are JSON arrays. The first element
+  # is used as a flag to indicate the type of object.
+  #
+  # If the first element is an integer greater than zero, it indicates an insert
+  # object.  The integer is the one based index into the zero based position
+  # array, indicating the index where the position of the current insert object
+  # should be inserted. The second element of the leaf tier object array is the
+  # record object.
+  #
+  # When we read the insert object, we will place the record in the record cache
+  # for the page, mapping the position to the record.
+
+  # Write an insert object.
+  writeInsert: (fd, page, object, index, _) ->
+    @_writeJSON fd, page, [ index + 1, object ], _
+
+  # If the first element is less than zero, it indicates a delete object. The
+  # absolute value of the integer is the one based index into the zero based
+  # position array, indicating the index of address array element that should be
+  # deleted.
+  #
+  # There are no other elements in the delete object.
+
+  # Write a delete object.
+  writeDelete: (fd, page, index, _) ->
+    @_writeJSON fd, page, [ -(index + 1) ], _
+
+  # On occasion, we can store a position array object. An position array object
+  # contains the position array itself.  We store a copy of a constructed
+  # position array object in the leaf page file so that we can read a large leaf
+  # page file quickly.
+  #
+  # When we read a leaf page file, if we read from the back of the file toward
+  # the front, we can read backward until we find an position array object. Then
+  # we can read forward to the end of the file, applying the inserts and deletes
+  # that occured after we wrote the position array object.
+  #
+  # When a leaf page file is large, stashing the constructed position array at
+  # the end means that the leaf page can be loaded quickly, because we will only
+  # have to read backwards a few entries to find a mostly completed position
+  # array. We can then read forward from the array to amend the position array
+  # with the inserts and deletes that occured after it was written.
+  #
+  # Not all of the records will be loaded when we go backwards, but we have
+  # their file position from the address array, so we can jump to them and load
+  # them as we need them. That is, if we need them, because owing to binary
+  # search, we might only need a few records out of a great many records to find
+  # the record we're looking for.
+
+  # Write an address array object.
+  writePositions: (fd, page, _) ->
+    @_writeJSON fd, page, [ 0, page.right, page.positions ], _
+
+  # Here is the backward search for a position in array in practice. We don't
+  # really ever start from the beginning. The backwards than forwards read is
+  # just as resillient.
+
+  #
+  readLeaves: (page, _) ->
+    # We don't cache file descriptors after the leaf page file read. We will
+    # close the file descriptors before the function returns.
+    filename  = @filename page.address
+    fd        = fs.open filename, "r+", _
+    stat      = fs.stat filename, _
+
+    # When we read backwards, we create a list of of the insert and delete
+    # objects in the splices array. When we have found a positions array, we
+    # stop going backwards.
+    splices   = []
+    # Note that if we don't find a position array that has been written to the
+    # leaf page file, then we'll start with an empty position array.
+    positions = []
+    #
+    line      = ""
+    offset    = -1
+    end       = stat.size
+    eol       = stat.size
+    cache     = {}
+    buffer    = new Buffer(1024)
+    while end
+      end     = eol
+      start   = end - buffer.length
+      start   = 0 if start < 0
+      read    = fs.read fd, buffer, 0, buffer.length, start, _
+      end    -= read
+      offset  = read
+      if buffer[--read] isnt 0x0A
+        throw new Error "corrupt leaves"
+      eos     = read + 1
+      stop    = if start is 0 then 0 else -1
+      while read != 0
+        read = read - 1
+        if buffer[read] is 0x0A or read is stop
+          record = JSON.parse buffer.toString("utf8", read, eos)
+          eos   = read + 1
+          index = record.shift()
+          if index is 0
+            [ positions, end ] = [ record, 0 ]
+            break
+          else
+            splices.push [ index, start + read ]
+            if index > 0
+              cache[start + read] = record.shift()
+      eol = start + eos
+    # Now we replay the inserts and deletes described by the insert and delete
+    # objects that we've gathered up in our splices array.
+    splices.reverse()
+    for splice in splices
+      [ index, address ] = splice
+      if index > 0
+        positions.splice(index - 1, 0, address)
+      else
+        positions.splice(-(index + 1), 1)
+    # Close the file descriptor.
+    fs.close fd, _
+    # Return the loaded page.
+    extend page, { addresses, cache, loaded: true }
+
+  # Each line is terminated by a newline. In case your concerned that this
+  # simple search will mistake a byte inside a multi-byte character for a
+  # newline, have a look at
+  # [UTF-8](http://en.wikipedia.org/wiki/UTF-8#Description). All bytes
+  # participating in a multi-byte character have their leading bit set, all
+  # single-byte characters have their leading bit unset.
+
+  # Search for the newline that separates JSON records.
+  _readJSON: (buffer, read) ->
+    for offset in [0...read]
+      if buffer[offset] is 0x0A
+        return JSON.parse buffer.toString("utf8", 0, offset + 1)
+
+  # Our backwards read can load a position array that has been written to the
+  # the leaf page file, without having to load all of the records referenced by
+  # the position array. We will have to load the records as they are requested.
+  #
+  # To load a record, we open the file and jump to the position indicated by the
+  # position array. We then read the insert object that introdced the record to
+  # the leaf page file.
+  #
+  # We open a file descriptor and then close it after the record has been read.
+  # The desire to cache the file descriptor is strong, but it would complicate
+  # the shutdown of the b-tree. As it stands, we can always simply let the
+  # b-tree succumb to the garbage collector, because we hold no other system
+  # resources that need to be explictly released.
+  #
+  # Note how we allow we keep track of the minimum buffer size that will
+  # accommodate the largest possible buffer.
+  #
+  # TODO Have a mininum buffer that we constantly reuse, uh no. That will be
+  # shared by descents.
+
+  #
+  readRecord: (page, address, _) ->
+    filename = @filename page.address
+    page.position or= fs.stat(filename, _).size
+    fd = fs.open filename, "r", _
+    loop
+      buffer = new Buffer(@length)
+      read = fs.read fd, buffer, 0, buffer.length, address, _
+      if json = @_readJSON(buffer, read)
+        break
+      if @length > page.position - address
+        throw new Error "cannot find end of record."
+      @length += @length >>> 1
+    fs.close fd, _
+    json.pop()
+
+
+  # Over time, a leaf page file can grow fat with deleted records. Each deleted
+  # record means both an insert object that is no longer useful, and the delete
+  # record that marks it as useless.  We vacuum a leaf page file by writing it
+  # to a replacement leaf page file, then using `relink` to replace the current
+  # leaf page file with the replacement.
+  #
+  # All of records referenced by the current position array are appended into
+  # the replacement leaf page file using insert objects. A position array object
+  # is appended to the end of the replacement leaf page file. The rewritten leaf
+  # page file will load quickly, because the position array object will be found
+  # immediately.
+
+  # Note that we close the file descriptor before this function returns.
+  rewriteLeaves: (page, _) ->
+    filename = @filename page.address, ".new"
+    fd = fs.open filename, "a", 0644, _
+    positions = []
+    cache = {}
+    for position, index in page.positions
+      object = page.cache[position] or @readRecord page, position, _
+      position = @writeInsert fd, page, object, index, _
+      positions.push position
+      cache[position] = object
+    extend page, { positions, cache }
+    @writePositions fd, page, _
+    fs.close fd, _
+
+  # ### B-Tree Initialization
+  #
+  # After creating a `Strata` object, the client will either open the existing
   # database, or create a new database.
-
-  # Create the database detroying any exisiting database.
-
-  # —
+  #
+  # #### Creation
+  #
+  # Creating a new database will not create the database directory. The database
+  # directory must already exist, it must be empty. We don't want to surprise
+  # the application developer by blithely obliterating an existing database.
+  #
+  # An empty database has a single root penultimate branch page with only a left
+  # child and no keys. The left child is a single leaf page that is empty.
+
+  #
   create: (_) ->
     # Create the directory if it doesn't exist.
-    try
-      stat = fs.stat @directory, _
-      if not stat.isDirectory()
-        throw new Error "database #{@directory} is not a directory."
-    catch e
-      throw e if e.code isnt "ENOENT"
-      fs.mkdir @directory, 0755, _
+    stat = fs.stat @directory, _
+    if not stat.isDirectory()
+      throw new Error "database #{@directory} is not a directory."
+    if fs.readdir(@directory, _).length
+      throw new Error "database #{@directory} is not empty."
     # Create a root branch with a single empty leaf.
     root = @allocateBranches true
     leaf = @allocateLeaves([], -1)
     root.addresses.push leaf.address
     # Write the root branch.
-    @writeBranches root, "", _
+    @rewriteBranches root, _
     @rewriteLeaves leaf, _
     @relink leaf, _
-
-  # Open an existing database.
+    @relink root, _
+
+  # #### Opening
+  #
+  # Opening an existing database is a matter checking for any evidence of a hard
+  # shutdown. You never know. There may be a banged up leaf page file, one who's
+  # last append did not complete. We won't know that until we open it.
+  #
+  # Ah, no. Let's revisit. Here's a simple strategy. Open touches a file.
+  # Closing deletes teh file. If we open and the file exists, then we probably
+  # have to inspect every file that was modified after the modification,
+  # adjusting for dst? No because we'll be using seconds since the epoch. Only
+  # if the system time is changed do we have a problem.
+  #
+  # Thus, we have a reference point. Any file after needs to be inspected. We
+  # load it, and our `readLeaves` function will check for bad JSON, finding it
+  # very quickly.
+  #
+  # Now, we might have been in the middle of a split. The presenence of `*.new`
+  # files would indicate that. We can probably delete the split. Hmm..
+  #
+  # We can add more to the suffix. `*.new.0`, or `*.commit`, which is the last
+  # relink. If we have a case where there is a file named `*.commit` that does
+  # not have a corresponding permanent file, then we have a case where the
+  # permenant file has been deleted and not linked, but all the others have
+  # been, since this operaiton will go last, so we complete it to go forward.
+  #
+  # Otherwise, we delete the `*.commit`. We delete all the replacments that are
+  # not commits.
+  #
+  # We can always do a thorough rebuild of some kind.

   # —
   open: (_) ->
@@ -433,60 +777,176 @@
         address = parseInt match[1], 10
         @nextAddress = address + 1 if address > @nextAddress

-  # TODO Very soon.
   close: (_) ->
-#
-#
-# A branch page file contains a single object that includes the array of page
-# addresses that reference the branch page's children.
-#
-# The branch page file also stores a penultimate flag, and the address of the
-# next sibling of the branch
-# page.
-# The keys associated with the child addresses are not stored in the
-# branch page.
-#
-# Put another way, each branch element in a branch page represents a leaf page.
-# The leaf pages are sorted by the tree collation. The object value used to sort
-# the branch page is first element in the sorted leaf page.
-#
-# TODO Okay, what? I'm forgetting that I need the addresses to inner tiers as
-# well as to leaf tiers, both, when the tier is not penultimate. Descending is
-# not going to work, that is going to load the entire tree into memory quickly.
-#
-# Hmm... Maybe not. Tree four deep means loading only a particular path. It
-# ensures that we read lock our way down the tree to get our record. Although, I
-# believe I thought long and hard about this, and jumping down to the leaves
-# wasn't a problem.
-#
-# We only need to store the sorted array of the leaf tier addresses in our
-# branch tier, since we can obtain the object used to collate the address
-
-  # Link tier to the head of the MRU list.
-  link: (entry) ->
-    next = @head.next
-    entry.next = next
-    next.previous = entry
-    @head.next = entry
-    entry.previous = @head
-    entry
-
-  # Unlnk a tier from the MRU list.
-  unlink: (entry) ->
-    { next, previous } = entry
-    next.previous = previous
-    previous.next = next
-    entry
-
-  allocateLeaves: (addresses, right) ->
-    address = @nextAddress++
-    @cache[address] = @link
-      leaf: true
-      address: address
-      addresses: addresses
-      cache: {}
-      right: right
-      io: new LeafIO @filename address
+
+  # TODO Ensure that you close the current tier I/O. Also, you must also be very
+  # much locked before you use this, but I'm sure you know that.
+
+  # ### Concurrency
+  #
+  # The b-tree must only be read and written by a single Node.js process. It is
+  # not suitable for use with multiple node processes, or the cluster API.
+  #
+  # Although there is only a single thread in a Node.js process, the b-tree is
+  # still a concurrent data structure. Instead of thinking about concurrency in
+  # terms of threads we talk about concurrent *descents* of the b-tree.
+  #
+  # When we search the tree or alter the tree, we must descend the tree.
+  #
+  # Decents of the b-tree can become concurrent when descent encounters a page
+  # that is not in memory. While it is waiting on evented I/O to load the page
+  # files, the main thread of the process can make progress on another request
+  # to search or alter the b-tree, it can make process on another descent.
+  #
+  # This concurrency keeps the CPU and I/O loaded.
+  #
+  # ### Locking
+  #
+  # Locking prevents race conditions where an evented I/O request returns to to
+  # find that the sub-tree it was descending has been altered in way that causes
+  # an error. Pages may have split or merged by the main thread, records may
+  # have been inserted or deleted. While evented I/O is performed, the sub-tree
+  # needs to be locked to prevent it from being altered.
+  #
+  # The b-tree is locked page by page. We are able to lock only the pages of
+  # interest to a particular descent of the tree.
+  #
+  # Futhermore, the b-tree destinguishes between shared read locks and exclusive
+  # write locks. Multiple descents can read a traverse that is read locked, but
+  # only the descent that holds an exclusive write lock can traverse it or write
+  # to it.
+  #
+  # Of course, Node.js doesn't have the concept of mutexes to protect critical
+  # sections of code the way that threaded programming platforms do. There are
+  # no standard read and write lock APIs for use to use.
+  #
+  # Nor do we use file system locking.
+  #
+  # Instead, we simulate locks using callbacks. A call to `lock` is an evented
+  # function call that provides a callback. If the `lock` method can grant the
+  # lock request to the caller, the lock method will invoke the callback.
+  #
+  # If the `lock` method cannot grant the lock request, the `lock` method will
+  # queue the callback into a queue of callbacks assocated with the page. When
+  # other descents release the locks that prevent the lock request, the lock
+  # request callback is dequeued, and the callback invoked.
+  #
+  # The locking mechanism is a writer preferred shared read, exclusive write
+  # lock. If a descent holds an exclusive write lock, then all lock requests by
+  # other descents are blocked. If one or more descents hold a shared read lock,
+  # then any request for an exclusive write lock is blocked. Any request for a
+  # shared read lock is granted, unless an exclusive write lock is queued.
+  #
+  # The locks are not re-entrant.
+  #
+  # #### Lock
+
+  #
+  lock: (exclusive, address, leaf, callback) ->
+    # We must make sure that we have one and only one page object to represent
+    # the page. We the page object will maintain the lock queue for the page. It
+    # won't due to have different descents consulting different lock queues.
+    # There can be only one.
+    #
+    # The queue is implemented using an array of arrays. Shared locks are
+    # grouped inside one of the arrays in the queue element. Exclusive locks are
+    # queued alone as a single element in the array in the queue element.
+    if not page = @cache[address]
+      @cache[address] = page =
+        @link
+          leaf: leaf
+          address: address
+          cache: {}
+          addresses: []
+          locks: [[]]
+
+    # If the page needs to be laoded, we must load the page only after a lock
+    # has been obtained. Loading is a read, so we can load regardless of whether
+    # the lock is exclusive read/write or shared read.
+    if page.loaded
+      lock = callback
+    else
+      lock = (error, page) =>
+        if error
+          callback error
+        else if page.loaded
+          callback null, page
+        else if leaf
+          @readLeaves page, callback
+        else
+          @readBranches page, callback
+
+    # The callback is always added to the queue, even if it is not blocked and
+    # will execute immediately. The array in the queue element acts as a lock
+    # count.
+    #
+    # If the callback we add to the queue is added to the the first queue
+    # element is executed immediately. Otherwise, it will be executed when the
+    # preceeding queue elements have compeleted.
+    #
+    # When an exclusive lock is queued, an empty array is appended to the queue.
+    # Subsequent read lock callbacks are appened to the array in the last
+    # element. This gives exclusive lock callbacks priority.
+    locks = page.locks
+    if exclusive
+      throw new Error "already locked" unless locks.length % 2
+      locks.push [ lock ]
+      locks.push []
+      if locks[0].length is 0
+        locks.shift()
+        lock(null, tier)
+    else
+      locks[locks.length - 1].push lock
+      if locks.length is 1
+        lock(null, tier)
+
+  # #### Unlock
+
+  # When we release a lock, we simply shift a callback off of the array in the
+  # first element of the queue to decrement the lock count. We are only
+  # interested in the count, so it doesn't matter if the callback shifted by the
+  # descent is the one that it queued.
+
+  #
+  release: (tier) ->
+    locks = tier.locks
+    running = locks[0]
+    running.shift()
+    say { locks }
+    if running.length is 0 and locks.length isnt 1
+      locks.shift()
+      @resume tier, locks[0] if locks[0].length
+
+  # We call resume with the list of callbacks shifted off of a pages lock queue.
+  # The callbacks are scheduled to run in the next tick.
+  resume: (page, continuations) ->
+    process.nextTick =>
+      for callback in continuations
+        callback(null, page)
+
+  upgrade: (tier, _) ->
+    @release tier
+    @lock true, tier.address, false, _
+
+# Descent of a tree.
+
+#
+class Mutation
+  # The constructor always felt like a dangerous place to be doing anything
+  # meaningful in C++ or Java.
+  constructor: (@strata, @object, @key, @operation) ->
+    @io         = @strata._io
+    @options    = @strata._options
+
+    @parent     = { operations: [], locks: [] }
+    @child      = { operations: [], locks: [] }
+    @exclusive  = []
+    @shared     = []
+    @pivots     = []
+
+    { @extractor, @comparator } = @options
+
+    @key        = @extractor @object if @object? and not key?

   # Going forward, every will story keys, so that when we encounter a record, we
   # don't have to run the extractor, plus we get some caching.
@@ -495,13 +955,16 @@
   # each key as an object cache, by adding an object member to the key. But,
   # they cache container is a hash table, which is a compactish representation,
   # so why not make it yet anohter hash table? The logic gets a lot easier.
-  object: (tier, index, _) ->
-    address = tier.addresses[index]
-    if not object = tier.cache[address]
-      object = tier.cache[address] = tier.io.readObject address, _
-    object
-
-  key: (tierOrAddress, index, _) ->
+  #
+  # TODO None of this is right.
+  record: (page, index, _) ->
+    position = page.positions[index]
+    if not record = page.cache[position]
+      record = page.cache[position] = @readRecord page, position, _
+    record
+
+  # TODO Descend. Ah, well, find is in `Mutation`, so this moves to `Mutation`.
+  _key: (tierOrAddress, index, _) ->
     if typeof tierOrAddress is "number"
       leaf  = @lock false, tierOrAddress, true, _
       key = @extractor @object leaf, index, _
@@ -525,160 +988,6 @@
         # exclusive and shared locks to clear.
         key = tierOrAddress.cache[address] = @key address, 0, _
       key
-
-  # TODO Consider: can't I always just rewrite and link?
-  rewriteBranches: (tier, _) ->
-    @writeBranches(tier, ".new", _)
-
-  # TODO Ensure that you close the current tier I/O. Also, you must also be very
-  # much locked before you use this, but I'm sure you know that.
-
-  # Compact a leaf tier file by writing it to a temporary file that will become
-  # the permanent file. All of records referenced by the current address array a
-  # appended to a new leaf tier file using insert objects. An address array
-  # object is appended to the end of new leaf tier file. The new file will load
-  # quickly, because the address array object will be found immediately.
-  rewriteLeaves: (tier, _) ->
-    io = new LeafIO @filename(tier.address, ".new")
-    addresses = []
-    cache = {}
-    for i in [0...tier.addresses.length]
-      object = @object tier, i, _
-      address = io.writeInsert(object, i, _)
-      addresses.push address
-      cache[address] = object
-    io.writeAddresses(tier.right, addresses, _)
-    io.close(_)
-    extend tier, { addresses, cache }
-
-  readLeaves: (tier, _) ->
-    filename = @filename tier.address
-    stat = fs.stat filename, _
-    fd = fs.open filename, "r+", _
-
-    # Obviously, while all this is going on, I could end up writing to the
-    # leaf, because it is not actually locked. First lock, then load. The
-    # entry object needs to be linked, then loaded.
-    line      = ""
-    offset    = -1
-    end       = stat.size
-    eol       = stat.size
-    splices   = []
-    addresses = []
-    cache     = {}
-    buffer    = new Buffer(1024)
-    while end
-      end     = eol
-      start   = end - buffer.length
-      start   = 0 if start < 0
-      read    = fs.read fd, buffer, 0, buffer.length, start, _
-      end    -= read
-      offset  = read
-      if buffer[--read] isnt 0x0A
-        throw new Error "corrupt leaves"
-      eos     = read + 1
-      stop    = if start is 0 then 0 else -1
-      while read != 0
-        read = read - 1
-        if buffer[read] is 0x0A or read is stop
-          record = JSON.parse buffer.toString("utf8", read, eos)
-          eos   = read + 1
-          index = record.shift()
-          if index is 0
-            [ addresses, end ] = [ record, 0 ]
-            break
-          else
-            splices.push [ index, start + read ]
-            if index > 0
-              cache[start + read] = record.shift()
-      eol = start + eos
-    splices.reverse()
-    for splice in splices
-      [ index, address ] = splice
-      if index > 0
-        addresses.splice(index - 1, 0, address)
-      else
-        addresses.splice(-(index + 1), 1)
-    fs.close fd, _
-    loaded = true
-    io = new LeafIO @filename tier.address
-    tier = extend tier, { addresses, cache, loaded, io }
-
-  # TODO Move down, bring read and write up.
-  lock: (exclusive, address, leaf, callback) ->
-    if not tier = @cache[address]
-      @cache[address] = tier =
-        @link({ leaf, address, cache: {}, addresses: [], locks: [[]] })
-    locks = tier.locks
-    if tier.loaded
-      lock = callback
-    else
-      lock = (error, tier) =>
-        if error
-          callback error
-        else if tier.loaded
-          callback null, tier
-        else if leaf
-          @readLeaves tier, callback
-        else
-          @readBranches tier, callback
-    if exclusive
-      throw new Error "already locked" unless locks.length % 2
-      locks.push [ lock ]
-      locks.push []
-      if locks[0].length is 0
-        locks.shift()
-        lock(null, tier)
-    else
-      locks[locks.length - 1].push lock
-      if locks.length is 1
-        lock(null, tier)
-
-  resume: (tier, continuations) ->
-    process.nextTick =>
-      for callback in continuations
-        callback(null, tier)
-
-  # Remove the lock callback from the callback list..
-  release: (tier) ->
-    locks = tier.locks
-    running = locks[0]
-    running.shift()
-    say { locks }
-    if running.length is 0 and locks.length isnt 1
-      locks.shift()
-      @resume tier, locks[0] if locks[0].length
-
-  upgrade: (tier, _) ->
-    @release tier
-    @lock true, tier.address, false, _
-
-# Glossary:
-#
-#  * Descent - What we call an attempt to move from a parent node to a child
-#              node in  the tree, which may be paused because it encounters a
-#              lock on a tier.
-#  * Record - A JSON object stored in the b-tree.
-#  * Object - JavaScript objects are referred to as objects in this
-#             documentation and are not to be confused with actual records.
-
-#
-class Mutation
-  # The constructor always felt like a dangerous place to be doing anything
-  # meaningful in C++ or Java.
-  constructor: (@strata, @object, @key, @operation) ->
-    @io         = @strata._io
-    @options    = @strata._options
-
-    @parent     = { operations: [], locks: [] }
-    @child      = { operations: [], locks: [] }
-    @exclusive  = []
-    @shared     = []
-    @pivots     = []
-
-    { @extractor, @comparator } = @options
-
-    @key        = @extractor @object if @object? and not key?

   # Descend the tree, optionally starting an exclusive descent. Once the descent
   # is exclusive, all subsequent descents are exclusive.
@@ -914,187 +1223,6 @@
       locks.shift()
       true
     false
-
-# ### B-Tree Structure
-#
-# We call page of the b-tree, ah, maybe we stop using the word tier, start using
-# the word page, because we're not going to have to reduce the ambiguity with
-# some sort of subdivided binary file structure.
-#
-# NOTE Come back and look at this. It is succinct.
-#
-# There are two types of pages. Branch pages and leaf pages.
-#
-# Leaf pages store the records. They maintain the collation
-#
-# Leaf pages are all uniformly the same.
-#
-# ### Leaf Tier Files
-#
-# A leaf tier maintains an array of file positions called an address array. A
-# file position in the address array references a record in the leaf tier file
-# by its file position. The addresses in the address array are sorted by the
-# the sort order of the referenced records according to the sort order of the
-# b-tree.
-#
-# A leaf tier file contains JSON objects, one object on each line.
-#
-# There are three types of objects in a leaf tier file, insert objects, delete
-# objects, and address array objects.
-#
-# An insert object contains a *record* and the index in the address array where
-# the record's address would be inserted to preserve the sort order of the
-# address array.
-#
-# Beginning with an empty array and reading from the start of the file, the leaf
-# tier address array is reconstituted by reading the object at the current
-# position in the file, then inserting the position of the current object into
-# the address array at the index in the insert object.
-#
-# We record a deletion by appending a delete object. A delete object contains
-# an index in the address array that should be deleted. When reading the leaf
-# tier from the start of the file, we insert an address into the address array
-# when we encounter an insert object, and we delete an address when we encounter
-# a delete object.
-#
-# On occasion, we can store an address array object, An address array object
-# contains the address array itself.  We store a copy of a constructed address
-# array object in the leaf tier file so that we can read a large leaf tier file
-# quickly.
-#
-# When we read a leaf tier file, if we read from the back of the file toward the
-# front, we can read backward until we find an address array object. Then we can
-# read forward to the end of the file, applying the inserts and deletes that
-# occured after we wrote the address array object.
-#
-# When a leaf tier file is large, stashing the constructed address array at the
-# end means that the leaf tier can be loaded quickly, because we will only have
-# to read backwards a few entries to find a mostly completed address array. We
-# can then read forward from the array to amend the address array with the
-# inserts and deletes that occured after it was written.
-#
-# Not all of the records will be loaded when we go backwards, but we have their
-# file position from the address array, so we can jump to them and load them as
-# we need them. That is, if we need them, because owing to binary search, we
-# might only need a few records out of a great many records to find the record
-# we're looking for.
-#
-# Over time, a leaf tier file can grow fat with deleted records — an
-# insert object and a subsequent delete object. We vacuum a leaf tier file by
-# writing it to a new leaf tier file.
-
-# `LeafIO` — Keeps track of a file descriptor opened for append, and the
-# append file position. The `LeafIO` class does *not* contain all methods
-# relating to leaf tier file I/O. You can find more leaf tier file methods in
-# the `IO` class below.
-#
-# TODO Rename `RecordIO`.
-class LeafIO
-  # Construct a leaf file tier writer for the given file name.
-  constructor: (@filename) ->
-    @length = 1024
-
-  # #### Leaf Tier File Object Format
-  #
-  # Each leaf tier object is a JSON array. The first element of the array is an
-  # integer.
-  #
-  # If the integer is greater than zero, it indicates an insert object.  The
-  # integer is the one based index into the zero based address array, indicating
-  # the index where the position of the current insert object should be
-  # inserted. The second element of the leaf tier object array is the record
-  # object.
-
-  # Write an insert object.
-  writeInsert: (object, index, _) ->
-    @_writeJSON [ index + 1, object ], _
-
-  # If the integer is less than zero, it indicates a delete object. The absolute
-  # value of the integer is the one based index into the zero based addrss
-  # array, indicating the index of address array element that should be deleted.
-  # There are no other elements in the leaf tier object array.
-
-  # Write a delete object.
-  writeDelete: (index, _) ->
-    @_writeJSON [ -(index + 1) ], _
-
-  # If the integer is zero, it indicates an address array object. The integer
-  # value of zero is used only as an indicator. The second element is the
-  # address of the leaf tier to the right of current leaf tier. The third
-  # element of the leaf tier object array is the address array.
-
-  # Write an address array object.
-  writeAddresses: (right, addresses, _) ->
-    @_writeJSON [ 0, right, addresses ], _
-
-  # Append an object to the leaf tier file as a single line of JSON.
-  _writeJSON: (object, _) ->
-    @fd           or= fs.open @filename, "a+", 0644, _
-    @position     or= fs.stat(@filename, _).size
-
-    json            = JSON.stringify object
-    position        = @position
-    length          = Buffer.byteLength(json, "utf8")
-    buffer          = new Buffer(length + 1)
-    offset          = 0
-
-    # Write JSON and newline.
-    buffer.write json
-    buffer[length] = 0x0A
-
-    # Write may be interrupted by a signal, so we keep track of how many bytes
-    # are actually written and write the difference if we come up short.
-    while offset != buffer.length
-      count = buffer.length - offset
-      written = fs.write @fd, buffer, offset, count, @position, _
-      @position += written
-      offset += written
-
-    # What's the point if it doesn't make it to disk?
-    fs.fsync @fd, _
-
-    # Return the file position of the inserted record.
-    position
-
-  # Each line is terminated by a newline. In case your concerned that this
-  # simple search will mistake a byte inside a multi-byte character for a
-  # newline, have a look at
-  # [UTF-8](http://en.wikipedia.org/wiki/UTF-8#Description). All bytes
-  # participating in a multi-byte character have their leading bit set, all
-  # single-byte characters have their leading bit unset.
-
-  # Search for the newline that separates JSON records.
-  _readJSON: (buffer, read) ->
-    for offset in [0...read]
-      if buffer[offset] is 0x0A
-        return JSON.parse buffer.toString("utf8", 0, offset + 1)
-
-  # Jump to a position in the file and read a specific object. Because objects
-  # are cached in memory by a leaf tier, we're not going to get a request to
-  # read an object that has been written to the current write stream. When we
-  # read in a leaf tier, we'll cache any objects we encounter prior to read the
-  # ordering array.
-  #
-  # Note how we allow we keep track of the minimum buffer size that will
-  # accommodate the largest possible buffer.
-  readObject: (address, _) ->
-    @fd or= fs.open @filename, "a+", _
-    @position or= fs.stat(@filename, _).size
-    loop
-      buffer = new Buffer(@length)
-      read = fs.read @in, buffer, 0, buffer.length, address, _
-      if json = @_readJSON(buffer, read)
-        break
-      if @length > @position - address
-        throw new Error "cannot find end of record."
-      @length += @length >>> 1
-    json.pop()
-
-  # Close the file descriptor if it is open and reset the file descriptor and
-  # append position.
-  close: (_) ->
-    fs.close(@fd, _) if @fd
-    @position = @fd = null
 class exports.Strata
   # Construct the Strata from the options.
   constructor: (options) ->


Change "a little bit" from 10% to 3% [#143266129]

--- src/code/models/relation-factory.coffee
+++ src/code/models/relation-factory.coffee
@@ -198,11 +198,11 @@
     id: "aLittleBit"
     text: tr "~NODE-RELATION-EDIT.A_LITTLE_BIT"
     postfixIco: "a-little-bit"
-    formula: "in * 0.10"
-    magnitude: 0
-    gradual: 0
-    func: (scope) ->
-      return scope.in * 0.1
+    formula: "in * 0.03"
+    magnitude: 0
+    gradual: 0
+    func: (scope) ->
+      return scope.in * 0.03

   @iconName: (incdec,amount)->
     "icon-#{incdec.prefixIco}-#{amount.postfixIco}"


read file -- check size first

read file -- check size first

--- src/smc-hub/kucalc/compute-client.coffee
+++ src/smc-hub/kucalc/compute-client.coffee
@@ -463,14 +463,28 @@
         content = undefined
         async.series([
             (cb) =>
-                dbg("starting project if necessary...")
-                @start(cb:cb)
-            (cb) ->
+                # (this also starts the project)
                 # TODO: get listing and confirm size
                 # TODO - obviusly we should just stream... so there is much less of a limit... though
                 # limits are good, as this frickin' costs!
-                cb()
-            (cb) ->
+                {dir, base} = require('path').parse(opts.path)
+                if not base
+                    cb("not a file -- '#{base}'")
+                    return
+                @directory_listing
+                    path   : dir
+                    hidden : true
+                    cb     : (err, listing) =>
+                        if err
+                            cb(err)
+                        else
+                            for x in listing?.files ? []
+                                if x.name == base
+                                    if x.size <= opts.maxsize
+                                        cb()
+                                        return
+                        cb('file too big or not found in listing')
+            (cb) =>
                 url = "http://project-#{@project_id}:6001/#{@project_id}/raw/#{opts.path}"
                 dbg("fetching file from '#{url}'")
                 misc.retry_until_success
@@ -482,7 +496,7 @@
                     start_delay : 2000
                     max_delay   : 7000
                     cb          : cb
-        ], (err) ->
+        ], (err) =>
             opts.cb(err, content)
         )


improve an error message

improve an error message

--- src/smc-hub/postgres-user-queries.coffee
+++ src/smc-hub/postgres-user-queries.coffee
@@ -1418,7 +1418,7 @@
         #dbg = @dbg("_syncstrings_check")
         #dbg(misc.to_json([obj, account_id, project_id]))
         if not misc.is_valid_uuid_string(obj?.project_id)
-            cb("project_id must be a valid uuid")
+            cb("project_id (='#{obj?.project_id}') must be a valid uuid")
             return
         if project_id?
             if project_id == obj.project_id


Fixed AbstractTreeViewer height

Fixed AbstractTreeViewer height

--- app/assets/javascripts/oxalis/controller/abstract_tree_controller.coffee
+++ app/assets/javascripts/oxalis/controller/abstract_tree_controller.coffee
@@ -13,7 +13,7 @@
   constructor : (@model) ->

     container = $("#abstractTreeViewer")
-    @view = new AbstractTreeView(container.width(), container.height() - 20)
+    @view = new AbstractTreeView(container.width(), container.height() - 40)
     container.append(@view.canvas)

     @bind()
@@ -68,4 +68,4 @@

     position = model.route.getActiveNodePos()
     if position
-      model.flycam.setPosition(position)+      model.flycam.setPosition(position)


Menu

Menu

- Fixed showing of layer when content is loaded asynchronously:
  Trigger content-resize after item load. This fixes
  invisible Layers in "Select".

--- src/elements/Menu/Menu.coffee
+++ src/elements/Menu/Menu.coffee
@@ -45,6 +45,10 @@

 		if not @isShown() and @__itemList
 			@__itemList.render(@, @__event)
+			.done =>
+				Events.trigger
+					type: "content-resize"
+					node: @__itemList

 		super(@__event)


Display effectiveness messages.

Display effectiveness messages.

--- server/move.coffee
+++ server/move.coffee
@@ -108,12 +108,18 @@
     damage = damage * 2  if user.crit
     damage = Math.floor(((100 - battle.rng.randInt(0, 15, "damage roll")) * damage) / 100)
     damage = @modify(damage, @stabModifier(battle, user, target))
-    damage = Math.floor(@typeEffectiveness(battle, user, target) * damage)
+    effectiveness = @typeEffectiveness(battle, user, target)
+    damage = Math.floor(effectiveness * damage)
     damage = Math.floor(@burnCalculation(user) * damage)
     damage = Math.max(damage, 1)
     damage = @modify(damage, @modifyDamage(battle, user, target))
     damage = target.editDamage(this, damage)
     damage = Math.min(target.currentHP, damage)
+
+    if effectiveness < 1
+      battle.message "It's not very effective..."
+    else if effectiveness > 1
+      battle.message "It's super effective!"

     if user.crit
       battle.message "A critical hit!"


Revert widget

Revert widget

--- src/smc-webapp/widget-markdown-input/main.cjsx
+++ src/smc-webapp/widget-markdown-input/main.cjsx
@@ -95,7 +95,7 @@

     to_html: ->
         if @props.default_value
-            html = markdown.markdown_to_html_v2(@props.default_value)
+            html = markdown.markdown_to_html_v2(@props.default_value).s
             {__html: html}
         else
             {__html: ''}


Removed RequireJS Config

Removed RequireJS Config

--- js/main.coffee
+++ js/main.coffee
@@ -1,10 +1,5 @@
 ---
 ---
-
-require.config
-  paths:
-    jquery: '//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min'
-    underscore: '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min'

 require ['particle', 'vector', 'camera', 'time'], (Particle, Vector, Camera, Time) ->
   PARTICLE_COUNT = 100


Add access method

Add access method

--- src/filesystem/asyncfs.coffee
+++ src/filesystem/asyncfs.coffee
@@ -72,9 +72,47 @@
   SEP_RE      = /\/+/
   SEP_LAST_RE = /\/+$/

+  F_OK = 0
+  R_OK = 4
+  W_OK = 2
+  X_OK = 1
+
+  #--------------------------------------------------------------------------------
+  # Node.js compatible properties
+  #
+
+  @classProperty("constants", get: ->
+    return (@_constants or= Object.freeze({
+      F_OK: F_OK
+      R_OK: R_OK
+      W_OK: W_OK
+      X_OK: X_OK
+    }))
+  )
+
   #--------------------------------------------------------------------------------
   # Node.js compatible methods
   #
+
+  ###*
+  @method
+    Test permissions
+  @param {string} path
+    File path / directory path
+  @param {number} [mode]
+    Mode (permissions)
+  @param {function(Error/null):undefined} [callback]
+    Callback function when Promise is not used
+  @return {undefined/Promise}
+    Promise object when callback is omitted
+  ###
+  access: (path, mode = F_OK, callback) ->
+    if typeof(mode) == "function"
+      callback = mode
+      mode = F_OK
+    return invokeCallback(callback, @access(path, mode)) if callback?
+    path = path.split(SEP_RE).join(SEP).replace(SEP_LAST_RE, "")
+    return @accessImpl(path, mode)

   ###*
   @method
@@ -346,6 +384,20 @@
   ###*
   @protected
   @method
+    Implement of access method
+  @param {string} path
+    File path / directory path
+  @param {number} mode
+    Mode (permissions)
+  @return {Promise}
+    Promise object
+  ###
+  accessImpl: (path, mode) ->
+    return Promise.reject(Error("Not supported"))
+
+  ###*
+  @protected
+  @method
     Implement of mkdir method
   @param {string} path
     Directory path


bongo bugfix: all constructors are ending up in global shared constructors

--- node_modules_koding/bongo/lib/base/index.coffee
+++ node_modules_koding/bongo/lib/base/index.coffee
@@ -353,7 +353,6 @@
     # TODO: outsource this for scalability reasons.
     # defineProperty constructor, 'sharedInstances', value: {}
     Base.sharedConstructors[name] = konstructor
-    Base.globalSharedConstructors[name] = konstructor
     @

   @inheritanceChain =(Klass=@, glue)->


Just show related article types for reply.

--- app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
@@ -28,49 +28,71 @@
   constructor: ->
     super

+    # set possble article types
+    possibleArticleType =
+      note: true
+      phone: true
+    if @ticket && @ticket.create_article_type_id
+      articleTypeCreate = App.TicketArticleType.find(@ticket.create_article_type_id).name
+      if articleTypeCreate is 'twitter status'
+        possibleArticleType['twitter status'] = true
+      else if articleTypeCreate is 'twitter direct-message'
+        possibleArticleType['twitter direct-message'] = true
+      else if articleTypeCreate is 'email'
+        possibleArticleType['email'] = true
+    if @ticket && @ticket.customer_id
+      customer = App.User.find(@ticket.customer_id)
+      if customer.email
+        possibleArticleType['email'] = true
+
     # gets referenced in @setArticleType
     @type = @defaults['type'] || 'note'
-    @articleTypes = [
-      {
+    @articleTypes = []
+    if possibleArticleType.note
+      @articleTypes.push {
         name:       'note'
         icon:       'note'
         attributes: []
         features:   ['attachment']
-      },
-      {
+      }
+    if possibleArticleType.email
+      @articleTypes.push {
         name:       'email'
         icon:       'email'
         attributes: ['to', 'cc']
         features:   ['attachment']
-      },
-      {
+      }
+    if possibleArticleType.facebook
+      @articleTypes.push {
         name:       'facebook'
         icon:       'facebook'
         attributes: []
-      },
-      {
+      }
+    if possibleArticleType['twitter status']
+      @articleTypes.push {
         name:              'twitter status'
         icon:              'twitter'
         attributes:        []
         features:          ['body:limit']
         maxTextLength:     140
         warningTextLength: 30
-      },
-      {
+      }
+    if possibleArticleType['twitter direct-message']
+      @articleTypes.push {
         name:              'twitter direct-message'
         icon:              'twitter'
         attributes:        ['to']
         features:          ['body:limit']
         maxTextLength:     10000
         warningTextLength: 500
-      },
-      {
+      }
+    if possibleArticleType.phone
+      @articleTypes.push {
         name:       'phone'
         icon:       'phone'
         attributes: []
         features:   ['attachment']
-      },
-    ]
+      }

     if @isRole('Customer')
       @type = 'note'
@@ -249,8 +271,8 @@
         params.sender_id = sender.id
       else
         sender           = App.TicketArticleSender.findByAttribute('name', 'Agent')
+        type             = App.TicketArticleType.findByAttribute('name', params['type'])
         params.sender_id = sender.id
-        type             = App.TicketArticleType.findByAttribute('name', params['type'])
         params.type_id   = type.id

     if params.type is 'twitter status'


draw full-width rectangle

draw full-width rectangle

--- app/assets/javascripts/components/mark/tools/text-row-tool/index.cjsx
+++ app/assets/javascripts/components/mark/tools/text-row-tool/index.cjsx
@@ -57,9 +57,8 @@
         strokeWidth={SELECTED_STROKE_WIDTH/scale}
         onMouseDown={@props.onSelect unless @props.disabled}
       >
-
         <Draggable onDrag={@handleDrag}>
-          <rect x='100' y='100' width='400' />
+          <rect x={0-@props.mark.x} y={0} width='100%' height='100' />
         </Draggable>

         { if @props.selected


Pass in the responses to Sharify

Pass in the responses to Sharify

--- apps/artwork/routes.coffee
+++ apps/artwork/routes.coffee
@@ -6,12 +6,14 @@
   artwork.fetch
     cache: true
     error: res.backboneError
-    success: ->
+    success: (model, response, options) ->
+      res.locals.sd.ARTWORK = response
       artist = new Artist artwork.get('artist')
       artist.fetch
         cache: true
         error: res.backboneError
-        success: ->
+        success: (model, response, options) ->
+          res.locals.sd.ARTIST = response
           res.render 'index',
             artwork: artwork
             artist: artist


Safari on iOS devices can now scroll submissions in speedgrader

desc

fixes CNVS-2468

test plan:
As a student
	Turn in a long submission

As Teacher on an iOS device
	Open up the submission in speedgrader
	attempt to scroll the submission text
	it should work....

-

Change-Id: I2f82e34a297cb696160420a71a7833fd6f8962d4
Reviewed-on: https://gerrit.instructure.com/37555
Reviewed-by: Simon Williams <088e16a1019277b15d58faf0541e11910eb756f6@instructure.com>
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
QA-Review: Caleb Guanzon <54fa1f30d451e4f3c341a8f3c47c9215fd0be9c6@instructure.com>
Product-Review: Brad Horrocks <7b5c5d1f2eba6c540b8e53f17d87a9bd7dbf604b@instructure.com>

--- app/coffeescripts/bundles/legacy/submissions_show_preview_text.coffee
+++ app/coffeescripts/bundles/legacy/submissions_show_preview_text.coffee
@@ -2,6 +2,13 @@
   "jquery"
 ], ($) ->
   $(document).ready ->
+    # this is so iOS devices can scroll submissions in speedgrader
+    $("body,html").css({
+      "height": "100%",
+      "overflow": "auto",
+      "-webkit-overflow-scrolling": "touch"
+    })
+
     $(".data_view").change(->
       if $(this).val() is "paper"
         $("#submission_preview").removeClass("plain_text").addClass "paper"


test(engine): ensure condition run before schema

--- packages/engine/test/plugins/conditions/index.coffee
+++ packages/engine/test/plugins/conditions/index.coffee
@@ -9,3 +9,15 @@
         if: true
       , ({conditions}) ->
         conditions.if.should.be.eql [true]
+
+  it 'before schema', ->
+    {status} = await nikita.call
+      if: false
+      metadata:
+        schema:
+          type: 'object'
+          properties: 'a_key': type: 'boolean'
+          required: ['a_key']
+    , -> true
+    status.should.be.false()
+


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/game_state_cameras.coffee
+++ src/game_state_cameras.coffee
@@ -29,4 +29,3 @@
       else
         return cameras
   }
-


a doesn't have value

a doesn't have value

--- resources/assets/coffee/react/beatmaps/search-filter.coffee
+++ resources/assets/coffee/react/beatmaps/search-filter.coffee
@@ -46,7 +46,6 @@
             key: i
             href: @href(option)
             className: cssClasses
-            value: option.id
             'data-filter-value': option.id
             onClick: @select
             option.name
@@ -56,8 +55,8 @@
     BeatmapsetFilter.castFromString[@props.name]?(value) ? value ? null

-  href: (option) =>
-    i = @cast(option.id)
+  href: ({ id, name }) =>
+    i = @cast(id)
     newSelection =
       if @props.multiselect
         _(@currentSelection())[if @selected(i) then 'without' else 'concat'](i).sort().join('.')


Made the exp array more readable

Made the exp array more readable


Don't track admins in Snowplow

Don't track admins in Snowplow

--- lib/analytics.coffee
+++ lib/analytics.coffee
@@ -44,6 +44,8 @@
   snowplow?('trackPageView')

 module.exports.snowplowStruct = (category, action, label, property, value = '0.0', contexts = {}) ->
+  # Don't track admins
+  return if sd.CURRENT_USER?.type is 'Admin'
   # in general: https://github.com/snowplow/snowplow/wiki/2-Specific-event-tracking-with-the-Javascript-tracker#custom-structured-events
   # contexts json: http://snowplowanalytics.com/blog/2014/01/27/snowplow-custom-contexts-guide/#contexts
   snowplow?('trackStructEvent', category, action, label, property, value, contexts)


Native promises don't support .each

Native promises don't support .each

--- src/liquid/block.coffee
+++ src/liquid/block.coffee
@@ -1,6 +1,18 @@
 Liquid = require("../liquid")
 util = require "util"
 Promise = require "native-or-bluebird"
+
+# Iterates over promises sequentially
+Promise_each = (promises, cb) ->
+  iterator = (index) ->
+    return Promise.resolve() if index >= promises.length
+    promise = promises[index]
+
+    Promise.resolve(promise).then (value) ->
+      Promise.resolve(cb(value)).then ->
+        iterator(index + 1)
+
+  iterator(0)

 module.exports = class Block extends Liquid.Tag
   @IsTag             = ///^#{Liquid.TagStart.source}///
@@ -84,7 +96,7 @@
   renderAll: (list, context) ->
     accumulator = []

-    Promise.each list, (token) ->
+    Promise_each list, (token) ->
       unless typeof token?.render is "function"
         accumulator.push token
         return


removing _ from _map_label_values

removing _ from _map_label_values

--- app/assets/javascripts/admin/stores.js.coffee
+++ app/assets/javascripts/admin/stores.js.coffee
@@ -64,9 +64,9 @@
         unless gon.authenticated is false
             fetch_user_photos(render_user_photo_feed, { product_slug: data.slug })

-    # build csv of label data
+    # build fields_for instagram_tag, color and size
     # i.e. size: small,medium,large
-    _map_label_values = (name, $context) ->
+    map_label_values = (name, $context) ->
         container = $('<div />').attr('class', 'hide')
         ($ ".label-#{name}").each (i, label) ->
             data =
@@ -337,9 +337,8 @@
                             product_image_n: i))
                 ($ @).append(container)

-                # query instagram tag, color and sizes labels and
-                # update appropriate input with csv
-                _map_label_values(input, ($ @)) for input in multiple_value_inputs
+                # build nested attribute inputs for instagram_tag, color and size
+                map_label_values(input, ($ @)) for input in multiple_value_inputs

                 verb = if @id is 'new_product' then 'created' else 'updated'


copy update

copy update

--- app/assets/javascripts/helpers/i18n.js.coffee
+++ app/assets/javascripts/helpers/i18n.js.coffee
@@ -99,7 +99,7 @@
       customer_history: 'Customer history'
       customers:        'Customers'
       dashboard:        'Dashboard'
-      details_for:      'Details For'
+      details_for:      'Details for'
       help:             'Help'
       inbound_calls:    'Inbound Calls'
       my_settings:      'My Settings'


Consider a page finished if non-numeric grades are selected

--- app/assets/javascripts/reviewEditor.js.coffee
+++ app/assets/javascripts/reviewEditor.js.coffee
@@ -260,7 +260,7 @@
   # Calculates average grade
   # If @numericGrading if true, average grade is calculated as the average value of the given grades.
   # If @numericGrading if false, average index is used instead.
-  # If some grade null or undefined, undefined is returned.
+  # If some grade is null or undefined, undefined is returned.
   # grades: array of grade values (strings or numbers)
   #
   calculateGradeMean: (grades) ->
@@ -288,7 +288,7 @@

     if @numericGrading
       if nonNumericGradesSeen
-        return undefined  # Grade must be selected manually
+        return ''  # Grade must be selected manually
       else
         meanGrade = Math.round(gradeSum / grades.length)
         return meanGrade


Electron: start: code refactor, initialise in order, sync storage on window getting closed

--- elektron/src/start.coffee
+++ elektron/src/start.coffee
@@ -15,6 +15,26 @@

 module.exports = ->

+  # Set application menu
+  new ApplicationMenu
+
+  # Start listening the web app
+  new IPCReporter
+
+  # Prepare AppStorage
+  storage = new Storage template : STORAGE_TEMPLATE
+
+  syncStorage = (callback = (->)) ->
+
+    return  unless mainWindow
+
+    settings = storage.get()
+    settings['last-route'] = mainWindow.webContents.getURL()
+    storage.write settings, callback
+
+  # Sync storage on quit
+  app.on 'will-quit', syncStorage
+
   # Create the browser window.
   mainWindow = new BrowserWindow
     width             : 1280
@@ -28,16 +48,6 @@
       preload         : NODE_REQUIRE
       nodeIntegration : no

-
-  # Set application menu
-  new ApplicationMenu
-
-  # Start listening the web app
-  new IPCReporter
-
-  # Prepare AppStorage
-  storage = new Storage template : STORAGE_TEMPLATE
-
   # and load the index.html of the app.
   mainWindow.loadURL storage.get()['last-route'] or ROOT_URL

@@ -45,15 +55,10 @@
     e.preventDefault()
     shell.openExternal url

-  app.on 'will-quit', (e) ->
-    settings = storage.get()
-    settings['last-route'] = mainWindow.webContents.getURL()
-    storage.write settings
-
   # Emitted when the window is closed.
-  mainWindow.on 'closed', ->
+  mainWindow.on 'close', ->
     # Dereference the window object, usually you would store windows
     # in an array if your app supports multi windows, this is the time
     # when you should delete the corresponding element.
-    mainWindow = null
-
+    syncStorage ->
+      mainWindow = null


Custom content type in response.

Custom content type in response.

--- lib/auth/base.coffee
+++ lib/auth/base.coffee
@@ -7,7 +7,8 @@
   constructor: (@options, @checker) ->
     @options.msg401 = "401 Unauthorized" if not @options.msg401
     @options.msg407 = "407 Proxy authentication required" if not @options.msg407
-
+    @options.contentType = "text/plain" if not @options.contentType
+
     # Loading users from file, if file is set.
     @options.users = []
     @loadUsers() if not @checker and @options.file
@@ -43,8 +44,11 @@

   # Ask for authentication.
   ask: (res, result) ->
-    header = @generateHeader result
-
+
+    header = @generateHeader result
+
+    res.setHeader "Content-Type", @options.contentType
+
     if @proxy # Proxy authentication.
       res.setHeader "Proxy-Authenticate", header
       res.writeHead 407