Make pause_threshold_ms configurable

Make pause_threshold_ms configurable

--- lib/metrics.coffee
+++ lib/metrics.coffee
@@ -46,8 +46,8 @@

 # log_metrics logs node process metrics at the specified frequency. It also logs every time the
 # node event loop stops processing all the events for more than a second.
-module.exports.log_metrics = (source, frequency_ms) ->
+module.exports.log_metrics = (source, frequency_ms, pause_threshold_ms = 1000) ->
   setInterval _.partial(log_memory_usage, source), frequency_ms
-  start_pause_detector source, 100, 6000
+  start_pause_detector source, 100, pause_threshold_ms
   setInterval _.partial(log_pauses, source), frequency_ms


Use NODE_ENV instead of ENV

Use NODE_ENV instead of ENV

--- lib/metrics.coffee
+++ lib/metrics.coffee
@@ -1,7 +1,7 @@
 _ = require 'underscore'
 kayvee = require 'kayvee'

-env = process.env.ENV or 'staging'
+env = process.env.NODE_ENV or 'staging'

 # log_memory_usage logs HeapUsed, HeapTotal, and RSS in the kayvee format
 log_memory_usage = (source) ->


Update master

Update master

--- vendor/assets/javascripts/jquery.drappers.popup.js.coffee
+++ vendor/assets/javascripts/jquery.drappers.popup.js.coffee
@@ -24,7 +24,6 @@
     show: ->
       @hideAnotherPopups()
       @element.show()
-      @element.click (e)-> e.stopPropagation()
       $('body').click => @hide()

     hideAnotherPopups: ->


edit example to use targetNodeSelector

edit example to use targetNodeSelector

--- example/app.coffee
+++ example/app.coffee
@@ -46,7 +46,7 @@
           Shortcuts
             name: @constructor.displayName
             handler: @_handleShortcuts
-            targetNode: document.getElementById('app')
+            targetNodeSelector: '#app'
             className: 'content',

             div null,


Filled out additional missing types for testing expression equality

--- math/expression_equality_spec.coffee
+++ math/expression_equality_spec.coffee
@@ -22,6 +22,7 @@
           @comps.number.build(value: 10),
           @comps.number.build(value: 10)
           )).toEqual true
+

   describe "expression comparison", ->
     it "accepts for empty expressions", ->
@@ -105,6 +106,35 @@
       expected = @exp_builder('^': [10, 22])
       expect(new_exp).not.toBeAnEqualExpressionTo(expected)

+  describe "multiplication", ->
+    it "accepts two multiplication symbols", ->
+      expect(
+        @isEqual(
+          @comps.multiplication.build(),
+          @comps.multiplication.build()
+        )).toEqual true

+  describe "division", ->
+    it "accepts two division symbols", ->
+      expect(
+        @isEqual(
+          @comps.division.build(),
+          @comps.division.build()
+        )).toEqual true

+  describe "pi", ->
+    it "accepts two pi symbols", ->
+      expect(
+        @isEqual(
+          @comps.pi.build(),
+          @comps.pi.build()
+        )).toEqual true

+  describe "subtraction", ->
+    it "accepts two subtraction symbols", ->
+      expect(
+        @isEqual(
+          @comps.subtraction.build(),
+          @comps.subtraction.build()
+        )).toEqual true
+


Tweak tests.

Tweak tests.

