Pub/Sub instead of Polling

Pub/Sub instead of Polling

--- src/fairy.coffee
+++ src/fairy.coffee
@@ -87,12 +87,11 @@

 # Log active workers while waiting for all workers to clean-up.
 log_registered_workers = ->
-  console.log "\nFairy is waiting for #{registered_workers.length} workers to clean-up before exit:"
-  for registered_worker in registered_workers
-    worker_info = registered_worker.split '|'
-    console.log "  * Client Id: [#{worker_info[0]}], Task: [#{worker_info[1]}]"
-
-cleanup_required = off
+  console.log "\nFairy is waiting for #{workers.length} workers to clean-up before exit:"
+  for worker in workers
+    console.log "  * Client Id: [#{worker.fairy.id}], Task: [#{worker.name}]"
+
+workers = []

 # Fairy will enter cleanup mode before exit when:
 #
@@ -100,43 +99,51 @@
 #   + `uncaughtException` captured.
 #
 # If there's no registered workers, exit directly.
-enter_cleanup_mode = ->
-  if registered_workers.length
-    log_registered_workers()
-    cleanup_required = on
-    exiting = on
-  else
-    return process.exit()
+first_time = on
+clean_up = ->
+  log_registered_workers() unless first_time
+  first_time = off
+  return process.exit() unless workers.length
+  # console.log 'abc'
+  exiting = on
+  for worker in workers
+    worker.unregist() if worker.is_idling

 # When below signals are captured, gracefully exit the program by notifying all
 # workers entering cleanup mode and exit after all are cleaned up.
 #
-# + `SIGINT` (e.g. `Control-C`)
+# + `SIGINT` (`Control-C`)
 # + `SIGHUP`
 # + `SIGQUIT`
 # + `SIGUSR1`
 # + `SIGUSR2`
 # + `SIGTERM`
 # + `SIGABRT`
-process.on 'SIGINT', enter_cleanup_mode
-process.on 'SIGHUP', enter_cleanup_mode
-process.on 'SIGQUIT', enter_cleanup_mode
-process.on 'SIGUSR1', enter_cleanup_mode
-process.on 'SIGUSR2', enter_cleanup_mode
-process.on 'SIGTERM', enter_cleanup_mode
-process.on 'SIGABRT', enter_cleanup_mode
+capturable_signals = [
+  'SIGINT'
+  'SIGHUP'
+  'SIGQUIT'
+  'SIGUSR1'
+  'SIGUSR2'
+  'SIGTERM'
+  'SIGABRT'
+]
+
+process.on signal, clean_up for signal in capturable_signals

 # When `uncaughtException` is captured, **Fairy** can not tell if this is caught
 # by the handling function, as well as which queue cause the exception.
 # **Fairy** will fail all processing tasks and block the according group.
+#
+# TODO: Use `Domain` to manage exception handling.
 process.on 'uncaughtException', (err) ->
   console.log 'Exception:', err.stack
   console.log 'Fairy workers will block their processing groups before exit.' if registered_workers.length
-  enter_cleanup_mode()
+  clean_up()

 # Say goodbye on exit.
 process.on 'exit', ->
-  console.log "Fairy cleaned up, exiting..." if cleanup_required
+  # console.log "Fairy cleaned up, exiting..." if cleanup_required

 # ## Helper Methods
@@ -187,6 +194,7 @@
   # values are according objects of class `Queue`.
   constructor: (@options) ->
     @redis = create_client options
+    @pubsub = create_client options
     @id = fairy_id++
     @queue_pool = {}

@@ -278,7 +286,7 @@
   # The constructor of class `Queue` stores the Redis connection and the name
   # of the queue as instance properties.
   constructor: (@fairy, @name) ->
-    @redis = fairy.redis
+    {@redis, @pubsub} = fairy

   # ### Function to Resolve Key Name
@@ -310,7 +318,6 @@
   #   + Retry interval in milliseconds
   #   + Storage capacity for newly finished tasks
   #   + Storage capacity for slowest tasks
-  polling_interval : 5
   retry_limit      : 2
   retry_delay      : 0.1 * 1000
   recent_size      : 10
@@ -335,14 +342,15 @@
   #     queue.enqueue 'group_id', 'param2', (err, res) -> # YOUR CODE
   #
   # A transaction ensures the atomicity.
-  enqueue: (args..., callback) =>
+  enqueue: (group, args..., callback) =>
     if typeof callback isnt 'function'
       args.push callback
       callback = undefined
     @redis.multi()
-      .rpush(@key('SOURCE'), JSON.stringify([uuid.v4(), args..., Date.now()]))
-      .sadd(@key('GROUPS'), args[0])
+      .rpush(@key('SOURCE'), JSON.stringify([uuid.v4(), group, args..., Date.now()]))
+      .sadd(@key('GROUPS'), group)
       .hincrby(@key('STATISTICS'), 'TOTAL', 1)
+      .publish(@key('ENQUEUED'), null)
       .exec(callback)

   # ### Register Handler
@@ -360,206 +368,8 @@
   #       console.log param1, param2
   #       callback()
   regist: (@handler) =>
-    registered_workers.push "#{@fairy.id}|#{@name}"
-    worker_id = uuid.v4()
-    @redis.hset @key('WORKERS'), worker_id, "#{os.hostname()}|#{server_ip()}|#{process.pid}|#{Date.now()}"
-
-    process.on 'uncaughtException', (err) =>
-      if @_handler_callback
-        console.log "Worker [#{worker_id.split('-')[0]}] registered for Task [#{@name}] will block its current processing group"
-        @_handler_callback {do: 'block', message: err.stack}
-      else
-        @_try_exit()
-
-    process.on 'exit', => @redis.hdel @key('WORKERS'), worker_id
-    @_poll()
-
-
-  # ### Poll New Task
-
-  # **Private** method. If any task presents in the `SOURCE` list, `lpop` from
-  # `SOURCE` (`rpush`) into `QUEUED`. The `lpop` and `rpush` are protected by
-  # a transaction.
-  #
-  # Since the task being popped and pushed should be known in prior of the
-  # begin of the transaction (aka, the `multi` command), we need to get the
-  # first task of the source list, and, watch the source list to prevent the
-  # same task being taken by two different workers.
-  #
-  # If there's no pending tasks of the same group, then process the task
-  # immediately.
-  #
-  # If there's no tasks in the `SOURCE` list, poll again after an interval of
-  # `polling_interval` milliseconds.
-  _poll: =>
-    return @_try_exit() if exiting
-    @redis.watch @key('SOURCE')
-    @redis.lindex @key('SOURCE'), 0, (err, res) =>
-      if res
-        task = JSON.parse res
-        @redis.multi()
-        .lpop(@key('SOURCE'))
-        .rpush("#{@key('QUEUED')}:#{task[1]}", res)
-        .exec (multi_err, multi_res) =>
-          return @_poll() unless multi_res and multi_res[1] is 1
-          @_process task
-      else
-        @redis.unwatch()
-        setTimeout @_poll, @polling_interval
-
-
-  # ### Exit When All Queues are Cleaned Up
-
-  # **Private** method. Wait if there're queues still working, or exit the
-  # process immediately.
-  _try_exit: =>
-    registered_workers.splice registered_workers.indexOf "#{@fairy.id}|#{@name}", 1
-    process.exit() unless registered_workers.length
-    log_registered_workers()
-
-
-  # ### Process Each Group's First Task
-
-  # **Private** method. The real job is done by the passed in `handler` of
-  # `regist`ered method, when the job is:
-  #
-  #   + **successed**, pop the finished job from the group queue, and:
-  #     - continue process task of the same group if there's pending job(s) in
-  #     the same `QUEUED` list, or
-  #     - poll task from the `SOURCE` queue.
-  #   + **failed**, then inspect the passed in argument, retry or block
-  #   according to the `do` property of the error object.
-  #
-  # Calling the callback function is the responsibility of you. Otherwise
-  # `Fairy` will stop dispatching tasks.
-  _process: (task) =>
-
-    @redis.hset @key('PROCESSING'), task[0], JSON.stringify([task..., start_time = Date.now()])
-
-    # Before Processing the Task:
-    #
-    #   1. Keep start time of processing.
-    #   2. Set `PROCESSING` hash in Redis.
-    #   3. Allow `retry_limit` times of retries.
-    processing = task[0]
-    retry_count = @retry_limit
-    errors = []
-
-    @_handler_callback = handler_callback = (err, res) =>
-
-      @_handler_callback = null
-
-      # Error handling routine:
-      #
-      #   1. Keep the error message.
-      #   2. According to specific error handling request, when:
-      #     + `block`: block the group immediately.
-      #     + `block-after-retry`: retry n times, block the group if still
-      #     fails (blocking).
-      #     + or: retry n times, skip the task if still fails (non-blocking).
-      #     Set `retry_limit` to `0` to skip failed tasks immediately.
-      if err
-        errors.push err.message or null
-        switch err.do
-          when 'block'
-            @redis.multi()
-              .rpush(@key('FAILED'), JSON.stringify([task..., Date.now(), errors]))
-              .hdel(@key('PROCESSING'), processing)
-              .sadd(@key('BLOCKED'), task[1])
-              .exec()
-            return @_poll()
-          when 'block-after-retry'
-            return setTimeout call_handler, @retry_delay if retry_count--
-            @redis.multi()
-              .rpush(@key('FAILED'), JSON.stringify([task..., Date.now(), errors]))
-              .hdel(@key('PROCESSING'), processing)
-              .sadd(@key('BLOCKED'), task[1])
-              .exec()
-            return @_poll()
-          else
-            return setTimeout call_handler, @retry_delay if retry_count--
-            @redis.multi()
-              .rpush(@key('FAILED'), JSON.stringify([task..., Date.now(), errors]))
-              .hdel(@key('PROCESSING'), processing)
-              .exec()
-
-      # Success handling routine:
-      #
-      #   1. Remove last task from processing hash.
-      #   2. Update statistics hash:
-      #     + total number of `finished` tasks;
-      #     + total `pending` time;
-      #   3. Track recent finished tasks in `RECENT` list.
-      #   4. Track tasks take the longest processing time in `SLOWEST` sorted
-      #   set.
-      else
-        finish_time  = Date.now()
-        process_time = finish_time - start_time
-        @redis.multi()
-          .hdel(@key('PROCESSING'), processing)
-          .hincrby(@key('STATISTICS'), 'FINISHED', 1)
-          .hincrby(@key('STATISTICS'), 'TOTAL_PENDING_TIME', start_time - task[task.length - 1])
-          .hincrby(@key('STATISTICS'), 'TOTAL_PROCESS_TIME', process_time)
-          .lpush(@key('RECENT'), JSON.stringify([task..., finish_time]))
-          .ltrim(@key('RECENT'), 0, @recent_size - 1)
-          .zadd(@key('SLOWEST'), process_time, JSON.stringify([task..., start_time]))
-          .zremrangebyrank(@key('SLOWEST'), 0, - @slowest_size - 1)
-          .exec()
-
-      @_continue_group task[1]
-
-    do call_handler = =>
-      @handler task[1...-1]..., (@_handler_callback = handler_callback)
-
-  # ### Continue Process a Group
-
-  # **Private** method. Upon successful execution of a task, or skipping a
-  # failed task:
-  #
-  #   1. `lpop` the current task from `QUEUED` list.
-  #   2. Check if there exists task in the same `QUEUED` list.
-  #   3. Process if `YES`, or poll `SOURCE` if `NO`.
-  #
-  # Above commands are protected by a transaction to prevent multiple workers
-  # processing a same task.
-  _continue_group: (group) =>
-    @redis.watch "#{@key('QUEUED')}:#{group}"
-    @redis.lindex "#{@key('QUEUED')}:#{group}", 1, (err, res) =>
-      if res
-        task = JSON.parse res
-        @redis.unwatch()
-        @redis.lpop "#{@key('QUEUED')}:#{group}"
-        return @_requeue_group group if exiting
-        @_process task
-      else
-        @redis.multi()
-        .lpop("#{@key('QUEUED')}:#{group}")
-        .exec (multi_err, multi_res) =>
-          return @_continue_group group unless multi_res
-          return @_try_exit() if exiting
-          @_poll()
-
-
-  # ### Requeue Tasks on Exit
-
-  # **Private** method. Before exiting, requeue all tasks in the processing
-  # `QUEUED` list back into `SOURCE` list to **prevent blocking** the group.
-  #
-  # To ensure the correct order of tasks, `lpush` tasks in the `QUEUED` list
-  # in their reverse order. The `lrange` and `lpush` is protected by a
-  # transaction for atomicity since other workers may still shifting tasks in
-  # the `SOURCE` list into `QUEUED` list.
-  #
-  # When tasks are requeued successfully, `_try_exit` the process.
-  _requeue_group: (group) =>
-    @redis.watch "#{@key('QUEUED')}:#{group}"
-    @redis.lrange "#{@key('QUEUED')}:#{group}", 0, -1, (err, res) =>
-      @redis.multi()
-      .lpush("#{@key('SOURCE')}", res.reverse()...)
-      .del("#{@key('QUEUED')}:#{group}")
-      .exec (multi_err, multi_res) =>
-        return @_requeue_group group unless multi_res
-        return @_try_exit()
+    workers.push new Worker @, @handler
+

   # ### Re-Schedule Failed and Blocked Tasks

