Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Rename to Pebble

  • Loading branch information...
commit 18cb14d5cafa4f659879d5397d624feb257afbe2 1 parent 5514e53
@Sutto authored
View
36 README.md
@@ -1,6 +1,6 @@
-# Chainsaw App v2
+# Pebble
-A complete rewrite of the old convoluted chainsaw codebase to make it:
+Pebble is real time event streams / pub sub done simply.
* Cleaner in terms of code
* Simpler to run (One process, versus many)
@@ -22,7 +22,7 @@ Publishers are just the general idea of a configurable item which can push thing
For example, to implement a simple publisher, you can do (in CoffeeScript):
```coffeescript
-Base = require('chainsaw/base).Base
+Base = require('pebble/base).Base
class MyPublisher extends Base
name: "my-publisher"
@@ -42,9 +42,9 @@ makes it possible to build a wide variety of publishers.
### Configuration
-Chainsaw uses a standard `config.json` file which uses nested keys according to:
+Pebble uses a standard `config.json` file which uses nested keys according to:
-* Chainsaw Settings (`chainsaw`)
+* Pebble Settings (`pebble`)
* Redis settings (`redis`)
* Publisher settings (either the publisher name or the `configNamespace` value on a given publisher).
@@ -53,23 +53,29 @@ Please note that most options are optional.
Lastly, in the case of the following, they can also be overridden by an environment variable:
-* `chainsaw.listen.host` (by `HOST`)
-* `chainsaw.listen.port` (by `PORT`)
-* `chainsaw.redis.host` (by `REDIS_HOST`)
-* `chainsaw.redis.port` (by `REDIS_HOST`)
-* `chainsaw.redis.password` (by `REDIS_PASSWORD`)
-* `chainsaw.redis.maxHistory` (by `REDIS_MAXHISTORY`)
+* `pebble.listen.host` (by `HOST`)
+* `pebble.listen.port` (by `PORT`)
+* `pebble.redis.host` (by `REDIS_HOST`)
+* `pebble.redis.port` (by `REDIS_HOST`)
+* `pebble.redis.password` (by `REDIS_PASSWORD`)
+* `pebble.redis.maxHistory` (by `REDIS_MAXHISTORY`)
### The Public JavasScript Portion
Please note the public portion can be found in `public/` and that
-your application uses Chainsaw. It'd be pretty simple to port it to use
+your application uses Pebble. It'd be pretty simple to port it to use
something other than jQuery but that is left as an exercise for the reader.
-## Developing Chainsaw
+## Developing Pebble
-To develop chainsaw, you'll also need to install coffee-script via:
+To develop pebble, you'll also need to install coffee-script via:
```bash
npm install coffee-script
-````
+````
+
+Next, to compile the javascript, you can run:
+
+```bash
+cake build
+```
View
6 config.example.json
@@ -1,5 +1,5 @@
{
- "chainsaw": {
+ "pebble": {
"listen": {
"host": "localhost",
"port": 3004
@@ -8,7 +8,7 @@
},
"redis": {
"host": "localhost",
- "namespace": "chainsaw",
+ "namespace": "pebble",
"password": "something",
"port": 6379,
"maxHistory": 10
@@ -26,7 +26,7 @@
},
"irc": {
"server": "irc.freenode.net",
- "user": "chainsawv2",
+ "user": "pebble",
"channels": ["#railsrumble"]
}
}
View
8 package.json
@@ -1,10 +1,10 @@
{
- "name": "chainsaw-app",
- "version": "2.0.0",
- "description": "Chainsaw app is a series of tools for building real time event notifiers / streams.",
+ "name": "pebble",
+ "version": "1.0.0",
+ "description": "pebble is a series of tools for building real time event notifiers / streams.",
"keys": ["realtime", "events", "stream"],
"author": "Darcy Laycock <sutto@sutto.net> (http://blog.ninjahideout.com/)",
- "main": "./lib/chainsaw",
+ "main": "./lib/pebble",
"directories": {
"lib": "./lib"
},
View
45 public/coffeescripts/pebble.coffee
@@ -0,0 +1,45 @@
+# Public distributable version of the client.
+
+class Pebble
+
+ @run: (host, callback) ->
+ pebble = new Pebble
+ callback pebble
+ pebble
+
+ constructor: (host) ->
+ @events = {}
+ @socket = io.connect host
+
+ on: (event, callback) ->
+ @events[event]?= []
+ @events[event].push callback
+
+ watchAll: (channels...) ->
+ for name, callback of channels
+ @watch name, callback
+
+ watch: (channel, callback) ->
+ @on channel, callback
+ @loadHistoryFor @, channel, =>
+ @socket.on channel, (data) => @receive channel, data
+
+ trigger: (name, args...) ->
+ callbacks = (@events[name] ?= [])
+ for callback in callbacks
+ callback.apply this, args
+
+ disconnected: (callback) -> @on 'disconnect', callback
+ connected: (callback) -> @on 'connect', callback
+ reconnected: (callback) -> @on 'reconnect', callback
+
+ receive: (channel, message) ->
+ @trigger channel, message
+
+ loadHistory: (channel, callback) ->
+ $.getJSON "/history/#{channel}", (data) =>
+ for message in data.reverse()
+ @receive channel, message
+ callback() if callback instanceof Function
+
+window['Pebble'] = Pebble
View
59 src/pebble.coffee
@@ -0,0 +1,59 @@
+path = require 'path'
+sys = require 'sys'
+fs = require 'fs'
+io = require 'socket.io'
+redis = require('./pebble/redis').RedisWrapper
+web = require('./pebble/web').Web
+
+class Pebble
+
+ constructor: (@config) ->
+ @publishers = []
+
+ # Adds a publisher to the current runner.
+ add: (publisher) ->
+ @publishers.push new publisher(@)
+
+ addFromRequire: (path) ->
+ @add require(path).publisher
+
+ run: ->
+ sys.puts "Starting pebble..."
+ @redis = new redis @
+ @web = new web @
+ @broadcast = io.listen @web.app
+ @broadcast.set 'log level', 0
+ for publisher in @publishers
+ publisher.run()
+ @web.run()
+
+ visitableURL: ->
+ unless @_visitableURL?
+ @_visitableURL = "http://#{@host()}"
+ if @port()?
+ @visitableURL += ":#{@port()}"
+ @_visitableURL += "/"
+ @_visitableURL
+
+ host: -> process.env.HOST or @get 'pebble.listen.host', 'localhost'
+ port: -> process.env.PORT or @get 'pebble.listen.port', 3003
+
+ get: (key, defaultValue) ->
+ key_parts = key.split "."
+ config = @config
+ while key_parts.length > 0
+ part = key_parts.shift()
+ config = config[part]
+ return defaultValue unless config?
+ config
+
+
+ @run: (config_path, callback) ->
+ config = JSON.parse fs.readFileSync(config_path)
+ runner = new @(config)
+ callback runner if callback instanceof Function
+ runner.run()
+ runner
+
+Pebble.Base = require("./pebble/base").Base
+module.exports = Pebble
View
32 src/pebble/base.coffee
@@ -0,0 +1,32 @@
+sys = require 'sys'
+
+class Base
+
+ name: "unknown"
+
+ constructor: (@runner) ->
+ @configNamespace?= @name
+ @namespace?= @name
+
+ isEnabled: -> @runner.get('pebble.enabled', []).indexOf(@name) > -1
+
+ run: ->
+ if @isEnabled()
+ sys.puts "Starting publisher: #{@name}"
+ @setup()
+
+ get: (key, defaultValue) ->
+ @runner.get "#{@configNamespace}.#{key}", defaultValue
+
+ config: -> @runner.get @configNamespace
+
+ emit: (key, message) ->
+ if message?
+ key = "#{@namespace}:#{key}"
+ else
+ message = key
+ key = @namespace
+ @runner.io.sockets.emit key, message
+ @runner.redis.addHistory key, JSON.stringify(message)
+
+exports.Base = Base
View
25 src/pebble/irc.coffee
@@ -0,0 +1,25 @@
+irc = require('irc')
+Base = require('./base').Base
+
+class IRC extends Base
+
+ name: "irc"
+
+ setup: ->
+ @channels = @get('channels')
+ @client = new irc.Client @get('server'), @get('user'),
+ channels: @channels
+ @setupListeners()
+
+ listeningTo: (channel) ->
+ @channels.indexOf(channel) > -1
+
+ setupListeners: ->
+ @client.addListener 'message', (from, to, message) =>
+ if @listeningTo to
+ @emit 'message',
+ message: message
+ channel: to,
+ user: from
+
+exports.publisher = IRC
View
65 src/pebble/redis.coffee
@@ -0,0 +1,65 @@
+redis = require 'redis'
+sys = require 'sys'
+
+class RedisWrapper
+
+ constructor: (@runner) ->
+ @host = process.env.REDIS_HOST || @runner.get 'redis.host', 'localhost'
+ @port = process.env.REDIS_PORT || @runner.get 'redis.port', 6379
+ @password = process.env.REDIS_PASSWORD || @runner.get 'redis.password'
+ @namespace = @runner.get 'redis.namespace', 'juggernaut'
+ @maxHistory = @runner.get 'redis.maxHistory', 100
+ sys.puts "Connecting to Redis at #{@host}:#{@port}"
+ @redis = redis.createClient @port, @host
+ if @password?
+ sys.puts 'Authing with redis password.'
+ @redis.auth @password
+
+ publish: (key, message) ->
+ if message?
+ key = "#{@namespace}:#{key}"
+ else
+ message = key
+ key = @namespace
+ sys.puts "Publishing to #{key}"
+ @redis.publish key, message
+
+ historyKeyFor: (channel) -> "#{@namespace}:history:#{channel}"
+
+ incrementCountFor: (key, offset) ->
+ @redis.hincrby "#{@namespace}:#{key}", offset, 1
+
+ getCounts: (key, maximum, callback) ->
+ @redis.hgetall "#{@namespace}:#{key}", (err, results) =>
+ if err
+ callback err
+ else
+ counts = []
+ for i in [0..(maximum - 1)]
+ counts.push parseInt(results[i] || "0", 10)
+ callback false, JSON.stringify counts
+
+
+ addHistory: (channel, data) ->
+ key = @historyKeyFor channel
+ @redis.lpush key, data
+ @redis.ltrim key, 0, @maxHistory - 1
+
+ getHistory: (channel, callback) ->
+ key = @historyKeyFor channel
+ @redis.lrange key, 0, @maxHistory - 1, (err, result) =>
+ sys.puts "Getting history for #{key} - #{@maxHistory - 1}"
+ if err
+ sys.puts "Hadd error: #{err}"
+ callback err
+ else if not result
+ callback true
+ else
+ callback false, "[#{result.join(", ")}]"
+
+ debugResponse: (err, result) ->
+ sys.puts "Error: #{sys.inspect err}"
+ sys.puts "Result: #{sys.inspect result}"
+
+
+exports.RedisWrapper = RedisWrapper
View
36 src/pebble/twitter.coffee
@@ -0,0 +1,36 @@
+NTwitter = require 'ntwitter'
+Base = require('./base').Base
+sys = require 'sys'
+
+class Twitter extends Base
+
+ name: "twitter"
+
+ setup: ->
+ config = @config()
+ @twitter = new NTwitter({
+ consumer_key: config.consumer.key
+ consumer_secret: config.consumer.secret
+ access_token_key: config.access_token.key
+ access_token_secret: config.access_token.secret
+ })
+ outer = @
+ @twitter.stream 'statuses/filter', track: config.track, (stream) =>
+ stream.on 'data', (tweet) =>
+ outer.emit 'tweet', outer.filtered tweet
+ stream.on 'end', (resp) ->
+ sys.puts "Twitter Connection ended, Status code was #{resp.statusCode}"
+ stream.on 'error', (error) ->
+ sys.puts "Error in Twitter: #{error.message}"
+
+ filtered: (tweet) ->
+ text: tweet.text
+ created_at: tweet.created_at
+ id_str: tweet.id_str
+ retweeted: tweet.retweeted
+ user:
+ name: tweet.user.name
+ profile_image_url: tweet.user.profile_image_url
+ screen_name: tweet.user.screen_name
+
+exports.publisher = Twitter
View
59 src/pebble/web.coffee
@@ -0,0 +1,59 @@
+express = require 'express'
+sys = require 'sys'
+EventEmitter = require('events').EventEmitter
+
+class Web extends EventEmitter
+
+ constructor: (@runner) ->
+ EventEmitter.call @
+ @app = express.createServer()
+ @configuration = {}
+
+ configure: ->
+ @app.configure =>
+ @emit "beforeConfigure", @app, @
+ @app.use express.methodOverride()
+ @app.use express.bodyParser()
+ @app.use @app.router
+ @emit "afterConfigure", @app, @
+ @app.configure 'development', =>
+ @emit "beforeDevelopmentConfigure", @app, @
+ @app.use express.errorHandler
+ dumpExceptions: true
+ showStack: true
+ @emit "afterDevelopmentConfigure", @app, @
+ @setupEndpoints()
+
+ setupEndpoints: ->
+ @emit "beforeEndpoints", @app, @
+ @app.get '/version', (req, res) =>
+ @respondWithJSON req, res, JSON.stringify
+ application: "Pebble"
+ version: @runner.version
+ @app.get '/history/:key', (req, res) =>
+ key = req.params.key
+ @runner.redis.getHistory key, (err, data) =>
+ if err
+ data = @errorResponse "Unable to get history for channel '#{key}'"
+ @respondWithJSON req, res, data
+ @emit "afterEndpoints", @app, @
+
+ errorResponse: (message) ->
+ JSON.stringify
+ error: message
+
+ respondWithJSON: (req, res, inner) ->
+ if req.query.callback
+ res.header 'Content-Type', 'application/javascript'
+ res.send "#{req.query.callback}(#{inner});"
+ else
+ res.header 'Content-Type', 'application/json'
+ res.send inner
+
+
+ run: ->
+ @configure()
+ @app.listen @runner.port()
+
+
+exports.Web = Web
Please sign in to comment.
Something went wrong with that request. Please try again.