--- spec/end-to-end.coffee
+++ spec/end-to-end.coffee
@@ -1569,12 +1569,12 @@

       """
       engine.then (solution) ->
-        expect(solution["$box[x]"]).to.eql (((400 - 16 * 7) / 8) + 16) * 2
+        expect(Math.floor solution["$box[x]"]).to.eql (((400 - 16 * 7) / 8) + 16) * 2

         engine.$id('box').click()

         engine.then (solution) ->
-          expect(solution["$box[x]"]).to.eql (((400 - 16 * 7) / 8) + 16) * 5
+          expect(Math.floor solution["$box[x]"]).to.eql (((400 - 16 * 7) / 8) + 16) * 5
           done()

@@ -2571,7 +2571,9 @@
           ::scope[left] == 0;
           ::scope[top] == 0;

-          @v |(article)... in(::scope);
+          @v |(article)... in(::scope) {
+            height: >= 0;
+          }

           article {
             @v |


Export Drop instead of drop

Export Drop instead of drop

--- drop.coffee
+++ drop.coffee
@@ -347,8 +347,7 @@
         $target

-window.drop = drop
-
+window.Drop = drop

 $.fn.drop = (options) ->
     if jQueryMethods[options]


Refactoring: tests of getActionName

Refactoring: tests of getActionName

--- spec/navigation-spec.coffee
+++ spec/navigation-spec.coffee
@@ -58,15 +58,25 @@
       atom.workspace = atom.workspaceView.model

     describe "when an action file is open", ->
-
-      it "gets the view from the filename", ->
-        editor = null
+      beforeEach ->
         runs ->
           waitsForPromise ->
             atom.workspace.open("/app/views/users/index.html.erb")
+      it "gets the view from the filename", ->
         runs ->
           editor = atom.workspace.getEditors()[0]
           expect(Navigation.getActionName(editor)).toBe "index"
+
+    describe "When can't discover the action name", ->
+      beforeEach ->
+        runs ->
+          waitsForPromise ->
+            atom.workspace.open("README.md")
+      it "returns null", ->
+        runs ->
+          editor = atom.workspace.getEditors()[0]
+          expect(Navigation.getActionName(editor)).toBe null
+

   describe "goTo", ->


Adjust a comparison test

Adjust a comparison test

--- spec/powershell-spec.coffee
+++ spec/powershell-spec.coffee
@@ -249,8 +249,8 @@
           operatorPlus = operator + "ual"
           {tokens} = grammar.tokenizeLine operatorPlus
           expect(tokens.length).toBe(2)
-          expect(tokens[0]).not.toHaveScopes ["keyword.operator.comparison.powershell"]
-          expect(tokens[1]).not.toHaveScopes ["keyword.operator.comparison.powershell"]
+          expect(tokens[0]).not.toHaveScopes ["source.powershell", "keyword.operator.comparison.powershell"]
+          expect(tokens[1]).not.toHaveScopes ["source.powershell", "keyword.operator.comparison.powershell"]

   describe "Automatic variables", ->
     automaticVariables = [


debugging

debugging

--- app/assets/javascripts/forge/controllers.js.coffee
+++ app/assets/javascripts/forge/controllers.js.coffee
@@ -66,6 +66,7 @@
     NgMap.getMap().then (map) ->
       map.overlayMapTypes.removeAt(1) #if map.overlayMapTypes.length > 0
       if layer.id
+        debugger
         url = "/layers/#{$scope.layer2.id}/wms?"
         # fitToBoundingBox(map, $scope.layer.bbox)
         wmslayerBottom = loadWMS map, url, null, 1


Fix another decoration test

Fix another decoration test

- Also fix dispatch to match others.

--- spec/jumpy-spec.coffee
+++ spec/jumpy-spec.coffee
@@ -228,15 +228,16 @@

     describe "when the jumpy:a event is triggered", ->
         it "updates the status bar with a", ->
-            atom.commands.dispatch textEditorElement, 'jumpy:a'
+            atom.commands.dispatch workspaceElement, 'jumpy:a'
             expect(document
                 .querySelector '#status-bar-jumpy .status'
                     .innerHTML).toBe 'a'
         it "removes all labels that don't begin with a", ->
-            atom.commands.dispatch textEditorElement, 'jumpy:a'
-            expect(textEditorElement.shadowRoot
-                .querySelectorAll('.jumpy.label:not(.irrelevant)').length)
-                    .toBe 26
+            atom.commands.dispatch workspaceElement, 'jumpy:a'
+            decorations = textEditor.getOverlayDecorations()
+            relevantDecorations = decorations.filter (d) ->
+                not d.getProperties().item.classList.contains 'irrelevant'
+            expect(relevantDecorations).toHaveLength 26

     describe "when the jumpy:reset event is triggered", ->
         it "clears first entered key and lets a new jump take place", ->


Generate the delta in a cursor-aware manner.

This ensures that the delta is as close to the user's action as
possible.

--- src/core/editor.coffee
+++ src/core/editor.coffee
@@ -1,4 +1,5 @@
 _         = require('lodash')
+Delta     = require('rich-text/lib/delta')
 dom       = require('../lib/dom')
 Document  = require('./document')
 Line      = require('./line')
@@ -18,6 +19,10 @@
     @length = @delta.length()
     @selection = new Selection(@doc, @quill)
     @timer = setInterval(_.bind(this.checkUpdate, this), @options.pollInterval)
+    @savedRange = null;
+    @quill.on("selection-change", (range) =>
+      @savedRange = range
+    )
     this.enable() unless @options.readOnly

   destroy: ->
@@ -168,11 +173,22 @@
     )

   _trackDelta: (fn) ->
+    oldIndex = @savedRange?.start
     fn()
     newDelta = @doc.toDelta()
-    # TODO need to get this to prefer earlier insertions
-    delta = @delta.diff(newDelta)
-    return delta
+    @savedRange = @selection.getRange()
+    newIndex = @savedRange?.start
+    try
+      if oldIndex? and newIndex? and oldIndex <= @delta.length() and newIndex <= newDelta.length()
+        oldLeftDelta = @delta.slice(0, oldIndex)
+        oldRightDelta = @delta.slice(oldIndex)
+        newLeftDelta = newDelta.slice(0, newIndex)
+        newRightDelta = newDelta.slice(newIndex)
+        diffLeft = oldLeftDelta.diff(newLeftDelta)
+        diffRight = oldRightDelta.diff(newRightDelta)
+        return new Delta(diffLeft.ops.concat(diffRight.ops))
+    catch ignored
+    return @delta.diff(newDelta)

   _update: ->
     return false if @innerHTML == @root.innerHTML


Set spec type after all specs in the category are required

--- spec/spec-suite.coffee
+++ spec/spec-suite.coffee
@@ -8,16 +8,21 @@
   requireSpecs = (directoryPath, specType) ->
     for specPath in fsUtils.listTreeSync(path.join(directoryPath, 'spec')) when /-spec\.coffee$/.test specPath
       require specPath
+
+  setSpecType = (specType) ->
     for spec in jasmine.getEnv().currentRunner().specs() when not spec.specType?
       spec.specType = specType

   # Run core specs
-  requireSpecs(window.resourcePath, 'core')
+  requireSpecs(window.resourcePath)
+  setSpecType('core')

   # Run bundled package specs
   for packagePath in fsUtils.listTreeSync(config.nodeModulesDirPath) when atom.isInternalPackage(packagePath)
     requireSpecs(packagePath, 'bundled')
+  setSpecType('bundled')

   # Run user package specs
   for packagePath in fsUtils.listTreeSync(config.userPackagesDirPath)
     requireSpecs(packagePath, 'user')
+  setSpecType('user')


fix map update on tab click

fix map update on tab click

--- app/assets/javascripts/modules/maps/base.js.coffee
+++ app/assets/javascripts/modules/maps/base.js.coffee
@@ -46,25 +46,33 @@

       COLORS = ['#71a32b', '#c6e3cb', '#80cbd1', '#40aed2', '#3383b9', '#27589e', '#1b2c85']
       updateMap: (ids) ->
+        @resetProtectedAreas()
+        @$mapContainer.data('wdpa-ids', ids)
+
+        for wdpaid, index in ids
+          @loadProtectedArea(wdpaid, index)
+
+      resetProtectedAreas: ->
+        # remove tooltip if any
+        if window.ProtectedPlanet?.Map.marker?
+          @map.removeLayer window.ProtectedPlanet.Map.marker
+
+        # remove geometries from the map
+        for paLayer in (window.ProtectedPlanet.Map.protected_areas || [])
+          @map.removeLayer(paLayer)
+
+        # annihilate stored protected areas
         window.ProtectedPlanet.Map.protected_areas = []

-        @$mapContainer.data('wdpa-ids', ids)
+      loadProtectedArea: (wdpaid, index) ->
+        $.getJSON("/api/v3/protected_areas/#{wdpaid}/geojson", (data) =>
+          pa_layer = L.geoJSON(data, style: ->
+            {fillOpacity: .6, weight: 1, fillColor: COLORS[index], color: "#FF6600"}
+          ).addTo(@map)

-        config = @$mapContainer.data()
-        Interactive.listen(@map)
-
-        loadProtectedArea = (wdpaid, index) =>
-          $.getJSON("/api/v3/protected_areas/#{wdpaid}/geojson", (data) =>
-
-            pa_layer = L.geoJSON(data, style: ->
-              {fillOpacity: .6, weight: 1, fillColor: COLORS[index], color: "#FF6600"}
-            ).addTo(@map)
-
-            window.ProtectedPlanet.Map.protected_areas[index] = pa_layer
-          )
-
-        for wdpaid, index in ids
-          loadProtectedArea(wdpaid, index)
+          window.ProtectedPlanet.Map.protected_areas[index] = pa_layer
+        )

     return Map
+
 )


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -157,7 +157,7 @@
             for key, value of root
               if value is Module
                 Module._name = moduleName = key # Cache module name
-
+
           if moduleName
             unless I.includedModules.include moduleName
               I.includedModules.push moduleName


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -35,11 +35,9 @@

     myObject.attrAccessor "r", "g", "b"

-    myObject.r(myObject.r() - 1)

-    => 254
-
-    myObject.I.r
+    myObject.r(254)
+    myObject.r()

     => 254

@@ -142,4 +140,3 @@
     ###
     include: (Module) ->
       self.extend Module(I, self)
-


don't attempt to draw the county map twice

--- source/javascripts/county-map.coffee
+++ source/javascripts/county-map.coffee
@@ -1,4 +1,5 @@
 render = (event, env) ->
+  return if d3.select('#county-map svg').node()
   selectedSchool = null

   tip = d3.tip().attr('class', 'd3-tip')


work on kitecontroller

work on kitecontroller

--- client/landing/app/Applications/Inbox.kdapplication/Controllers/InboxMessageListController.coffee
+++ client/landing/app/Applications/Inbox.kdapplication/Controllers/InboxMessageListController.coffee
@@ -8,7 +8,7 @@
     @removeAllItems()
     @utils.wait 7000, => callback?()
     KD.whoami().fetchMail
-      limit       : 20
+      # limit       : 20
       sort        :
         timestamp : -1
     , (err, messages)=>


remove bar chart code, it's pretty useless

--- source/javascripts/county-map.coffee
+++ source/javascripts/county-map.coffee
@@ -23,23 +23,8 @@
   # Returns nothing
   updateCountyInfo = (county) ->
     props = county.properties
-    container = d3.select '.js-county-info'
-    margin = top: 20, left: 40, bottom: 0, right: 0
-    vwidth = parseFloat(container.style 'width') - margin.left - margin.right
-    vheight = 225 - margin.top - margin.bottom
-
-    props.timeline.forEach (p) ->
-      p.percent = ((p.count / props.male_18_24) * 100).toFixed(2)
-
-    vy = d3.scale.ordinal().domain(props.timeline.map (p) -> p.year).rangeBands [vheight, 0], 0.1
-    vx = d3.scale.linear().domain([0, env.maxyear]).range [0, vwidth]
-
-    yax = d3.svg.axis().scale(vy).orient('left')
-    xax = d3.svg.axis()
-      .scale(vx)
-      .orient('top')
-      .ticks(4)
-      .tickSize(vheight)
+    container = d3.select('.js-county-info')
+      .style 'display', 'block'

     el = container.append('div')
       .attr('class', 'js-county')
@@ -69,35 +54,9 @@

     el.append('span')
       .attr('class', 'cam')
-      .html((d) ->
+      .html (d) ->
         "#{d3.format(',')(d.male_18_24)}
           males 18-24yo according to The U.S. Census Bureau."
-      )
-
-    vis = el.append('svg')
-      .attr('width', vwidth + margin.left + margin.right)
-      .attr('height', vheight + margin.top + margin.bottom)
-    .append('g')
-      .attr('transform', "translate(#{margin.left}, #{margin.top})")
-
-    vis.append('g')
-      .attr('class', 'y axis')
-      .call(yax)
-
-    vis.append('g')
-      .attr('transform', "translate(0, #{vheight})")
-      .attr('class', 'x axis')
-      .call(xax)
-
-    grect = vis.selectAll('.county-bar')
-      .data((d) -> d.timeline)
-    .enter().append('g')
-      .attr('class', 'county-bar')
-      .attr('transform', (d, i) -> "translate(0, #{vy d.year})")
-
-    grect.append('rect')
-      .attr('height', vy.rangeBand())
-      .attr('width', (d) -> vx(d.count))

   clearCountyInfo = (county) ->
     d3.select('.js-county').remove()


Keep session ID's unique when updating reps.

--- jobs/scrape-representatives.coffee
+++ jobs/scrape-representatives.coffee
@@ -36,7 +36,7 @@
         db.collection(peopleCollectionName).update
           generalAssemblyId: assemblyMemberId
         ,
-          "$push":
+          "$addToSet":
             activeSessions: session._id
         ,
           safe: true


Improve weapon position player clone

Improve weapon position player clone

--- app/scripts/components/entities/PlayerClone.coffee
+++ app/scripts/components/entities/PlayerClone.coffee
@@ -3,7 +3,12 @@
     @requires 'Enemy, playerShip'

   playerClone: (attr = {}) ->
-    @attr _.defaults(attr, h: 45, w: 71, health: 900)
+    @attr _.defaults(attr,
+      h: 45,
+      w: 71,
+      health: 900,
+      weaponOrigin: [5, 30]
+    )
     @origin 'center'
     @flipX()
     @colorOverride '#808080', 'partial'


Disable failing snippets spec for now

Disable failing snippets spec for now

--- spec/extensions/snippets-spec.coffee
+++ spec/extensions/snippets-spec.coffee
@@ -150,7 +150,7 @@
         expect(editor.getCursorScreenPosition()).toEqual [0, 5]

     describe "when a previous snippet expansion has just been undone", ->
-      fit "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", ->
+      xit "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", ->
         editor.insertText 't5\n'
         editor.setCursorBufferPosition [0, 2]
         editor.trigger keydownEvent('tab', target: editor[0])


Drop context menu items on StagingView headers

--- menus/git.cson
+++ menus/git.cson
@@ -77,25 +77,6 @@
       'command': 'github:open-link-in-browser'
     }
   ]
-  '.github-UnstagedChanges .github-StagingView-header': [
-    {
-      'label': 'Stage All Changes'
-      'command': 'github:stage-all-changes'
-    }
-    {
-      'type': 'separator'
-    }
-    {
-      'label': 'Discard All Changes'
-      'command': 'github:discard-all-changes'
-    }
-  ]
-  '.github-StagedChanges .github-StagingView-header': [
-    {
-      'label': 'Unstage All Changes'
-      'command': 'github:unstage-all-changes'
-    }
-  ]
   '.github-CommitView': [
     {
       'type': 'separator'


--- spec/integration/changes-spec.coffee
+++ spec/integration/changes-spec.coffee
@@ -59,7 +59,7 @@
       runs ->
         expect(workspaceElement.querySelector('.unstaged.files').innerHTML).toContain('foo.md')

-  describe 'keyboard navigation', ->
+  xdescribe 'keyboard navigation', ->
     it 'focuses a hunk when the status list is focused and the right arrow key is pressed', ->
       runs ->
         makeChange()


rename the filtered variable to filterCache to be more descriptive and add a comment to describe the ‘term’ argument

--- modules/data-table/main/index.coffee
+++ modules/data-table/main/index.coffee
@@ -1078,13 +1078,14 @@
     hx.defined validFilters

-getFiltered = (rows, term, filtered, filterCacheTerm, fn) ->
-  if (term?.length and (filtered is undefined or filterCacheTerm isnt term))
+getFiltered = (rows, term, filterCache, filterCacheTerm, fn) ->
+  # term can be a string (regular filter) or an array (advanced search)
+  if (term?.length and (filterCache is undefined or filterCacheTerm isnt term))
     rows.filter fn
-  else if filtered is undefined or not term?.length
+  else if filterCache is undefined or not term?.length
     rows.slice()
   else
-    filtered
+    filterCache

 objectFeed = (data, options) ->
   options = hx.merge({
@@ -1099,7 +1100,7 @@
   options.advancedSearch ?= getAdvancedSearchFilter(options.cellValueLookup, options.termLookup)

   # cached values
-  filtered = undefined
+  filterCache = undefined
   filterCacheTerm = undefined
   sorted = undefined
   sortCacheTerm = {}
@@ -1111,16 +1112,16 @@
     totalCount: (cb) -> cb(data.rows.length)
     rows: (range, cb) ->
       if range.sort?.column isnt sortCacheTerm.column
-        filtered = undefined
+        filterCache = undefined

       if range.useAdvancedSearch
         advancedSearchFilterFn = (row) -> options.advancedSearch(range.advancedSearch, row)
-        filtered = getFiltered(data.rows, range.advancedSearch, filtered, filterCacheTerm, advancedSearchFilterFn)
+        filterCache = getFiltered(data.rows, range.advancedSearch, filterCache, filterCacheTerm, advancedSearchFilterFn)
         filterCacheTerm = range.advancedSearch
         sorted = undefined
       else
         filterFn = (row) -> options.filter(range.filter, row)
-        filtered = getFiltered(data.rows, range.filter, filtered, filterCacheTerm, filterFn)
+        filterCache = getFiltered(data.rows, range.filter, filterCache, filterCacheTerm, filterFn)
         filterCacheTerm = range.filter
         sorted = undefined

@@ -1128,9 +1129,9 @@
         sorted = if range.sort and range.sort.column
           direction = if range.sort.direction is 'asc' then 1 else -1
           column = range.sort.column
-          filtered.sort (r1, r2) -> direction * options.compare(r1.cells[column], r2.cells[column])
-          filtered
-        else filtered
+          filterCache.sort (r1, r2) -> direction * options.compare(r1.cells[column], r2.cells[column])
+          filterCache
+        else filterCache
         sortCacheTerm.column = range.sort?.column
         sortCacheTerm.direction = range.sort?.direction


Sort tables by alias after schema load

Sort tables by alias after schema load

--- src/db/database.coffee
+++ src/db/database.coffee
@@ -64,6 +64,7 @@

     loadSchema: (schema) ->
         @addSchemaItems(Table, schema.tables, @)
+        @tables = _.sortBy(@tables, (t) -> t.alias)
         @addSchemaItems(Column, schema.columns)
         @addSchemaItems(Key, schema.keys)
         @addSchemaItems(ForeignKey, schema.foreignKeys)


another native

another native

--- src/natives.coffee
+++ src/natives.coffee
@@ -49,6 +49,7 @@
               stack.push {'op':sf.pc, 'line':ln, 'file':source_file, 'method':sf.method.name, 'cls':cls}
             _this.ref
         o 'printStackTrace(L!/io/PrintWriter;)V', (rs) -> # NOP, since we didn't fill in anything
+        o 'getStackTraceDepth()I', (rs, _this) -> _this.fields.$stack.length
       ]
       StringCoding: [
         o 'deref(L!/!/ThreadLocal;)L!/!/Object;', (rs) -> null


disconnect on mongoose connection error

disconnect on mongoose connection error

--- lib/mongodb.coffee
+++ lib/mongodb.coffee
@@ -19,6 +19,7 @@
 # connection error
 mongoose.connection.on 'error', (err) ->
   log.error "MongoDB connection error: #{err}"
+  mongoose.disconnect()

 # disconnected connection
 mongoose.connection.on 'disconnected', ->


actually test the pivot table functionality...

--- modules/pivot-table/test/spec.coffee
+++ modules/pivot-table/test/spec.coffee
@@ -70,7 +70,9 @@
       leftHead: leftHead
       topHead: topHead

-    pt = new hx.PivotTable(hx.detached('div').node(), {
+    sel = hx.detached('div')
+
+    pt = new hx.PivotTable(sel.node(), {
       data: data
       fullWidth: true
       stickyHeaders: false
@@ -78,3 +80,4 @@

     pt.options.fullWidth.should.equal(true)
     should.not.exist(pt.stickyTableHeaders)
+    sel.selectAll('.hx-table-full').size().should.equal(1)


cleaned comment

cleaned comment


fall back if the alert has no subject

fall back if the alert has no subject

--- lib/pagerv2.coffee
+++ lib/pagerv2.coffee
@@ -520,7 +520,10 @@
           "[#{message.data.incident.service.name}]",
           colors[level]
         )
-        description = message.data.incident.trigger_summary_data.subject.replace(' (CRITICAL)', '')
+        if message.data.incident.trigger_summary_data.subject?
+            description = message.data.incident.trigger_summary_data.subject.replace(' (CRITICAL)', '')
+        else
+            description = "(no subject)"
         who = if message.type is 'incident.resolve' and message.data.incident.resolved_by_user?
                 message.data.incident.resolved_by_user.name
               else if message.data.incident.assigned_to_user?


Correctly finished removing eyes.

Correctly finished removing eyes.

--- lib/testers.coffee
+++ lib/testers.coffee
@@ -2,7 +2,6 @@
 path = require('path')
 _ = underscore = require('underscore')
 balUtil = require('bal-util')
-eyes = require('eyes')
 chai = require('chai')
 expect = chai.expect
 assert = chai.assert
@@ -16,7 +15,6 @@
 # Tester
 class Tester
 	# Requires
-	eyes: eyes
 	chai: chai
 	expect: expect
 	assert: assert
@@ -241,7 +239,6 @@
 	ServerTester,
 	underscore,
 	balUtil,
-	eyes,
 	chai,
 	expect,
 	assert,


Added color select dropdown.

Added color select dropdown.

--- src/MultiColorPicker.coffee
+++ src/MultiColorPicker.coffee
@@ -14,6 +14,7 @@

 DOM = require("./DOM.coffee")
 Templates = require("./Templates.coffee")
+colors = require("./lib/colors.coffee")

 class MultiColorPicker

@@ -27,61 +28,52 @@
     # Build the color picker boxes from the template
     #
     _setColorPickersHex:() ->
-        DOM.elemById('GENERATOR', 'COLORPICKER_ACTIVE_HEX').value = @BUS.get('board.cell.style.activeBackgroundColor')
-        DOM.elemById('GENERATOR', 'COLORPICKER_BORDER_HEX').value = @BUS.get('board.cell.style.borderColor')
-        DOM.elemById('GENERATOR', 'COLORPICKER_INACTIVE_HEX').value = @BUS.get('board.cell.style.inactiveBackgroundColor')
+        @elCPActive.value = @BUS.get('board.cell.style.activeBackgroundColor')
+        @elCPBorder.value = @BUS.get('board.cell.style.borderColor')
+        @elCPInactive.value = @BUS.get('board.cell.style.inactiveBackgroundColor')
+
+    _buildColorSelectOptions:() ->
+        options = ""
+        for color in colors
+            options += Templates.colorPickerOption(color)
+        return options

     #
     # Enable the color picker
     #
     enableColorPicker:() ->
-        colorPickerElem = DOM.elemById('GENERATOR', 'COLORPICKER_CONTAINER')
-        colorPickerElem.innerHTML = Templates.generatorColorpicker
-        colorPickerElem.style.display = "block"
+        @elContainer = DOM.elemById('COLORPICKER', 'CONTAINER')
+        @elContainer.innerHTML = Templates.colorPickers
+        @elContainer.style.display = "block"
+
+        @elCPActive = DOM.elemById('COLORPICKER', 'ACTIVE_HEX')
+        @elCPBorder = DOM.elemById('COLORPICKER', 'BORDER_HEX')
+        @elCPInactive = DOM.elemById('COLORPICKER', 'INACTIVE_HEX')
+
+        @elCPActive.innerHTML = @_buildColorSelectOptions()
+        @elCPBorder.innerHTML = @_buildColorSelectOptions()
+        @elCPInactive.innerHTML = @_buildColorSelectOptions()

         @_setColorPickersHex()

-        cpActive = ColorPicker(DOM.elemById('GENERATOR','COLORPICKER_ACTIVE'),
-            (hex)=>
-                @BUS.broadcast('change.cell.style.activebackground', hex)
-                @_setColorPickersHex()
+        @elCPActive.addEventListener('change', (e)=>
+            @BUS.broadcast('change.cell.style.activebackground', e.target.value)
+            @_setColorPickersHex()
         )
-        cpActive.setHex(@BUS.get('board.cell.style.activeBackgroundColor'))
-
-        cpBorder = ColorPicker(DOM.elemById('GENERATOR','COLORPICKER_BORDER'),
-            (hex)=>
-                @BUS.broadcast('change.cell.style.bordercolor', hex)
-                @_setColorPickersHex()
+        @elCPBorder.addEventListener('change', (e)=>
+            @BUS.broadcast('change.cell.style.bordercolor', e.target.value)
+            @_setColorPickersHex()
         )
-        cpBorder.setHex(@BUS.get('board.cell.style.borderColor'))
-
-        cpInActive = ColorPicker(DOM.elemById('GENERATOR','COLORPICKER_INACTIVE'),
-            (hex)=>
-                @BUS.broadcast('change.cell.style.inactivebackground', hex)
-                @_setColorPickersHex()
-        )
-        cpInActive.setHex(@BUS.get('board.cell.style.inactiveBackgroundColor'))
-
-
-        DOM.elemById('GENERATOR', 'COLORPICKER_ACTIVE_HEX').addEventListener('input', (e)=>
-            @BUS.broadcast('change.cell.style.activebackground', e.target.value)
-            cpActive.setHex(e.target.value)
-        )
-        DOM.elemById('GENERATOR', 'COLORPICKER_BORDER_HEX').addEventListener('input', (e)=>
-            @BUS.broadcast('change.cell.style.bordercolor', e.target.value)
-            cpBorder.setHex(e.target.value)
-        )
-        DOM.elemById('GENERATOR', 'COLORPICKER_INACTIVE_HEX').addEventListener('input', (e)=>
+        @elCPInactive.addEventListener('change', (e)=>
             @BUS.broadcast('change.cell.style.inactivebackground', e.target.value)
-            cpInActive.setHex(e.target.value)
+            @_setColorPickersHex()
         )

     #
     # Disable the color picker
     #
     disableColorPicker:() ->
-        containerElem = DOM.elemById('GENERATOR','COLORPICKER_CONTAINER')
-        containerElem.innerHTML = ""
-        containerElem.style.display = "none"
+        @elContainer.innerhtml = ""
+        @elContainer.style.display = "none"

 module.exports = MultiColorPicker


use getPaneView rather than getPane

use getPaneView rather than getPane

--- src/editor-view.coffee
+++ src/editor-view.coffee
@@ -135,7 +135,7 @@
   Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth()
   Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0]
   Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1]
-  Object.defineProperty @::, 'active', get: -> @is(@getPane()?.activeView)
+  Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView)
   Object.defineProperty @::, 'isFocused', get: -> @component?.state.focused
   Object.defineProperty @::, 'mini', get: -> @component?.props.mini

@@ -218,7 +218,7 @@
       To duplicate this editor into the split use:
       editorView.getPaneView().getModel().splitLeft(copyActiveItem: true)
     """