@@ -624,9 +434,10 @@
               multi.exec (multi_err, multi_res) =>
                 if multi_err
                   client.quit()
-                  return callback multi_err
+                  return callback multi_err
                 if multi_res
                   client.quit()
+                  @redis.publish(@key('ENQUEUED'), "")
                   @statistics callback
                 else
                   reschedule callback
@@ -644,239 +455,6 @@
                   requeued_tasks.push res...
                   start_transaction() unless --total_groups
             else start_transaction()
-
-
-  # ### Get Recently Finished Tasks Asynchronously
-
-  # Recently finished tasks are tasks stored in the `RECENT` list (in the
-  # reverse order of finished time), which will be limited to a maximum size
-  # default to 10.
-  #
-  # `recently_finished_tasks` is an asynchronous method. Arguments of the
-  # callback function follow node.js error handling convention: `err` and `res`.
-  #
-  # Below is an example `res` array:
-  #
-  #     [{ id:       '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
-  #        params:   [ 'param1', 'param2' ],
-  #        queued:   Sat, 12 May 2012 07:41:33 GMT // Date Object
-  #        finished: Sat, 12 May 2012 07:41:59 GMT // Date Object
-  #      }, ...]
-  #
-  # **Usage:**
-  #
-  #     queue.recently_finished_tasks (err, tasks) -> YOUR CODE
-  recently_finished_tasks: (callback) =>
-    @redis.lrange @key('RECENT'), 0, -1, (err, res) ->
-      return callback err if err
-      callback null, res.map (entry) ->
-        entry = JSON.parse entry
-        id: entry[0]
-        params: entry[1..-3]
-        finished: new Date entry.pop()
-        queued: new Date entry.pop()
-
-
-  # ### Get Failed Tasks Asynchronously
-
-  # Failed tasks are stored in the `FAILED` list.
-  #
-  # `failed_tasks` is an asynchronous method. Arguments of the callback function
-  # follow node.js convention: `err` and `res`.
-  #
-  # Below is an example `res` array:
-  #
-  #     [{ id:     '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
-  #        params: [ 'param1', 'param2' ],
-  #        queued: Sat, 12 May 2012 07:41:33 GMT // Date Object
-  #        failed: Sat, 12 May 2012 07:41:59 GMT // Date Object
-  #        reason: [ 'failure reason 1', 'failure reason 2', ...]
-  #      }, ...]
-  #
-  # **Usage:**
-  #
-  #     queue.failed_tasks (err, tasks) -> YOUR CODE
-  failed_tasks: (callback) =>
-    @redis.lrange @key('FAILED'), 0, -1, (err, res) ->
-      return callback err if err
-      callback null, res.map (entry) ->
-        entry = JSON.parse entry
-        id: entry[0]
-        params: entry[1..-4]
-        reason: entry.pop()
-        failed: new Date entry.pop()
-        queued: new Date entry.pop()
-
-
-  # ### Get Blocked Groups Asynchronously
-
-  # Blocked groups' identifiers are stored in the `BLOCKED` set.
-  #
-  # `blocked_groups` is an asynchronous method. Arguments of the callback
-  # function follow node.js async callback pattern: `err` and `res`.
-  #
-  # Below is an example `res` array:
-  #
-  #     [ 'group1', 'group2', ...]
-  #
-  # **Usage:**
-  #
-  #     queue.blocked_groups (err, groups) -> YOUR CODE
-  blocked_groups: (callback) ->
-    @redis.smembers @key('BLOCKED'), (err, res) ->
-      return callback err if err
-      callback null, res.map (entry) ->
-        entry = JSON.parse entry
-
-
-  # ### Get Slowest Tasks Asynchronously
-
-  # Slowest tasks are tasks stored in the `SLOWEST` ordered set, which will be
-  # limited to a maximum size default to 10.
-  #
-  # `slowest_tasks` is an asynchronous method. Arguments of the callback
-  # function follow node.js error handling convention: `err` and `res`.
-  #
-  # Below is an example `res` array:
-  #
-  #     [{ id:      '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
-  #        params:  [ 'param1', 'param2' ],
-  #        queued:  Sat, 12 May 2012 07:41:33 GMT // Date Object
-  #        started: Sat, 12 May 2012 07:41:39 GMT // Date Object
-  #        time:    1876 // time taken in milliseconds
-  #      }, ...]
-  #
-  # **Usage:**
-  #
-  #     queue.slowest_tasks (err, task) -> YOUR CODE
-  #
-  # `slowest_tasks` is an asynchronous method. The only arg of the callback
-  # function is an array of slowest tasks in the reverse order by processing
-  # time. The actual processing time will be appended at the end of the task's
-  # original arguments.
-  slowest_tasks: (callback) ->
-    @redis.zrevrange @key('SLOWEST'), 0, -1, "WITHSCORES", (err, res) ->
-      return callback err if err
-      res = res.map (entry) -> JSON.parse entry
-      callback null, ([res[i]...,res[i + 1]] for i in [0...res.length] by 2).map (entry) ->
-        id: entry[0]
-        params: entry[1..-4]
-        time: entry.pop()
-        started: new Date entry.pop()
-        queued: new Date entry.pop()
-
-
-  # ### Get Processing Tasks Asynchronously
-
-  # Currently processing tasks are tasks in the `PROCESSING` list.
-  #
-  # `processing_tasks` is an asynchronous method. Arguments of the callback
-  # function follow node.js error handling convention: `err` and `res`.
-  #
-  # Below is an example `res` array:
-  #
-  #     [{ id:      '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
-  #        params:  [ 'param1', 'param2' ],
-  #        queued:  Sat, 12 May 2012 07:41:33 GMT // Date Object
-  #        started: Sat, 12 May 2012 07:41:39 GMT // Date Object
-  #      }, ...]
-  #
-  # **Usage:**
-  #
-  #     queue.processing_tasks (err, tasks) -> YOUR CODE
-  processing_tasks: (callback) ->
-    @redis.hvals @key('PROCESSING'), (err, res) ->
-      return callback err if err
-      callback null, res.map (entry) ->
-        entry = JSON.parse(entry)
-        id: entry[0]
-        params: entry[1..-3]
-        start: new Date entry.pop()
-        queued: new Date entry.pop()
-
-
-
-  # ### Get Source Tasks Asynchronously
-
-  # Get tasks in the `SOURCE` list. **NOTE:** Tasks in the `SOURCE` list does
-  # **NOT** equal to **pending** tasks! There may be tasks in the `QUEUED` lists
-  # need be processed before processing tasks in the `SOURCE` list.
-  #
-  # Accepted parameters are:
-  #
-  #   1. `skip` *(optional)*, the number of skipped tasks. Defaults to 0.
-  #   2. `take` *(optional)*, the number of tasks need be taken. Defaults to 10.
-  #   3. `callback`, the callback function. Arguments of which follows node.js error
-  #   handling convention: `err` and `res`.
-  #
-  # Below is an example `res` array:
-  #
-  #     [{ id:     '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
-  #        params: [ 'param1', 'param2' ],
-  #        queued: Sat, 12 May 2012 07:41:33 GMT // Date Object
-  #      }, ...]
-  #
-  # Possible combos of arguments are:
-  #
-  #   1. `callback`. (leave `skip` defaults to 0, `take` defaults to 10).
-  #   2. `skip` and `callback`. (leave `take` defaults to 10).
-  #   3. `skip`, `take`, and `callback`.
-  #
-  # **Usage:**
-  #
-  #     queue.source_tasks 20, 5, (err, tasks) -> YOUR CODE
-  source_tasks: (args..., callback) ->
-    skip = args[0] or 0
-    take = args[1] or 10
-    @redis.lrange @key('SOURCE'), skip, skip + take - 1, (err, res) ->
-      callback err if err
-      callback null, res.map (entry) ->
-        entry = JSON.parse entry
-        id: entry[0]
-        params: entry[1..-2]
-        queued: new Date entry.pop()
-
-
-  # ### Get Workers Asynchronously
-
-  # Asynchronous method to get all **live** workers of the queue. **Live**
-  # workers are registered in the `WORKERS` hash. Values of `WORKERS` hash are
-  # in `hostname|ip|pid|since` format.
-  #
-  # Arguments of the callback function follow node.js error handling convention:
-  # `err` and `res`. `res` is an array of live workers. Each worker object have:
-  #
-  #   + `host`, the host name of the worker machine.
-  #   + `ip`, the first external IPv4 address of the worker machine.
-  #   + `pid`, the process id of the working process.
-  #   + `since`, the born date of the worker.
-  #
-  # Below is an example of returned workers:
-  #
-  #     [{
-  #        host: 'baoshan',
-  #        ip: '192.168.2.7',
-  #        pid: 1628
-  #        since: Sat, 12 May 2012 07:28:21 GMT // Date Object
-  #      }, ...]
-  #
-  # **Usage:**
-  #
-  #     queue.workers (err, workers) -> YOUR CODE
-  workers: (callback) =>
-    @redis.hvals @key('WORKERS'), (err, res) ->
-      return callback err if err
-      callback null, res.map((entry) ->
-        entry = entry.split '|'
-        host: entry[0]
-        ip: entry[1]
-        pid: parseInt entry[2]
-        since: new Date parseInt entry[3]
-      ).sort (a, b) ->
-        return  1 if a.ip  > b.ip
-        return -1 if a.ip  < b.ip
-        return  1 if a.pid > b.pid
-        return -1 if a.pid < b.pid

   # ### Clear A Queue
