fix sendReversal

fix sendReversal

--- server/methods/mail/sendMail/sendReversal.coffee
+++ server/methods/mail/sendMail/sendReversal.coffee
@@ -1,32 +1,34 @@
 Meteor.methods
+
 	sendReversal: (shiftId, teamId, userId) ->
 		shift = Shifts.findOne shiftId
 		project = Projects.findOne shift.projectId, fields: name: 1, email: 1
 		user = Meteor.users.findOne userId, fields: profile: 1

-		check { userId: Meteor.userId(), projectId: projectId }, isShiftAdmin
+		if user? and shift?
+			check { userId: Meteor.userId(), projectId: shift.projectId }, isShiftAdmin

-		if user? and shift?
 			thisMoment = moment(shift.date, 'YYYYDDDD')
 			thisMoment.locale(user.profile.language)
 			date = thisMoment.format('dddd, DD.MM.YYYY')
 			time = moment(shift.start, 'Hmm').format('HH:mm') + ' - ' + moment(shift.end, 'Hmm').format('HH:mm')

+			dateTime = date: date, time: time
 			name = user.profile.firstname + ' ' + user.profile.lastname
 			teamNr = shift.teams.map((e) -> e._id).indexOf(teamId)

-		Meteor.call 'sendMail',
-			recipient: user.profile.email
-			sender: project.name
-			from: project.email
-			subject: TAPi18n.__('mail.reversal.subject', '', user.profile.language)
-			template: 'reversal'
-			language: user.profile.language
-			data:
-				project: project.name
-				name: name
-				team: shift.teams[teamNr].name
-				datetime: TAPi18n.__('mail.reversal.datetime', {date: date, time: time}, user.profile.language)
-				content: getMailTexts 'reversal', user.profile.language
-		, (err, res) -> if err
-			console.log 'sendMail failed: ' + err
+			Meteor.call 'sendMail',
+				recipient: user.profile.email
+				sender: project.name
+				from: project.email
+				subject: TAPi18n.__('mail.reversal.subject', '', user.profile.language)
+				template: 'reversal'
+				language: user.profile.language
+				data:
+					project: project.name
+					name: name
+					team: shift.teams[teamNr].name
+					datetime: TAPi18n.__('mail.reversal.datetime', dateTime, user.profile.language)
+					content: getMailTexts 'reversal', user.profile.language
+			, (err, res) -> if err
+				console.log 'sendMail failed: ' + err


downsample logging

downsample logging

--- services/real-time/app/coffee/EventLogger.coffee
+++ services/real-time/app/coffee/EventLogger.coffee
@@ -35,11 +35,12 @@
 			return # order is ok
 		if (count == previous)
 			metrics.inc "event.#{channel}.duplicate"
-			logger.warn {channel:channel, message_id:message_id}, "duplicate event"
+			if Math.random() < 0.01
+				logger.warn {channel:channel, message_id:message_id}, "duplicate event (sampled at 1%)"
 			return "duplicate"
 		else
 			metrics.inc "event.#{channel}.out-of-order"
-			logger.warn {channel:channel, message_id:message_id, key:key, previous: previous, count:count}, "out of order event"
+			# logger.warn {channel:channel, message_id:message_id, key:key, previous: previous, count:count}, "out of order event"
 			return "out-of-order"

 	_storeEventCount: (key, count) ->


fixed coffeelint errors on workers/social/lib/social/models/appstorage.coffee

--- workers/social/lib/social/models/appstorage.coffee
+++ workers/social/lib/social/models/appstorage.coffee
@@ -2,7 +2,7 @@

 module.exports = class JAppStorage extends JStorage

-  {signature} = require 'bongo'
+  { signature } = require 'bongo'

   @share()

@@ -13,7 +13,7 @@
     sharedMethods :
       static      : {}
        #it's secure to have save and update, since JAppStorage can only be gotten by a client that owns that appstorage
-      instance    :
+      instance    :
         save:
           (signature Function)
         update:
@@ -26,3 +26,5 @@
       bucket      :
         type      : Object
         default   : -> {}
+
+


Define email sanitize module as email property setter

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -107,7 +107,7 @@
       email         :
         type        : String
         validate    : require('../name').validateEmail
-        set         : (value) -> value.trim().toLowerCase()
+        set         : require './emailsanitize'
       password      : String
       salt          : String
       twofactorkey  : String


removed unused quotaExceedErr variable

removed unused quotaExceedErr variable

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1133,7 +1133,6 @@
     error            = null
     newToken         = null
     invitation       = null