-    pane = @getPane()
+    pane = @getPaneView()
     pane?.splitLeft(pane?.copyActiveItem()).activeView

   splitRight: ->
@@ -227,7 +227,7 @@
       To duplicate this editor into the split use:
       editorView.getPaneView().getModel().splitRight(copyActiveItem: true)
     """
-    pane = @getPane()
+    pane = @getPaneView()
     pane?.splitRight(pane?.copyActiveItem()).activeView

   splitUp: ->
@@ -236,7 +236,7 @@
       To duplicate this editor into the split use:
       editorView.getPaneView().getModel().splitUp(copyActiveItem: true)
     """
-    pane = @getPane()
+    pane = @getPaneView()
     pane?.splitUp(pane?.copyActiveItem()).activeView

   splitDown: ->
@@ -245,7 +245,7 @@
       To duplicate this editor into the split use:
       editorView.getPaneView().getModel().splitDown(copyActiveItem: true)
     """
-    pane = @getPane()
+    pane = @getPaneView()
     pane?.splitDown(pane?.copyActiveItem()).activeView

   # Public: Get this {EditorView}'s {PaneView}.


Rename EditorView::getPane -> ::getPaneView()

--- src/editor-view.coffee
+++ src/editor-view.coffee
@@ -233,11 +233,14 @@
     pane = @getPane()
     pane?.splitDown(pane?.copyActiveItem()).activeView

-  # Public: Get this view's pane.
-  #
-  # Returns a {Pane}.
+  # Public: Get this {EditorView}'s {PaneView}.
+  #
+  # Returns a {PaneView}
+  getPaneView: ->
+    @parent('.item-views').parents('.pane').view()
   getPane: ->
-    @parent('.item-views').parents('.pane').view()
+    deprecate 'Use EditorView::getPaneView() instead'
+    @getPaneView()

   show: ->
     super


Hotfix: remove extra loading indicator.

Hotfix: remove extra loading indicator.

Loading indicator was activated also when
returning back from unit details to search,
with the same query.

--- src/control.coffee
+++ src/control.coffee
@@ -281,18 +281,19 @@
             @addService service, null, cancelToken

         _search: (query, filters, cancelToken) ->
-            cancelToken.activate()
-            @_clearRadius()
-            @selectedPosition.clear()
-            @clearUnits all: true
-            canceled = false
-            @listenToOnce cancelToken, 'canceled', -> canceled = true
-
             sm.withDeferred (deferred) =>
                 if @searchResults.query == query
                     @searchResults.trigger 'ready'
                     deferred.resolve()
                     return
+
+                cancelToken.activate()
+                @_clearRadius()
+                @selectedPosition.clear()
+                @clearUnits all: true
+                canceled = false
+                @listenToOnce cancelToken, 'canceled', -> canceled = true
+
                 if 'search' in _(@units.filters).keys()
                     @units.reset []
                 unless @searchResults.isEmpty()


added array index to Barrier.forArray

added array index to Barrier.forArray

--- async/Barrier.coffee
+++ async/Barrier.coffee
@@ -43,11 +43,11 @@
     # callback is called on every element of the given array. its result is returned (and therefore accessible in Barrier::funcResults when done)
     @forArray: (array = [], callback, start = true) ->
         data = []
-        for elem in array
+        for elem, index in array
             data.push {
-                func: do (elem) ->
+                func: do (elem, index) ->
                     return () ->
-                        return callback(elem)
+                        return callback(elem, index)
             }
         return new JSUtils.Barrier(data, start)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/flickerable.coffee
+++ src/flickerable.coffee
@@ -56,7 +56,7 @@
         alpha: 0.3

       # => This causes the sprite to flicker between full opacity
-      # => and 30% opacity every 5 frames for 90 frames
+      # => and 30% opacity every 0.2 seconds for 5 seconds

   @name flicker
   @methodOf Flickerable#


Revert "runtime: Fix network being stopped when going into edit mode"

This reverts commit 317311d0c956a5b05511d46eae6b21037260cc3a.

--- lib/runtime.coffee
+++ lib/runtime.coffee
@@ -214,8 +214,6 @@
         runtime.edgesForInspection = []

         sendMessage runtime, { protocol: 'graph', command: command, payload: payload }
-        # make sure. XXX: should maybe be done by splitting out ClearGraph from Reset
-        sendMessage runtime, { protocol: 'network', command: 'start', payload: {} }
     else if command is "addnode"
         graph.processes[payload.id] = payload
         graph.nodeMap[payload.id] = { id: graph.currentNodeId++ }


OHM-51: converted transactions to coffee

--- src/api/transactions.coffee
+++ src/api/transactions.coffee
@@ -3,6 +3,7 @@
 Q = require 'q'
 logger = require 'winston'
 authorisation = require './authorisation'
+utils = require "../utils"

 getChannelIDsArray = (channels) ->
   channelIDs = []
@@ -31,259 +32,221 @@
 ###
 # Retrieves the list of transactions
 ###
-exports.getTransactions = `function *getTransactions() {
-
-  try {
-
-    var filtersObject = this.request.query;
-
-    //construct date range filter option
-    if( filtersObject.startDate && filtersObject.endDate ){
-      filtersObject['request.timestamp'] = { $gte: filtersObject.startDate, $lt: filtersObject.endDate }
-
-      //remove startDate/endDate from objects filter (Not part of filtering and will break filter)
-      delete filtersObject.startDate;
-      delete filtersObject.endDate;
-    }
-
-    //get limit and page values
-    var filterLimit = filtersObject.filterLimit;
-    var filterPage = filtersObject.filterPage;
-    var filterRepresentation = filtersObject.filterRepresentation;
-
-    //remove limit/page/filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
-    delete filtersObject.filterLimit;
-    delete filtersObject.filterPage;
-    delete filtersObject.filterRepresentation;
-
-    //determine skip amount
-    var filterSkip = filterPage*filterLimit;
-
-    // Test if the user is authorised
-    if (authorisation.inGroup('admin', this.authenticated) === false) {
-      // if not an admin, restrict by transactions that this user can view
-      var channels = yield authorisation.getUserViewableChannels(this.authenticated);
-
-      if (!filtersObject.channelID) {
-        filtersObject.channelID = { $in: getChannelIDsArray(channels) };
-      }
-
-      // set 'filterRepresentation' to default if user isnt admin
-      filterRepresentation = '';
-    }
-
-    // get projection object
-    projectionFiltersObject = getProjectionObject( filterRepresentation );
-
-    // execute the query
-    this.body = yield transactions.Transaction.find(filtersObject, projectionFiltersObject).skip(filterSkip).limit(filterLimit).sort({ 'request.timestamp': -1 }).exec();
-
-  }catch (e){
-    this.message = e.message;
-    this.status = 'internal server error';
-  }
-}`
+exports.getTransactions = ->
+  try
+
+    filtersObject = this.request.query
+
+    #construct date range filter option
+    if filtersObject.startDate and filtersObject.endDate
+      filtersObject['request.timestamp'] = $gte: filtersObject.startDate, $lt: filtersObject.endDate
+
+      #remove startDate/endDate from objects filter (Not part of filtering and will break filter)
+      delete filtersObject.startDate
+      delete filtersObject.endDate
+
+    #get limit and page values
+    filterLimit = filtersObject.filterLimit
+    filterPage = filtersObject.filterPage
+    filterRepresentation = filtersObject.filterRepresentation
+
+    #remove limit/page/filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
+    delete filtersObject.filterLimit
+    delete filtersObject.filterPage
+    delete filtersObject.filterRepresentation
+
+    #determine skip amount
+    filterSkip = filterPage*filterLimit
+
+    # Test if the user is authorised
+    if not authorisation.inGroup 'admin', this.authenticated
+      # if not an admin, restrict by transactions that this user can view
+      channels = yield authorisation.getUserViewableChannels this.authenticated
+
+      if not filtersObject.channelID
+        filtersObject.channelID = $in: getChannelIDsArray channels
+
+      # set 'filterRepresentation' to default if user isnt admin
+      filterRepresentation = ''
+
+    # get projection object
+    projectionFiltersObject = getProjectionObject filterRepresentation
+
+    # execute the query
+    this.body = yield transactions.Transaction
+      .find filtersObject, projectionFiltersObject
+      .skip filterSkip
+      .limit filterLimit
+      .sort 'request.timestamp': -1
+      .exec()
+
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Could not retrieve transactions via the API: #{e}", 'error'

 ###
 # Adds an transaction
 ###
-exports.addTransaction = `function *addTransaction() {
-
-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to addTransaction denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to addTransaction denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  // Get the values to use
-  var transactionData = this.request.body;
-  var tx = new transactions.Transaction(transactionData);
-
-  try {
-    // Try to add the new transaction (Call the function that emits a promise and Koa will wait for the function to complete)
-    yield Q.ninvoke(tx, "save");
-    this.status = 'created';
-    logger.info('User %s created transaction with id %s', this.authenticated.email, tx.id);
-  } catch (e) {
-    logger.error('Could not add a transaction via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-
-}`
+exports.addTransaction = ->
+
+  # Test if the user is authorised
+  if not authorisation.inGroup 'admin', this.authenticated
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to addTransaction denied.", 'info'
+    return
+
+  # Get the values to use
+  transactionData = this.request.body
+  tx = new transactions.Transaction transactionData
+
+  try
+    # Try to add the new transaction (Call the function that emits a promise and Koa will wait for the function to complete)
+    yield Q.ninvoke tx, "save"
+    this.status = 'created'
+    logger.info 'User %s created transaction with id %s', this.authenticated.email, tx.id
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Could not add a transaction via the API: #{e}", 'error'

 ###
 # Retrieves the details for a specific transaction
 ###
-exports.getTransactionById = `function *getTransactionById(transactionId) {
-  // Get the values to use
-  var transactionId = unescape(transactionId);
-
-  try {
-
-    var filtersObject = this.request.query;
-    var filterRepresentation = filtersObject.filterRepresentation;
-
-    //remove filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
-    delete filtersObject.filterRepresentation;
-
-    // set filterRepresentation to 'full' if not supplied
-    if ( !filterRepresentation ){
-      filterRepresentation = 'full';
-    }
-
-    /* --------------Check if user has permission to view full content----------------- */
-    // if user NOT admin, determine their representation privileges.
-    if (authorisation.inGroup('admin', this.authenticated) === false) {
-      // retrieve transaction channelID
-      var txChannelID = yield transactions.Transaction.findById(transactionId, { 'channelID':1, _id:0 }).exec();
-      if (txChannelID === null || txChannelID.length === 0) {
-        this.body = "We could not find transaction with ID:'" + transactionId + "'.";
-        this.status = 'not found';
-      } else {
-        // assume user is not allowed to view all content - show only 'simpledetails'
-        filterRepresentation = 'simpledetails';
-
-        // get channel.txViewFullAcl information by channelID
-        var channel = yield Channel.findById(txChannelID.channelID, { 'txViewFullAcl':1, _id:0 }).exec();
-
-        // loop through user groups
-        for ( i=0; i<this.authenticated.groups.length; i++ ){
-          // if user role found in channel txViewFullAcl - user has access to view all content
-          if (channel.txViewFullAcl.indexOf(this.authenticated.groups[i]) >= 0) {
-            // update filterRepresentation object to be 'full' and allow all content
-            filterRepresentation = 'full';
-            break;
-          }
-        }
-      }
-    }
-    /* --------------Check if user has permission to view full content----------------- */
-
-    // get projection object
-    projectionFiltersObject = getProjectionObject( filterRepresentation );
-
-    var result = yield transactions.Transaction.findById(transactionId, projectionFiltersObject).exec();
-
-    // Test if the result if valid
-    if (result === null || result.length === 0) {
-      this.body = "We could not find transaction with ID:'" + transactionId + "'.";
-      this.status = 'not found';
-    // Test if the user is authorised
-    } else if (authorisation.inGroup('admin', this.authenticated) === false) {
-      var channels = yield authorisation.getUserViewableChannels(this.authenticated);
-      if (getChannelIDsArray(channels).indexOf(result.channelID.toString()) >= 0) {
+exports.getTransactionById = (transactionId) ->
+  # Get the values to use
+  transactionId = unescape transactionId
+
+  try
+    filtersObject = this.request.query
+    filterRepresentation = filtersObject.filterRepresentation
+
+    #remove filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
+    delete filtersObject.filterRepresentation
+
+    # set filterRepresentation to 'full' if not supplied
+    if not filterRepresentation then filterRepresentation = 'full'
+
+    # --------------Check if user has permission to view full content----------------- #
+    # if user NOT admin, determine their representation privileges.
+    if not authorisation.inGroup 'admin', this.authenticated
+      # retrieve transaction channelID
+      txChannelID = yield transactions.Transaction.findById(transactionId, channelID: 1, _id: 0).exec()
+      if txChannelID?.length is 0
+        this.body = "Could not find transaction with ID: #{transactionId}"
+        this.status = 'not found'
+        return
+      else
+        # assume user is not allowed to view all content - show only 'simpledetails'
+        filterRepresentation = 'simpledetails'
+
+        # get channel.txViewFullAcl information by channelID
+        channel = yield Channel.findById(txChannelID.channelID, txViewFullAcl: 1, _id: 0).exec()
+
+        # loop through user groups
+        for group in this.authenticated.groups
+          # if user role found in channel txViewFullAcl - user has access to view all content
+          if channel.txViewFullAcl.indexOf(group) >= 0
+            # update filterRepresentation object to be 'full' and allow all content
+            filterRepresentation = 'full'
+            break
+
+    # --------------Check if user has permission to view full content----------------- #
+    # get projection object
+    projectionFiltersObject = getProjectionObject filterRepresentation
+
+    result = yield transactions.Transaction.findById(transactionId, projectionFiltersObject).exec()
+
+    # Test if the result if valid
+    if result is null or result.length is 0
+      this.body = "Could not find transaction with ID: #{transactionId}"
+      this.status = 'not found'
+    # Test if the user is authorised
+    else if not authorisation.inGroup 'admin', this.authenticated
+      channels = yield authorisation.getUserViewableChannels(this.authenticated)
+      if getChannelIDsArray(channels).indexOf(result.channelID.toString()) >= 0
         this.body = result
-      } else {
-        this.body = "The user " + this.authenticated.email + " is not authorised to access this transaction.";
-        this.status = 'forbidden';
-      }
-    } else {
-      this.body = result;
-    }
-  } catch (e) {
-    // Error! So inform the user
-    logger.error('Could not get transaction by ID via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+      else
+        utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not authenticated to retrieve transaction #{transactionId}", 'info'
+    else
+      this.body = result
+
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Could not add a transaction via the API: #{e}", 'error'
+

 ###
 # Retrieves all transactions specified by clientId
 ###
-exports.findTransactionByClientId = `function *findTransactionByClientId(clientId){
-  var clientId = unescape(clientId)
-
-  try {
-
-    var filtersObject = this.request.query;
-    var filterRepresentation = filtersObject.filterRepresentation;
-
-    // get projection object
-    projectionFiltersObject = getProjectionObject( filterRepresentation );
-
-    filtersObject = {};
-    filtersObject.clientID = clientId;
-
-    // Test if the user is authorised
-    if (authorisation.inGroup('admin', this.authenticated) === false) {
-      // if not an admin, restrict by transactions that this user can view
-      var channels = yield authorisation.getUserViewableChannels(this.authenticated);
-
-      filtersObject.channelID = { $in: getChannelIDsArray(channels) };
-
-      // set 'filterRepresentation' to default if user isnt admin
-      filterRepresentation = '';
-    }
-
-    // execute the query
-    this.body = yield transactions.Transaction.find(filtersObject, projectionFiltersObject).sort({ 'request.timestamp': -1 }).exec();
+exports.findTransactionByClientId = (clientId) ->
+  clientId = unescape clientId
+
+  try
+
+    filtersObject = this.request.query
+    filterRepresentation = filtersObject.filterRepresentation
+
+    # get projection object
+    projectionFiltersObject = getProjectionObject filterRepresentation
+
+    filtersObject = {}
+    filtersObject.clientID = clientId
+
+    # Test if the user is authorised
+    if not authorisation.inGroup 'admin', this.authenticated
+      # if not an admin, restrict by transactions that this user can view
+      channels = yield authorisation.getUserViewableChannels this.authenticated
+
+      filtersObject.channelID = $in: getChannelIDsArray channels
+
+      # set 'filterRepresentation' to default if user isnt admin
+      filterRepresentation = ''
+
+    # execute the query
+    this.body = yield transactions.Transaction
+      .find filtersObject, projectionFiltersObject
+      .sort 'request.timestamp': -1
+      .exec()

-  } catch(e) {
-    logger.error('Could not find a transaction by client by via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Could not add a transaction via the API: #{e}", 'error'
+

 ###
 # Updates a transaction record specified by transactionId
 ###
-exports.updateTransaction = `function *updateTransaction(transactionId) {
-
-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to updateTransaction denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to updateTransaction denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  var transactionId = unescape(transactionId);
-  var updates = this.request.body;
-
-  try {
-    yield transactions.Transaction.findByIdAndUpdate(transactionId, updates).exec();
-    this.body = "Transaction with ID:"+transactionId+" successfully updated.";
-    this.status = 'ok';
-    logger.info('User %s updated transaction with id %s', this.authenticated.email, transactionId);
-  } catch(e) {
-    logger.error('Could not update a transaction via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+exports.updateTransaction = (transactionId) ->
+
+  # Test if the user is authorised
+  if not authorisation.inGroup 'admin', this.authenticated
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to updateTransaction denied.", 'info'
+    return
+
+  transactionId = unescape transactionId
+  updates = this.request.body
+
+  try
+    yield transactions.Transaction.findByIdAndUpdate(transactionId, updates).exec()
+    this.body = "Transaction with ID: #{transactionId} successfully updated"
+    this.status = 'ok'
+    logger.info 'User %s updated transaction with id %s', this.authenticated.email, transactionId
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Could not update transaction via the API: #{e}", 'error'

 ###
 #Removes a transaction
 ###
-exports.removeTransaction = `function *removeTransaction(transactionId) {
-
-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to removeTransaction denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to removeTransaction denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  // Get the values to use
-  var transactionId = unescape(transactionId);
-
-  try {
-    yield transactions.Transaction.findByIdAndRemove(transactionId).exec();
-    this.body = 'Transaction successfully deleted';
-    this.status = 'ok';
-    logger.info('User %s removed transaction with id %s', this.authenticated.email, transactionId);
-  }
-  catch (e) {
-    logger.error('Could not remove a transaction via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+exports.removeTransaction = (transactionId) ->
+
+  # Test if the user is authorised
+  if not authorisation.inGroup 'admin', this.authenticated
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to removeTransaction denied.", 'info'
+    return
+
+  # Get the values to use
+  transactionId = unescape transactionId
+
+  try
+    yield transactions.Transaction.findByIdAndRemove(transactionId).exec()
+    this.body = 'Transaction successfully deleted'
+    this.status = 'ok'
+    logger.info 'User %s removed transaction with id %s', this.authenticated.email, transactionId
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Could not update transaction via the API: #{e}", 'error'


[master] truncate strings too long

[master] truncate strings too long

--- translator/run.coffee
+++ translator/run.coffee
@@ -55,6 +55,7 @@
               src = (entry.title + '\n' + entry.content)
               .replace /<br>/g, ' '
               .replace /[^0-9a-zA-Z\u4e00-\u9fa5]/g, ' '
+              .substring 0, 2000
               .trim()
               currentTranslator = round.get()
               process.stdout.write '\r'


Support SEC_EDGAR_URL env var in getAddress()

--- src/address.coffee
+++ src/address.coffee
@@ -4,7 +4,8 @@

 module.exports =
   getAddress: (company, callback) ->
-    url = "http://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&output=atom&start=0&count=1&CIK=#{company}"
+    baseUrl = process.env.SEC_EDGAR_URL ? 'http://www.sec.gov/cgi-bin/browse-edgar'
+    url = "#{baseUrl}?action=getcompany&output=atom&start=0&count=1&CIK=#{company}"

     request url, (error, response, body) ->
       if error?


modify GA params

modify GA params

--- lib/ga.coffee
+++ lib/ga.coffee
@@ -51,5 +51,8 @@
     # https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
     {
       v: 1
+      aip: true
+      ds: "app"
       tid: "UA-53539506-12"
+      ua: "Atom v#{atom.appVersion} #{atom.getReleaseChannel()}"
     }


Fix query splitting expression

Fix query splitting expression

--- app/assets/javascripts/rglossa/react/components/centre/search_inputs/cwb_multiword_input.jsx.coffee
+++ app/assets/javascripts/rglossa/react/components/centre/search_inputs/cwb_multiword_input.jsx.coffee
@@ -34,9 +34,9 @@
     max = null

     queryParts.forEach (item) =>
-      if m = item.match(/\[\]\{(.+)\}/)
+      if m = item.match(/\[\]\{(.+?)\}/)
         [min, max] = @handleIntervalSpecification(m)
-      else if m = item.match(/\[(.+)\]/)
+      else if m = item.match(/\[(.+?)\]/)
         dq.push @handleAttributes(m, min, max)
         min = null
         max = null
@@ -61,7 +61,7 @@

   splitQuery: (query) ->
-    query.match(/\[\]\{(.+)\}|".*?"|\[[^\]]+\]/g) or ['']
+    query.match(/\[\]\{(.+?)\}|".*?"|\[[^\]]+?\]/g) or ['']

   handleIntervalSpecification: (m) ->


Log when attempting to open non-session URL

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -255,6 +255,8 @@
       if sessionId
         bootstrapScript = 'collaboration/lib/bootstrap'
         new AtomWindow({bootstrapScript, @resourcePath, sessionId})
+    else
+      console.log "Opening unknown url #{urlToOpen}"

   openConfig: ->
     if @configWindow


Add toggle full screen to View menu

Add toggle full screen to View menu

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -150,6 +150,7 @@
       label: 'View'
       submenu:[
         { label: 'Reload', accelerator: 'Command+R', click: => BrowserWindow.getFocusedWindow()?.restart() }
+        { label: 'Toggle Full Screen', accelerator: 'Command+MacCtrl+F', click: => BrowserWindow.getFocusedWindow()?.setFullscreen(!BrowserWindow.getFocusedWindow().isFullscreen()) }
         { label: 'Toggle DevTools', accelerator: 'Alt+Command+I', click: => BrowserWindow.getFocusedWindow()?.toggleDevTools() }
       ]


form workflow: code simplification

form workflow: code simplification

--- client/landing/app/CommonViews/formworkflow.coffee
+++ client/landing/app/CommonViews/formworkflow.coffee
@@ -49,20 +49,11 @@

     return this

-  nextForm: ->
-    requirement = @nextRequirement()
-    return  unless requirement?
-
-    provider = @nextProvider requirement
-    return  unless provider?
-
-    @showForm provider
-
-    return provider
+  nextForm: -> try @showForm @nextProvider()

   nextRequirement: -> @collector.nextRequirement()

-  nextProvider: (key, from) ->
+  nextProvider: (key = @nextRequirement(), from) ->
     providers = @providers[key]
     providers.i = from ? providers.i ? 0
     provider = providers[providers.i++]
@@ -93,6 +84,7 @@

   hideForms: (forms = @getFormNames()) ->
     @forms[form]?.hide() for form in forms
+
     return this

   showForm: (form) ->
@@ -100,6 +92,7 @@
     form = @getForm form
     form.activate? this
     form.show()
+
     return this

   all: (fields...) -> new All fields
@@ -112,8 +105,9 @@
   @Collector = class Collector extends KDEventEmitter

     constructor: (@gate = new Gate) ->
-      super
-      @data = {}
+      super()
+
+      @data = @utils.dict()

       @gate.on 'status', (isSatisfied) =>
         if isSatisfied
@@ -171,7 +165,7 @@

       @addField field  for field in fields

-    createId: do (i = 0) -> -> i++
+    createId: KD.utils.createCounter()

     isGate: yes

@@ -180,8 +174,8 @@
         fields = @getFields()
         fields.push (child.getFields yes)... for own _, child of @children
         return fields
-      else
-        (key for own key of @fields when not (key of @children))
+
+      (key for own key of @fields when not (key of @children))

     nextNode: ->
       node = @ordered[@index]


rework styles embedding

rework styles embedding

--- src/client/style/style-scoper.coffee
+++ src/client/style/style-scoper.coffee
@@ -4,13 +4,8 @@
 ScopedCssRule  = require 'scopedcss/lib/cssRule';
 superagent          = require 'superagent'

-Styles     = {}
-MergedStyles  = null;
+Styles     = []

-# replaceRule = (sheet, selector, cssText, index)->
-#     if sheet.removeRule then sheet.removeRule(index) else sheet.deleteRule(index);
-#     rule = if selector then selector + '{' + cssText + '}' else cssText
-#     if sheet.addRule then sheet.addRule(selector, cssText, index) else sheet.insertRule(rule, index);

 createStyleSheet = (content="", disabled=false)->
   style = document.createElement('style')
@@ -22,46 +17,31 @@

 removeStyleSheet = (style)-> style.parentNode?.removeChild(style)

+# Create and initialize a new global stylesheet that will receive merged styles in the right order
+MergedStyles = createStyleSheet("") unless MergedStyles
+
+# A single promise will guarantee execution order;
+promise = new Promise (resolve, reject)-> resolve()
+
 getRules = (sheet)->
   return unless sheet?
   sheet.cssRules || sheet.rules
-
+appendRules = (entry, rules)->
+  r = _.map rules, (rule)->
+    prefixRule(entry.prefix, rule);
+  entry.style = r.join(' ');
+  true
 empty = (sheet)->
   if sheet.deleteRule?
     sheet.deleteRule(0) while getRules(sheet).length > 0
   else if sheet.removeRule?
     sheet.removeRule(0) while getRules(sheet).length > 0
-
 fetch = (href)->
   new Promise (resolve, reject)->
     superagent.get(href).end (err, res)-> resolve(res.text)
-
-appendStyle = (prefix, node, uri=window.location.href)->
-  rules = getRules(node.sheet)
-  if rules
-    appendRules(prefix, rules) if rules
-  else if !!node.href
-    # Loading External stylesheets (out of current domain)
-    # Only Chrome will use this, since we have tweaked the Polyfill to avoid 2 requests.
-    href = node.href
-    uri = getUriFromHref(node.href || uri)
-    node.parentNode.removeChild(node)
-    fetch(href).then (text)->
-      style = createStyleSheet(replaceLinkTagUrls(uri, text), true)
-      rules = getRules(style.sheet)
-      appendRules(prefix, rules)
-      removeStyleSheet(style)
-      true
-
 replaceLinkTagUrls = (uri, text)->
   re = new RegExp "url\\(['\"]?([^)]*?)['\"]?\\)", "g"
   text.replace(re, "url('#{uri}$1')")
-
-appendRules = (prefix, rules)->
-  _.map rules, (rule)->
-    Styles[prefix].style += prefixRule(prefix, rule);
-  true
-
 prefixRule = (prefix, rule)->
   return unless rule
   if rule.selectorText
@@ -78,55 +58,86 @@
     content = (prefixRule(prefix, subRule) for subRule, j in subSheet)
     query = rule.cssText.split('{');
     return "#{query[0]} {#{content.join(' ')}} "
-
 getUriFromHref = (href)->
   uri = href.split('/')
   uri.pop()
   uri.join('/')+'/'

-addLink = (prefix, node)->
-  appendStyle(prefix, node, getUriFromHref(node.href))
+appendStyle = (entry)->
+  rules = getRules(entry.node.sheet)
+  if rules
+    appendRules(entry, rules) if rules
+  else if !!entry.node.href
+    # Loading External stylesheets (out of current domain)
+    # Only Chrome will use this, since we have tweaked the Polyfill to avoid 2 requests.
+    href = entry.node.href
+    uri = getUriFromHref(entry.node.href)
+    entry.node.parentNode.removeChild(entry.node)
+    fetch(href).then (text)->
+      style = createStyleSheet(replaceLinkTagUrls(uri, text), true)
+      rules = getRules(style.sheet)
+      appendRules(entry, rules)
+      removeStyleSheet(style)
+      true
+addSheet = (prefix, node, doc)->
+  entry = findNode(prefix, node)
+  unless entry
+    entry = {prefix: prefix, node:node, doc:doc, style:""}
+    Styles.push(entry)
+  unless node.disabled
+    node.disabled=true
+    promise = promise.then ()->
+      appendStyle(entry)
+    ,throwErr

-promise = new Promise (resolve, reject)-> resolve()

-###*
- * Processes a complete document and writes a merged, prefixed stylesheet into the Main Document's HEAD.
- * @param  {string} prefix prefix to add to every style
- * @param  {[type]} doc    document to collapse into a single Stylesheet
-###
-processDocument = (prefix, doc)->
-  # Promise manipulation ensures stylesheets are loaded in the correct order even if they are async.
-  styles = _.toArray(doc.querySelectorAll('style,link'))
-  # IE doesn't support inserting at any position.
-  # Reverse stylesheets and insert from the end first.
-  _.map styles, (style)->
-    unless style.disabled
-      style.disabled=true
-      promise = promise.then ()->
-        appendStyle(prefix, style)
-      , throwErr
+# findDoc = (prefix, doc)->
+#   _.find Styles, (d)-> d.doc==doc && d.prefix == prefix

+findNode = (prefix, node)->
+  _.find Styles, (d)-> d.node==node && d.prefix == prefix
+
+finalize = ()->
   promise.then ()->
     MergedStyles.textContent = _.pluck(Styles,'style').join(' ');
   , throwErr

+###*
+ * Adds a whole document's styles to the Styles tag
+ *
+ * Under Firefox, you can't change css Rules or the containing
+ * style tag or those will be reset when touching anything else.
+ * Resort to building an entirely new stylesheet without copying cssRules
+ * @param {[type]} prefix [description]
+ * @param {[type]} doc    [description]
+###
+addDocument = (prefix, doc)->
+  styles = _.toArray(doc.querySelectorAll('style,link'))
+  addSheet(prefix, node, doc) for index, node of styles
+  finalize()

-addDocument = (prefix, doc)->
-  # Create and initialize a new global stylesheet that will receive merged styles in the right order
-  MergedStyles = createStyleSheet("") unless MergedStyles
-  # Under Firefox, you can't change css Rules or the containing
-  # style tag or those will be reset when touching anything else.
-  # Resort to building an entirely new stylesheet without copying cssRules
-  Styles[prefix] = {doc: doc, style:""}
-  processDocument(prefix, hash.doc) for prefix, hash of Styles
+removeDocument = (prefix, doc)->
+  Styles = _.omit Styles, (style)-> style.doc == doc
+  finalize()

 addStyle = (prefix, node)->
   # Array.prototype.slice.call(node.querySelectorAll('style:not([data-scoped])')).forEach (node)->
   node.setAttribute('data-scoped',true);
   new ScopedCss(prefix, null, node).process()

+addLink = (prefix, node)->
+  addSheet(prefix, node)
+  finalize()
+
+removeLink = (node)->
+  Styles = _.omit Styles, (style)-> style.node == node
+  finalize()
+
+
+
 module.exports = {
-  addDocument : addDocument
-  addStyle    : addStyle
-  addLink     : addLink
+  addDocument    : addDocument
+  removeDocument : removeDocument
+  addStyle       : addStyle
+  addLink        : addLink
 }


switched to doing the transformation completely serially.  This prevents EAGAIN errors.

--- lib/jsau.iced
+++ lib/jsau.iced
@@ -28,11 +28,11 @@
   'width' : 'the height of the user interface'
   'height' : 'the width of the user interface'

-transform_file = (src, project, target) ->
+transform_file = (src, project, target, next) ->

   # copy the file.
   proc = child_process.spawn 'cp', [src, target], {'stdio' : 'ignore'}
-  await proc.on 'exit', defer code
+  await proc.on 'exit', defer()

   # replace each key with each replacement using sed
   args = ['-i', '']
@@ -40,11 +40,11 @@
     args = args.concat ['-e', "s/#{key}/#{project[rep]}/g"]
   args = args.concat [target]

-  proc = child_process.spawn 'sed', args, {'stdio' : ['ignore', 'ignore', 2]}
-  await proc.on 'exit', defer code
+  proc = child_process.spawn 'sed', args, {'stdio' : 'ignore'}
+  proc.on 'exit', next

-transform_dir = (dir, project, target) ->
+transform_dir = (dir, project, target, next) ->

   # create the destination
   await fs.mkdir target, defer err
@@ -67,10 +67,13 @@
     throw err if err?

     # if it's a file, transform the file
-    if stats.isFile()
-      transform_file src_path, project, target_path
-    else if stats.isDirectory()
-      transform_dir src_path, project, target_path
+    await
+      if stats.isFile()
+        transform_file src_path, project, target_path, defer()
+      else if stats.isDirectory()
+        transform_dir src_path, project, target_path, defer()
+
+  next()

 module.exports = (json_file) ->

@@ -87,4 +90,4 @@
   throw err if err?
   target = (path.join process.cwd(), project.project)

-  transform_dir fullpath, project, target+  await transform_dir fullpath, project, target, defer()


clean a little

clean a little

--- src/coffee/directives/dynamic.coffee
+++ src/coffee/directives/dynamic.coffee
@@ -25,6 +25,6 @@
         service: '='
       template: ""
       link: l
-      }
-
+    }
+
   )


Working on muffin deploy.

Working on muffin deploy.

--- src/command.coffee
+++ src/command.coffee
@@ -359,7 +359,41 @@
     logging.fatal "Must choose a platform from the following: heroku, jitsu, gae, gh-pages"

   buildDir = sysPath.relative(process.cwd(), project.buildDir)
+  serverDir = sysPath.relative(process.cwd(), project.serverDir)
+
   switch dest
+    when 'heroku'
+      script =
+      """
+        git checkout deployment
+        git merge master -m"Merge master"
+        muffin minify
+        git add -f #{buildDir}
+        git add -u #{buildDir}
+        git commit -m"Commit for deployment"
+        git subtree push --prefix #{serverDir} heroku master
+        git checkout master
+      """
+    when 'jitsu'
+      script =
+      """
+        muffin minify
+        cd #{serverDir}
+        jitsu deploy
+        cd ..
+      """
+    when 'gae'
+      script =
+      """
+        git checkout deployment
+        git merge master -m"Merge master"
+        muffin minify
+        git add -f #{buildDir}
+        git add -u #{buildDir}
+        git commit -m"Commit for deployment"
+        git subtree push --prefix #{serverDir} appengine master
+        git checkout master
+      """
     when 'gh-pages'
       script =
       """