@@ -897,6 +475,241 @@
             return callback? err if err
             return @clear callback unless res
             @statistics callback if callback
+
+
+  # ##  Read-Only Operations
+
+  # ### Get Recently Finished Tasks Asynchronously
+
+  # Recently finished tasks are tasks stored in the `RECENT` list (in the
+  # reverse order of finished time), which will be limited to a maximum size
+  # default to 10.
+  #
+  # `recently_finished_tasks` is an asynchronous method. Arguments of the
+  # callback function follow node.js error handling convention: `err` and `res`.
+  #
+  # Below is an example `res` array:
+  #
+  #     [{ id:       '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
+  #        params:   [ 'param1', 'param2' ],
+  #        queued:   Sat, 12 May 2012 07:41:33 GMT // Date Object
+  #        finished: Sat, 12 May 2012 07:41:59 GMT // Date Object
+  #      }, ...]
+  #
+  # **Usage:**
+  #
+  #     queue.recently_finished_tasks (err, tasks) -> YOUR CODE
+  recently_finished_tasks: (callback) =>
+    @redis.lrange @key('RECENT'), 0, -1, (err, res) ->
+      return callback err if err
+      callback null, res.map (entry) ->
+        entry = JSON.parse entry
+        id: entry[0]
+        params: entry[1..-3]
+        finished: new Date entry.pop()
+        queued: new Date entry.pop()
+
+
+  # ### Get Failed Tasks Asynchronously
+
+  # Failed tasks are stored in the `FAILED` list.
+  #
+  # `failed_tasks` is an asynchronous method. Arguments of the callback function
+  # follow node.js convention: `err` and `res`.
+  #
+  # Below is an example `res` array:
+  #
+  #     [{ id:     '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
+  #        params: [ 'param1', 'param2' ],
+  #        queued: Sat, 12 May 2012 07:41:33 GMT // Date Object
+  #        failed: Sat, 12 May 2012 07:41:59 GMT // Date Object
+  #        reason: [ 'failure reason 1', 'failure reason 2', ...]
+  #      }, ...]
+  #
+  # **Usage:**
+  #
+  #     queue.failed_tasks (err, tasks) -> YOUR CODE
+  failed_tasks: (callback) =>
+    @redis.lrange @key('FAILED'), 0, -1, (err, res) ->
+      return callback err if err
+      callback null, res.map (entry) ->
+        entry = JSON.parse entry
+        id: entry[0]
+        params: entry[1..-4]
+        reason: entry.pop()
+        failed: new Date entry.pop()
+        queued: new Date entry.pop()
+
+
+  # ### Get Blocked Groups Asynchronously
+
+  # Blocked groups' identifiers are stored in the `BLOCKED` set.
+  #
+  # `blocked_groups` is an asynchronous method. Arguments of the callback
+  # function follow node.js async callback pattern: `err` and `res`.
+  #
+  # Below is an example `res` array:
+  #
+  #     [ 'group1', 'group2', ...]
+  #
+  # **Usage:**
+  #
+  #     queue.blocked_groups (err, groups) -> YOUR CODE
+  blocked_groups: (callback) ->
+    @redis.smembers @key('BLOCKED'), (err, res) ->
+      return callback err if err
+      callback null, res.map (entry) ->
+        entry = JSON.parse entry
+
+
+  # ### Get Slowest Tasks Asynchronously
+
+  # Slowest tasks are tasks stored in the `SLOWEST` ordered set, which will be
+  # limited to a maximum size default to 10.
+  #
+  # `slowest_tasks` is an asynchronous method. Arguments of the callback
+  # function follow node.js error handling convention: `err` and `res`.
+  #
+  # Below is an example `res` array:
+  #
+  #     [{ id:      '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
+  #        params:  [ 'param1', 'param2' ],
+  #        queued:  Sat, 12 May 2012 07:41:33 GMT // Date Object
+  #        started: Sat, 12 May 2012 07:41:39 GMT // Date Object
+  #        time:    1876 // time taken in milliseconds
+  #      }, ...]
+  #
+  # **Usage:**
+  #
+  #     queue.slowest_tasks (err, task) -> YOUR CODE
+  #
+  # `slowest_tasks` is an asynchronous method. The only arg of the callback
+  # function is an array of slowest tasks in the reverse order by processing
+  # time. The actual processing time will be appended at the end of the task's
+  # original arguments.
+  slowest_tasks: (callback) ->
+    @redis.zrevrange @key('SLOWEST'), 0, -1, "WITHSCORES", (err, res) ->
+      return callback err if err
+      res = res.map (entry) -> JSON.parse entry
+      callback null, ([res[i]...,res[i + 1]] for i in [0...res.length] by 2).map (entry) ->
+        id: entry[0]
+        params: entry[1..-4]
+        time: entry.pop()
+        started: new Date entry.pop()
+        queued: new Date entry.pop()
+
+
+  # ### Get Processing Tasks Asynchronously
+
+  # Currently processing tasks are tasks in the `PROCESSING` list.
+  #
+  # `processing_tasks` is an asynchronous method. Arguments of the callback
+  # function follow node.js error handling convention: `err` and `res`.
+  #
+  # Below is an example `res` array:
+  #
+  #     [{ id:      '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
+  #        params:  [ 'param1', 'param2' ],
+  #        queued:  Sat, 12 May 2012 07:41:33 GMT // Date Object
+  #        started: Sat, 12 May 2012 07:41:39 GMT // Date Object
+  #      }, ...]
+  #
+  # **Usage:**
+  #
+  #     queue.processing_tasks (err, tasks) -> YOUR CODE
+  processing_tasks: (callback) ->
+    @redis.hvals @key('PROCESSING'), (err, res) ->
+      return callback err if err
+      callback null, res.map (entry) ->
+        entry = JSON.parse(entry)
+        id: entry[0]
+        params: entry[1..-3]
+        start: new Date entry.pop()
+        queued: new Date entry.pop()
+
+
+
+  # ### Get Source Tasks Asynchronously
+
+  # Get tasks in the `SOURCE` list. **NOTE:** Tasks in the `SOURCE` list does
+  # **NOT** equal to **pending** tasks! There may be tasks in the `QUEUED` lists
+  # need be processed before processing tasks in the `SOURCE` list.
+  #
+  # Accepted parameters are:
+  #
+  #   1. `skip` *(optional)*, the number of skipped tasks. Defaults to 0.
+  #   2. `take` *(optional)*, the number of tasks need be taken. Defaults to 10.
+  #   3. `callback`, the callback function. Arguments of which follows node.js error
+  #   handling convention: `err` and `res`.
+  #
+  # Below is an example `res` array:
+  #
+  #     [{ id:     '8c0c3eab-8114-41d6-8808-2ae8615d38b4',
+  #        params: [ 'param1', 'param2' ],
+  #        queued: Sat, 12 May 2012 07:41:33 GMT // Date Object
+  #      }, ...]
+  #
+  # Possible combos of arguments are:
+  #
+  #   1. `callback`. (leave `skip` defaults to 0, `take` defaults to 10).
+  #   2. `skip` and `callback`. (leave `take` defaults to 10).
+  #   3. `skip`, `take`, and `callback`.
+  #
+  # **Usage:**
+  #
+  #     queue.source_tasks 20, 5, (err, tasks) -> YOUR CODE
+  source_tasks: (args..., callback) ->
+    skip = args[0] or 0
+    take = args[1] or 10
+    @redis.lrange @key('SOURCE'), skip, skip + take - 1, (err, res) ->
+      callback err if err
+      callback null, res.map (entry) ->
+        entry = JSON.parse entry
+        id: entry[0]
+        params: entry[1..-2]
+        queued: new Date entry.pop()
+
+
+  # ### Get Workers Asynchronously
+
+  # Asynchronous method to get all **live** workers of the queue. **Live**
+  # workers are registered in the `WORKERS` hash. Values of `WORKERS` hash are
+  # in `hostname|ip|pid|since` format.
+  #
+  # Arguments of the callback function follow node.js error handling convention:
+  # `err` and `res`. `res` is an array of live workers. Each worker object have:
+  #
+  #   + `host`, the host name of the worker machine.
+  #   + `ip`, the first external IPv4 address of the worker machine.
+  #   + `pid`, the process id of the working process.
+  #   + `since`, the born date of the worker.
+  #
+  # Below is an example of returned workers:
+  #
+  #     [{
+  #        host: 'baoshan',
+  #        ip: '192.168.2.7',
+  #        pid: 1628
+  #        since: Sat, 12 May 2012 07:28:21 GMT // Date Object
+  #      }, ...]
+  #
+  # **Usage:**
+  #
+  #     queue.workers (err, workers) -> YOUR CODE
+  workers: (callback) =>
+    @redis.hvals @key('WORKERS'), (err, res) ->
+      return callback err if err
+      callback null, res.map((entry) ->
+        entry = entry.split '|'
+        host: entry[0]
+        ip: entry[1]
+        pid: parseInt entry[2]
+        since: new Date parseInt entry[3]
+      ).sort (a, b) ->
+        return  1 if a.ip  > b.ip
+        return -1 if a.ip  < b.ip
+        return  1 if a.pid > b.pid
+        return -1 if a.pid < b.pid

   # ### Get Statistics of a Queue Asynchronously
@@ -1011,4 +824,226 @@
           callback null, result