-    quotaExceedErr   = null
     foreignAuthType  = null

     queue = [


add the rest of the CRUD methods for JVocabulary

--- workers/social/lib/social/models/vocabulary.coffee
+++ workers/social/lib/social/models/vocabulary.coffee
@@ -2,18 +2,35 @@

 module.exports = class JVocabulary extends Module

+  @share()
+
   {daisy} = require 'bongo'
   {KodingError} = require '../error'
+
+  {permit} = require './group/permissionset'
+
+  Validators = require './group/validators'
+  {Inflector} = require 'bongo'

   @trait __dirname, '../traits/protected'

   @set
+    sharedMethods   :
+      static        : ['create']
+      instance      : ['remove', 'modify']
     schema          :
       title         : String
       description   : String
       exclusive     :
         type        : Boolean
         default     : no
+      group         :
+        type        : String
+        validate    : require('./name').validateName
+      collectionName:
+        get         : ->
+          {name} = require './tag'
+          "#{Inflector.pluralize name}_#{@group}"
     relationships   :
       tag           :
         targetType  : 'JTag'
@@ -25,11 +42,17 @@
       'edit own vocabularies'   : ['moderator']
       'delete own vocabularies' : ['moderator']

-  @create =(client, formData, callback)->
+  @create$ = permit 'create vocabularies'
+    success:(client, formData, callback)->
+      formData.group = client.context.group
+      @create formData, callback
+
+  @create = (formData, callback)->
     JGroup = require './group'
-    vocabulary = new @
+    vocabulary = new this
       title       : formData.title
       description : formData.description
+      group       : formData.group
     queue = [
       ->
         vocabulary.save (err)->
@@ -49,3 +72,18 @@
     queue.push -> callback null, vocabulary
     daisy queue

+  remove$: permit
+    advanced: [
+      { permission: 'delete vocabularies' }
+      { permission: 'delete own vocabularies', validateWith: Validators.own }
+    ]
+    success:(client, callback)-> @remove callback
+
+  modify: permit
+    advanced: [
+      { permission: 'edit vocabularies' }
+      { permission: 'edit own vocabularies', validateWith: Validators.own }
+    ]
+    success:(client, formData, callback)->
+      formData.group = client.content.group
+      @update { $set:formData }, callback


filterable: escape non-word constituents after trimming seed

--- workers/social/lib/social/traits/filterable.coffee
+++ workers/social/lib/social/traits/filterable.coffee
@@ -11,7 +11,7 @@
     limit     ?= 10
     blacklist or= []
     blacklist = blacklist.map(ObjectId)
-    cleanSeed = seed.replace(/[^\w\s-]/).replace(/(\W)/g, "\\$1").trim() #TODO: this is wrong for international charsets
+    cleanSeed = seed.replace(/[^\w\s-]/).trim().replace(/(\W)/g, "\\$1") #TODO: this is wrong for international charsets
     startsWithSeedTest = RegExp '^'+cleanSeed, "i"
     startsWithOptions = {limit, blacklist, skip}
     @findSuggestions client, startsWithSeedTest, startsWithOptions, (err, suggestions)=>


spaces to tabs.  Will revert for pull request

--- sync.coffee
+++ sync.coffee
@@ -1,1074 +1,1074 @@
 # A namespace for sync objects.  Same object abstraction level as DB table: posts, notes, ...
 class bucket
-    constructor: (@s, @name, b_opts) ->
-        @jd = @s.jd
-        @options = @jd.deepCopy @s.options
-        for own name, val of b_opts
-            @options[name] = val
-        @chan = @options['n']
-        @space = "#{@options['app_id']}/#{@name}"
-        @username = @options['username']
-        @namespace = "#{@username}:#{@space}"
-
-        @clientid = null
-        try
-            @clientid = localStorage.getItem "#{@namespace}/clientid"
-        catch error
-            @clientid = null
-        if not @clientid? or @clientid.indexOf("sjs") != 0
-            @clientid = "sjs-#{@s.bversion}-#{@uuid(5)}"
-            try
-                localStorage.setItem "#{@namespace}/clientid", @clientid
-            catch error
-                console.log "#{@name}: couldnt set clientid"
-
-        @cb_events = ['notify', 'notify_init', 'notify_version', 'local', 'get', 'ready', 'notify_pending', 'error']
-        @cbs = {}
-        @cb_e = @cb_l = @cb_ni = @cb_np = null
-        @cb_n = @cb_nv = @cb_r = ->
-
-        @initialized = false
-        @authorized = false
-        @data =
-            last_cv: 0
-            ccid: @uuid()
-            store: {}
-            send_queue: []
-            send_queue_timer: null
-
-        @started = false
-
-        if not ('nostore' of @options)
-            @_load_meta()
-            @loaded = @_load_data()
-            console.log "#{@name}: localstorage loaded #{@loaded} entities"
-        else
-            console.log "#{@name}: not loading from localstorage"
-            @loaded = 0
-
-        @_last_pending = null
-        @_send_backoff = 15000
-        @_backoff_max = 120000
-        @_backoff_min = 15000
-
-        console.log "#{@namespace}: bucket created: opts: #{JSON.stringify(@options)}"
-
-    S4: ->
-        (((1+Math.random())*0x10000)|0).toString(16).substring(1)
-
-    uuid: (n) ->
-        n = n || 8
-        s = @S4()
-        while n -= 1
-            s += @S4()
-        return s
-
-    now: ->
-        new Date().getTime()
-
-    on: (event, callback) =>
-        if event in @cb_events
-            @cbs[event] = callback
-            switch event
-                when 'get', 'local'
-                    @cb_l = callback
-                when 'notify'
-                    @cb_n = callback
-                when 'notify_version'
-                    @cb_nv = callback
-                when 'notify_pending'
-                    @cb_np = callback
-                when 'notify_init'
-                    @cb_ni = callback
-                when 'ready'
-                    @cb_r = callback
-                when 'error'
-                    @cb_e = callback
-        else
-            throw new Error("unsupported callback event")
-
-    show_data: =>
-        if not @s.supports_html5_storage()
-            return
-        total = 0
-        for i in [0..localStorage.length-1]
-            key = localStorage.key(i)
-            datastr = localStorage.getItem( key )
-            console.log "[#{key}]: #{datastr}"
-            total = total + 1
-        console.log "#{total} total"
-
-    _save_meta: ->
-        if not @s.supports_html5_storage()
-            return false
-        console.log "#{@name}: save_meta ccid:#{@data.ccid} cv:#{@data.last_cv}"
-        try
-            localStorage.setItem "#{@namespace}/ccid", @data.ccid
-            localStorage.setItem "#{@namespace}/last_cv", @data.last_cv
-        catch error
-            return false
-        return true
-
-    _load_meta: =>
-        if not @s.supports_html5_storage()
-            return
-        @data.ccid = localStorage.getItem "#{@namespace}/ccid"
-        @data.ccid ?= 1
-        @data.last_cv = localStorage.getItem "#{@namespace}/last_cv"
-        @data.last_cv ?= 0
-        console.log "#{@name}: load_meta ccid:#{@data.ccid} cv:#{@data.last_cv}"
-
-    _verify: (data) =>
-        if !('object' of data) or !('version' of data)
-            return false
-        if @jd.entries(data['object']) == 0
-
-            if @jd.entries(data['last']) > 0
-                return true
-            else
-                return false
-        else
-            if !(data['version']?)
-                return false
-        return true
-
-    _load_data: =>
-        if not @s.supports_html5_storage()
-            return
-        prefix = "#{@namespace}/e/"
-        p_len = prefix.length
-        loaded = 0
-        if localStorage.length == 0
-            return 0
-        for i in [0..localStorage.length-1]
-            key = localStorage.key(i)
-            if key? and key.substr(0, p_len) is prefix
-                id = key.substr(p_len, key.length-p_len)
-                try
-                    data = JSON.parse localStorage.getItem key
-                catch error
-                    data = null
-                if not data?
-                    continue
-                if @_verify data
-                    data['check'] = null
-                    @data.store[id] = data
-                    loaded = loaded + 1
-                else
-                    console.log "ignoring CORRUPT data: #{JSON.stringify(data)}"
-                    @_remove_entity id
-        return loaded
-
-    _save_entity: (id) =>
-        if not @s.supports_html5_storage()
-            return false
-        key = "#{@namespace}/e/#{id}"
-        store_data = @data.store[id]
-        datastr = JSON.stringify(store_data)
-        console.log "LSTORE:", key, datastr
-        try
-            localStorage.setItem key, datastr
-        catch error
-            return false
-        ret_data = JSON.parse localStorage.getItem key
-        if @jd.equals store_data, ret_data
-            console.log "saved #{key} len: #{datastr.length}"
-            return true
-        else
-            console.log "ERROR STORING ENTITY: store: #{JSON.stringify(store_data)}, retrieve: #{JSON.stringify(ret_data)}"
-            return false
-
-    _remove_entity: (id) =>
-        if not @s.supports_html5_storage()
-            return false
-        key = "#{@namespace}/e/#{id}"
-        try
-            localStorage.removeItem key
-        catch error
-            return false
-        return true
-
-    start: =>
-        console.log "#{@space}: started initialized: #{@initialized} authorized: #{@authorized}"
-        @namespace = "#{@username}:#{@space}"
-        @started = true
-        @first = false
-        if not @authorized
-            if @s.connected
-                @first = true
-                if 'limit' of @options
-                    index_query = "i:1:::#{@options['limit']}"
-                else
-                    index_query = "i:1:::40"
-                @irequest_time = @now()
-                @index_request = true
-                @notify_index = {}
-                opts =
-                    app_id:  @options.app_id
-                    token:  @options.token
-                    name:   @name
-                    clientid: @clientid
-                    build: @s.bversion
-                if not @initialized
-                    opts.cmd = index_query
-                @send("init:#{JSON.stringify(opts)}")
-                console.log "#{@name}: sent init #{JSON.stringify(opts)} waiting for auth"
-            else
-                console.log "#{@name}: waiting for connect"
-            return
-        if not @initialized
-            if not @first
-                @notify_index = {}
-                @_refresh_store()
-        else
-            console.log "#{@name}: retrieve changes from start"
-            @retrieve_changes()
-
-    on_data: (data) =>
-        if data.substr(0, 5) == "auth:"
-            user = data.substr(5)
-            if user is "expired"
-                console.log "auth expired"
-                @started = false
-                if @cb_e?
-                    @cb_e "auth"
-                return
-            else
-                @username = user
-                @authorized = true
-                if @initialized
-                    @start()
-        else if data.substr(0, 4) == "cv:?"
-            console.log "#{@name}: cv out of sync, refreshing index"
-            setTimeout => @_refresh_store()
-        else if data.substr(0, 2) == "c:"
-            changes = JSON.parse(data.substr(2))
-            if @data.last_cv is "0" and changes.length is 0 and not @cv_check
-                @cv_check = true
-                @_refresh_store()
-            @on_changes changes
-        else if data.substr(0, 2) == "i:"
-            console.log "#{@name}:  index msg received: #{@now() - @irequest_time}"
-            @on_index_page JSON.parse(data.substr(2))
-        else if data.substr(0, 2) == "e:"
-            key_end = data.indexOf("\n")
-            evkey = data.substr(2, key_end-2)
-            version = evkey.substr(evkey.lastIndexOf('.')+1)
-            key = evkey.substr(0, evkey.lastIndexOf('.'))
-            entitydata = data.substr(key_end+1)
-            if entitydata is "?"
-                @on_entity_version null, key, version
-            else
-                entity = JSON.parse(entitydata)
-                @on_entity_version entity['data'], key, version
-        else
-            console.log "unknown message: #{data}"
-
-    send: (message) =>
-        console.log "sending: #{@chan}:#{message}"
-        @s.send("#{@chan}:#{message}")
-
-    _refresh_store: =>
-        # load the index
-        console.log "#{@name}: _refresh_store(): loading index"
-        if 'limit' of @options
-            index_query = "i:1:::#{@options['limit']}"
-        else
-            index_query = "i:1:::40"
-        @send(index_query)
-        @irequest_time = @now()
-        @index_request = true
-        return
-
-    on_index_page: (response) =>
-        now = @now()
-        elapsed = now - @irequest_time
-        console.log "#{@name}: index response time: #{elapsed}"
-        console.log "#{@name}: on_index_page(): index page received, current= #{response['current']}"
-        console.log response
-
-        loaded = 0
-        for item in response['index']
-            @notify_index[item['id']] = false
-            loaded++
-            setTimeout do(item) =>
-                => @on_entity_version(item['d'], item['id'], item['v'])
-
-        if not ('mark' of response) or 'limit' of @options
-            @index_request = false
-            if 'current' of response
-                @data.last_cv = response['current']
-                @_save_meta()
-            else
-                @data.last_cv = 0
-            if loaded is 0
-                @_index_loaded()
-        else
-            @index_request = true
-            console.log "#{@name}: index last process time: #{@now() - now}, page_delay: #{@options['page_delay']}"
-            mark = response['mark']
-            page_req = (mark) =>
-                @send("i:1:#{mark}::100")
-                @irequest_time = @now()
-
-            setTimeout( (do(mark) =>
-                => page_req(mark)), @options['page_delay'])
-
-    on_index_error: =>
-        console.log "#{@name}: index doesnt exist or other error"
-
-    load_versions: (id, versions) =>
-        if not (id of @data.store)
-            return false
-        min = Math.max(@data.store[id]['version'] - (versions+1), 1)
-        for v in [min..@data.store[id]['version']-1]
-            console.log "#{@name}: loading version #{id}.#{v}"
-            @send("e:#{id}.#{v}")
-
-    get_version: (id, version) =>
-        evkey = "#{id}.#{version}"
-        @send("e:#{evkey}")
-
-    on_entity_version: (data, id, version) =>
-        console.log "#{@name}: on_entity_version(#{data}, #{id}, #{version})"
-        if data?
-            data_copy = @jd.deepCopy(data)
-        else
-            data_copy = null
-        if @initialized is false and @cb_ni?
-            notify_cb = @cb_ni
-        else
-            notify_cb = @cb_n
-        if id of @data.store and 'last' of @data.store[id] and @jd.entries(@data.store[id]['last'])
-            data_copy = @jd.deepCopy(@data.store[id]['last'])
-
-            notify_cb id, data_copy, null
-            @notify_index[id] = true
-            @_check_update(id)
-
-        else if id of @data.store and version < @data.store[id]['version']
-            @cb_nv id, data_copy, version
-        else
-            if not (id of @data.store)
-                @data.store[id] = {}
-            @data.store[id]['id'] = id
-            @data.store[id]['object'] = data
-            @data.store[id]['version'] = parseInt(version)
-
-            notify_cb id, data_copy, version
-            @notify_index[id] = true
-            to_load = 0
-            for own nid of @notify_index
-                if @notify_index[nid] is false
-                    to_load++
-            if to_load is 0 and @index_request is false
-                @_index_loaded()
-
-    _index_loaded: =>
-        console.log "#{@name}: index loaded, initialized: #{@initialized}"
-        if @initialized is false
-            @cb_r()
-        @initialized = true
-        console.log "#{@name}: retrieve changes from index loaded"
-        @retrieve_changes()
-
-    # id: id of object
-    # new_object: latest server version of object (includes latest diff)
-    # orig_object: the previous server version of object that the client had
-    # diff: the newly received incoming diff (orig_object + diff = new_object)
-    _notify_client: (key, new_object, orig_object, diff, version) =>
-        console.log "#{@name}: _notify_client(#{key}, #{new_object}, #{orig_object}, #{JSON.stringify(diff)})"
-        if not @cb_l?
-            console.log "#{@name}: no get callback, notifying without transform"
-            @cb_n key, new_object, version
-            return
-
-        c_object = @cb_l key
-        t_object = null
-        t_diff = null
-        cursor = null
-        offsets = []
-
-        if @jd.typeOf(c_object) is 'array'
-            element = c_object[2]
-            fieldname = c_object[1]
-            c_object = c_object[0]
-            cursor = @s._captureCursor element
-            if cursor
-                offsets[0] = cursor['startOffset']
-                if 'endOffset' of cursor
-                    offsets[1] = cursor['endOffset']
-
-        if c_object? and orig_object?
+	constructor: (@s, @name, b_opts) ->
+		@jd = @s.jd
+		@options = @jd.deepCopy @s.options
+		for own name, val of b_opts
+			@options[name] = val
+		@chan = @options['n']
+		@space = "#{@options['app_id']}/#{@name}"
+		@username = @options['username']
+		@namespace = "#{@username}:#{@space}"
+
+		@clientid = null
+		try
+			@clientid = localStorage.getItem "#{@namespace}/clientid"
+		catch error
+			@clientid = null
+		if not @clientid? or @clientid.indexOf("sjs") != 0
+			@clientid = "sjs-#{@s.bversion}-#{@uuid(5)}"
+			try
+				localStorage.setItem "#{@namespace}/clientid", @clientid
+			catch error
+				console.log "#{@name}: couldnt set clientid"
+
+		@cb_events = ['notify', 'notify_init', 'notify_version', 'local', 'get', 'ready', 'notify_pending', 'error']
+		@cbs = {}
+		@cb_e = @cb_l = @cb_ni = @cb_np = null
+		@cb_n = @cb_nv = @cb_r = ->
+
+		@initialized = false
+		@authorized = false
+		@data =
+			last_cv: 0
+			ccid: @uuid()
+			store: {}
+			send_queue: []
+			send_queue_timer: null
+
+		@started = false
+
+		if not ('nostore' of @options)
+			@_load_meta()
+			@loaded = @_load_data()
+			console.log "#{@name}: localstorage loaded #{@loaded} entities"
+		else
+			console.log "#{@name}: not loading from localstorage"
+			@loaded = 0
+
+		@_last_pending = null
+		@_send_backoff = 15000
+		@_backoff_max = 120000
+		@_backoff_min = 15000
+
+		console.log "#{@namespace}: bucket created: opts: #{JSON.stringify(@options)}"
+
+	S4: ->
+		(((1+Math.random())*0x10000)|0).toString(16).substring(1)
+
+	uuid: (n) ->
+		n = n || 8
+		s = @S4()
+		while n -= 1
+			s += @S4()
+		return s
+
+	now: ->
+		new Date().getTime()
+
+	on: (event, callback) =>
+		if event in @cb_events
+			@cbs[event] = callback
+			switch event
+				when 'get', 'local'
+					@cb_l = callback
+				when 'notify'
+					@cb_n = callback
+				when 'notify_version'
+					@cb_nv = callback
+				when 'notify_pending'
+					@cb_np = callback
+				when 'notify_init'
+					@cb_ni = callback
+				when 'ready'
+					@cb_r = callback
+				when 'error'
+					@cb_e = callback
+		else
+			throw new Error("unsupported callback event")
+
+	show_data: =>
+		if not @s.supports_html5_storage()
+			return
+		total = 0
+		for i in [0..localStorage.length-1]
+			key = localStorage.key(i)
+			datastr = localStorage.getItem( key )
+			console.log "[#{key}]: #{datastr}"
+			total = total + 1
+		console.log "#{total} total"
+
+	_save_meta: ->
+		if not @s.supports_html5_storage()
+			return false
+		console.log "#{@name}: save_meta ccid:#{@data.ccid} cv:#{@data.last_cv}"
+		try
+			localStorage.setItem "#{@namespace}/ccid", @data.ccid
+			localStorage.setItem "#{@namespace}/last_cv", @data.last_cv
+		catch error
+			return false
+		return true
+
+	_load_meta: =>
+		if not @s.supports_html5_storage()
+			return
+		@data.ccid = localStorage.getItem "#{@namespace}/ccid"
+		@data.ccid ?= 1
+		@data.last_cv = localStorage.getItem "#{@namespace}/last_cv"
+		@data.last_cv ?= 0
+		console.log "#{@name}: load_meta ccid:#{@data.ccid} cv:#{@data.last_cv}"
+
+	_verify: (data) =>
+		if !('object' of data) or !('version' of data)
+			return false
+		if @jd.entries(data['object']) == 0
+
+			if @jd.entries(data['last']) > 0
+				return true
+			else
+				return false
+		else
+			if !(data['version']?)
+				return false
+		return true
+
+	_load_data: =>
+		if not @s.supports_html5_storage()
+			return
+		prefix = "#{@namespace}/e/"
+		p_len = prefix.length
+		loaded = 0
+		if localStorage.length == 0
+			return 0
+		for i in [0..localStorage.length-1]
+			key = localStorage.key(i)
+			if key? and key.substr(0, p_len) is prefix
+				id = key.substr(p_len, key.length-p_len)
+				try
+					data = JSON.parse localStorage.getItem key
+				catch error
+					data = null
+				if not data?
+					continue
+				if @_verify data
+					data['check'] = null
+					@data.store[id] = data
+					loaded = loaded + 1
+				else
+					console.log "ignoring CORRUPT data: #{JSON.stringify(data)}"
+					@_remove_entity id
+		return loaded
+
+	_save_entity: (id) =>
+		if not @s.supports_html5_storage()
+			return false
+		key = "#{@namespace}/e/#{id}"
+		store_data = @data.store[id]
+		datastr = JSON.stringify(store_data)
+		console.log "LSTORE:", key, datastr
+		try
+			localStorage.setItem key, datastr
+		catch error
+			return false
+		ret_data = JSON.parse localStorage.getItem key
+		if @jd.equals store_data, ret_data
+			console.log "saved #{key} len: #{datastr.length}"
+			return true
+		else
+			console.log "ERROR STORING ENTITY: store: #{JSON.stringify(store_data)}, retrieve: #{JSON.stringify(ret_data)}"
+			return false
+
+	_remove_entity: (id) =>
+		if not @s.supports_html5_storage()
+			return false
+		key = "#{@namespace}/e/#{id}"
+		try
+			localStorage.removeItem key
+		catch error
+			return false
+		return true
+
+	start: =>
+		console.log "#{@space}: started initialized: #{@initialized} authorized: #{@authorized}"
+		@namespace = "#{@username}:#{@space}"
+		@started = true
+		@first = false
+		if not @authorized
+			if @s.connected
+				@first = true
+				if 'limit' of @options
+					index_query = "i:1:::#{@options['limit']}"
+				else
+					index_query = "i:1:::40"
+				@irequest_time = @now()
+				@index_request = true
+				@notify_index = {}
+				opts =
+					app_id:  @options.app_id
+					token:  @options.token
+					name:   @name
+					clientid: @clientid
+					build: @s.bversion
+				if not @initialized
+					opts.cmd = index_query
+				@send("init:#{JSON.stringify(opts)}")
+				console.log "#{@name}: sent init #{JSON.stringify(opts)} waiting for auth"
+			else
+				console.log "#{@name}: waiting for connect"
+			return
+		if not @initialized
+			if not @first
+				@notify_index = {}
+				@_refresh_store()
+		else
+			console.log "#{@name}: retrieve changes from start"
+			@retrieve_changes()
+
+	on_data: (data) =>
+		if data.substr(0, 5) == "auth:"
+			user = data.substr(5)
+			if user is "expired"
+				console.log "auth expired"
+				@started = false
+				if @cb_e?
+					@cb_e "auth"
+				return
+			else
+				@username = user
+				@authorized = true
+				if @initialized
+					@start()
+		else if data.substr(0, 4) == "cv:?"
+			console.log "#{@name}: cv out of sync, refreshing index"
+			setTimeout => @_refresh_store()
+		else if data.substr(0, 2) == "c:"
+			changes = JSON.parse(data.substr(2))
+			if @data.last_cv is "0" and changes.length is 0 and not @cv_check
+				@cv_check = true
+				@_refresh_store()
+			@on_changes changes
+		else if data.substr(0, 2) == "i:"
+			console.log "#{@name}:  index msg received: #{@now() - @irequest_time}"
+			@on_index_page JSON.parse(data.substr(2))
+		else if data.substr(0, 2) == "e:"
+			key_end = data.indexOf("\n")
+			evkey = data.substr(2, key_end-2)
+			version = evkey.substr(evkey.lastIndexOf('.')+1)
+			key = evkey.substr(0, evkey.lastIndexOf('.'))
+			entitydata = data.substr(key_end+1)
+			if entitydata is "?"
+				@on_entity_version null, key, version
+			else
+				entity = JSON.parse(entitydata)
+				@on_entity_version entity['data'], key, version
+		else
+			console.log "unknown message: #{data}"
+
+	send: (message) =>
+		console.log "sending: #{@chan}:#{message}"
+		@s.send("#{@chan}:#{message}")
+
+	_refresh_store: =>
+		# load the index
+		console.log "#{@name}: _refresh_store(): loading index"
+		if 'limit' of @options
+			index_query = "i:1:::#{@options['limit']}"
+		else
+			index_query = "i:1:::40"
+		@send(index_query)
+		@irequest_time = @now()
+		@index_request = true
+		return
+
+	on_index_page: (response) =>
+		now = @now()
+		elapsed = now - @irequest_time
+		console.log "#{@name}: index response time: #{elapsed}"
+		console.log "#{@name}: on_index_page(): index page received, current= #{response['current']}"
+		console.log response
+
+		loaded = 0
+		for item in response['index']
+			@notify_index[item['id']] = false
+			loaded++
+			setTimeout do(item) =>
+				=> @on_entity_version(item['d'], item['id'], item['v'])
+
+		if not ('mark' of response) or 'limit' of @options
+			@index_request = false
+			if 'current' of response
+				@data.last_cv = response['current']
+				@_save_meta()
+			else
+				@data.last_cv = 0
+			if loaded is 0
+				@_index_loaded()
+		else
+			@index_request = true
+			console.log "#{@name}: index last process time: #{@now() - now}, page_delay: #{@options['page_delay']}"
+			mark = response['mark']
+			page_req = (mark) =>
+				@send("i:1:#{mark}::100")
+				@irequest_time = @now()
+
+			setTimeout( (do(mark) =>
+				=> page_req(mark)), @options['page_delay'])
+
+	on_index_error: =>
+		console.log "#{@name}: index doesnt exist or other error"
+
+	load_versions: (id, versions) =>
+		if not (id of @data.store)
+			return false
+		min = Math.max(@data.store[id]['version'] - (versions+1), 1)
+		for v in [min..@data.store[id]['version']-1]
+			console.log "#{@name}: loading version #{id}.#{v}"
+			@send("e:#{id}.#{v}")
+
+	get_version: (id, version) =>
+		evkey = "#{id}.#{version}"
+		@send("e:#{evkey}")
+
+	on_entity_version: (data, id, version) =>
+		console.log "#{@name}: on_entity_version(#{data}, #{id}, #{version})"
+		if data?
+			data_copy = @jd.deepCopy(data)
+		else
+			data_copy = null
+		if @initialized is false and @cb_ni?
+			notify_cb = @cb_ni
+		else
+			notify_cb = @cb_n
+		if id of @data.store and 'last' of @data.store[id] and @jd.entries(@data.store[id]['last'])
+			data_copy = @jd.deepCopy(@data.store[id]['last'])
+
+			notify_cb id, data_copy, null
+			@notify_index[id] = true
+			@_check_update(id)
+
+		else if id of @data.store and version < @data.store[id]['version']
+			@cb_nv id, data_copy, version
+		else
+			if not (id of @data.store)
+				@data.store[id] = {}
+			@data.store[id]['id'] = id
+			@data.store[id]['object'] = data
+			@data.store[id]['version'] = parseInt(version)
+
+			notify_cb id, data_copy, version
+			@notify_index[id] = true
+			to_load = 0
+			for own nid of @notify_index
+				if @notify_index[nid] is false
+					to_load++
+			if to_load is 0 and @index_request is false
+				@_index_loaded()
+
+	_index_loaded: =>
+		console.log "#{@name}: index loaded, initialized: #{@initialized}"
+		if @initialized is false
+			@cb_r()
+		@initialized = true
+		console.log "#{@name}: retrieve changes from index loaded"
+		@retrieve_changes()
+
+	# id: id of object
+	# new_object: latest server version of object (includes latest diff)
+	# orig_object: the previous server version of object that the client had
+	# diff: the newly received incoming diff (orig_object + diff = new_object)
+	_notify_client: (key, new_object, orig_object, diff, version) =>
+		console.log "#{@name}: _notify_client(#{key}, #{new_object}, #{orig_object}, #{JSON.stringify(diff)})"
+		if not @cb_l?
+			console.log "#{@name}: no get callback, notifying without transform"
+			@cb_n key, new_object, version
+			return
+
+		c_object = @cb_l key
+		t_object = null
+		t_diff = null
+		cursor = null
+		offsets = []
+
+		if @jd.typeOf(c_object) is 'array'
+			element = c_object[2]
+			fieldname = c_object[1]
+			c_object = c_object[0]
+			cursor = @s._captureCursor element
+			if cursor
+				offsets[0] = cursor['startOffset']
+				if 'endOffset' of cursor
+					offsets[1] = cursor['endOffset']
+
+		if c_object? and orig_object?
 #            console.log "going to do object diff new diff: #{JSON.stringify(diff)}"

-            o_diff = @jd.object_diff orig_object, c_object
-            console.log "client/server version diff: #{JSON.stringify(o_diff)}"
-            if @jd.entries(o_diff) is 0
-                console.log "local diff 0 entries"
-                t_diff = diff
-                t_object = orig_object
-            else
-                console.log "o_diff"
-                console.log o_diff
-                console.log "orig_object"
-                console.log orig_object
-                console.log "c_object"
-                console.log c_object
-                console.log "client modified doing transform"
-                # client has local modifications, so we need to transform to apply new
-                # changes to client's data
-                # this transforms >o_diff< which is the local client modification with
-                # respect to >diff< which is the new incoming change received from server
-                # orig_object + diff = new_object
-                # orig_object + o_diff (clients local modifications) = c_object (current
-                # client data)
-                # new client data (that includes both diffs) = orig_object + diff + T(o_diff)
-                # new client data = (orig_object + diff) + T(o_diff)
-                # new client data = new_object + T(o_diff)
-                # new client data = new_object + t_diff
-                t_diff = @jd.transform_object_diff o_diff, diff, orig_object
-                t_object = new_object
-
-            if cursor
-                new_data = @jd.apply_object_diff_with_offsets t_object, t_diff, fieldname, offsets
-
-                if element? and 'value' of element
-                    element['value'] = new_data[fieldname]
-
-                cursor['startOffset'] = offsets[0]
-                if offsets.length > 1
-                    cursor['endOffset'] = offsets[1]
-                    if cursor['startOffset'] >= cursor['endOffset']
-                        cursor['collapsed'] = true
-                @s._restoreCursor element, cursor
-            else
-                console.log "in regular apply_object_diff"
-                console.log "t_object"
-                console.log t_object
-                console.log "t_diff"
-                console.log t_diff
-                new_data = @jd.apply_object_diff t_object, t_diff
+			o_diff = @jd.object_diff orig_object, c_object
+			console.log "client/server version diff: #{JSON.stringify(o_diff)}"
+			if @jd.entries(o_diff) is 0
+				console.log "local diff 0 entries"
+				t_diff = diff
+				t_object = orig_object
+			else
+				console.log "o_diff"
+				console.log o_diff
+				console.log "orig_object"
+				console.log orig_object
+				console.log "c_object"
+				console.log c_object
+				console.log "client modified doing transform"
+				# client has local modifications, so we need to transform to apply new
+				# changes to client's data
+				# this transforms >o_diff< which is the local client modification with
+				# respect to >diff< which is the new incoming change received from server
+				# orig_object + diff = new_object
+				# orig_object + o_diff (clients local modifications) = c_object (current
+				# client data)
+				# new client data (that includes both diffs) = orig_object + diff + T(o_diff)
+				# new client data = (orig_object + diff) + T(o_diff)
+				# new client data = new_object + T(o_diff)
+				# new client data = new_object + t_diff
+				t_diff = @jd.transform_object_diff o_diff, diff, orig_object
+				t_object = new_object
+
+			if cursor
+				new_data = @jd.apply_object_diff_with_offsets t_object, t_diff, fieldname, offsets
+
+				if element? and 'value' of element
+					element['value'] = new_data[fieldname]
+
+				cursor['startOffset'] = offsets[0]
+				if offsets.length > 1
+					cursor['endOffset'] = offsets[1]
+					if cursor['startOffset'] >= cursor['endOffset']
+    					cursor['collapsed'] = true
+				@s._restoreCursor element, cursor
+			else
+				console.log "in regular apply_object_diff"
+				console.log "t_object"
+				console.log t_object
+				console.log "t_diff"
+				console.log t_diff
+				new_data = @jd.apply_object_diff t_object, t_diff
 #                console.log "transformed diff: #{JSON.stringify(t_diff)}"
 #            console.log "#{@name}: notifying client of new data for #{key}: #{JSON.stringify(new_data)}"
-            @cb_n key, new_data, version
-        else if new_object
-            @cb_n key, new_object, version
-        else
-            @cb_n key, null, null
-
-    _check_update: (id) =>
-        console.log "#{@name}: _check_update(#{id})"
-        if not (id of @data.store)
-            return false
-        s_data = @data.store[id]
-
-        if s_data['change']
-            found = false
-            for change in @data.send_queue
-                if change['id'] is s_data['change']['id'] and change['ccid'] is s_data['change']['ccid']
-                    found = true
-            if !found
-                @_queue_change s_data['change']
-                return true
-            return false
-
-        if s_data['check']?
-            return false
-
-        if s_data['last']? and @jd.entries(s_data['last']) > 0
-            if @jd.equals s_data['object'], s_data['last']
-                s_data['last'] = null
-                @_remove_entity id
-                return false
-
-        change = @_make_change id
-        if change?
-            s_data['change'] = change
-            @_queue_change change
-        else
-            @_remove_entity id
-        return true
-
-    update: (id, object) =>
-        if arguments.length is 1
-            if @cb_l?
-                object = @cb_l id
-                if @jd.typeOf(object) is 'array'
-                    object = object[0]
-            else
-                throw new Error("missing 'local' callback")
-        console.log "#{@name}: update(#{id})"
-
-        if not id? and not object?
-            return false
-        if id?
-            if id.length is 0 or id.indexOf('/') isnt -1
-                return false
-        else
-            id = @uuid()
-        if not (id of @data.store)
-            @data.store[id] =
-                'id'        :   id
-                'object'    :   {}
-                'version'   :   null
-                'change'    :   null
-                'check'     :   null
-        s_data = @data.store[id]
-        s_data['last'] = @jd.deepCopy(object)
-        s_data['modified'] = @s._time()
-        @_save_entity id
-
-        for change in @data.send_queue
-            if String(id) is change['id']
-                console.log "#{@name}: update(#{id}) found pending change, aborting"
-                return null
-
-        if s_data['check']?
-            clearTimeout(s_data['check'])
-
-        s_data['check'] = setTimeout((do(id, s_data) =>
-            =>
-                s_data['check'] = null
-                s_data['change'] = @_make_change id
-                s_data['last'] = null
-                @_save_entity id
-                @_queue_change s_data['change']
-                ), @options['update_delay'])
-        return id
-
-        # create change objects
-    _make_change: (id) =>
+			@cb_n key, new_data, version
+		else if new_object
+			@cb_n key, new_object, version
+		else
+			@cb_n key, null, null
+
+	_check_update: (id) =>
+		console.log "#{@name}: _check_update(#{id})"
+		if not (id of @data.store)
+			return false
+		s_data = @data.store[id]
+
+		if s_data['change']
+			found = false
+			for change in @data.send_queue
+				if change['id'] is s_data['change']['id'] and change['ccid'] is s_data['change']['ccid']
+					found = true
+			if !found
+				@_queue_change s_data['change']
+				return true
+			return false
+
+		if s_data['check']?
+			return false
+
+		if s_data['last']? and @jd.entries(s_data['last']) > 0
+			if @jd.equals s_data['object'], s_data['last']
+				s_data['last'] = null
+				@_remove_entity id
+				return false
+
+		change = @_make_change id
+		if change?
+			s_data['change'] = change
+			@_queue_change change
+		else
+			@_remove_entity id
+		return true
+
+	update: (id, object) =>
+		if arguments.length is 1
+			if @cb_l?
+				object = @cb_l id
+				if @jd.typeOf(object) is 'array'
+					object = object[0]
+			else
+				throw new Error("missing 'local' callback")
+		console.log "#{@name}: update(#{id})"
+
+		if not id? and not object?
+			return false
+		if id?
+			if id.length is 0 or id.indexOf('/') isnt -1
+				return false
+		else
+			id = @uuid()
+		if not (id of @data.store)
+			@data.store[id] =
+				'id'        :   id
+				'object'    :   {}
+				'version'   :   null
+				'change'    :   null
+				'check'     :   null
+		s_data = @data.store[id]
+		s_data['last'] = @jd.deepCopy(object)
+		s_data['modified'] = @s._time()
+		@_save_entity id
+
+		for change in @data.send_queue
+			if String(id) is change['id']
+				console.log "#{@name}: update(#{id}) found pending change, aborting"
+				return null
+
+		if s_data['check']?
+			clearTimeout(s_data['check'])
+
+		s_data['check'] = setTimeout((do(id, s_data) =>
+			=>
+				s_data['check'] = null
+				s_data['change'] = @_make_change id
+				s_data['last'] = null
+				@_save_entity id
+				@_queue_change s_data['change']
+				), @options['update_delay'])
+		return id
+
+		# create change objects
+	_make_change: (id) =>
 #        console.log "#{@name}: _make_change(#{id})"
-        s_data = @data.store[id]
-
-        change =
-            'id'    :   String(id)
-            'ccid'  :   @uuid()
-
-        if not @initialized
-            if s_data['last']?
-                c_object = s_data['last']
-            else
-                return null
-        else
-            if @cb_l?
-                c_object = @cb_l id
-                if @jd.typeOf(c_object) is 'array'
-                    c_object = c_object[0]
-            else
-                if s_data['last']?
-                    c_object = s_data['last']
-                else
-                    return null
-
-        if s_data['version']?
-            change['sv'] = s_data['version']
-
-        if c_object is null and s_data['version']?
-            change['o'] = '-'
-            console.log "#{@name}: deletion requested for #{id}"
-        else if c_object? and s_data['object']?
-            change['o'] = 'M'
-            if 'sendfull' of s_data
-                change['d'] = @jd.deepCopy c_object
-                delete s_data['sendfull']
-            else
-                change['v'] = @jd.object_diff s_data['object'], c_object
-                if @jd.entries(change['v']) is 0
-                    change = null
-        else
-            change = null
+		s_data = @data.store[id]
+
+		change =
+			'id'    :   String(id)
+			'ccid'  :   @uuid()
+
+		if not @initialized
+			if s_data['last']?
+				c_object = s_data['last']
+			else
+				return null
+		else
+			if @cb_l?
+				c_object = @cb_l id
+				if @jd.typeOf(c_object) is 'array'
+					c_object = c_object[0]
+			else
+				if s_data['last']?
+					c_object = s_data['last']
+				else
+					return null
+
+		if s_data['version']?
+			change['sv'] = s_data['version']
+
+		if c_object is null and s_data['version']?
+			change['o'] = '-'
+			console.log "#{@name}: deletion requested for #{id}"
+		else if c_object? and s_data['object']?
+			change['o'] = 'M'
+			if 'sendfull' of s_data
+				change['d'] = @jd.deepCopy c_object
+				delete s_data['sendfull']
+			else
+				change['v'] = @jd.object_diff s_data['object'], c_object
+				if @jd.entries(change['v']) is 0
+					change = null
+		else
+			change = null
 #        console.log "_make_change(#{id}) returning: #{JSON.stringify(change)}"
-        return change
-
-    _queue_change: (change) =>
-        if not change?
-            return
-
-        console.log "_queue_change(#{change['id']}:#{change['ccid']}): sending"
-        @data.send_queue.push change
-        @send("c:#{JSON.stringify(change)}")
-        @_check_pending()
-
-        if @data.send_queue_timer?
-            clearTimeout(@data.send_queue_timer)
-
-        @data.send_queue_timer = setTimeout @_send_changes, @_send_backoff
-
-    _send_changes: =>
-        if @data.send_queue.length is 0
-            console.log "#{@name}: send_queue empty, done"
-            @data.send_queue_timer = null
-            return
-        if not @s.connected
-            console.log "#{@name}: _send_changes: not connected"
-        else
-            for change in @data.send_queue
-                console.log "#{@name}: sending change: #{JSON.stringify(change)}"
-                @send("c:#{JSON.stringify(change)}")
-
-        @_send_backoff = @_send_backoff * 2
-        if @_send_backoff > @_backoff_max
-            @_send_backoff = @_backoff_max
-
-        @data.send_queue_timer = setTimeout @_send_changes, @_send_backoff
-
-    retrieve_changes: =>
-        console.log "#{@name}: requesting changes since cv:#{@data.last_cv}"
-        @send("cv:#{@data.last_cv}")
+		return change
+
+	_queue_change: (change) =>
+		if not change?
+			return
+
+		console.log "_queue_change(#{change['id']}:#{change['ccid']}): sending"
+		@data.send_queue.push change
+		@send("c:#{JSON.stringify(change)}")
+		@_check_pending()
+
+		if @data.send_queue_timer?
+			clearTimeout(@data.send_queue_timer)
+
+		@data.send_queue_timer = setTimeout @_send_changes, @_send_backoff
+
+	_send_changes: =>
+		if @data.send_queue.length is 0
+			console.log "#{@name}: send_queue empty, done"
+			@data.send_queue_timer = null
+			return
+		if not @s.connected
+			console.log "#{@name}: _send_changes: not connected"
+		else
+			for change in @data.send_queue
+				console.log "#{@name}: sending change: #{JSON.stringify(change)}"
+				@send("c:#{JSON.stringify(change)}")
+
+		@_send_backoff = @_send_backoff * 2
+		if @_send_backoff > @_backoff_max
+			@_send_backoff = @_backoff_max
+
+		@data.send_queue_timer = setTimeout @_send_changes, @_send_backoff
+
+	retrieve_changes: =>
+		console.log "#{@name}: requesting changes since cv:#{@data.last_cv}"
+		@send("cv:#{@data.last_cv}")
 #        @sio.send("cv:#{@data.last_cv}")
-        return
-
-    on_changes: (response) =>
-        check_updates = []
-        reload_needed = false
-        @_send_backoff = @_backoff_min
-        console.log "#{@name}: on_changes(): response="
-        console.log response
-        for change in response
-            id = change['id']
-            console.log "#{@name}: processing id=#{id}"
-            pending_to_delete = []
-            for pending in @data.send_queue
-                if change['clientid'] is @clientid and id is pending['id']
+		return
+
+	on_changes: (response) =>
+		check_updates = []
+		reload_needed = false
+		@_send_backoff = @_backoff_min
+		console.log "#{@name}: on_changes(): response="
+		console.log response
+		for change in response
+			id = change['id']
+			console.log "#{@name}: processing id=#{id}"
+			pending_to_delete = []
+			for pending in @data.send_queue
+				if change['clientid'] is @clientid and id is pending['id']
 #                    console.log "#{@name}: deleting change for id #{id}"
-                    change['local'] = true
-                    pending_to_delete.push pending
-                    check_updates.push id
-            for pd in pending_to_delete
-                @data.store[pd['id']]['change'] = null
-                @_save_entity pd['id']
-                @data.send_queue = (p for p in @data.send_queue when p isnt pd)
-            if pending_to_delete.length > 0
-                @_check_pending()
+					change['local'] = true
+					pending_to_delete.push pending
+					check_updates.push id
+			for pd in pending_to_delete
+				@data.store[pd['id']]['change'] = null
+				@_save_entity pd['id']
+				@data.send_queue = (p for p in @data.send_queue when p isnt pd)
+			if pending_to_delete.length > 0
+				@_check_pending()
 #            console.log "#{@name}: send queue: #{JSON.stringify(@data.send_queue)}"

-            if 'error' of change
-                switch change['error']
-                    when 412
-                        console.log "#{@name}: on_changes(): empty change, dont check"
-                        idx = check_updates.indexOf(change['id'])
-                        if idx > -1
-                            check_updates.splice(idx, 1)
-                    when 409
-                        console.log "#{@name}: on_changes(): duplicate change, ignoring"
-                    when 405
-                        console.log "#{@name}: on_changes(): bad version"
-                        if change['id'] of @data.store
-                            @data.store[change['id']]['version'] = null
-                        reload_needed = true
-                    when 440
-                        console.log "#{@name}: on_change(): bad diff, sending full object"
-                        @data.store[id]['sendfull'] = true
-                    else
-                        console.log "#{@name}: error for last change, reloading"
-                        if change['id'] of @data.store
-                            @data.store[change['id']]['version'] = null
-                        reload_needed = true
-            else
-                op = change['o']
-                if op is '-'
-                    delete @data.store[id]
-                    @_remove_entity id
-                    if not ('local' of change)
+			if 'error' of change
+				switch change['error']
+					when 412
+    					console.log "#{@name}: on_changes(): empty change, dont check"
+    					idx = check_updates.indexOf(change['id'])
+    					if idx > -1
+        					check_updates.splice(idx, 1)
+					when 409
+    					console.log "#{@name}: on_changes(): duplicate change, ignoring"
+					when 405
+    					console.log "#{@name}: on_changes(): bad version"
+    					if change['id'] of @data.store
+        					@data.store[change['id']]['version'] = null
+    					reload_needed = true
+					when 440
+    					console.log "#{@name}: on_change(): bad diff, sending full object"
+    					@data.store[id]['sendfull'] = true
+					else
+    					console.log "#{@name}: error for last change, reloading"
+    					if change['id'] of @data.store
+        					@data.store[change['id']]['version'] = null
+    					reload_needed = true
+			else
+				op = change['o']
+				if op is '-'
+					delete @data.store[id]
+					@_remove_entity id
+					if not ('local' of change)
 #                        setTimeout do(change) =>
 #                            => @_notify_client change['id'], null, null, null
-                        @_notify_client change['id'], null, null, null, null
-
-                else if op is 'M'
-                    s_data = @data.store[id]
-                    if ('sv' of change and s_data? and s_data['version']? and s_data['version'] == change['sv']) or !('sv' of change) or (change['ev'] == 1)
-                        if not s_data?
-                            @data.store[id] =
-                                'id'        :   id
-                                'object'    :   {}
-                                'version'   :   null
-                                'last'      :   null
-                                'change'    :   null
-                                'check'     :   null
-                            s_data = @data.store[id]
+    					@_notify_client change['id'], null, null, null, null
+
+				else if op is 'M'
+					s_data = @data.store[id]
+					if ('sv' of change and s_data? and s_data['version']? and s_data['version'] == change['sv']) or !('sv' of change) or (change['ev'] == 1)
+    					if not s_data?
+        					@data.store[id] =
+            					'id'        :   id
+            					'object'    :   {}
+            					'version'   :   null
+            					'last'      :   null
+            					'change'    :   null
+            					'check'     :   null
+        					s_data = @data.store[id]
 #                        console.log "#{@name}: processing modify for #{JSON.stringify(s_data)}"
-                        orig_object = @jd.deepCopy s_data['object']
-                        s_data['object'] = @jd.apply_object_diff s_data['object'], change['v']
-                        s_data['version'] = change['ev']
-
-                        new_object = @jd.deepCopy s_data['object']
-
-                        if not ('local' of change)
+    					orig_object = @jd.deepCopy s_data['object']
+    					s_data['object'] = @jd.apply_object_diff s_data['object'], change['v']
+    					s_data['version'] = change['ev']
+
+    					new_object = @jd.deepCopy s_data['object']
+
+    					if not ('local' of change)
 #                            setTimeout do(change, new_object, orig_object) =>
 #                                => @_notify_client change['id'], new_object, orig_object, change['v']
-                            @_notify_client change['id'], new_object, orig_object, change['v'], change['ev']
-                    else if s_data? and s_data['version']? and change['ev'] <= s_data['version']
-                        console.log "#{@name}: old or duplicate change received, ignoring, change.ev=#{change['ev']}, s_data.version:#{s_data['version']}"
-                    else
-                        if s_data?
-                            console.log "#{@name}: version mismatch couldnt apply change, change.ev:#{change['ev']}, s_data.version:#{s_data['version']}"
-                        else
-                            console.log "#{@name}: version mismatch couldnt apply change, change.ev:#{change['ev']}, s_data null"
-                        if s_data?
-                            @data.store[id]['version'] = null
-                        reload_needed = true
-                else
-                    console.log "#{@name}: no operation found for change"
-                if not reload_needed
-                    @data.last_cv = change['cv']
-                    @_save_meta()
-                    console.log "#{@name}: checkpoint cv=#{@data.last_cv} ccid=#{@data.ccid}"
-        if reload_needed
-            console.log "#{@name}: reload needed, refreshing store"
-            setTimeout => @_refresh_store()
-        else
-            for id in check_updates
-                do (id) => setTimeout (=> @_check_update id), @options['update_delay']
-        return
-
-    pending: =>
-        x = (change['id'] for change in @data.send_queue)
-        console.log "#{@name}: pending: #{JSON.stringify(x)}"
-        (change['id'] for change in @data.send_queue)
-
-    _check_pending: =>
-        if @cb_np?
-            curr_pending = @pending()
-            diff = true
-            if @_last_pending
-                diff = false
-                if @_last_pending.length is curr_pending.length
-                    for x in @_last_pending
-                        if curr_pending.indexOf(x) is -1
-                            diff = true
-                else
-                    diff = true
-            if diff
-                @_last_pending = curr_pending
-                @cb_np curr_pending
+        					@_notify_client change['id'], new_object, orig_object, change['v'], change['ev']
+					else if s_data? and s_data['version']? and change['ev'] <= s_data['version']
+    					console.log "#{@name}: old or duplicate change received, ignoring, change.ev=#{change['ev']}, s_data.version:#{s_data['version']}"
+					else
+    					if s_data?
+        					console.log "#{@name}: version mismatch couldnt apply change, change.ev:#{change['ev']}, s_data.version:#{s_data['version']}"
+    					else
+        					console.log "#{@name}: version mismatch couldnt apply change, change.ev:#{change['ev']}, s_data null"
+    					if s_data?
+        					@data.store[id]['version'] = null
+    					reload_needed = true
+				else
+					console.log "#{@name}: no operation found for change"
+				if not reload_needed
+					@data.last_cv = change['cv']
+					@_save_meta()
+					console.log "#{@name}: checkpoint cv=#{@data.last_cv} ccid=#{@data.ccid}"
+		if reload_needed
+			console.log "#{@name}: reload needed, refreshing store"
+			setTimeout => @_refresh_store()
+		else
+			for id in check_updates
+				do (id) => setTimeout (=> @_check_update id), @options['update_delay']
+		return
+
+	pending: =>
+		x = (change['id'] for change in @data.send_queue)
+		console.log "#{@name}: pending: #{JSON.stringify(x)}"
+		(change['id'] for change in @data.send_queue)
+
+	_check_pending: =>
+		if @cb_np?
+			curr_pending = @pending()
+			diff = true
+			if @_last_pending
+				diff = false
+				if @_last_pending.length is curr_pending.length
+					for x in @_last_pending
+    					if curr_pending.indexOf(x) is -1
+        					diff = true
+				else
+					diff = true
+			if diff
+				@_last_pending = curr_pending
+				@cb_np curr_pending

 class simperium
-    lowerstrip: (s) ->
-        s = s.toLowerCase()
-        if String::trim? then s.trim() else s.replace /^\s+|\s+$/g, ""
-
-    _time: ->
-        d = new Date()
-        Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds())/1000
-
-    supports_html5_storage: ->
-        try
-            return 'localStorage' of window && window['localStorage'] != null
-        catch error
-            return false
-
-    constructor: (@app_id, @options) ->
-        @bversion = 2012121301
-        @jd = new jsondiff()
-        @dmp = jsondiff.dmp
-        @auth_token = null
-        @options = @options || {}
-
-        @options['app_id'] = @app_id
-        @sock_opts = { 'debug' : false }
-        if 'sockjs' of @options
-            for own name, val of @options['sockjs']
-                @sock_opts[name] = val
-
-        if not ('host' of @options) then @options['host'] = 'api.simperium.com'
-        if not ('port' of @options) then @options['port'] = 80
-        if 'token' of @options then @auth_token = @options['token']
-
-        if @options['host'].indexOf("simperium.com") != -1
-            scheme = "https"
-        else
-            scheme = "http"
-
-        @buckets = {}
-        @channels = 0
-
-        if not ('update_delay' of @options)
-            @options['update_delay'] = 0
-        if not ('page_delay' of @options)
-            @options['page_delay'] = 0
-
-        @options['prefix'] = 'sock'
-
-        @options['port'] = parseInt(@options['port'])
-        if @options['port'] != 80 and @options['port'] != 443
-            @sock_url = "#{scheme}://#{@options['host']}:#{@options['port']}/#{@options['prefix']}"
-        else
-            @sock_url = "#{scheme}://#{@options['host']}/#{@options['prefix']}"
-
-        @stopped = false
-        @_sock_backoff = 3000
-        @_sock_hb = 1
-        @_sock_connect()
-
-    bucket: (name, b_opts) =>
-        name = @lowerstrip(name)
-        b_opts = b_opts || {}
-        b_opts['n'] = @channels++
-        b = new bucket(@, name, b_opts)
-        @buckets[b_opts['n']] = b
-        return b
-
-    on: (bucket, event, callback) =>
-        @buckets[bucket].on(event, callback)
-
-    start: =>
-        @stopped = false
-        for own name, bucket of @buckets
-            bucket.start()
-
-    stop: =>
-        @stopped = true
-        if @sock?
-            @sock.close()
-
-    send: (data) =>
-        @sock.send(data)
-
-    synced: =>
-        for own name, bucket of @buckets
-            if bucket.pending().length > 0
-                return false
-        return true
-
-    _sock_connect: =>
-        console.log "simperium: connecting to #{@sock_url}"
-        @connected = false
-        @authorized = false
-        @sock = new SockJS(@sock_url, undefined, @sock_opts)
-        @sock.onopen = @_sock_opened
-        @sock.onmessage = @_sock_message
-        @sock.onclose = @_sock_closed
-
-    _sock_opened: =>
-        @_sock_backoff = 3000
-        @connected = true
-        @_sock_hb_timer = setTimeout @_sock_hb_check, 20000
-        for own name, bucket of @buckets
-            if bucket.started
-                bucket.start()
-
-    _sock_closed: =>
-        @connected = false
-        for own name, bucket of @buckets
-            bucket.authorized = false
-        console.log "simperium: sock js closed"
-        if @_sock_backoff < 4000
-            @_sock_backoff = @_sock_backoff + 1
-        else
-            @_sock_backoff = 15000
-        if @_sock_hb_timer
-            clearTimeout @_sock_hb_timer
-            @_sock_hb_timer = null
-        if not @stopped
-            setTimeout @_sock_connect, @_sock_backoff
-
-    _sock_hb_check: =>
-        delay = new Date().getTime() - @_sock_msg_time
-        if @connected is false
-            return
-        if delay > 40000
-            console.log "simperium: force conn close"
-            @sock.close()
-        else if delay > 15000
-            console.log "simperium: send hb #{@_sock_hb}"
-            @sock.send("h:#{@_sock_hb}")
-        @_sock_hb_timer = setTimeout @_sock_hb_check, 20000
-
-    _sock_message: (e) =>
-        @_sock_msg_time = new Date().getTime()
-        data = e.data
-        sep = data.indexOf(":")
-        chan = null
-        if sep is 1 and data.charAt(0) is 'h'
-            @_sock_hb = data.substr(2)
-            return
-        try
-            chan = parseInt(data.substr(0, sep))
-            data = data.substr(sep+1)
-        catch error
-            chan = null
-        if chan is null
-            return
-        if not (chan of @buckets)
-            return
-        @buckets[chan].on_data(data)
-
-    _captureCursor: (element) =>
-        return
-    _captureCursor: `function(element) {
-        if ('activeElement' in element && !element.activeElement) {
-            // Safari specific code.
-            // Restoring a cursor in an unfocused element causes the focus to jump.
-            return null;
-        }
-        var padLength = this.dmp.Match_MaxBits / 2;    // Normally 16.
-        var text = element.value;
-        var cursor = {};
-        if ('selectionStart' in element) {    // W3
-            try {
-                var selectionStart = element.selectionStart;
-                var selectionEnd = element.selectionEnd;
-            } catch (e) {
-                // No cursor; the element may be "display:none".
-                return null;
-            }
-            cursor.startPrefix = text.substring(selectionStart - padLength, selectionStart);
-            cursor.startSuffix = text.substring(selectionStart, selectionStart + padLength);
-            cursor.startOffset = selectionStart;
-            cursor.collapsed = (selectionStart == selectionEnd);
-            if (!cursor.collapsed) {
-                cursor.endPrefix = text.substring(selectionEnd - padLength, selectionEnd);
-                cursor.endSuffix = text.substring(selectionEnd, selectionEnd + padLength);
-                cursor.endOffset = selectionEnd;
-            }
-        } else {    // IE
-            // Walk up the tree looking for this textarea's document node.
-            var doc = element;
-            while (doc.parentNode) {
-                doc = doc.parentNode;
-            }
-            if (!doc.selection || !doc.selection.createRange) {
-                // Not IE?
-                return null;
-            }
-            var range = doc.selection.createRange();
-            if (range.parentElement() != element) {
-                // Cursor not in this textarea.
-                return null;
-            }
-            var newRange = doc.body.createTextRange();
-
-            cursor.collapsed = (range.text == '');
-            newRange.moveToElementText(element);
-            if (!cursor.collapsed) {
-                newRange.setEndPoint('EndToEnd', range);
-                cursor.endPrefix = newRange.text;
-                cursor.endOffset = cursor.endPrefix.length;
-                cursor.endPrefix = cursor.endPrefix.substring(cursor.endPrefix.length - padLength);
-            }
-            newRange.setEndPoint('EndToStart', range);
-            cursor.startPrefix = newRange.text;
-            cursor.startOffset = cursor.startPrefix.length;
-            cursor.startPrefix = cursor.startPrefix.substring(cursor.startPrefix.length - padLength);
-
-            newRange.moveToElementText(element);
-            newRange.setEndPoint('StartToStart', range);
-            cursor.startSuffix = newRange.text.substring(0, padLength);
-            if (!cursor.collapsed) {
-                newRange.setEndPoint('StartToEnd', range);
-                cursor.endSuffix = newRange.text.substring(0, padLength);
-            }
-        }
-
-        // Record scrollbar locations
-        if ('scrollTop' in element) {
-            cursor.scrollTop = element.scrollTop / element.scrollHeight;
-            cursor.scrollLeft = element.scrollLeft / element.scrollWidth;
-        }
-
-        // alert(cursor.startPrefix + '|' + cursor.startSuffix + ' ' +
-        //         cursor.startOffset + '\n' + cursor.endPrefix + '|' +
-        //         cursor.endSuffix + ' ' + cursor.endOffset + '\n' +
-        //         cursor.scrollTop + ' x ' + cursor.scrollLeft);
-        return cursor;
-    }`
-
-    _restoreCursor: (element, cursor) =>
-        return
-    _restoreCursor: `function(element, cursor) {
-        // Set some constants which tweak the matching behaviour.
-        // Maximum distance to search from expected location.
-        this.dmp.Match_Distance = 1000;
-        // At what point is no match declared (0.0 = perfection, 1.0 = very loose)
-        this.dmp.Match_Threshold = 0.9;
-
-        var padLength = this.dmp.Match_MaxBits / 2;    // Normally 16.
-        var newText = element.value;
-
-        // Find the start of the selection in the new text.
-        var pattern1 = cursor.startPrefix + cursor.startSuffix;
-        var pattern2, diff;
-        var cursorStartPoint = this.dmp.match_main(newText, pattern1,
-                cursor.startOffset - padLength);
-        if (cursorStartPoint !== null) {
-            pattern2 = newText.substring(cursorStartPoint,
-                                                                     cursorStartPoint + pattern1.length);
-            //alert(pattern1 + '\nvs\n' + pattern2);
-            // Run a diff to get a framework of equivalent indicies.
-            diff = this.dmp.diff_main(pattern1, pattern2, false);
-            cursorStartPoint += this.dmp.diff_xIndex(diff, cursor.startPrefix.length);
-        }
-
-        var cursorEndPoint = null;
-        if (!cursor.collapsed) {
-            // Find the end of the selection in the new text.
-            pattern1 = cursor.endPrefix + cursor.endSuffix;
-            cursorEndPoint = this.dmp.match_main(newText, pattern1,
-                    cursor.endOffset - padLength);
-            if (cursorEndPoint !== null) {
-                pattern2 = newText.substring(cursorEndPoint,
-                                                                         cursorEndPoint + pattern1.length);
-                //alert(pattern1 + '\nvs\n' + pattern2);
-                // Run a diff to get a framework of equivalent indicies.
-                diff = this.dmp.diff_main(pattern1, pattern2, false);
-                cursorEndPoint += this.dmp.diff_xIndex(diff, cursor.endPrefix.length);
-            }
-        }
-
-        // Deal with loose ends
-        if (cursorStartPoint === null && cursorEndPoint !== null) {
-            // Lost the start point of the selection, but we have the end point.
-            // Collapse to end point.
-            cursorStartPoint = cursorEndPoint;
-        } else if (cursorStartPoint === null && cursorEndPoint === null) {
-            // Lost both start and end points.
-            // Jump to the offset of start.
-            cursorStartPoint = cursor.startOffset;
-        }
-        if (cursorEndPoint === null) {
-            // End not known, collapse to start.
-            cursorEndPoint = cursorStartPoint;
-        }
-
-        // Restore selection.
-        if ('selectionStart' in element) {    // W3
-            element.selectionStart = cursorStartPoint;
-            element.selectionEnd = cursorEndPoint;
-        } else {    // IE
-            // Walk up the tree looking for this textarea's document node.
-            var doc = element;
-            while (doc.parentNode) {
-                doc = doc.parentNode;
-            }
-            if (!doc.selection || !doc.selection.createRange) {
-                // Not IE?
-                return;
-            }
-            // IE's TextRange.move functions treat '\r\n' as one character.
-            var snippet = element.value.substring(0, cursorStartPoint);
-            var ieStartPoint = snippet.replace(/\r\n/g, '\n').length;
-
-            var newRange = doc.body.createTextRange();
-            newRange.moveToElementText(element);
-            newRange.collapse(true);
-            newRange.moveStart('character', ieStartPoint);
-            if (!cursor.collapsed) {
-                snippet = element.value.substring(cursorStartPoint, cursorEndPoint);
-                var ieMidLength = snippet.replace(/\r\n/g, '\n').length;
-                newRange.moveEnd('character', ieMidLength);
-            }
-            newRange.select();
-        }
-
-        // Restore scrollbar locations
-        if ('scrollTop' in cursor) {
-            element.scrollTop = cursor.scrollTop * element.scrollHeight;
-            element.scrollLeft = cursor.scrollLeft * element.scrollWidth;
-        }
-    }`
+	lowerstrip: (s) ->
+		s = s.toLowerCase()
+		if String::trim? then s.trim() else s.replace /^\s+|\s+$/g, ""
+
+	_time: ->
+		d = new Date()
+		Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds())/1000
+
+	supports_html5_storage: ->
+		try
+			return 'localStorage' of window && window['localStorage'] != null
+		catch error
+			return false
+
+	constructor: (@app_id, @options) ->
+		@bversion = 2012121301
+		@jd = new jsondiff()
+		@dmp = jsondiff.dmp
+		@auth_token = null
+		@options = @options || {}
+
+		@options['app_id'] = @app_id
+		@sock_opts = { 'debug' : false }
+		if 'sockjs' of @options
+			for own name, val of @options['sockjs']
+				@sock_opts[name] = val
+
+		if not ('host' of @options) then @options['host'] = 'api.simperium.com'
+		if not ('port' of @options) then @options['port'] = 80
+		if 'token' of @options then @auth_token = @options['token']
+
+		if @options['host'].indexOf("simperium.com") != -1
+			scheme = "https"
+		else
+			scheme = "http"
+
+		@buckets = {}
+		@channels = 0
+
+		if not ('update_delay' of @options)
+			@options['update_delay'] = 0
+		if not ('page_delay' of @options)
+			@options['page_delay'] = 0
+
+		@options['prefix'] = 'sock'
+
+		@options['port'] = parseInt(@options['port'])
+		if @options['port'] != 80 and @options['port'] != 443
+			@sock_url = "#{scheme}://#{@options['host']}:#{@options['port']}/#{@options['prefix']}"
+		else
+			@sock_url = "#{scheme}://#{@options['host']}/#{@options['prefix']}"
+
+		@stopped = false
+		@_sock_backoff = 3000
+		@_sock_hb = 1
+		@_sock_connect()
+
+	bucket: (name, b_opts) =>
+		name = @lowerstrip(name)
+		b_opts = b_opts || {}
+		b_opts['n'] = @channels++
+		b = new bucket(@, name, b_opts)
+		@buckets[b_opts['n']] = b
+		return b
+
+	on: (bucket, event, callback) =>
+		@buckets[bucket].on(event, callback)
+
+	start: =>
+		@stopped = false
+		for own name, bucket of @buckets
+			bucket.start()
+
+	stop: =>
+		@stopped = true
+		if @sock?
+			@sock.close()
+
+	send: (data) =>
+		@sock.send(data)
+
+	synced: =>
+		for own name, bucket of @buckets
+			if bucket.pending().length > 0
+				return false
+		return true
+
+	_sock_connect: =>
+		console.log "simperium: connecting to #{@sock_url}"
+		@connected = false
+		@authorized = false
+		@sock = new SockJS(@sock_url, undefined, @sock_opts)
+		@sock.onopen = @_sock_opened
+		@sock.onmessage = @_sock_message
+		@sock.onclose = @_sock_closed
+
+	_sock_opened: =>
+		@_sock_backoff = 3000
+		@connected = true
+		@_sock_hb_timer = setTimeout @_sock_hb_check, 20000
+		for own name, bucket of @buckets
+			if bucket.started
+				bucket.start()
+
+	_sock_closed: =>
+		@connected = false
+		for own name, bucket of @buckets
+			bucket.authorized = false
+		console.log "simperium: sock js closed"
+		if @_sock_backoff < 4000
+			@_sock_backoff = @_sock_backoff + 1
+		else
+			@_sock_backoff = 15000
+		if @_sock_hb_timer
+			clearTimeout @_sock_hb_timer
+			@_sock_hb_timer = null
+		if not @stopped
+			setTimeout @_sock_connect, @_sock_backoff
+
+	_sock_hb_check: =>
+		delay = new Date().getTime() - @_sock_msg_time
+		if @connected is false
+			return
+		if delay > 40000
+			console.log "simperium: force conn close"
+			@sock.close()
+		else if delay > 15000
+			console.log "simperium: send hb #{@_sock_hb}"
+			@sock.send("h:#{@_sock_hb}")
+		@_sock_hb_timer = setTimeout @_sock_hb_check, 20000
+
+	_sock_message: (e) =>
+		@_sock_msg_time = new Date().getTime()
+		data = e.data
+		sep = data.indexOf(":")
+		chan = null
+		if sep is 1 and data.charAt(0) is 'h'
+			@_sock_hb = data.substr(2)
+			return
+		try
+			chan = parseInt(data.substr(0, sep))
+			data = data.substr(sep+1)
+		catch error
+			chan = null
+		if chan is null
+			return
+		if not (chan of @buckets)
+			return
+		@buckets[chan].on_data(data)
+
+	_captureCursor: (element) =>
+		return
+	_captureCursor: `function(element) {
+		if ('activeElement' in element && !element.activeElement) {
+			// Safari specific code.
+			// Restoring a cursor in an unfocused element causes the focus to jump.
+			return null;
+		}
+		var padLength = this.dmp.Match_MaxBits / 2;    // Normally 16.
+		var text = element.value;
+		var cursor = {};
+		if ('selectionStart' in element) {    // W3
+			try {
+				var selectionStart = element.selectionStart;
+				var selectionEnd = element.selectionEnd;
+			} catch (e) {
+				// No cursor; the element may be "display:none".
+				return null;
+			}
+			cursor.startPrefix = text.substring(selectionStart - padLength, selectionStart);
+			cursor.startSuffix = text.substring(selectionStart, selectionStart + padLength);
+			cursor.startOffset = selectionStart;
+			cursor.collapsed = (selectionStart == selectionEnd);
+			if (!cursor.collapsed) {
+				cursor.endPrefix = text.substring(selectionEnd - padLength, selectionEnd);
+				cursor.endSuffix = text.substring(selectionEnd, selectionEnd + padLength);
+				cursor.endOffset = selectionEnd;
+			}
+		} else {    // IE
+			// Walk up the tree looking for this textarea's document node.
+			var doc = element;
+			while (doc.parentNode) {
+				doc = doc.parentNode;
+			}
+			if (!doc.selection || !doc.selection.createRange) {
+				// Not IE?
+				return null;
+			}
+			var range = doc.selection.createRange();
+			if (range.parentElement() != element) {
+				// Cursor not in this textarea.
+				return null;
+			}
+			var newRange = doc.body.createTextRange();
+
+			cursor.collapsed = (range.text == '');
+			newRange.moveToElementText(element);
+			if (!cursor.collapsed) {
+				newRange.setEndPoint('EndToEnd', range);
+				cursor.endPrefix = newRange.text;
+				cursor.endOffset = cursor.endPrefix.length;
+				cursor.endPrefix = cursor.endPrefix.substring(cursor.endPrefix.length - padLength);
+			}
+			newRange.setEndPoint('EndToStart', range);
+			cursor.startPrefix = newRange.text;
+			cursor.startOffset = cursor.startPrefix.length;
+			cursor.startPrefix = cursor.startPrefix.substring(cursor.startPrefix.length - padLength);
+
+			newRange.moveToElementText(element);
+			newRange.setEndPoint('StartToStart', range);
+			cursor.startSuffix = newRange.text.substring(0, padLength);
+			if (!cursor.collapsed) {
+				newRange.setEndPoint('StartToEnd', range);
+				cursor.endSuffix = newRange.text.substring(0, padLength);
+			}
+		}
+
+		// Record scrollbar locations
+		if ('scrollTop' in element) {
+			cursor.scrollTop = element.scrollTop / element.scrollHeight;
+			cursor.scrollLeft = element.scrollLeft / element.scrollWidth;
+		}
+
+		// alert(cursor.startPrefix + '|' + cursor.startSuffix + ' ' +
+		//         cursor.startOffset + '\n' + cursor.endPrefix + '|' +
+		//         cursor.endSuffix + ' ' + cursor.endOffset + '\n' +
+		//         cursor.scrollTop + ' x ' + cursor.scrollLeft);
+		return cursor;
+	}`
+
+	_restoreCursor: (element, cursor) =>
+		return
+	_restoreCursor: `function(element, cursor) {
+		// Set some constants which tweak the matching behaviour.
+		// Maximum distance to search from expected location.
+		this.dmp.Match_Distance = 1000;
+		// At what point is no match declared (0.0 = perfection, 1.0 = very loose)
+		this.dmp.Match_Threshold = 0.9;
+
+		var padLength = this.dmp.Match_MaxBits / 2;    // Normally 16.
+		var newText = element.value;
+
+		// Find the start of the selection in the new text.
+		var pattern1 = cursor.startPrefix + cursor.startSuffix;
+		var pattern2, diff;
+		var cursorStartPoint = this.dmp.match_main(newText, pattern1,
+				cursor.startOffset - padLength);
+		if (cursorStartPoint !== null) {
+			pattern2 = newText.substring(cursorStartPoint,
+                                                 					cursorStartPoint + pattern1.length);
+			//alert(pattern1 + '\nvs\n' + pattern2);
+			// Run a diff to get a framework of equivalent indicies.
+			diff = this.dmp.diff_main(pattern1, pattern2, false);
+			cursorStartPoint += this.dmp.diff_xIndex(diff, cursor.startPrefix.length);
+		}
+
+		var cursorEndPoint = null;
+		if (!cursor.collapsed) {
+			// Find the end of the selection in the new text.
+			pattern1 = cursor.endPrefix + cursor.endSuffix;
+			cursorEndPoint = this.dmp.match_main(newText, pattern1,
+					cursor.endOffset - padLength);
+			if (cursorEndPoint !== null) {
+				pattern2 = newText.substring(cursorEndPoint,
+                                                     					cursorEndPoint + pattern1.length);
+				//alert(pattern1 + '\nvs\n' + pattern2);
+				// Run a diff to get a framework of equivalent indicies.
+				diff = this.dmp.diff_main(pattern1, pattern2, false);
+				cursorEndPoint += this.dmp.diff_xIndex(diff, cursor.endPrefix.length);
+			}
+		}
+
+		// Deal with loose ends
+		if (cursorStartPoint === null && cursorEndPoint !== null) {
+			// Lost the start point of the selection, but we have the end point.
+			// Collapse to end point.
+			cursorStartPoint = cursorEndPoint;
+		} else if (cursorStartPoint === null && cursorEndPoint === null) {
+			// Lost both start and end points.
+			// Jump to the offset of start.
+			cursorStartPoint = cursor.startOffset;
+		}
+		if (cursorEndPoint === null) {
+			// End not known, collapse to start.
+			cursorEndPoint = cursorStartPoint;
+		}
+
+		// Restore selection.
+		if ('selectionStart' in element) {    // W3
+			element.selectionStart = cursorStartPoint;
+			element.selectionEnd = cursorEndPoint;
+		} else {    // IE
+			// Walk up the tree looking for this textarea's document node.
+			var doc = element;
+			while (doc.parentNode) {
+				doc = doc.parentNode;
+			}
+			if (!doc.selection || !doc.selection.createRange) {
+				// Not IE?
+				return;
+			}
+			// IE's TextRange.move functions treat '\r\n' as one character.
+			var snippet = element.value.substring(0, cursorStartPoint);
+			var ieStartPoint = snippet.replace(/\r\n/g, '\n').length;
+
+			var newRange = doc.body.createTextRange();
+			newRange.moveToElementText(element);
+			newRange.collapse(true);
+			newRange.moveStart('character', ieStartPoint);
+			if (!cursor.collapsed) {
+				snippet = element.value.substring(cursorStartPoint, cursorEndPoint);
+				var ieMidLength = snippet.replace(/\r\n/g, '\n').length;
+				newRange.moveEnd('character', ieMidLength);
+			}
+			newRange.select();
+		}
+
+		// Restore scrollbar locations
+		if ('scrollTop' in cursor) {
+			element.scrollTop = cursor.scrollTop * element.scrollHeight;
+			element.scrollLeft = cursor.scrollLeft * element.scrollWidth;
+		}
+	}`

 window['Simperium'] = simperium