hook up new exercise action

hook up new exercise action

--- src/components/exercise-wrapper.cjsx
+++ src/components/exercise-wrapper.cjsx
@@ -11,9 +11,11 @@
   getInitialState: ->
     {}

-
   addNew: ->
-    @setState({exerciseId: ExerciseStore.freshLocalId() })
+    id = ExerciseStore.freshLocalId()
+    template = ExerciseStore.getTemplate()
+    ExerciseActions.loaded(template, id)
+    @setState({exerciseId: id })

   loadExercise: ->
     exerciseId = this.refs.exerciseId.getDOMNode().value


fixed transition for delete and cancel

fixed transition for delete and cancel

--- src/components/task-plan/footer.cjsx
+++ src/components/task-plan/footer.cjsx
@@ -18,16 +18,16 @@
     @context.router.transitionTo('taskplans', {courseId})

   onDelete: ->
-    {id} = @props
+    {id, courseId} = @props
     if confirm('Are you sure you want to delete this?')
       TaskPlanActions.delete(id)
-      @context.router.transitionTo('dashboard')
+      @context.router.transitionTo('taskplans', {courseId})

   onCancel: ->
-    {id} = @props
+    {id, courseId} = @props
     if confirm('Are you sure you want to cancel?')
       TaskPlanActions.reset(id)