+# ### Worker Definition
+
+class Worker
+
+  constructor: (@queue, @handler) ->
+    {@name, @fairy, @redis, @pubsub} = queue
+    @worker_id = worker_id = uuid.v4()
+    @redis.hset @key('WORKERS'), worker_id, "#{os.hostname()}|#{server_ip()}|#{process.pid}|#{Date.now()}"
+
+    process.on 'uncaughtException', (err) =>
+      if @_handler_callback
+        console.log "Worker [#{worker_id.split('-')[0]}] registered for Task [#{@name}] will block its current processing group"
+        @_handler_callback {do: 'block', message: err.stack}
+      else
+        @unregist()
+
+    @pubsub.subscribe @key('ENQUEUED')
+    @pubsub.on 'message', (channel, message) =>
+      return unless channel is @key('ENQUEUED')
+      @_new_task = on
+      @awake() if @is_idling
+      # console.log 'awake'
+    @awake()
+
+  key: (key) -> "#{prefix}:#{key}:#{@name}"
+
+  # ### Exit When All Queues are Cleaned Up
+
+  # **Private** method. Wait if there're queues still working, or exit the
+  # process immediately.
+  unregist: =>
+    @redis.hdel @key('WORKERS'), @worker_id, ->
+      workers.splice workers.indexOf(@), 1
+      process.exit() unless workers.length
+      log_registered_workers()
+
+
+  # ### Poll New Task
+
+  # **Private** method. If any task presents in the `SOURCE` list, `lpop` from
+  # `SOURCE` (`rpush`) into `QUEUED`. The `lpop` and `rpush` are protected by
+  # a transaction.
+  #
+  # Since the task being popped and pushed should be known in prior of the
+  # begin of the transaction (aka, the `multi` command), we need to get the
+  # first task of the source list, and, watch the source list to prevent the
+  # same task being taken by two different workers.
+  #
+  # If there's no pending tasks of the same group, then process the task
+  # immediately.
+  #
+  # If there's no tasks in the `SOURCE` list, poll again after an interval of
+  # `polling_interval` milliseconds.
+  awake: =>
+    # console.log 'awake'
+    @is_idling = @_new_task = off
+    return @unregist() if exiting
+    # console.log @key('SOURCE')
+    @redis.watch @key('SOURCE')
+    @redis.lindex @key('SOURCE'), 0, (err, res) =>
+      if res
+        task = JSON.parse res
+        # console.log task
+        @redis.multi()
+        .lpop(@key('SOURCE'))
+        .rpush("#{@key('QUEUED')}:#{task[1]}", res)
+        .exec (multi_err, multi_res) =>
+          return @awake() unless multi_res and multi_res[1] is 1
+          @_process task
+      else
+        # console.log 'else'
+        @redis.unwatch()
+        return @awake() if @_new_task
+        @redis.llen @key('SOURCE'), (err, a, b) =>
+          return @awake() if b
+          @is_idling = on
+
+  # ### Process Each Group's First Task
+
+  # **Private** method. The real job is done by the passed in `handler` of
+  # `regist`ered method, when the job is:
+  #
+  #   + **successed**, pop the finished job from the group queue, and:
+  #     - continue process task of the same group if there's pending job(s) in
+  #     the same `QUEUED` list, or
+  #     - poll task from the `SOURCE` queue.
+  #   + **failed**, then inspect the passed in argument, retry or block
+  #   according to the `do` property of the error object.
+  #
+  # Calling the callback function is the responsibility of you. Otherwise
+  # `Fairy` will stop dispatching tasks.
+  _process: (task) =>
+    # console.log 'process'
+    @redis.hset @key('PROCESSING'), task[0], JSON.stringify([task..., start_time = Date.now()])
+
+    # Before Processing the Task:
+    #
+    #   1. Keep start time of processing.
+    #   2. Set `PROCESSING` hash in Redis.
+    #   3. Allow `retry_limit` times of retries.
+    processing = task[0]
+    retry_count = @retry_limit
+    errors = []
+
+    @_handler_callback = handler_callback = (err, res) =>
+
+      @_handler_callback = null
+
+      # Error handling routine:
+      #
+      #   1. Keep the error message.
+      #   2. According to specific error handling request, when:
+      #     + `block`: block the group immediately.
+      #     + `block-after-retry`: retry n times, block the group if still
+      #     fails (blocking).
+      #     + or: retry n times, skip the task if still fails (non-blocking).
+      #     Set `retry_limit` to `0` to skip failed tasks immediately.
+      if err
+        errors.push err.message or null
+        switch err.do
+          when 'block'
+            @redis.multi()
+              .rpush(@key('FAILED'), JSON.stringify([task..., Date.now(), errors]))
+              .hdel(@key('PROCESSING'), processing)
+              .sadd(@key('BLOCKED'), task[1])
+              .exec()
+            return @awake()
+          when 'block-after-retry'
+            return setTimeout call_handler, @retry_delay if retry_count--
+            @redis.multi()
+              .rpush(@key('FAILED'), JSON.stringify([task..., Date.now(), errors]))
+              .hdel(@key('PROCESSING'), processing)
+              .sadd(@key('BLOCKED'), task[1])
+              .exec()
+            return @awake()
+          else
+            return setTimeout call_handler, @retry_delay if retry_count--
+            @redis.multi()
+              .rpush(@key('FAILED'), JSON.stringify([task..., Date.now(), errors]))
+              .hdel(@key('PROCESSING'), processing)
+              .exec()
+
+      # Success handling routine:
+      #
+      #   1. Remove last task from processing hash.
+      #   2. Update statistics hash:
+      #     + total number of `finished` tasks;
+      #     + total `pending` time;
+      #   3. Track recent finished tasks in `RECENT` list.
+      #   4. Track tasks take the longest processing time in `SLOWEST` sorted
+      #   set.
+      else
+        finish_time  = Date.now()
+        process_time = finish_time - start_time
+        @redis.multi()
+          .hdel(@key('PROCESSING'), processing)
+          .hincrby(@key('STATISTICS'), 'FINISHED', 1)
+          .hincrby(@key('STATISTICS'), 'TOTAL_PENDING_TIME', start_time - task[task.length - 1])
+          .hincrby(@key('STATISTICS'), 'TOTAL_PROCESS_TIME', process_time)
+          .lpush(@key('RECENT'), JSON.stringify([task..., finish_time]))
+          .ltrim(@key('RECENT'), 0, @recent_size - 1)
+          .zadd(@key('SLOWEST'), process_time, JSON.stringify([task..., start_time]))
+          .zremrangebyrank(@key('SLOWEST'), 0, - @slowest_size - 1)
+          .exec()
+
+      @_continue_group task[1]
+
+    do call_handler = =>
+      @handler task[1...-1]..., (@_handler_callback = handler_callback)
+
+  # ### Continue Process a Group
+
+  # **Private** method. Upon successful execution of a task, or skipping a
+  # failed task:
+  #
+  #   1. `lpop` the current task from `QUEUED` list.
+  #   2. Check if there exists task in the same `QUEUED` list.
+  #   3. Process if `YES`, or poll `SOURCE` if `NO`.
+  #
+  # Above commands are protected by a transaction to prevent multiple workers
+  # processing a same task.
+  _continue_group: (group) =>
+    @redis.watch "#{@key('QUEUED')}:#{group}"
+    @redis.lindex "#{@key('QUEUED')}:#{group}", 1, (err, res) =>
+      if res
+        task = JSON.parse res
+        @redis.unwatch()
+        @redis.lpop "#{@key('QUEUED')}:#{group}"
+        return @requeue group if exiting
+        @_process task
+      else
+        @redis.multi()
+        .lpop("#{@key('QUEUED')}:#{group}")
+        .exec (multi_err, multi_res) =>
+          return @_continue_group group unless multi_res
+          return @unregist() if exiting
+          @is_idling = on
+          @awake()
+
+
+  # ### Requeue Tasks on Exit
+
+  # **Private** method. Before exiting, requeue all tasks in the processing
+  # `QUEUED` list back into `SOURCE` list to **prevent blocking** the group.
+  #
+  # To ensure the correct order of tasks, `lpush` tasks in the `QUEUED` list
+  # in their reverse order. The `lrange` and `lpush` is protected by a
+  # transaction for atomicity since other workers may still shifting tasks in
+  # the `SOURCE` list into `QUEUED` list.
+  #
+  # When tasks are requeued successfully, the worker `unregist` itself.
+  requeue: (group) =>
+    # @redis.publish "requeue", null
+    @redis.watch "#{@key('QUEUED')}:#{group}"
+    @redis.lrange "#{@key('QUEUED')}:#{group}", 0, -1, (err, res) =>
+      @redis.multi()
+      .lpush("#{@key('SOURCE')}", res.reverse()...)
+      .del("#{@key('QUEUED')}:#{group}")
+      .exec (multi_err, multi_res) =>
+        return @requeue group unless multi_res
+        @unregist()
+
 # ### Known Bugs:


Fixed #1056.

Fixed #1056.

--- app/core/social-handlers/GPlusHandler.coffee
+++ app/core/social-handlers/GPlusHandler.coffee
@@ -52,7 +52,7 @@
     @loggedIn = true
     try
       # Without removing this, we sometimes get a cross-domain error
-      d = JSON.stringify(_.omit(e, 'g-oauth-window'))
+      d = _.omit(e, 'g-oauth-window')
       storage.save(GPLUS_TOKEN_KEY, d)
     catch e
       console.error 'Unable to save G+ token key', e


Add failing tests for error reporting

Add failing tests for error reporting

error: pathspec 'test/error-func-undefined.coffee' did not match any file(s) known to git

--- test/error-func-undefined.coffee
+++ test/error-func-undefined.coffee
@@ -0,0 +1,61 @@
+describe 'compilation error "undefined function"', ->
+
+  it 'occurs if a function in the spec does not exist in the context', ->
+    a.throws (-> ss a: b: $bar: 5),
+    """
+Error: The function "$bar" is not defined.
+
+{
+  "a": {
+    "b": {
+      "$bar": 5
+        ^^^
+    }
+  }
+}"""
+
+
+  it 'correctly highlights position within a top-level compiling function', ->
+    a.throws (-> ss $not: $bar: 5 ),
+    """
+Error: The function "$bar" is not defined.
+
+{
+  "$not": {
+    "$bar": 5
+      ^^^
+  }
+}"""
+
+  it 'correctly highlights position within a sub-level compiling function', ->
+    a.throws (-> ss a: $not: $bar: 5 ),
+    """
+Error: The function "$bar" is not defined.
+
+{
+  "a": {
+    "$not": {
+      "$bar": 5
+        ^^^
+    }
+  }
+}"""
+
+  it 'correctly highlights position within a sub-level compiling function that accepts arrays', ->
+    a.throws (-> ss a: $or: [{$bar: 5}, {$gte: 6}] ),
+    """
+Error: The function "$bar" is not defined.
+
+{
+  "a": {
+    "$or": [
+      {
+        "$bar": 5
+          ^^^
+      },
+      {
+        "$gte": 6
+      }
+    ]
+  }
+}"""


add bands controller with basic crud

add bands controller with basic crud

error: pathspec 'controllers/bands.coffee' did not match any file(s) known to git

--- controllers/bands.coffee
+++ controllers/bands.coffee
@@ -0,0 +1,35 @@
+Band = require '../models/band'
+
+# GET /api/bands
+exports.index = (req, res) ->
+  Band.find().sort(created_at: 'descending').exec(
+    (err, results) ->
+      res.status(500).json error: err if err?
+      res.status(200).json results
+  )
+
+# GET /api/bands/:id
+exports.show = (req, res) ->
+  Band.findById req.params.id, (err, result) ->
+    res.status(500).json error: err if err?
+    res.status(200).json results
+
+# POST /api/bands
+exports.create = (req, res) ->
+  new Band(req.body)
+    .save (err, results) ->
+      res.status(500).json error: err if err?
+      res.status(200).json results
+
+# PUT /api/bands/:id
+exports.update = (req, res) ->
+  console.log req.body
+  Band.update {_id: req.params.id}, req.body, (err, count) ->
+    res.status(500).json error: err if err?
+    res.status(200).json "#{count} records updated"
+
+# DELETE /api/bands/:id
+exports.delete = (req, res) ->
+  Band.remove {_id: req.params.id}, (err, count) ->
+    res.status(500).json error: err if err?
+    res.status(200).json "#{count} records deleted"


Draft implementation of the abstract tree node

error: pathspec 'library/visualize/tree.coffee' did not match any file(s) known to git