set the plan's billing-email and billing-name when we pay

--- js/inner/billing.coffee
+++ js/inner/billing.coffee
@@ -182,7 +182,9 @@
       type: 'POST'
       data: JSON.stringify
         token: token
-        plan: plan_id
+        'base-template-id': plan_id
+        'billing-email': @billing_email() || VM.current_user().selected_email()
+        'billing-name': @billing_name() || @org_name()
       success: (data) =>
         mixpanel.track('Paid')
         @loadPlanData(data)


update bug (jBone has no method text, replace to html)

--- lib/client/javascripts/nali/application.js.coffee
+++ lib/client/javascripts/nali/application.js.coffee
@@ -44,7 +44,7 @@

   setTitle: ( @title ) ->
     @titleBox ?= if ( exists = @_ 'head title' ).lenght then exists else @_( '<title>' ).appendTo 'head'
-    @titleBox.text @title
+    @titleBox.html @title
     @trigger 'update.title'
     @


Check authorization when handling 'GET' request

Signed-off-by: Manson Li <f2a598d7717cc6757bef28b84a9ab02e5efc6048@matchstick.tv>

--- lib/dial/handler/ApplicationControlHandler.coffee
+++ lib/dial/handler/ApplicationControlHandler.coffee
@@ -55,7 +55,12 @@
         # ping/pong
         token = req.headers["authorization"]
         if token