-      @context.router.transitionTo('dashboard')
+      @context.router.transitionTo('taskplans', {courseId})

   onViewStats: ->
     {id, courseId} = @props


add static server for visual testing

add static server for visual testing

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -8,6 +8,7 @@
 rename     = require 'gulp-rename'
 uglify     = require 'gulp-uglify'
 streamify  = require 'gulp-streamify'
+stat       = require 'node-static'

 # application entry point to generate standalone library
 ENTRY = './src/gerber-to-svg.coffee'
@@ -56,3 +57,19 @@

 gulp.task 'testwatch', ['test', 'default'], ->
   gulp.watch ['./src/*', './test/*'], ['test', 'default']
+
+gulp.task 'testvisual', [ 'default' ], ->
+  server = new stat.Server '.'
+  require('http').createServer( (request, response) ->
+    request.addListener( 'end', ->
+      if request.url is '/'
+        server.serveFile '/test/index.html', 200, {}, request, response
+        gutil.log "served #{request.url}"
+      else
+        server.serve(request, response, (error, result)->
+          if error then gutil.log "error serving #{request.url}"
+          else gutil.log "served #{request.url}"
+        )
+    ).resume()
+  ).listen 4242
+  gutil.log "test server started at http://localhost:4242\n"


Add support for `<` (reverse bird tracks)

