Skip to content

Commit

Permalink
Rename to Pebble
Browse files Browse the repository at this point in the history
  • Loading branch information
Sutto committed Jul 24, 2011
1 parent 5514e53 commit 18cb14d
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 22 deletions.
36 changes: 21 additions & 15 deletions 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 * Cleaner in terms of code
* Simpler to run (One process, versus many) * Simpler to run (One process, versus many)
Expand All @@ -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): For example, to implement a simple publisher, you can do (in CoffeeScript):


```coffeescript ```coffeescript
Base = require('chainsaw/base).Base Base = require('pebble/base).Base
class MyPublisher extends Base class MyPublisher extends Base
name: "my-publisher" name: "my-publisher"
Expand All @@ -42,9 +42,9 @@ makes it possible to build a wide variety of publishers.
### Configuration ### 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`) * Redis settings (`redis`)
* Publisher settings (either the publisher name or the `configNamespace` value on a given publisher). * Publisher settings (either the publisher name or the `configNamespace` value on a given publisher).
Expand All @@ -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: Lastly, in the case of the following, they can also be overridden by an environment variable:
* `chainsaw.listen.host` (by `HOST`) * `pebble.listen.host` (by `HOST`)
* `chainsaw.listen.port` (by `PORT`) * `pebble.listen.port` (by `PORT`)
* `chainsaw.redis.host` (by `REDIS_HOST`) * `pebble.redis.host` (by `REDIS_HOST`)
* `chainsaw.redis.port` (by `REDIS_HOST`) * `pebble.redis.port` (by `REDIS_HOST`)
* `chainsaw.redis.password` (by `REDIS_PASSWORD`) * `pebble.redis.password` (by `REDIS_PASSWORD`)
* `chainsaw.redis.maxHistory` (by `REDIS_MAXHISTORY`) * `pebble.redis.maxHistory` (by `REDIS_MAXHISTORY`)
### The Public JavasScript Portion ### The Public JavasScript Portion
Please note the public portion can be found in `public/` and that 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. 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 ```bash
npm install coffee-script npm install coffee-script
```` ````
Next, to compile the javascript, you can run:
```bash
cake build
```
6 changes: 3 additions & 3 deletions config.example.json
@@ -1,5 +1,5 @@
{ {
"chainsaw": { "pebble": {
"listen": { "listen": {
"host": "localhost", "host": "localhost",
"port": 3004 "port": 3004
Expand All @@ -8,7 +8,7 @@
}, },
"redis": { "redis": {
"host": "localhost", "host": "localhost",
"namespace": "chainsaw", "namespace": "pebble",
"password": "something", "password": "something",
"port": 6379, "port": 6379,
"maxHistory": 10 "maxHistory": 10
Expand All @@ -26,7 +26,7 @@
}, },
"irc": { "irc": {
"server": "irc.freenode.net", "server": "irc.freenode.net",
"user": "chainsawv2", "user": "pebble",
"channels": ["#railsrumble"] "channels": ["#railsrumble"]
} }
} }
8 changes: 4 additions & 4 deletions package.json
@@ -1,10 +1,10 @@
{ {
"name": "chainsaw-app", "name": "pebble",
"version": "2.0.0", "version": "1.0.0",
"description": "Chainsaw app is a series of tools for building real time event notifiers / streams.", "description": "pebble is a series of tools for building real time event notifiers / streams.",
"keys": ["realtime", "events", "stream"], "keys": ["realtime", "events", "stream"],
"author": "Darcy Laycock <sutto@sutto.net> (http://blog.ninjahideout.com/)", "author": "Darcy Laycock <sutto@sutto.net> (http://blog.ninjahideout.com/)",
"main": "./lib/chainsaw", "main": "./lib/pebble",
"directories": { "directories": {
"lib": "./lib" "lib": "./lib"
}, },
Expand Down
45 changes: 45 additions & 0 deletions 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
59 changes: 59 additions & 0 deletions 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
32 changes: 32 additions & 0 deletions 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
25 changes: 25 additions & 0 deletions 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
65 changes: 65 additions & 0 deletions 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
36 changes: 36 additions & 0 deletions 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

0 comments on commit 18cb14d

Please sign in to comment.