-            SessionManager.getInstance().touchSession token
+            if not SessionManager.getInstance().checkSession token
+                Log.e "SenderSession #{token} maybe timeout!!!"
+                @_onResponse req, res, 400, null, null
+                return
+            else
+                SessionManager.getInstance().touchSession token

         body = []

@@ -231,7 +236,6 @@
             "Content-Length": "0"
         @_onResponse req, res, 200, headers, null

-
     _onResponse: (req, res, statusCode, headers, body) ->
         if headers
             res.writeHead statusCode, headers


resolve conflicts

resolve conflicts

--- keymaps/inno-setup.cson
+++ keymaps/inno-setup.cson
@@ -1,6 +1,6 @@
 # https://atom.io/docs/latest/advanced/keymaps
 '.platform-darwin atom-text-editor, .platform-linux atom-text-editor':
-  'cmd-alt-m': 'inno-setup:save-&-compile'
+  'ctrl-shift-i': 'inno-setup:save-&-compile'

 '.platform-win32 atom-text-editor':
-  'shift-alt-m': 'inno-setup:save-&-compile'
+  'ctrl-alt-i': 'inno-setup:save-&-compile'


adding fix for extra tel added by slack

adding fix for extra tel added by slack

--- scripts/utils.coffee
+++ scripts/utils.coffee
@@ -39,12 +39,13 @@
       msg.send "   #{command.name} #{command.description}"

   robot.hear /locate ip (.*)/i, (msg)->