--- src/haskell.coffee
+++ src/haskell.coffee
@@ -640,8 +640,8 @@
           include: 'source.haskell'
       ]
     ,
-      begin: /^(?=> )/
-      end: /^(?!> )/
+      begin: /^(?=[><] )/
+      end: /^(?![><] )/
       name: 'meta.embedded.haskell'
       patterns: haskellGrammar.patterns.concat
         match: /^> /
@@ -651,8 +651,8 @@
   ]
   repository: haskellGrammar.repository

-literateHaskellGrammar.macros.maybeBirdTrack = /^> /
+literateHaskellGrammar.macros.maybeBirdTrack = /^(?:>|<) /
 literateHaskellGrammar.macros.indentBlockEnd =
-  /^(?!> \1{indentChar}|> {indentChar}*$)|^(?!> )/
+  /^(?!(?:>|<) \1{indentChar}|(?:>|<) {indentChar}*$)|^(?!(?:>|<) )/

 makeGrammar literateHaskellGrammar, "grammars/literate haskell.cson"


Optimize some documentation

Optimize some documentation

--- src/service.coffee
+++ src/service.coffee
@@ -225,7 +225,8 @@
     # Update the current connection state periodially.
     $interval (-> wrapped.getConnectionState(true)), sockjsStateInterval