--- library/visualize/tree.coffee
+++ library/visualize/tree.coffee
@@ -0,0 +1,74 @@
+###
+Copyright (c) 2013, Alexander Cherniuk <ts33kr@gmail.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+###
+
+_ = require "lodash"
+uuid = require "node-uuid"
+asciify = require "asciify"
+connect = require "connect"
+logger = require "winston"
+events = require "events"
+colors = require "colors"
+nconf = require "nconf"
+https = require "https"
+paths = require "path"
+http = require "http"
+util = require "util"
+fs = require "fs"
+
+# This class represents an abstract tree node that has no particular
+# semantics implemented, but rather has a common behavior patterns
+# that must be inherited by all kinds of different implementations.
+# Defines the foundation for tree traversal and event propagation.
+module.exports.Abstract = class Abstract extends events.EventEmitter
+
+    # This is a marker that indicates to some internal substsems
+    # that this class has to be considered abstract and therefore
+    # can not be treated as a complete class implementation. This
+    # mainly is used to exclude or account for abstract classes.
+    @abstract yes
+
+    # A public node constructor. Be sure to invoke it in your node
+    # implementations. By default it accepts parent node as param.
+    # Each node should generally know its parent in order to create
+    # complete and functional hierarchical tree for navigating it.
+    constructor: (@parent, @tag = uuid.v1()) ->
+        correct = @parent instanceof Abstract
+        correct = correct or not @parent?
+        abstract = "Cannot create abstract nodes"
+        invalid = "Not valid parent node object"
+        throw new Error abstract if @abstract()
+        throw new Error invalid unless correct
+
+    # The overriding of the standard `EventEmiter` emit method. This
+    # implementation always keep track of the sender and propagates
+    # the event up to the parent no, if such node has been defined.
+    # The exact semantics of propagation and such can be changed.
+    emit: (event, origin, parameters...) ->
+        correct = origin instanceof Abstract
+        invalid = "Not a valid origin node object"
+        throw new Error invalid unless correct
+        listened = super event, origin, parameters...
+        return if @blackhole or @suppress or not @parent?
+        try @parent?.emit event, origin, parameters...


Add a cursor-line decoration to the gutter

--- src/editor.coffee
+++ src/editor.coffee
@@ -1263,6 +1263,7 @@
   addCursor: (marker) ->
     cursor = new Cursor(editor: this, marker: marker)
     @cursors.push(cursor)
+    @addDecorationForMarker(marker, {class: 'cursor-line'})
     @emit 'cursor-added', cursor
     cursor


Removed "mesh" suffix.

Removed "mesh" suffix.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/urbanetic/meteor-atlas-util.git/'


rm dud

rm dud

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/damianb/crimson.git/'


added negative numbers to smartnumber directive

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/UM-USElab/gradecraft-development.git/'


added more custom conditional tags

added more custom conditional tags

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/wenchenglai/firehose-ember-shared.git/'


testes and querying for projects index page