-    ip = msg.match[1].remove('http://').remove('https://')
+    ip = msg.match[1].remove('http://').remove('tel:')
     url = "http://www.freegeoip.net/json/#{ip}"
     log url
     url.GET_Json (data)->

       url_Map = "http://maps.googleapis.com/maps/api/staticmap?center=#{data.latitude},#{data.longitude}&zoom=12&size=800x900&sensor=false"
+      log url
       msg.reply data.json_Str() + "\n\n" + url_Map

       #msg.reply data.json_Str() + "\n\n" + "http://maps.google.com/maps?q=#{data.latitude},#{data.longitude}"


syntax

syntax

--- server/server.coffee
+++ server/server.coffee
@@ -83,7 +83,7 @@
             request.on 'end', ->
                 if message
                     try
-                        bag = JSON.parse(message)
+                        bag = JSON.parse message
                     catch e
                         respond response, {
                             success: false,


Fix parent node seeking

Fix parent node seeking

--- lib/BehaviorTree.coffee
+++ lib/BehaviorTree.coffee
@@ -202,6 +202,16 @@

     source = @nodes[choice.promiseSource]
     while source
+      if gotPositive and source.type in PositiveResults
+        # Skip this one, keep looking for a negative
+        source = @nodes[source.promiseSource]
+        continue
+      if gotNegative and source.type in NegativeResults
+        # Skip this one, keep looking for a positive
+        source = @nodes[source.promiseSource]
+        continue
+
+      # Add edge between source and node
       source.destinations.push choice if source.destinations.indexOf(choice) is -1
       choice.sources.push source if choice.sources.indexOf(source) is -1

@@ -227,14 +237,6 @@
         gotNegative = true
         break if gotPositive
       source = @nodes[source.promiseSource]
-      if gotPositive and source.type in PositiveResults
-        # Skip this one, keep looking for a negative
-        source = @nodes[source.promiseSource]
-        continue
-      if gotNegative and source.type in NegativeResults
-        # Skip this one, keep looking for a positive
-        source = @nodes[source.promiseSource]
-        continue
     choice.sources

   toDOT: ->


fixed bug where cursor blinking wasn't disabled properly

--- lib/block-cursor.coffee
+++ lib/block-cursor.coffee
@@ -34,12 +34,12 @@
       @updateCursorStyleForScope scopedConfig, editors, scopeName

   updateCursorStyleForScope: (scopedConfig, editors, scopeName) =>
+    scopedConfig = @prepareConfig scopedConfig
     @updateStylesheet scopeName, scopedConfig
     for editor in editors
       @updateEditor editor, scopedConfig

   updateStylesheet: (scopeName, scopedConfig) ->
-    scopedConfig = @prepareConfig scopedConfig
     @removeCSSRulesForScope scopeName
     @insertCSSRulesForScope scopeName, scopedConfig

@@ -69,7 +69,6 @@
     if useHardwareAcceleration and secondaryColor.alpha is 0
       secondaryColor.property = 'opacity'
       secondaryColor.toRGBAString = -> 0
-
     # return new config object
     config = {cursorType, primaryColor, secondaryColor,
       blinkInterval, useHardwareAcceleration, cursorLineFix}
@@ -89,6 +88,7 @@
   insertCSSRulesForScope: (scopeName, scopedConfig) ->
     {primaryColor, secondaryColor, pulseDuration, cursorThickness} = scopedConfig
     @cssRulesIndexes[scopeName] ?= @stylesheet.sheet.cssRules.length
+
     @stylesheet.sheet.insertRule """
       #{@selectorForScope scopeName} {
         #{primaryColor.property}: #{primaryColor.toRGBAString()};
@@ -96,6 +96,7 @@
         #{if cursorThickness? then "border-width: #{cursorThickness};" else ''}
       }
     """, @cssRulesIndexes[scopeName]
+
     @stylesheet.sheet.insertRule """
       #{@selectorForScope scopeName, true} {
         #{secondaryColor.property}: #{secondaryColor.toRGBAString()};
@@ -105,15 +106,20 @@
   updateEditor: (editor, config) ->
     {cursorType, blinkInterval, cursorLineFix} = config
     editorView = atom.views.getView editor
+
     editorView.classList.remove 'cursor-block', 'cursor-bordered-box', 'cursor-i-beam', 'cursor-underline'
     editorView.classList.add "cursor-#{cursorType}"
-    if editorView.component?.presenter?
-      process.nextTick ->
-        editorPresenter = editorView.component.presenter
-        editorPresenter.stopBlinkingCursors true
-        editorPresenter.cursorBlinkPeriod = blinkInterval
-        editorPresenter.startBlinkingCursors()
-    editorView.classList[if cursorLineFix then 'add' else 'remove']('cursor-line-fix')
+
+    if cursorLineFix
+      editorView.classList.add 'cursor-line-fix'
+    else
+      editorView.classList.remove 'cursor-line-fix'
+
+    editorPresenter = editorView.component?.presenter
+    if editorPresenter?
+      editorPresenter.stopBlinkingCursors true
+      editorPresenter.cursorBlinkPeriod = blinkInterval
+      editorPresenter.startBlinkingCursors()

   selectorForScope: (scopeName, blinkOff = '') ->
     if scopeName isnt ''


Fixed string quoting.

Fixed string quoting.

--- app/assets/javascripts/app/lib/app_post/websocket.js.coffee
+++ app/assets/javascripts/app/lib/app_post/websocket.js.coffee
@@ -246,7 +246,7 @@
         if @backend is 'websocketPort'
           if @backend_port
             port = ":#{@backend_port}"
-          @log 'notice', 'no websocket connection on port #{port}, use ajax long polling as fallback'
+          @log 'notice', "no websocket connection on port #{port}, use ajax long polling as fallback"
           @backend = 'ajax'
           @connect()
           return


finished up some list features in markupable

--- app/assets/javascripts/mercury/regions/markupable.js.coffee
+++ app/assets/javascripts/mercury/regions/markupable.js.coffee
@@ -91,10 +91,20 @@
           selection = @selection()
           text = @textarea.val()
           start = text.lastIndexOf('\n', selection.start)
+          end = text.indexOf('\n', selection.end)
+          end = text.length if end < start
           start = text.lastIndexOf('\n', selection.start - 1) if text[start] == '\n'
           if text[start + 1] == '-'
             selection.replace('\n- ', false, true)
             event.preventDefault()
+          if /\d/.test(text[start + 1])
+            lineText = text.substring(start, end)
+            console.debug(lineText)
+            if /(\d+)\./.test(lineText)
+              console.debug(2)
+              number = parseInt(RegExp.$1)
+              selection.replace("\n#{number += 1}. ", false, true)
+              event.preventDefault()

         when 90 # undo / redo
           return unless event.metaKey
@@ -209,9 +219,9 @@
       console.log(selection, options)
     #selection.insertNode(options.value)

-    insertunorderedlist: (selection) -> selection.addList('-')
-
-    insertorderedlist: (selection) -> selection.addList('1.')
+    insertunorderedlist: (selection) -> selection.addList('unordered')
+
+    insertorderedlist: (selection) -> selection.addList('ordered')

     style: (selection, options) -> selection.wrap("", '')

@@ -338,7 +348,7 @@
     @select(savedSelection.start - left.length, savedSelection.end - left.length) if reselect && changed

-  addList: (content) ->
+  addList: (type) ->
     text = @element.val()
     start = text.lastIndexOf('\n', @start)
     end = text.indexOf('\n', @end)
@@ -346,7 +356,11 @@
     start = text.lastIndexOf('\n', @start - 1) if text[start] == '\n'
     @select(start + 1, end)
     lines = @text.split('\n')
-    @replace("#{content} " + lines.join("\n#{content} "), true)
+    if type == 'unordered'
+      @replace("- " + lines.join("\n- "), true)
+    else
+      @replace(("#{index + 1}. #{line}" for line, index in lines).join('\n'), true)
+

   deselectNewLines: ->


10243: Add comment

10243: Add comment

--- app/assets/javascripts/views/return_to_draft_view.js.coffee
+++ app/assets/javascripts/views/return_to_draft_view.js.coffee
@@ -15,7 +15,9 @@
     'keyup #override': 'handleKeyup'

   handleLinkClicked: (event) ->
+    # If accept button was clicked, we just let the link do it's thing.
     return if @accepted
+
     event.preventDefault()
     event.stopPropagation()
     @$('#return-to-draft-modal').modal('show')


Avatar area popupmessage click logic changed:

 - First try to select message
 - If it fails try to fetchmessages
 - And try again to select message
 - If fails again just show the message without selecting it

--- client/app/Applications/Inbox.kdapplication/AppView.coffee
+++ client/app/Applications/Inbox.kdapplication/AppView.coffee
@@ -91,22 +91,24 @@
         messagesSplit.didResizeBefore = yes
         @newMessageBar.enableMessageActionButtons()

-        selectMessage = =>
-          return if not item
+        messageIsSelectable = =>
           {items} = inboxMessagesList
+          return no if items.length is 0
+          {_id} = item.getData()
           wasMessageInList = no
           items.forEach (message) =>
-            if message.getData()?.getId() is item.getData().getId()
+            if message.getData()?.getId() is _id
               message.click()
               wasMessageInList = yes
-          if not wasMessageInList
-            @propagateEvent KDEventType : "MessageIsSelected", {item, event}
+          wasMessageInList

-        if inboxMessagesList.items.length > 0
-          selectMessage()
+        if item
+          if not messageIsSelectable()
+            inboxMessageListController.loadMessages =>
+              if not messageIsSelectable()
+                @propagateEvent KDEventType : "MessageIsSelected", {item, event}
         else
-          inboxMessageListController.loadMessages =>
-            selectMessage()
+          inboxMessageListController.loadMessages()

     return tab


Fix bug where editor was not focused when selecting a path in fuzzy finder

This is hard to test. What was happening before is that the opening of
the file in the editor was causing the mini editor in the fuzzy finder
to be unfocused, which was then triggering the mini editor to cancel.
Some strange interaction of a focus triggered by a focus was causing
the editor to actually lose focus. By using @cancel to close the editor
instead of @detach, we avoid spurious handling of the unfocusing of the
editor because @cancel already avoids that.

--- src/extensions/fuzzy-finder/fuzzy-finder.coffee
+++ src/extensions/fuzzy-finder/fuzzy-finder.coffee
@@ -29,8 +29,8 @@

   confirmed : (path) ->
     return unless path.length
+    @cancel()
     @rootView.open(path, {@allowActiveEditorChange})
-    @detach()

   cancelled: ->
     @miniEditor.setText('')


Finder: update finder appStorage version to 1.2

--- client/finder/filetree/controllers/findercontroller.coffee
+++ client/finder/filetree/controllers/findercontroller.coffee
@@ -25,7 +25,7 @@
     TreeControllerClass = options.treeControllerClass or NFinderTreeController
     @treeController     = new TreeControllerClass treeOptions, []

-    @appStorage = KD.getSingleton('appStorageController').storage 'Finder', '1.1.1.1'
+    @appStorage = KD.getSingleton('appStorageController').storage 'Finder', '1.2'

     @watchers = {}


clean up logging

clean up logging

--- services/track-changes/app/coffee/PackManager.coffee
+++ services/track-changes/app/coffee/PackManager.coffee
@@ -291,7 +291,8 @@
 				# select only the new packs not already in the index
 				newPacks = (pack for pack in historyPacks when not indexResult?[pack._id]?)
 				newPacks = (_.omit(pack, 'doc_id', 'project_id', 'n', 'sz') for pack in newPacks)
-				logger.log {project_id, doc_id, n: newPacks.length}, "found new packs"
+				if newPacks.length
+					logger.log {project_id, doc_id, n: newPacks.length}, "found new packs"
 				callback(null, newPacks)

 	insertPacksIntoIndexWithLock: (project_id, doc_id, newPacks, callback) ->
@@ -388,27 +389,29 @@
 					PackManager.findUnarchivedPacks project_id, doc_id, (err, unarchivedPacks) ->
 						return markAsChecked(err) if err?
 						if not unarchivedPacks?.length
-							logger.log "no packs need archiving"
+							logger.log {project_id, doc_id}, "no packs need archiving"
 							return markAsChecked()
 						async.eachSeries unarchivedPacks, (pack, cb) ->
 							PackManager.archivePack project_id, doc_id, pack._id, cb
 						, (err) ->
 							return markAsChecked(err) if err?
-							logger.log "done processing"
+							logger.log  {project_id, doc_id}, "done processing"
 							markAsChecked()

 	finaliseIfNeeded: (project_id, doc_id, pack_id, pack, callback) ->
-		logger.log {project_id, doc_id}, "archiving old packs"
 		sz = pack.sz / (1024 * 1024) # in fractions of a megabyte
 		n = pack.n / 1024 # in fraction of 1024 ops
 		age = (Date.now() - pack.meta.end_ts) / DAYS
 		if age < 30 # always keep if less than 1 month old
+			logger.log {project_id, doc_id, pack_id, age}, "less than 30 days old"
 			return callback()
 		# compute an archiving threshold which decreases for each month of age
 		archive_threshold = 30 / age
 		if sz > archive_threshold or n > archive_threshold or age > 90
+			logger.log {project_id, doc_id, pack_id, age, archive_threshold, sz, n}, "meets archive threshold"
 			PackManager.markPackAsFinalisedWithLock project_id, doc_id, pack_id, callback
 		else
+			logger.log {project_id, doc_id, pack_id, age, archive_threshold, sz, n}, "does not meet archive threshold"
 			callback()

 	markPackAsFinalisedWithLock: (project_id, doc_id, pack_id, callback) ->
@@ -448,7 +451,8 @@
 			return callback(err) if err?
 			indexPacks = indexResult?.packs or []
 			unArchivedPacks = (pack for pack in indexPacks when not pack.inS3?)
-			logger.log {project_id, doc_id, n: unArchivedPacks.length}, "find unarchived packs"
+			if unArchivedPacks.length
+				logger.log {project_id, doc_id, n: unArchivedPacks.length}, "find unarchived packs"
 			callback(null, unArchivedPacks)

 	# Archive locking flags


Test port aliasing via exports

Test port aliasing via exports

--- spec/Subgraph.coffee
+++ spec/Subgraph.coffee
@@ -56,6 +56,34 @@
         ]
         done()
       g.send
+        processes:
+          Split:
+            component: 'Split'
+          Merge:
+            component: 'Merge'
+        connections: [
+          src:
+            process: 'Merge'
+            port: 'out'
+          tgt:
+            process: 'Split'
+            port: 'in'
+        ]
+    it 'should expose only exported ports when they exist', (done) ->
+      c.baseDir = root
+      c.once 'ready', ->
+        chai.expect(c.inPorts).to.have.keys [
+          'graph'
+        ]
+        chai.expect(c.outPorts).to.have.keys [
+          'out'
+        ]
+        done()
+      g.send
+        exports: [
+          public: 'out'
+          private: 'split.out'
+        ]
         processes:
           Split:
             component: 'Split'


Fixed tests

Fixed tests

--- spec/ViewSpec.coffee
+++ spec/ViewSpec.coffee
@@ -1,6 +1,6 @@
 $ = require "jquery"
 sinon = require "sinon"
-{View, Model} = require "../antifreeze"
+{View, Model, HelperBroker} = require "../antifreeze"

 class TestView extends View
 	$: $