-    api =
+    ### building and exposing the actual service API ###
+    return (
       on : wrapped.registerHandler
       addListener : wrapped.registerHandler
       un : wrapped.unregisterHandler
@@ -238,7 +239,7 @@
       getBufferCount: -> messageQueueHolder.size()
       isValidSession : -> validSession
       login : wrapped.login
-    return api
+    )

   return
 )


Implement concurrent page load.

Implement concurrent page load.

Multiple descents may have detected that a page has not yet been loaded
simultaneously. If a descent visited a page that was not yet loaded, it would
load it. While that was going on, another descent might visit the same unloaded
page. It would also load it.

Duplicating the reads was probably benign, probably would not not have cased
errors. If the pages were read locked, then they'd replace the page contents,
but not undo any changes, because no changes could be made, because of the read
lock. If the page was write locked, then there would only be one load anyway.

There is now a `load` queue property for an unloaded page. The presence of the
`load` queue indicates that the page is unloaded. The Docco describes the
operation. The first descent through pushes its callback onto the `load` queue,
then invokes the page read function with a callback that will invoke all the
callbacks in the `load` queue. Subsequent descents push their callbacks onto the
`load` queue and do nothing more.

No more `loaded` flag. It is the lack of a `load` queue that indicates that a
page is loaded.

I cleaned up the preamble to locking while writing the Docco for loading.

Renamed `IO.readBranches` to `IO.readBranch`. The `IO.load` function is the only
function that calls `IO.readBranch`, so it seemed like a good time to bring it
in line with `IO.readLeaf`.

Went ahead and renamed `IO.rewriteBranches` to `IO.rewriteBranch` as long as
we're working on consistency.

--- src/lib/strata_.coffee
+++ src/lib/strata_.coffee
@@ -1,8 +1,12 @@
 # A Streamline.js friendly evented I/O b‑tree for Node.js.
 #
+# TK Define ***least child page***.
+#
+# **REMEMBER**: Alan! Your audience is you, or someone who knows enough Node.js,
+# CoffeeScript and algorithms to hack on this. This is not a lesson on
+# b‑trees. (Note to self as I write Docco.)
+#
 # ## Purpose
-#
-# TK Define ***least child page***.
 #
 # Strata stores JSON objects on disk, according to a sort order of your
 # choosing, indexed for fast retrieval.
@@ -458,7 +462,6 @@
       balancers: 0
       cache: {}
       length: 0
-      loaded: false
       locks: [[]]
       offset: 0
       positions: []
@@ -734,7 +737,7 @@
     fs.close fd, _

     # Return the loaded page.
-    extend page, { loaded: true }
+    page

   # 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
@@ -1010,7 +1013,6 @@
       balancers: 0
       cache: {}
       length: 0
-      loaded: false
       locks: [[]]
       penultimate: true
       right: 0
@@ -1097,7 +1099,7 @@
   # until place using `replace`.

   #
-  rewriteBranches: (page, suffix, _) ->
+  rewriteBranch: (page, suffix, _) ->
     filename = @filename page.address, ".#{suffix}"
     record = [ page.right, page.addresses ]
     json = JSON.stringify(record)
@@ -1111,7 +1113,7 @@
   # described in the b‑tree overview above.

   #
-  readBranches: (page, _) ->
+  readBranch: (page, _) ->
     # Read addresses from JSON branch file.
     filename = @filename page.address
     json = fs.readFile filename, "utf8", _
@@ -1122,7 +1124,7 @@
     @splice page, 0, 0, addresses

     # Extend the existing page with the properties read from file.
-    extend page, { right, loaded: true }
+    extend page, { right }

   # ### B-Tree Initialization
   #
@@ -1152,11 +1154,11 @@
     if fs.readdir(@directory, _).filter((f) -> not /^\./.test(f)).length
       throw new Error "database #{@directory} is not empty."
     # Create a root branch with a single empty leaf.
-    root = @encache @createBranch @nextAddress++, penultimate: true, loaded: true
-    leaf = @encache @createLeaf -(@nextAddress++), loaded: true
+    root = @encache @createBranch @nextAddress++, penultimate: true
+    leaf = @encache @createLeaf -(@nextAddress++)
     @splice root, 0, 0, leaf.address
     # Write the root branch.
-    @rewriteBranches root, "replace", _
+    @rewriteBranch root, "replace", _
     @rewriteLeaf leaf, "replace", _
     @replace leaf, "replace", _
     @replace root, "replace", _
@@ -1210,63 +1212,82 @@
         address = parseInt match[1], 10
         @nextAddress = address + 1 if address > @nextAddress

+  # We close after every write, so there are no open file handles.
+  #
+  # TODO Close medic file.
+
+  # —
   close: (_) ->

-  # 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.
+  # b‑tree is still a concurrent data structure.
+  #
+  # When we navigate the tree from the root to search for a key in the tree, we
+  # descend the b&x2011;. The act of descending the b‑tree is called a
+  # ***descent***.
+  #
+  # A descent is a unit of work in our concurrency model. Once descent of the
+  # b‑tree can make progress while another descent of the tree is waiting
+  # on evented I/O.  Instead of thinking about concurrency in terms of threads
+  # we talk about concurrent descents of the b‑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.
   #
+  # Note that the b‑tree must only be read and written by a single
+  # Node.js process. You can't have two Node.js processes serving the same
+  # database directory.
+  #
   # ### 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.
-  #
-  # Locks prevent cache purges.
-  #
-  # 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.
+  # Every page has a read/write lock. Pages are obtained through the `IO.lock`
+  # method, so that every time we obtain a page, it comes to us locked.
+  #
+  # Locking is internal and not exposed to the application developer. However,
+  # the application developer must remember to close cursors, in order to
+  # release the locks that cursors hold.
+  #
+  # #### Shared and Exclusive
+  #
+  # When we obtain a lock to read the page, we obtain a ***shared lock***. The
+  # shared lock allows other descents to also obtain a lock on the page. With a
+  # shared lock, you can read the page, knowing that it will not change.
+  #
+  # When we obtain a lock to write the page, we obtain an ***exclusive lock***.
+  # The exclusive lock prevents all other descents from obtaining any sort of
+  # lock on the page. No other page will be able to obtain neither a shared nor
+  # an exclusive lock.
+  #
+  # #### Locking Sub-Trees
+  #
+  # When we descend the tree, moving from parent page to child page, we obtain
+  # locks in descending order. This means that if we obtain an exclusive lock on
+  # a page, no other descent will be able to travel from the parent of that
+  # page, to the children of that page. It effectively blocks new descents from
+  # entering the ***sub‑tree*** that is defined by the exclusively
+  # locked page and all its children.
+  #
+  # #### Rationale for Locking
+  #
+  # Locking prevents race conditions where a descent that is waiting on an
+  # evented I/O request returns to find that the structure of the b‑tree
+  # has changed drastically. For example, while a descent was waiting for a leaf
+  # page to load into memory, another descent might load the same page into
+  # memory, merge the leaf page with its sibling, and delete it.
+  #
+  # #### Caching
+  #
+  # Locks prevent cache purges. When a cache purge is triggered, a page will not
+  # be purged from the cache, if there are outstanding locks on the page.
+  #
+  # #### Lock Properties
   #
   # 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
@@ -1275,21 +1296,8 @@
   # shared read lock is granted, unless an exclusive write lock is queued.
   #
   # The locks are not re-entrant.