[#95231438]

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/netguru/review.git/'


Add recolor library for hoc2014.

Add recolor library for hoc2014.

error: pathspec 'content/lib/recolor.coffee' did not match any file(s) known to git

--- content/lib/recolor.coffee
+++ content/lib/recolor.coffee
@@ -0,0 +1,156 @@
+# The recolor function, for Hour of Code 2014.
+#
+# How to use this function:
+# recolor ['/img/400x600-flower', '/img/400x600-red-flower'],
+#   (r1, g1, b1, r2, g2, b2) ->
+#     red: r1, green: g2, blue: (r2 + b1) / 2
+#
+# The first argument is an array of image URLs to load.
+# The second argument is a recoloring function that takes
+#   RGB components of the input functions and outputs RGB
+#   components of the output.
+# Then recolor will briefly show all the input images on the
+#   screen, and then calculate and output image on the
+#   screen based on your function.
+#
+# Some extra details:
+#   Returning a single number creates a grayscale image.
+#   Returning an 'alpha' component adds transparency.
+#   Grayscale images contribute only 1 channel.
+#   Full-color plus alpha images contribute 4 channels.
+#   Images are centered on top of each other if different sizes.
+
+global = window
+global.recolor = (urls, fn) ->
+  # Accept just a function
+  if not fn? and _.isFunction(urls) then fn = urls; urls = null
+  # Accept also nothing.
+  if not urls? then urls = []
+  # Accept bare url strings, not in an array.
+  if _.isString(urls)
+    # Accept any number of arguments.
+    args = Array.prototype.slice.call(arguments, 0);
+    # If the last one is a function, that's fn.
+    if _.isFunction(args[args.length - 1])
+      fn = args.pop()
+    urls = args
+  # If there's no function, just assume identity.
+  if not _.isFunction(fn) then fn = _.identity
+
+  # Set things up.  Set a zero sized turtle.
+  turtle.speed Infinity
+  wear width: 0, height: 0
+  if urls.length > 1
+    label '...',
+      zIndex: 3, color: white, font: '20px sans-serif',
+      textShadow: '0px 0px 7px black', turtleSpeed: Infinity
+  turtle.speed 'turtle'
+
+  # A couple short timeouts.
+  sec = (fn) -> setTimeout(fn, 1000)
+  moment = (fn) -> setTimeout(fn, 0)
+
+  # Create a sprite for each argument, wearing the requested image.
+  # Load them one at a time, so they are visible briefly.
+  sprites = []
+  data = []
+  for url in urls
+    s = new Sprite width: 0, height: 0
+    s.speed Infinity
+    s.css zIndex: 2
+    # Load a url, and wait for it to finish loading.
+    s.wear(url)
+    await s.done defer()
+    # How many color channels does this sprite have?
+    d = s.imagedata()
+    b = d.data
+    # Start assuming one channel.
+    d.channels = 1
+    for i in [0...b.length] by 4
+      # If red or green or blue differ by more than 1%, then three channels.
+      # if d.channels is 1 and not (b[i] is b[i + 1] is b[i + 2])
+      #   console.log('not gray', i, b[i], b[i+1], b[i+2])
+      if abs(b[i] - b[i+1]) > 3 or abs(b[i] - b[i+2]) > 3
+        d.channels = 3
+      # And also note an alpha channel.
+      if b[i + 3] < 255
+        d.alpha = true
+    if d.alpha and d.channels is 3 then d.channels = 4
+    sprites.push s
+    data.push d
+    # Describe the image, its dimensions, and number of channels.
+    $('label').text url.split('/').pop() +
+      ": #{d.width}x#{d.height}x#{d.channels}"
+    $('label').moveto window
+    await sec defer()
+
+  # Get the maximum dimensions of all the sprites.
+  maxheight = max(64, _.max _.map sprites, (s) -> s.height())
+  maxwidth = max(64, _.max _.map sprites, (s) -> s.width())
+  for d in data
+    d.xoff = floor((maxwidth - d.width) / 2)
+    d.yoff = floor((maxheight - d.height) / 2)
+
+  # Make an output sprite.
+  out = turtle
+  out.wear width: maxwidth, height: maxheight
+  out.css zIndex: 5
+  a = (0 for [0...maxheight * maxwidth * 4])
+
+  # This function fills in one pixel using the student fn.
+  dopixel = (x, y) ->
+    # Collect this pixel from each of the input channels.
+    inputs = []
+    for d in data
+      for c in [0...d.channels]
+        if d.xoff <= x < d.xoff + d.width and
+           d.yoff <= y < d.yoff + d.height
+          inputs.push d.data[c + 4 *
+              (x - d.xoff + (y - d.yoff) * d.width)]
+        else
+          inputs.push 0
+    # Call the student function, providing all inputs.
+    result = fn.apply(null, inputs)
+    # If the answer is a number, then it's grayscale.
+    if $.isNumeric(result) then result = [result]
+    # If the answer is an object, look for {red:, green:, blue:}.
+    if $.isPlainObject(result) then result = [
+      result.red ? result.r ? 0
+      result.green ? result.g ? 0
+      result.blue ? result.b ? 0
+      result.alpha ? result.a ? 255
+    ]
+    # Nothing returned?  Assume blackness.
+    # if not result? then result = [0]
+    # If it's less than 3 components, gray-up to 3.
+    while result.length < 3 then result.push(result[0] or 0)
+    # If less than 4 components, assume it's opaque.
+    if result.length < 4 then result.push(255)
+    # If longer than 4 components, truncate to 4.
+    result.length = 4
+    # Now copy in the components, pegging to 0..255.
+    loc = (y * maxwidth + x) * 4
+    for j in [0...4]
+      a[loc + j] = min(255, max(0, round(result[j])))
+
+  # Now go through in 64x64 blocks and call the student
+  # function to combine the image data
+  for yy in [0...maxheight] by 64
+    for xx in [0...maxwidth] by 64
+      # Do just 64x64 pieces of work.
+      for dy in [0...64]
+        y = yy + dy
+        if y >= maxheight then continue
+        for dx in [0...64]
+          x = xx + dx
+          if x >= maxwidth then continue
+          dopixel x, y
+      # draw after each 64x64 block is done
+      imd = width: maxwidth, height: maxheight, data: a
+      out.imagedata imd
+      # wait a moment to let the browser draw
+      await moment defer()
+
+  # clean up the temporary objects
+  remove $('label'), sprites
+  speed 1


Initial commit

Initial commit

--- boiler.coffee
+++ boiler.coffee
@@ -0,0 +1,135 @@
+shalow = (from, to) ->
+  to[key] = value for key, value of from
+  to
+file = process.argv[2]
+require file
+cache = shalow require.cache, {}
+find = (id) ->
+  obj = cache[id]
+  #if not obj?
+  #  console.log "dead end #{id}"
+  #  return
+  delete cache[id]
+  l =  (find v.id for v in obj.children)
+  l = l.reduce((a,b) -> a.concat b) if l.length > 0
+  l.concat([obj])
+ordered = find __filename
+ordered.pop()
+# OK to load other modules
+fs = require 'fs'
+path = require 'path'
+dir = path.dirname file
+serve = (code) ->
+  """
+  (function(everything){
+    var res={};
+    function resp(){
+      var resolvedPath = '',
+          resolvedAbsolute = false;
+
+      for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+        var path = (i >= 0) ? arguments[i] : '';
+
+        // Skip empty and invalid entries
+        if (typeof path !== 'string' || !path) {
+          continue;
+        }
+
+        resolvedPath = path + '/' + resolvedPath;
+        resolvedAbsolute = path.charAt(0) === '/';
+      }
+
+      // At this point the path should be resolved to a full absolute path, but
+      // handle relative paths to be safe (might happen when process.cwd() fails)
+
+      // Normalize the path
+      //resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) {
+      //  return !!p;
+      //}), !resolvedAbsolute).join('/');
+
+      return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
+    }
+    function relp(from,to){
+      from = resp(from).substr(1);
+      to = resp(to).substr(1);
+
+      function trim(arr) {
+        var start = 0;
+        for (; start < arr.length; start++) {
+          if (arr[start] !== '') break;
+        }
+
+        var end = arr.length - 1;
+        for (; end >= 0; end--) {
+          if (arr[end] !== '') break;
+        }
+
+        if (start > end) return [];
+        return arr.slice(start, end - start + 1);
+      }
+
+      var fromParts = trim(from.split('/'));
+      var toParts = trim(to.split('/'));
+
+      var length = Math.min(fromParts.length, toParts.length);
+      var samePartsLength = length;
+      for (var i = 0; i < length; i++) {
+        if (fromParts[i] !== toParts[i]) {
+          samePartsLength = i;
+          break;
+        }
+      }
+
+      var outputParts = [];
+      for (var i = samePartsLength; i < fromParts.length; i++) {
+        outputParts.push('..');
+      }
+
+      outputParts = outputParts.concat(toParts.slice(samePartsLength));
+
+      return outputParts.join('/');
+    }
+    function register(name, filename, paths, factory){
+      function require(path){
+        console.log('requiering '+path);
+        if (res[path]){
+          return res[path];
+        }else{
+          for (var i=0;i<paths.length;i++){
+            relfilename = paths[i]+'/'+path;
+            console.log(relfilename);
+            if (res[relfilename]){
+              console.log('Success!');
+              return res[relfilename];
+            }
+          }
+        }
+      }
+      var module={exports:{}};
+      factory.call(this,require,module.exports,module);
+      res[filename]=module.exports;
+    }
+    everything.call(this,register);
+  }).call(this,function(register){
+  #{code}
+  });
+  """
+boil = (obj) ->
+  name = path.basename obj.filename, '.js'
+  rfilename = path.relative dir, obj.filename
+  modulePath = path.dirname rfilename
+  modulePath = './'+rfilename.replace(/.js$/,'') if modulePath.indexOf('node_modules') < 0
+  paths = (path.relative(dir, p) for p in obj.paths)
+  code = fs.readFileSync obj.filename
+  """
+  register.call(this,
+    '#{name}',
+    '#{modulePath}',
+    ['#{paths.join "','"}'],
+    function(require, exports, module){
+  #{code}
+    }
+  );
+  """
+
+process.stdout.write serve (boil obj for obj in ordered).join '\n'


Update script to use correct coffeescript operators.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/appneta/hubot-jenkins-deploy.git/'


readability

readability

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/DocuSignDev/Jeeves.git/'


Add #getLibrary method to get access to underlying library.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/vjpr/onelog.git/'


help tag id was added to the List class

help tag id was added to the List class

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/NightingaleStudio/exam.js.git/'


Improve Documentation

Improve Documentation

--- src/fairy.coffee
+++ src/fairy.coffee
@@ -281,7 +281,6 @@
   #     + continue process task of the same group if there's pending job(s) in
   #     the same `QUEUED` list, or
   #     + poll task from the `SOURCE` queue.
-  #
   #   * **failed**, then inspect the passed in argument, retry or block
   #   according to the `do` property of the error object.
   #


Add more info to new. Closes gh-619.

Add more info to new. Closes gh-619.

--- src/index.coffee
+++ src/index.coffee
@@ -8,8 +8,9 @@
 create = (skeleton, path = '.') ->
   unless skeleton
     logger.error '''
-  You must specify skeleton (boilerplate) from which brunch will initialize
-  new application.
+  You must specify skeleton (boilerplate) from which brunch will initialize new app:
+
+  brunch new --skeleton <path-or-URI>

   You can specify directory on disk, Git URL or GitHub url (gh:user/repo).


moved CreateUniverse

moved CreateUniverse

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/ChHeLeJa/SpaceTrader.git/'


more slack functions

more slack functions

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/bubblegroup/bubblebot.git/'


increasing timeout

increasing timeout


+ test string whether function

+ test string whether function

error: pathspec 'src/is/string/function.coffee' did not match any file(s) known to git

--- src/is/string/function.coffee
+++ src/is/string/function.coffee
@@ -0,0 +1,4 @@
+module.exports = (aFuncString)->
+  result = /^\s*function\s+\S*\s*\(.*\)\s*{.*}\s*$/.test(aFuncString)
+
+


Add matching file model

Add matching file model

error: pathspec 'lib/model/matching-file.coffee' did not match any file(s) known to git

--- lib/model/matching-file.coffee
+++ lib/model/matching-file.coffee
@@ -0,0 +1,3 @@
+module.exports =
+class MatchingFile
+  constructor: (@filePath, @relativePath) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

error: pathspec 'src/tween.coffee' did not match any file(s) known to git

--- src/tween.coffee
+++ src/tween.coffee
@@ -0,0 +1,26 @@
+###*
+The Flickerable module provides a method to flicker a sprite between solid and 50% opacity.
+
+@name Flickerable
+@module
+@constructor
+@param {Object} I Instance variables
+@param {Core} self Reference to including object
+###
+Tween = (I={}, ) ->
+  # Set some default properties
+  Object.reverseMerge I,
+    color: "blue"
+    height: 32
+    width: 32
+    # spriteName: "block" # Use the name of a sprite in the images folder
+
+  # Inherit from game object
+  self = GameObject(I)
+
+  # Add events and methods here
+  self.bind "update", ->
+    ; # Add update method behavior
+
+  # We must always return self as the last line
+  return self


logic fixes

logic fixes

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/nickrobson/party.git/'


Made some changes into the french file

Made some changes into the french file

--- app/locale/fr.coffee
+++ app/locale/fr.coffee
@@ -158,7 +158,7 @@
     unwatch: "Ne plus regarder"
     submit_patch: "Soumettre un correctif"
     submit_changes: "Soumettre des Changements"
-#    save_changes: "Save Changes"
+    save_changes: "Sauvegarder les modifications"

   general:
     and: "et"
@@ -263,7 +263,7 @@
     victory_experience_gained: "XP gagnée"
     victory_gems_gained: "Gemmes gagnées"
     victory_new_item: "Nouvel item"
-#    victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
+    victory_viking_code_school: "Par la barbe d'Odin, c'est un niveau difficile que tu viens de compléter! Si tu n'est pas un développeur de logiciel, tu devrais l'être ! Ceci vient de te propulser dans l'école de Code Vikings où tu pourras amener tes habilités au prochain niveau et devenir un développer web profesionnel en deux semaines."
     victory_become_a_viking: "Devenez un viking"
     guide_title: "Guide"
     tome_minion_spells: "Les sorts de vos soldats" # Only in old-style levels.
@@ -455,24 +455,24 @@
     was_free_until: "Vous aviez un abonnement gratuit jusqu'à"
     managed_subs: "Gestion des abonnements"
     managed_subs_desc: "Ajout d'abonnements pour les autres joueurs (étudiants,enfants,etc.) for other players."
-#    managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
-#    group_discounts: "Group discounts"
-#    group_discounts_1: "We also offer group discounts for bulk subscriptions."
-#    group_discounts_1st: "1st subscription"
-#    group_discounts_full: "Full price"
-#    group_discounts_2nd: "Subscriptions 2-11"
-#    group_discounts_20: "20% off"
-#    group_discounts_12th: "Subscriptions 12+"
-#    group_discounts_40: "40% off"
-#    subscribing: "Subscribing..."
-#    recipient_emails_placeholder: "Enter email address to subscribe, one per line."
-#    subscribe_users: "Subscribe Users"
-#    users_subscribed: "Users subscribed:"
-#    no_users_subscribed: "No users subscribed, please double check your email addresses."
-#    current_recipients: "Current Recipients"
-#    unsubscribing: "Unsubscribing..."
-#    subscribe_prepaid: "Click Subscribe to use prepaid code"
-#    using_prepaid: "Using prepaid code for monthly subscription"
+    managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
+    group_discounts: "Rabais de groupes"
+    group_discounts_1: "Nous offrons des rabais de groupe pour les gros abonnements"
+    group_discounts_1st: "Premier abonnement"
+    group_discounts_full: "Plein prix"
+    group_discounts_2nd: "Abonnements 2-11"
+    group_discounts_20: "Rabais de 20%"
+    group_discounts_12th: "Abonnements 12+"
+    group_discounts_40: "Rabais de 40%"
+    subscribing: "S'inscrit..."
+    recipient_emails_placeholder: "Entrez votre courriel pour vous abonner, un par ligne."
+    subscribe_users: "Seulement pour les usagers aboonés"
+    users_subscribed: "Usagers abonnés:"
+    no_users_subscribed: "Aucun usager abonnés, veuillez vérifier vos courriels."
+    current_recipients: "Recipients courant"
+    unsubscribing: "Desincription en cours..."
+    subscribe_prepaid: "Cliquer S'abonner pour utiliser du code prépayé"
+    using_prepaid: "Utiliser le code prépayé pour un abonnement mensuel"

   choose_hero:
     choose_hero: "Choisissez votre héros"
@@ -500,14 +500,14 @@
     blocks: "Absorbe" # As in "this shield blocks this much damage"
     backstab: "Poignardé" # As in "this dagger does this much backstab damage"
     skills: "Compétences"
-#    attack_1: "Deals"
-#    attack_2: "of listed"
-#    attack_3: "weapon damage."
-#    health_1: "Gains"
-#    health_2: "of listed"
-#    health_3: "armor health."
-#    speed_1: "Moves at"
-#    speed_2: "meters per second."
+    attack_1: "Inflige"
+    attack_2: "Classé de"
+    attack_3: "Dommage causé par l'arme"
+    health_1: "Gains"
+    health_2: "Provenance de la liste"
+    health_3: "Endurance de l'armure"
+    speed_1: "Se mouvoit à"
+    speed_2: "mètres par seconde"
     available_for_purchase: "Disponible à l'achat" # Shows up when you have unlocked, but not purchased, a hero in the hero store
     level_to_unlock: "Niveau à débloquer :" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
     restricted_to_certain_heroes: "Seulement certains héros peuvent jouer ce niveau."
@@ -581,13 +581,13 @@
     michael_blurb: "Sys Admin"
     matt_title: "Programmeur" # {change}
     matt_blurb: "Bicycliste"
-#    cat_title: "Chief Artisan"
-#    cat_blurb: "Airbender"
-#    josh_title: "Game Designer"
-#    josh_blurb: "Floor Is Lava"
-#    jose_title: "Music"
-#    jose_blurb: "Taking Off"
-#    retrostyle_title: "Illustration"
+    cat_title: "Chef Artisan"
+    cat_blurb: "Seigneur de l'air"
+    josh_title: "Designer de jeu"
+    josh_blurb: "Le plancher est de la lave"
+    jose_title: "Musique"
+    jose_blurb: "Décollage"
+    retrostyle_title: "Illustration"
 #    retrostyle_blurb: "RetroStyle Games"

   teachers:
@@ -595,7 +595,7 @@
     intro_1: "CodeCombat est un jeu en ligne qui enseigne la programmation. Les élèves écrivent du code dans de vrais langages de programmation."
     intro_2: "Aucune expérience requise !"
     free_title: "Combien cela coûte-t-il ?"
-#    cost_china: "CodeCombat in China is free for the first five levels, after which it costs $9.99 USD per month for access to our other 140+ levels on our exclusive China servers."
+    cost_china: "CodeCombat en Chine est gratuit pour les cinq premiers niveaux,après le jeu coûte 9.99$ US par mois pour avoir un accès aux autres 140+ niveaux sur les serveurs exlcusifs chinois"
     free_1: "La version de base de CodeCombat est gratuite ! Il y a 70+ niveaux gratuits qui couvrent chaque concepts." # {change}
     free_2: "Un abonnement mensuel fournit l'accès à des vidéos de tutoriels ainsi qu'à des niveaux d'entraînement supplémentaires."
     teacher_subs_title: "Les enseignants reçoivent un abonnement gratuit !"
@@ -608,14 +608,14 @@
     sub_includes_4: "Support email premium"
     sub_includes_5: "7 nouveaux héros avec des capacités uniques à maitriser"
     sub_includes_6: "3500 gemmes bonus chaque mois"
-#    sub_includes_7: "Private Clans"
-#    monitor_progress_title: "How do I monitor student progress?"
-#    monitor_progress_1: "Student progress can be monitored by creating a"
-#    monitor_progress_2: "for your class."
-#    monitor_progress_3: "To add a student, send them the invite link for your Clan, which is on the"
-#    monitor_progress_4: "page."
-#    monitor_progress_5: "After they join, you will see a summary of the student's progress on your Clan's page."
-#    private_clans_1: "Private Clans provide increased privacy and detailed progress information for each student."
+    sub_includes_7: "Clans Privées"
+    monitor_progress_title: "Comment puis-je faire pour surveiller les progrès des étudiants?"
+    monitor_progress_1: "Le progès des étudiants peut être surveiller en créant un"
+    monitor_progress_2: "pour votre classe"
+    monitor_progress_3: "Pour ajouter un étudiant, envoyer leur le lien contenant une invitation pour votre Clan"
+    monitor_progress_4: "page."
+    monitor_progress_5: "After they join, you will see a summary of the student's progress on your Clan's page."
+    private_clans_1: "Private Clans provide increased privacy and detailed progress information for each student."
     private_clans_2: "Pour créer un Clan privé, veuillez vous référer à la boîte à cocher 'Faire un clan privé' pendant la création du clan."
     private_clans_3: "."
     who_for_title: "A qui CodeCombat est t-il destiné ?"


Still possible to use supersonic.logger.log

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/AppGyver/supersonic.git/'


:space_invader: 5 retries before 'try another task' message

--- app/assets/javascripts/lib/nypl_inspector.coffee
+++ app/assets/javascripts/lib/nypl_inspector.coffee
@@ -8,7 +8,7 @@

     window.nypl_inspector = @ # to make it accessible from console
     @desktopWidth = 600
-    @retries = 3
+    @retries = 5

     defaults =
       flaggableType: 'Polygon'


In Meteor tests for kill internal mongo, call done in a 'once' event listener

--- tests/MeteorTest.coffee
+++ tests/MeteorTest.coffee
@@ -232,7 +232,7 @@
         expect(resultList,"Found more than one mongod child").have.length.of 1
         meteor.kill()
         meteor.meteorMongodb.kill()
-        meteor.meteorMongodb.on "kill-done",->
+        meteor.meteorMongodb.once "kill-done",->
           lookUpMongodChilds pid,(err, resultList )->
             expect(err,"found mongod childs").not.to.be.null
             expect(resultList,"the mongod childs were not killed").not.to.be.ok


Added test/test.coffee (WIP unit test for Slowcopy class)

error: pathspec 'test/test.coffee' did not match any file(s) known to git

--- test/test.coffee
+++ test/test.coffee
@@ -0,0 +1,31 @@
+
+{ expect } = require('chai')
+
+{ Slowcopy } = require './../index.coffee'
+
+{ makeFileOfSizeInMBytes, verifyTwoFilesAreTheSame } = require './lib/test-utils.coffee'
+
+describe 'Slowcopy', ->
+
+  it 'Work in progress', (done)->
+    @timeout 5000
+
+    inputFilePath = './test/sample-files/dummy_autogenerated_1.file'
+
+    outputFilePath = './test/sample-files/dummy_autogenerated_1_output.file'
+
+    makeFileOfSizeInMBytes inputFilePath, 30, ->
+
+      sc = new Slowcopy inputFilePath, outputFilePath, 1
+
+      sc.on 'end', ->
+        verifyTwoFilesAreTheSame inputFilePath, outputFilePath, (areTheySame)->
+          expect(areTheySame).to.equal.true
+          done()
+
+      sc.copy()
+
+
+
+
+


add some tests

add some tests

--- test/test.coffee
+++ test/test.coffee
@@ -0,0 +1,36 @@
+
+assert = require 'assert'
+
+lib = require './../lib'
+
+
+# Tue, 07 Jun 2011 18:51:45 GMT
+t = new Date 1307472705867
+
+
+TESTS = [
+
+  ["%Y", "2011", "2011"]
+
+  ["%m", "06"]
+  ["%b", "Jun", "Jun"]
+  ["%B", "June", "June"]
+
+  ["%d", null, "07"]
+
+  ["%H", null, "18"]
+
+  ["%M", "51", "51"]
+
+  ["%S", "45", "45"]
+
+  ["%s", "1307472705"]
+]
+
+for [format, expectedNonUTC, expectedUTC] in TESTS
+  expectedUTC or= expectedNonUTC
+  for [name, expected] in [['strftime', expectedNonUTC], ['strftimeUTC', expectedUTC]]
+    if expected
+      got = lib[name] format, t
+      assert.equal expected, got, "Error for #{name}(#{JSON.stringify(format)}, t): expected #{JSON.stringify(expected)}, got #{JSON.stringify(got)}"
+


add ipfs release task

add ipfs release task

error: pathspec 'tasks/ipfs.coffee' did not match any file(s) known to git

--- tasks/ipfs.coffee
+++ tasks/ipfs.coffee
@@ -0,0 +1,19 @@
+module.exports = (grunt) ->
+  require('shelljs/global')
+
+  grunt.registerTask "ipfs", "distribute into ipfs", (env_)  =>
+    env = env_ || "development"
+
+    ipfs_path = "~/go/bin"
+    build_dir = "dist/dapp/"
+    cmd = "#{ipfs_path}/ipfs add -r #{build_dir}"
+
+    grunt.log.writeln("=== adding #{cmd} to ipfs")
+    result = exec(cmd)
+
+    rows = result.output.split("\n")
+    dir_row = rows[rows.length - 2]
+    dir_hash = dir_row.split(" ")[1]
+
+    grunt.log.writeln("=== DApp available at http://localhost:8080/ipfs/#{dir_hash}/")
+


_setCallbacks method was implemented

_setCallbacks method was implemented


snippetを用意してみた

snippetを用意してみた

error: pathspec 'snippets/language-review.cson' did not match any file(s) known to git


home/send-invites: add already member invitation modal

error: pathspec 'client/home/lib/myteam/components/hometeamsendinvites/alreadymemberinvitationmodal.coffee' did not match any file(s) known to git

--- client/home/lib/myteam/components/hometeamsendinvites/alreadymemberinvitationmodal.coffee
+++ client/home/lib/myteam/components/hometeamsendinvites/alreadymemberinvitationmodal.coffee
@@ -0,0 +1,21 @@
+kd = require 'kd'
+_ = require 'lodash'
+ContentModal = require 'app/components/contentModal'
+
+module.exports = class AlreadyMemberInvitationModal extends ContentModal
+
+  constructor: (options = {}, data) ->
+
+    options = _.assign {}, options,
+      title                   : 'Invites Already Sent'
+      overlay                 : yes
+      height                  : 'auto'
+      cssClass                : 'admin-invite-confirm-modal content-modal'
+      buttons :
+        OK :
+          title: 'OK'
+          itemClass : kd.ButtonView
+          style : 'solid medium'
+          callback : -> options.success()
+
+    super options, data


config: add trailing slash for stripping handler’s prefix

--- config/workers.coffee
+++ config/workers.coffee
@@ -83,7 +83,7 @@
         locations       : [
           {
             location    : '/countly/'
-            proxyPass   : "#{credentials.countly.host}"
+            proxyPass   : "#{credentials.countly.host}/"
             extraParams : [
               'proxy_hide_header X-Frame-Options;'
               'proxy_hide_header X-XSS-Protection;'


workers: remove trollmode

workers: remove trollmode

--- config/workers.coffee
+++ config/workers.coffee
@@ -295,13 +295,6 @@
         command         :
           run           : "#{GOBIN}/topicfeed"
           watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/topicfeed -watch socialapi/workers/topicfeed"
-
-    trollmode           :
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/trollmode"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/trollmode -watch socialapi/workers/trollmode"

     privatemessageemailfeeder:
       group             : "socialapi"


Remove references to windows as they close

--- src/browser/application.coffee
+++ src/browser/application.coffee
@@ -42,6 +42,8 @@

     appWindow.show()
     @windows.push(appWindow)
+    appWindow.on 'closed', =>
+      @removeAppWindow(appWindow)

   handleMenuItems: (menu) ->
     menu.on 'application:quit', -> app.quit()
@@ -57,6 +59,9 @@

     menu.on 'application:run-specs', =>
       @openWithOptions(test: true)
+
+  removeAppWindow: (appWindow) =>
+    @windows.splice(idx, 1) for w, idx in @windows when w is appWindow

   # Opens up a new {AtomWindow} to run specs within.
   #


Tests for showing thread in course discussion (both loaded and not loaded)

error: pathspec 'common/static/coffee/spec/discussion/discussion_router_spec.coffee' did not match any file(s) known to git

--- common/static/coffee/spec/discussion/discussion_router_spec.coffee
+++ common/static/coffee/spec/discussion/discussion_router_spec.coffee
@@ -0,0 +1,189 @@
+describe 'DiscussionRouter', ->
+  beforeEach ->
+    DiscussionSpecHelper.setUpGlobals()
+    DiscussionSpecHelper.setUnderscoreFixtures()
+    appendSetFixtures("""
+
+      """)
+
+    @threads = [
+      DiscussionViewSpecHelper.makeThreadWithProps({
+        id: "1",
+        title: "Thread1",
+        votes: {up_count: '20'},
+        pinned: true,
+        comments_count: 1,
+        created_at: '2013-04-03T20:08:39Z',
+      }),
+      DiscussionViewSpecHelper.makeThreadWithProps({
+        id: "2",
+        title: "Thread2",
+        votes: {up_count: '42'},
+        comments_count: 2,
+        created_at: '2013-04-03T20:07:39Z',
+      }),
+      DiscussionViewSpecHelper.makeThreadWithProps({
+        id: "3",
+        title: "Thread3",
+        votes: {up_count: '12'},
+        comments_count: 3,
+        created_at: '2013-04-03T20:06:39Z',
+      }),
+      DiscussionViewSpecHelper.makeThreadWithProps({
+        id: "4",
+        title: "Thread4",
+        votes: {up_count: '25'},
+        comments_count: 0,
+        pinned: true,
+        created_at: '2013-04-03T20:05:39Z',
+      }),
+    ]
+
+    spyOn(DiscussionUtil, 'makeWmdEditor')
+    @discussion = new Discussion(_.map(@threads, (thread_spec) -> new Thread(thread_spec)), {pages: 2, sort: 'date'})
+    @course_settings = new DiscussionCourseSettings({
+          "category_map": {
+            "children": ["Topic", "General"],
+            "entries": {
+              "Topic": {"is_cohorted": false, "id": "topic"},
+              "General": {"is_cohorted": false, "id": "general"}
+            }
+          },
+          "allow_anonymous": false,
+          "allow_anonymous_to_peers": false,
+          "is_cohorted": true,
+          "cohorts": [
+            {"id": 1, "name": "Cohort1"},
+            {"id": 2, "name": "Cohort2"}
+          ]
+        })
+    @router = new DiscussionRouter({discussion: @discussion, course_settings: @course_settings})
+
+  describe 'showThread', ->
+    existingCheck = (thread_id) ->
+      it "shows thread #{thread_id}, which is already in threads collection", ->
+        DiscussionSpecHelper.makeAjaxSpy(() -> )
+        spyOn(@router, 'renderThreadView')
+        # precondition check - thread is in router's collection
+        expect(@router.discussion.get(thread_id)).not.toBeUndefined()
+        @router.showThread("irrelevant forum name", thread_id)
+        expect($.ajax).not.toHaveBeenCalled()
+        expect(@router.renderThreadView).toHaveBeenCalled()
+
+    missingCheck = (forum_name, thread_id) ->
+      it "requests thread #{thread_id} in forum #{forum_name} if not already in collection", ->
+
+        DiscussionSpecHelper.makeAjaxSpy(
+          (params) -> expect(params.url.path()).toBe(DiscussionUtil.urlFor('retrieve_single_thread', forum_name, thread_id))
+        )
+        spyOn(@router, 'renderThreadView')
+        # precondition check - thread is in router's collection
+        expect(@router.discussion.get(thread_id)).toBeUndefined()
+        @router.showThread(forum_name, thread_id)
+        # in this case showThread makes a hidden async ajax request - it's hard to hook inot it, so it's simpler and
+        # sufficient to just schedule assertion to run as soon as possible, but not immediately
+        setTimeout (() -> expect(@router.renderThreadView).toHaveBeenCalled()), 0
+
+
+    existingCheck('1')
+    existingCheck('2')
+    existingCheck('3')
+    existingCheck('4')
+
+    missingCheck('forum1', '12')
+    missingCheck('forum2', '15')
+    missingCheck('forum3', '112')
+


Add log

Add log

--- views/update.cjsx
+++ views/update.cjsx
@@ -20,6 +20,7 @@
     if info == 'error'
       console.log 'Check update error.'
       return
+    console.log "Remote version: #{info.version}. Current version: #{POI_VERSION}"
     if info.version != POI_VERSION
       updateInfo = info
       title = '更新版本'


added specs

added specs

error: pathspec 'spec/block-cursor-spec.coffee' did not match any file(s) known to git

--- spec/block-cursor-spec.coffee
+++ spec/block-cursor-spec.coffee
@@ -0,0 +1,45 @@
+blockCursor = require '../lib/block-cursor'
+
+describe 'Block Cursor', ->
+  cursorColorStyleElement = blockCursor.getCursorColorStyleElement()
+
+  beforeEach ->
+    waitsForPromise -> atom.packages.activatePackage 'block-cursor'
+
+  it 'adds the block-cursor class to the workspaceView', ->
+    workspaceView = atom.views.getView atom.workspace
+    expect(workspaceView.className).toMatch /\s*block-cursor\s*/
+
+  it 'adds a style element to the atom-styles element in the head element', ->
+    foundStyleElement = false
+    for styleElement in document.querySelector('head atom-styles').childNodes when styleElement is cursorColorStyleElement
+      foundStyleElement = true
+      break
+    expect(foundStyleElement).toEqual(true)
+
+  describe 'Configuration', ->
+    describe 'the cursorBlink option', ->
+      it 'removes the block-cursor-no-blink class from the workspaceView when enabled', ->
+        workspaceView = atom.views.getView atom.workspace
+        atom.config.set 'block-cursor.cursorBlink', true
+        expect(workspaceView.className).not.toMatch /\s*block-cursor-no-blink\s*/
+
+      it 'adds the block-cursor-no-blink class to the workspaceView when disabled', ->
+        workspaceView = atom.views.getView atom.workspace
+        atom.config.set 'block-cursor.cursorBlink', false
+        expect(workspaceView.className).toMatch /\s*block-cursor-no-blink\s*/
+
+    describe 'the cursorColor option', ->
+      it 'changes the first rule of the stylesheet that is inserted by the package', ->
+        initialColor = atom.config.get 'block-cursor.cursorColor'
+        initialFirstRule = cursorColorStyleElement.sheet.cssRules[0].cssText
+        newColor = initialColor
+        newColor.red = (initialColor.red + 1) % 255
+        atom.config.set 'block-cursor.cursorColor', newColor
+        newFirstRule = cursorColorStyleElement.sheet.cssRules[0].cssText
+        newColorFromRule = newFirstRule.match /rgb\((\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3})\)/
+
+        expect(newFirstRule).not.toEqual initialFirstRule
+        expect(parseInt newColorFromRule[1], 10).toEqual newColor.red
+        expect(parseInt newColorFromRule[2], 10).toEqual newColor.green
+        expect(parseInt newColorFromRule[3], 10).toEqual newColor.blue


Fix tests

Fix tests

--- spec/fuzzy-finder-spec.coffee
+++ spec/fuzzy-finder-spec.coffee
@@ -259,8 +259,8 @@

       expect(finderView.find('.file').length).toBe 2

-      expect(finderView.find('.status.modified').length).toBe 1
-      expect(finderView.find('.status.new').length).toBe 1
+      expect(finderView.find('.status.status-modified').length).toBe 1
+      expect(finderView.find('.status.status-added').length).toBe 1

   describe "common behavior between file and buffer finder", ->
     describe "when the fuzzy finder is cancelled", ->
@@ -518,8 +518,8 @@
         project.getRepo().getPathStatus(editor.getPath())

         rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
-        expect(finderView.find('.status.modified').length).toBe 1
-        expect(finderView.find('.status.modified').closest('li').find('.file').text()).toBe 'sample.js'
+        expect(finderView.find('.status.status-modified').length).toBe 1
+        expect(finderView.find('.status.status-modified').closest('li').find('.file').text()).toBe 'sample.js'

     describe "when a new file is shown in the list", ->
@@ -528,8 +528,8 @@
         project.getRepo().getPathStatus(editor.getPath())

         rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
-        expect(finderView.find('.status.new').length).toBe 1
-        expect(finderView.find('.status.new').closest('li').find('.file').text()).toBe 'newsample.js'
+        expect(finderView.find('.status.status-added').length).toBe 1
+        expect(finderView.find('.status.status-added').closest('li').find('.file').text()).toBe 'newsample.js'

   describe "when the filter text contains a colon followed by a number", ->
     it "opens the selected path to that line number", ->


Assert preserved search text is selected

--- spec/fuzzy-finder-spec.coffee
+++ spec/fuzzy-finder-spec.coffee
@@ -752,7 +752,7 @@
           expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor1.getPath()
           expect(atom.workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [3, 4]

-  describe "Preserve last search", ->
+  describe "preserve last search", ->
     it "does not preserve last search by default", ->
       dispatchCommand('toggle-file-finder')
       expect(atom.workspace.panelForItem(projectView).isVisible()).toBe true
@@ -765,7 +765,7 @@
       expect(atom.workspace.panelForItem(projectView).isVisible()).toBe true
       expect(projectView.filterEditorView.getText()).toBe ''

-    it "preserves last search when config is set", ->
+    it "preserves last search when the config is set", ->
       atom.config.set("fuzzy-finder.preserveLastSearch", true)

       dispatchCommand('toggle-file-finder')
@@ -778,6 +778,7 @@
       dispatchCommand('toggle-file-finder')
       expect(atom.workspace.panelForItem(projectView).isVisible()).toBe true
       expect(projectView.filterEditorView.getText()).toBe 'this should show up next time we open finder'
+      expect(projectView.filterEditorView.getModel().getSelectedText()).toBe 'this should show up next time we open finder'

   describe "Git integration", ->
     [projectPath, gitRepository, gitDirectory] = []


Loads tooltip component

Loads tooltip component

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/mxabierto/adela.git/'


update [skip ci]

update [skip ci]

--- app/assets/javascripts/uploadcare/locale/ar.js.coffee
+++ app/assets/javascripts/uploadcare/locale/ar.js.coffee
@@ -1,8 +1,8 @@
-# Note: English locale is the default and used as a fallback.
+# Note: English locale is the default and used as a fallback.
 uploadcare.namespace 'uploadcare.locale.translations', (ns) ->
   ns.ar =
-    uploading: 'Uploading... Please wait.'
-    loadingInfo: 'Loading info...'
+    uploading: 'الرجاء الإنتظار.... يتم الرفع'
+    loadingInfo: '...تحميل المعلومات'
     errors:
       default: 'خطأ'
       baddata: 'قيمة غير صحيحة'
@@ -35,7 +35,7 @@
           preview: 'معاينة'
           file: 'الملفات المحلية'
           url: 'روابط التعسفية'
-          camera: 'Camera'
+          camera: 'كاميرا'
         file:
           drag: 'إفلات الملف هنا'
           nodrop: 'تحميل الملفات من جهاز الكمبيوتر الخاص بك'
@@ -50,16 +50,16 @@
           input: 'الصق الرابط الخاص بك هنا ...'
           button: 'تحميل'
         camera:
-          capture: 'Take a photo'
-          mirror: 'Mirror'
-          retry: 'Request permissions again'
+          capture: 'إلتقط صورة'
+          mirror: 'مرآة'
+          retry: 'إعادة طلب الأذونات'
           pleaseAllow:
-            title: 'Please allow access to your camera'
-            text: 'You have been prompted to allow camera access from this site. ' +
-                  'In order to take pictures with your camera you must approve this request.'
+            title: 'الرجاء السماح بتشغيل كميرتك '
+            text: 'لقد تم السماح للكاميرا بالوصول لهذا الموقع. ' +
+                  'لكي تلتقط الصور بكاميرتك، يجب السماح لهذا الطلب '
           notFound:
-            title: 'No camera detected'
-            text: 'Looks like you have no camera connected to this device.'
+            title: 'لم يتم العثور على كاميرا '
+            text: 'يبدو أنه لا يوجد كاميرا موصولة بهذا الجهاز'
         preview:
           unknownName: 'غير معروف'
           change: 'إلغاء'


spelling correction

spelling correction

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/GregDesplaces/classe-1914.git/'


atom.workspaceView is deprecated. Fixes #8 and fixes #18

--- lib/toolbar-button-view.coffee
+++ lib/toolbar-button-view.coffee
@@ -23,7 +23,7 @@
     @on 'click', =>
       if !@hasClass('disabled')
         if typeof(callback) == 'string'
-          atom.workspaceView.trigger callback
+          atom.commands.dispatch atom.views.getView(atom.workspace), callback
         else
           callback()


fix resources:init

fix resources:init

--- src/steroids/Providers.coffee
+++ src/steroids/Providers.coffee
@@ -50,22 +50,27 @@

     console.log("Ensuring that your app has the SandboxDB data provider configured...")

-    @getProviderByName("appgyver_sandbox").then (provider) =>
-      if provider?
-        # provider exists, all good
-        deferred.resolve()
-      else
+    @getProviderByName("appgyver_sandbox").then(
+      (provider) =>
+        if provider?
+          # provider exists, all good
+          deferred.resolve()
+        else
+          deferred.reject "Got empty provider, something's wrong. :("
+      (error) =>
+        console.log "SandboxDB data provider not found, adding it for your app..."
         data =
           providerTypeId: 6    # appgyver_sandbox id specified in config api
           name: "appgyver_sandbox"

-      @config_api.post "/app/#{@getAppId()}/service_providers.json", data, (err, req, res, obj) =>
-        if obj['uid']
-          deferred.resolve()
-        else
-          deferred.reject JSON.stringify(err)
-
-        @config_api.close()
+        @config_api.post "/app/#{@getAppId()}/service_providers.json", data, (err, req, res, obj) =>
+          if obj['uid']
+            deferred.resolve "Provider successfully added!"
+          else
+            deferred.reject err
+
+          @config_api.close()
+    )

     deferred.promise

@@ -406,8 +411,8 @@
         #   You can then add the provider for your app with the command

         #     #{chalk.bold("$ steroids providers:add providerName")}
-
         #   """
+
         errorMsg =
           """
           Could not find the sandbox data provider for your app. Please run


CurrentBranch spec.

CurrentBranch spec.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/aki77/atomatigit.git/'