-  #
-  # #### Lock
-
-  # TODO Signature: Address should go first.
-  #
-  # TODO If the page is not loaded, and we simply move forward when we encounter
-  # the page, that is, we have two descents, one encounters this unloaded page,
-  # but it is a read, so we go ahead, it makes an evented I/O call and waits,
-  # we make progress on a new descent, the desent encounters the unloaded page,
-  # and it loads the page, and waits.
-  #
-  # Maybe we can queue the people waiting on the load? The simplest thing would
-  # be to put the callbacks in an array of onloads.
-
-  #
+
+  # —
   lock: (address, exclusive, 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
@@ -1299,86 +1307,134 @@
     # 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]
+
+    #
+    if page = @cache[address]
+      # Move the page to the head of the most-recently user list.
+      @link @mru, @unlink page
+    #
+    else
+      # Create a page to load with an empty `load` queue. The presence of the
+      # `load` queue indicates that the page needs to be read from file.
       creator = "create#{if address < 0 then "Leaf" else "Branch"}"
-      page = @encache @[creator](address)
-
-    # If the page needs to be loaded, 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.
-
-    # Currently blocked out to force sizer to fire.
-    if page.loaded and false
-      lock = callback
-    else
-      # Here's a temporary block of code that will assert that we're keeping an
-      # accurate count of the heft of our pages. After a while, this code will
-      # be removed, and we'll count on assertions in our unit tests to catch
-      # errors.
-
-      # We also put our relinker in here, so this will surivive a cull.
-      sizer = (error, page) =>
-        if error
-          callback error
-        else
-          # Move the page to the head of the most-recently user list.
-          @link @mru, @unlink page
-          # Check that the JSON size is correct for the current cached page
-          # contents.
-          size = 0
-          if page.address < 0
-            if page.positions.length
-              size += JSON.stringify(page.positions).length
-            for position, object of page.cache
-              { record, key } = object
-              if object.size isnt JSON.stringify({ record, key }).length
-                throw new Error "sizes are wrong"
-              size += object.size
-          else
-            if page.addresses.length
-              size += JSON.stringify(page.addresses).length
-            for position, object of page.cache
-              size += JSON.stringify(object).length
-          if size isnt page.size
-            throw new Error "sizes are wrong"
-          # Invoke the caller's callback.
-          callback null, page
-      # TODO Two descents, both reads, enter and then come though to this point,
-      # they both see the page as unloaded, they both load it. Need a loaders
-      # array, and we can wait for things to load.
-      lock = (error, page) =>
-        if error
-          callback error
-        else if page.loaded
-          sizer null, page
-        else if address < 0
-          @readLeaf page, sizer
-        else
-          @readBranches page, sizer
+      page = @encache @[creator](address, { load: [] })
+
+    # #### Lock Implementation
+    #
+    # We don't use mutexes, of course, because Node.js doesn't have the concept
+    # of mutexes to protect critical sections of code the way that threaded
+    # programming platforms do.
+    #
+    # 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 associated 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 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.
+    # If the callback we add to the queue is added to the first queue element is
+    # executed immediately. Otherwise, it will be executed when the preceding
+    # queue elements have completed.
     #
     # 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
+    # Subsequent read lock callbacks are appended 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 [ callback ]
       locks.push []
       if locks[0].length is 0
         locks.shift()
-        lock(null, page)
+        @load page, callback
     else
-      locks[locks.length - 1].push lock
+      locks[locks.length - 1].push callback
       if locks.length is 1
-        lock(null, page)
+        @load page, callback
+
+  # #### Check JSON Size
+
+  # Here's a temporary block of code that will assert that we're keeping an
+  # accurate count of the heft of our pages. After a while, this code will be
+  # removed, and we'll count on assertions in our unit tests to catch errors.
+
+  # This only check that the JSON size is correct for the give page contents,
+  # not for the entire b‑tree.
+  @checkJSONSize: (page) ->
+    size = 0
+    if page.address < 0
+      if page.positions.length
+        size += JSON.stringify(page.positions).length
+      for position, object of page.cache
+        { record, key } = object
+        if object.size isnt JSON.stringify({ record, key }).length
+          throw new Error "sizes are wrong"
+        size += object.size
+    else
+      if page.addresses.length
+        size += JSON.stringify(page.addresses).length
+      for position, object of page.cache
+        size += JSON.stringify(object).length
+    if size isnt page.size
+      throw new Error "sizes are wrong"
+
+  # ### Load
+
+  # One or more descents may encounter the same unloaded page. Only one descent
+  # should load it, the others should wait.
+  #
+  # In `load` we ensure that only the first descent will actually invoke the
+  # load function for the page. If we are the first to encounter an unloaded
+  # page, we push our callback onto the `load` queue of the page, and invoke the
+  # correct read function for the page time. We provide a callback that will
+  # invoke all the queued callbacks in the `load` queue to the read function.
+  #
+  # If we encounter an unloaded page, but there are already callbacks in the
+  # queue, we know that the first descent through has invoked read, and that our
+  # callback will be invoked if we will simply place it in the `load` queue and
+  # do nothing more.
+
+  # —
+  load: (page, callback) ->
+    # If the page is not loaded, load it.
+    if page.load
+      # Add our callback to the list of waiting callback.
+      page.load.push callback
+      # If we are the first one through, create a group callback function, then
+      # pass it to the load function for the page type.
+      if page.load.length is 1
+        # Create a callback that will invoke all the callbacks queued to wait
+        # for the page to load.
+        loaded = (error) ->
+          for callback in page.load
+            do (callback) -> process.nextTick -> callback error, page
+          # On error we reset the load list.
+          if error
+            page.load.length = 0
+          # Otherwise, we delete the load list, because no load list means the
+          # page is loaded.
+          else
+            IO.checkJSONSize page
+            delete page.load
+        # Invoke the correct read function for the page type.
+        if page.address < 0
+          @readLeaf page, loaded
+        else
+          @readBranch page, loaded
+    # If the page is already loaded, we wave the descent on through.
+    else
+      #
+      IO.checkJSONSize
+      callback null, page

   # #### Unlock

@@ -1389,6 +1445,9 @@

   #
   unlock: (page) ->
+    # Note that it is not possible for this method to be called on an page that
+    # has not already been loaded.
+    IO.checkJSONSize page
     locks = page.locks
     locked = locks[0]
     locked.shift()
@@ -1397,7 +1456,7 @@
       # Each callback is scheduled using next tick. If any callback waits on
       # I/O, then another one will resume. Concurrency.
       for callback in locks[0]
-        do (callback) -> process.nextTick -> callback(null, page)
+        do (callback) -> process.nextTick -> callback error, page

   # Read a record cache entry from the cache. Load the record and cache it of it
   # is not already cached.
@@ -1450,11 +1509,11 @@
 # ## Descent
 #
 # We use the term *descent* to describe b‑tree operations, because all
-# b‑tree
-# operations require a descent of the b‑tree, a traversal of the b‑tree starting
-# from the root. Whenever we are search the tree, insert or delete records, or
-# balance the tree with a page splits and merges, we first begin with a descent
-# of the b‑tree, from the root, to find the page we want to act upon.
+# b‑tree operations require a descent of the b‑tree, a traversal
+# of the b‑tree starting from the root. Whenever we are search the tree,
+# insert or delete records, or balance the tree with a page splits and merges,
+# we first begin with a descent of the b‑tree, from the root, to find the
+# page we want to act upon.
 #
 # #### Descent as Unit of Work
 #
@@ -2790,7 +2849,7 @@
       replacements.push split

       # Write the branches
-      @io.rewriteBranches penultimate.page, "pending", _
+      @io.rewriteBranch penultimate.page, "pending", _

       # Now rename the last action, committing to our balance.
       @io.rename penultimate.page, "pending", "commit", _
@@ -2998,7 +3057,7 @@
           @io.uncacheKey right, right.addresses[1]
         right.addresses.shift()

-      @io.rewriteBranches right, "pending", _
+      @io.rewriteBranch right, "pending", _

       # TODO If I succeed, how will I know to test the parents for balance? Got
       # to think all over again in medic about who is whose parent?
@@ -3097,10 +3156,10 @@

     @io.size -= child.right.page.size

-    @io.rewriteBranches child.left.page, "replace", _
+    @io.rewriteBranch child.left.page, "replace", _

     child.right.page.addresses.length = 0
-    @io.rewriteBranches child.right.page, "unlink", _
+    @io.rewriteBranch child.right.page, "unlink", _

     if parent.isPivot
       right = pivot.page
@@ -3111,7 +3170,7 @@
         @io.uncacheKey right, right.addresses[1]
       right.addresses.shift()

-    @io.rewriteBranches right, "pending", _
+    @io.rewriteBranch right, "pending", _

     # TODO If I succeed, how will I know to test the parents for balance? Got
     # to think all over again in medic about who is whose parent?
@@ -3144,10 +3203,10 @@
       @io.uncache page for page in pages
       root.addresses = left.addresses.concat(right.addresses)

-    @io.rewriteBranches left, "unlink", _
-    @io.rewriteBranches right, "unlink", _
-
-    @io.rewriteBranches root, "pending", _
+    @io.rewriteBranch left, "unlink", _
+    @io.rewriteBranch right, "unlink", _
+
+    @io.rewriteBranch root, "pending", _

     @io.rename root, "pending", "commit", _


TODOs

TODOs

--- src/ace_adaptor.coffee
+++ src/ace_adaptor.coffee
@@ -28,9 +28,11 @@
     env.editor.session.remove env.editor.getSelectionRange()

   paste: (env, args) ->
+    #TODO use args.times, p with no buffer shouldn't move cursor
     aceAdaptor.navigateRight(env, args)
     aceAdaptor.pasteBefore(env, args)
   pasteBefore: (env, args) ->
+    #TODO use args.times
     text = jim.registers[args.register]
     env.editor.insert text if text


Rename AltGraph to Alt on Windows

Rename AltGraph to Alt on Windows

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -168,6 +168,8 @@
   isNonCharacterKey = key.length > 1
   if isNonCharacterKey
     key = NON_CHARACTER_KEY_NAMES_BY_KEYBOARD_EVENT_KEY[key] ? key.toLowerCase()
+    if key is "altgraph" and process.platform is "win32"
+      key = "alt"
   else
     # Deal with caps-lock issues. Key bindings should always adjust the
     # capitalization of the key based on the shiftKey state and never the state


Well, commonJS modules are not working as smoothly as expected

--- src/udefine.coffee
+++ src/udefine.coffee
@@ -62,12 +62,17 @@
     unless Object.hasOwnProperty.call udefine.inject.modules, name
       if udefine.autoInject
         udefine.inject.add name, {root, name} if udefine.env.globals
+
+        # Auto injection does not work on CommonJS
+        # TODO: Need to investigate if it's even possible
+        ###
         if udefine.env.commonjs
           udefine.inject.add name,
             root: module.exports
             name: name
             ignoreName: true
-
+        ###
+
     # Inject result into defined namespace
     if Object.hasOwnProperty.call udefine.inject.modules, name
       injectObject = udefine.inject.modules[name]
@@ -165,8 +170,7 @@
   udefine.defaultConfig()

   # Configuration helper function
-  udefine.configure = (configFunc) ->
-    configFunc.apply udefine, [if hasModule then module.exports else root]
+  udefine.configure = (configFunc) -> configFunc.apply udefine, [root]

   # Export udefine function on CommonJS environments
   if hasModule then module.exports = udefine else root.udefine = udefine


removed backfaceVisibility setting

removed backfaceVisibility setting

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -177,7 +177,6 @@
     hPanel.style[transitionEasingProp] = @settings.easingMethod
     hPanel.style[transformOriginProp] = 'top'
     hPanel.style[transformStyleProp] = 'preserve-3d'
-    hPanel.style[backfaceProp] = 'hidden'
     hPanel.appendChild hMask

     for anchor in ['top', 'bottom']


accurate warning logs about max and min angles

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -349,10 +349,10 @@
     if isNaN angle
       0
     else if angle > 89
-      devMode and console.warn 'oriDomi: Maximum value is 90'
+      devMode and console.warn 'oriDomi: Maximum value is 89'
       89
     else if angle < -89
-      devMode and console.warn 'oriDomi: Minimum value is -90'
+      devMode and console.warn 'oriDomi: Minimum value is -89'
       -89
     else
       angle


Add a none option to the comment field

Add a none option to the comment field

--- lib/csv-editor-form-element.coffee
+++ lib/csv-editor-form-element.coffee
@@ -106,6 +106,7 @@
             selected: 'hash'
             options:
               'hash': "#"
+              'none': 'none'
           }

         @div class: 'panel', =>
@@ -287,6 +288,8 @@

     if comment is 'custom'
       options.comment = @commentTextEditor.getText()
+    else if comment is 'none'
+      options.comment = ''
     else
       options.comment = comment


ehn: now export and import include grid parameters

--- src/application.coffee
+++ src/application.coffee
@@ -182,7 +182,15 @@
   importData: (data)->
     try
       console.log "importing #{data}"
-      @cells = importField parseFieldData data
+      match = data.match /^(\d+)\$(\d+)\$(.*)$/
+      throw new Error("Data format unrecognized") unless match?
+      n = parseIntChecked match[1]
+      m = parseIntChecked match[2]
+
+      if n isnt @getGroup().n or m isnt @getGroup().m
+        console.log "Need to change grid"
+        @setGridImpl n, m
+      @cells = importField parseFieldData match[3]
       console.log "Imported #{@cells.count} cells"
     catch e
       alert "Faield to import data: #{e}"
@@ -549,15 +557,10 @@
 #  return parts.join " "

 doExport = ->
-  #data = JSON.stringify(exportField(application.cells))
   data = stringifyFieldData exportField application.cells
-  #edata = lzw_encode data
-
-  #data1 = exportTrivial application.cells
-  #edata1 = lzw_encode data1
-
-  #console.log "Data len before compression: #{data.length}, after compression: #{edata.length}, ratio: #{edata.length/data.length}"
-  showExporDialog data
+  n = application.getGroup().n
+  m = application.getGroup().m
+  showExportDialog "#{n}$#{m}$#{data}"

 doExportClose = ->
   E('export-dialog').style.display = 'none'
@@ -609,14 +612,17 @@
     visibleCells.put translatedCell, state
   return exportField visibleCells

-showExporDialog = (sdata) ->
+showExportDialog = (sdata) ->
   E('export').value = sdata
   E('export-dialog').style.display = ''
   E('export').focus()
   E('export').select()

 doExportVisible = ->
-  showExporDialog stringifyFieldData encodeVisible()
+  n = application.getGroup().n
+  m = application.getGroup().m
+  data = stringifyFieldData encodeVisible()
+  showExportDialog "#{n}$#{m}$#{data}"

 doShowImport = ->
   E('import-dialog').style.display = ''
@@ -765,4 +771,4 @@
 updatePlayButtons()
 redrawLoop()

-application.saveDialog.show()
+#application.saveDialog.show()