faye-rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +117 -0
- data/lib/faye-rails.rb +42 -0
- data/lib/faye-rails/controller.rb +53 -0
- data/lib/faye-rails/controller/channel.rb +61 -0
- data/lib/faye-rails/controller/message.rb +10 -0
- data/lib/faye-rails/controller/monitor.rb +9 -0
- data/lib/faye-rails/controller/observer_factory.rb +54 -0
- data/lib/faye-rails/filter.rb +179 -0
- data/lib/faye-rails/rack_adapter.rb +159 -0
- data/lib/faye-rails/routing_hooks.rb +51 -0
- data/lib/faye-rails/server_list.rb +28 -0
- data/lib/faye-rails/version.rb +3 -0
- data/vendor/assets/javascripts/faye-browser-min.js +2 -0
- data/vendor/assets/javascripts/faye-browser.js +2130 -0
- data/vendor/assets/javascripts/faye.js +2 -0
- metadata +197 -0
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# faye-rails [![Build Status](https://secure.travis-ci.org/RLovelett/faye-rails.png)](http://travis-ci.org/RLovelett/faye-rails)
|
2
|
+
|
3
|
+
faye-rails is a Ruby gem which handles embedding Faye's rack-based server into the rails stack and providing it with access to controllers and views based on bindings and observers.
|
4
|
+
|
5
|
+
# Embedded server
|
6
|
+
|
7
|
+
Due to the limitations of most Rack-based web servers available Faye can only be run on Thin, however if you are using thin, then you can add as many Faye servers as you want to the Rails router like so:
|
8
|
+
|
9
|
+
App::Application.routes.draw do
|
10
|
+
faye_server '/faye', :timeout => 25
|
11
|
+
end
|
12
|
+
|
13
|
+
You can also pass a block to `faye_server` which will be executed in the context of the Faye server, thus you can call any methods on `Faye::RackAdapter` from within the block:
|
14
|
+
|
15
|
+
App::Application.routes.draw do
|
16
|
+
faye_server '/faye', :timeout => 25 do
|
17
|
+
class MockExtension
|
18
|
+
def incoming(message, callback)
|
19
|
+
callback.call(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
add_extension(MockExtension.new)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
If you really want to, you can ask Faye to start it's own listening Thin server on an arbitrary port:
|
27
|
+
|
28
|
+
App::Application.routes.draw do
|
29
|
+
faye_server '/faye', :timeout => 25 do
|
30
|
+
listen(9292)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
You can also do some rudimentary routing using the map method:
|
35
|
+
|
36
|
+
App::Application.routes.draw do
|
37
|
+
faye_server '/faye', :timeout => 25 do
|
38
|
+
map '/widgets/**' => WidgetsController
|
39
|
+
map :default => :block
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
You can find more details on the #map method in the [rdoc](http://rubydoc.info/github/jamesotron/faye-rails/master/FayeRails/RackAdapter)
|
44
|
+
|
45
|
+
# Controller
|
46
|
+
|
47
|
+
faye-rails includes a controller for handling the binding between model events and channels with it's own DSL for managing channel-based events.
|
48
|
+
|
49
|
+
class WidgetController < FayeRails::Controller
|
50
|
+
end
|
51
|
+
|
52
|
+
## Model observers
|
53
|
+
|
54
|
+
You can subscribe to changes in models using the controller's observer DSL:
|
55
|
+
|
56
|
+
class WidgetController < FayeRails::Controller
|
57
|
+
observe Widget, :after_create do |new_widget|
|
58
|
+
WidgetController.publish('/widgets', new_widget.attributes)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
The available callbacks are derived from the ActiveRecord callback stack. See [ActiveRecord::Callbacks](http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html) for more information regarding the callback queue.
|
63
|
+
|
64
|
+
See the [rdoc](http://rubydoc.info/github/jamesotron/faye-rails/master/FayeRails/Controller.observe) for more information.
|
65
|
+
|
66
|
+
## Channel DSL
|
67
|
+
|
68
|
+
The controller DSL elegantly wraps channel-based aspects of the Faye API so that you can easily group code based on specific channels.
|
69
|
+
|
70
|
+
### Monitoring
|
71
|
+
|
72
|
+
You can make use of Faye's [monitoring API](http://faye.jcoglan.com/ruby/monitoring.html) by adding calls to `monitor` within the channel block. You are able to monitor `:subscribe`, `:unsubscribe` and `:publish` events. Blocks are executed within the context of a `FayeRails::Controller::Monitor` instance which will give you access to `#client_id`, `#channel` and `#data` (`#data` only having a value on `:publish` events).
|
73
|
+
|
74
|
+
class WidgetController < FayeRails::Controller
|
75
|
+
channel '/widgets' do
|
76
|
+
monitor :subscribe do
|
77
|
+
puts "Client #{client_id} subscribed to #{channel}."
|
78
|
+
end
|
79
|
+
monitor :unsubscribe do
|
80
|
+
puts "Client #{client_id} unsubscribed from #{channel}."
|
81
|
+
end
|
82
|
+
monitor :publish do
|
83
|
+
puts "Client #{client_id} published #{data.inspect} to #{channel}."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
### Filtering
|
89
|
+
|
90
|
+
You can quickly and easily filter incoming and outgoing messages for your specific channel using the controller's filter API, which wraps Faye's [extensions API](http://faye.jcoglan.com/ruby/extensions.html) in a concise and channel-specific way.
|
91
|
+
|
92
|
+
class WidgetController < FayeRails::Controller
|
93
|
+
channel '/widgets' do
|
94
|
+
filter :in do
|
95
|
+
puts "Inbound message #{message}."
|
96
|
+
pass
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
You can add filters for `:in`, `:out` and `:any`, which will allow you to filter messages entering the server, exiting the server or both. The block passed to the `filter` is executed in the context of a `FayeRails::Filter::DSL` instance, which gives you access to the `#message` method, which contains the entire message payload from the client (including meta information you wouldn't see other ways). You also have access to the `#pass`, `#modify`, `#block` and `#drop` methods which are sugar around Faye's callback system - which is accessible via the `#callback` method if you want to do it that way. Check out the [FayeRails::Filter::DSL rdoc](http://rubydoc.info/github/jamesotron/faye-rails/master/FayeRails/Filter/DSL) for more information. Please note that all filters must call `callback.call` either via the sugar methods or directly to ensure that requests are not lost (not to mention potential memory leaks).
|
102
|
+
|
103
|
+
### Subscribing
|
104
|
+
|
105
|
+
You can easily subscribe to a channel using the 'subscribe' method inside your channel block, like so:
|
106
|
+
|
107
|
+
class WidgetController < FayeRails::Controller
|
108
|
+
channel '/widgets' do
|
109
|
+
subscribe do
|
110
|
+
puts "Received on channel #{channel}: #{message.inspect}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Thanks.
|
116
|
+
|
117
|
+
Thanks to James Coglan for the excellent Faye Bayeux implementetation and great support for Faye users.
|
data/lib/faye-rails.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'faye'
|
2
|
+
require 'faye-rails/version'
|
3
|
+
require 'faye-rails/routing_hooks'
|
4
|
+
require 'faye-rails/server_list'
|
5
|
+
|
6
|
+
module FayeRails
|
7
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
8
|
+
|
9
|
+
if defined? ::Rails
|
10
|
+
class Engine < ::Rails::Engine
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
autoload :Controller, File.join(ROOT, 'faye-rails', 'controller')
|
15
|
+
autoload :RackAdapter, File.join(ROOT, 'faye-rails', 'rack_adapter')
|
16
|
+
autoload :Filter, File.join(ROOT, 'faye-rails', 'filter')
|
17
|
+
|
18
|
+
def self.servers
|
19
|
+
@servers ||= ServerList.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.server(where=nil)
|
23
|
+
if where
|
24
|
+
servers.at(where).first
|
25
|
+
else
|
26
|
+
servers.first
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.clients
|
31
|
+
servers.map(&:get_client)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.client(where=nil)
|
35
|
+
if where
|
36
|
+
servers.at(where).first.get_client
|
37
|
+
else
|
38
|
+
servers.first.get_client
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module FayeRails
|
2
|
+
class Controller
|
3
|
+
autoload :Channel, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'channel')
|
4
|
+
autoload :Monitor, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'monitor')
|
5
|
+
autoload :Message, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'message')
|
6
|
+
autoload :ObserverFactory, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'observer_factory')
|
7
|
+
|
8
|
+
attr :channels, :model
|
9
|
+
|
10
|
+
# Observe a model for any of the ActiveRecord::Callbacks
|
11
|
+
# as of v3.2.6 they are:
|
12
|
+
# before_validation
|
13
|
+
# after_validation
|
14
|
+
# before_save
|
15
|
+
# before_create
|
16
|
+
# after_create
|
17
|
+
# after_save
|
18
|
+
# after_commit
|
19
|
+
# http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
|
20
|
+
# action defaults to after_create
|
21
|
+
def self.observe(model_klass, action = :after_create, &block)
|
22
|
+
# Dynamically create a new observe class
|
23
|
+
ObserverFactory.define(model_klass, action, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def observe(model_klass, action = :after_create, &block)
|
27
|
+
# Dynamically create a new observe class
|
28
|
+
ObserverFactory.define(model_klass, action, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Bind a number of events to a specific channel.
|
32
|
+
def self.channel(channel, endpoint=nil, &block)
|
33
|
+
channel = Channel.new(channel, endpoint)
|
34
|
+
channel.instance_eval(&block)
|
35
|
+
(@channels ||= []) << channel
|
36
|
+
end
|
37
|
+
|
38
|
+
def channel(channel, endpoint=nil, &block)
|
39
|
+
channel = Channel.new(channel, endpoint)
|
40
|
+
channel.instance_eval(&block)
|
41
|
+
(@channels ||= []) << channel
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.publish(channel, message, endpoint=nil)
|
45
|
+
FayeRails.client(endpoint).publish(channel, message)
|
46
|
+
end
|
47
|
+
|
48
|
+
def publish(channel, message, endpoint=nil)
|
49
|
+
self.class.publish(channel, message, endpoint)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module FayeRails
|
2
|
+
class Controller
|
3
|
+
class Channel
|
4
|
+
|
5
|
+
attr_reader :channel, :endpoint
|
6
|
+
|
7
|
+
def initialize(channel, endpoint=nil)
|
8
|
+
@channel = channel
|
9
|
+
@endpoint = endpoint
|
10
|
+
end
|
11
|
+
|
12
|
+
def client
|
13
|
+
FayeRails.client(endpoint)
|
14
|
+
end
|
15
|
+
|
16
|
+
def publish(message)
|
17
|
+
FayeRails.client(endpoint).publish(channel, message)
|
18
|
+
end
|
19
|
+
|
20
|
+
def monitor(event, &block)
|
21
|
+
raise ArgumentError, "Unknown event #{event.inspect}" unless [:subscribe,:unsubscribe,:publish].member? event
|
22
|
+
|
23
|
+
FayeRails.server(endpoint).bind(event) do |*args|
|
24
|
+
Monitor.new.tap do |m|
|
25
|
+
m.client_id = args.shift
|
26
|
+
m.channel = args.shift
|
27
|
+
m.data = args.shift
|
28
|
+
m.instance_eval(&block) if m.channel == channel
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter(direction=:any, &block)
|
34
|
+
filter = FayeRails::Filter.new(channel, direction, block)
|
35
|
+
server = FayeRails.server(endpoint)
|
36
|
+
server.add_extension(filter)
|
37
|
+
filter.server = server
|
38
|
+
filter
|
39
|
+
end
|
40
|
+
|
41
|
+
def subscribe(&block)
|
42
|
+
EM.schedule do
|
43
|
+
@subscription = FayeRails.client(endpoint).subscribe(channel) do |message|
|
44
|
+
Message.new.tap do |m|
|
45
|
+
m.message = message
|
46
|
+
m.channel = channel
|
47
|
+
m.instance_eval(&block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def unsubscribe
|
54
|
+
EM.schedule do
|
55
|
+
FayeRails.client(endpoint).unsubscribe(@subscription)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module FayeRails
|
4
|
+
class Controller
|
5
|
+
|
6
|
+
# Module creates ActiveRecord::Observer instances
|
7
|
+
module ObserverFactory
|
8
|
+
|
9
|
+
# Create
|
10
|
+
def self.define(klass, method_name, &block)
|
11
|
+
# Make a name for the observer
|
12
|
+
klass_observer_name = "#{klass.to_s}Observer"
|
13
|
+
|
14
|
+
# Load the observer if one exists
|
15
|
+
klass_observer = ObserverFactory.observer(klass_observer_name)
|
16
|
+
|
17
|
+
new_observer = klass_observer.nil?
|
18
|
+
|
19
|
+
# Create a new observer if one does not exist
|
20
|
+
klass_observer = Object.const_set(klass_observer_name, Class.new(ActiveRecord::Observer) do
|
21
|
+
# TODO Work around this hack.
|
22
|
+
# Have to define all of the available methods when creating the Observer class for the
|
23
|
+
# first time. The methods can then be overriden by the observe DSL. However if they
|
24
|
+
# are not first defined then they will not be registerable.
|
25
|
+
[:before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save, :after_commit].each do |arg|
|
26
|
+
send :define_method, arg do |temp|
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end) if new_observer
|
30
|
+
|
31
|
+
# Add the method to the observer
|
32
|
+
klass_observer.instance_eval do
|
33
|
+
define_method(method_name, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Add the observer if needed
|
37
|
+
if new_observer
|
38
|
+
ActiveRecord::Base.observers << klass_observer
|
39
|
+
end
|
40
|
+
|
41
|
+
ActiveRecord::Base.instantiate_observers
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.observer(class_name)
|
45
|
+
klass = Module.const_get(class_name)
|
46
|
+
return klass if klass.is_a?(Class)
|
47
|
+
return nil
|
48
|
+
rescue
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module FayeRails
|
2
|
+
class Filter
|
3
|
+
|
4
|
+
attr_accessor :server
|
5
|
+
attr_reader :channel
|
6
|
+
attr_writer :logger
|
7
|
+
|
8
|
+
# Create a new FayeRails::Filter which can be passed to
|
9
|
+
# Faye::RackAdapter#add_extension.
|
10
|
+
#
|
11
|
+
# @param channel
|
12
|
+
# Optional channel name to limit messages to.
|
13
|
+
# @param direction
|
14
|
+
# :in, :out or :any.
|
15
|
+
# @param block
|
16
|
+
# A proc object to be called when filtering messages.
|
17
|
+
def initialize(channel='/**', direction=:any, block)
|
18
|
+
@channel = channel
|
19
|
+
@block = block
|
20
|
+
raise ArgumentError, "Block cannot be nil" unless block
|
21
|
+
if (direction == :in) || (direction == :any)
|
22
|
+
@in_filter = DSL
|
23
|
+
end
|
24
|
+
if (direction == :out) || (direction == :any)
|
25
|
+
@out_filter = DSL
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to?(method)
|
30
|
+
if (method == :incoming)
|
31
|
+
!!@in_filter
|
32
|
+
elsif (method == :outgoing)
|
33
|
+
!!@out_filter
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger
|
40
|
+
if defined?(::Rails)
|
41
|
+
@logger ||= Rails.logger
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def incoming(message, callback)
|
46
|
+
@in_filter.new(@block, message, channel, callback, :incoming) if @in_filter
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def outgoing(message, callback)
|
51
|
+
@out_filter.new(@block, message, channel, callback, :outgoing) if @out_filter
|
52
|
+
end
|
53
|
+
|
54
|
+
def destroy
|
55
|
+
if server
|
56
|
+
server.remove_extension(self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class DSL
|
61
|
+
|
62
|
+
# A small wrapper class around filter blocks to
|
63
|
+
# add some sugar to ease filter (Faye extension)
|
64
|
+
# creation.
|
65
|
+
|
66
|
+
attr_reader :channel, :message, :callback, :original_message, :direction
|
67
|
+
|
68
|
+
# Called by FayeRails::Filter when Faye passes
|
69
|
+
# messages in for evaluation.
|
70
|
+
# @param block
|
71
|
+
# The block you wish to execute whenever a matching
|
72
|
+
# message is recieved.
|
73
|
+
# @param channel
|
74
|
+
# optional: if present then the block will only be called for matching messages, otherwise all messages will be passed.
|
75
|
+
def initialize(block, message, channel='/**', callback, direction)
|
76
|
+
raise ArgumentError, "Block cannot be nil" unless block
|
77
|
+
@channel = channel
|
78
|
+
@original_message = message.dup
|
79
|
+
@message = message
|
80
|
+
@callback = callback
|
81
|
+
@direction = direction
|
82
|
+
|
83
|
+
if channel_matches?(@channel, @original_message['channel']) ||
|
84
|
+
(subscribing? && subscription?(@channel)) ||
|
85
|
+
(unsubscribing? && subscription?(@channel))
|
86
|
+
instance_eval(&block)
|
87
|
+
else
|
88
|
+
pass
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Easier than testing message['channel'] every time
|
93
|
+
def subscribing?
|
94
|
+
message['channel'] == '/meta/subscribe'
|
95
|
+
end
|
96
|
+
|
97
|
+
def unsubscribing?
|
98
|
+
message['channel'] == '/meta/unsubscribe'
|
99
|
+
end
|
100
|
+
|
101
|
+
def meta?
|
102
|
+
message['channel'][0..5] == '/meta/'
|
103
|
+
end
|
104
|
+
|
105
|
+
def service?
|
106
|
+
message['channel'][0..8] == '/service/'
|
107
|
+
end
|
108
|
+
|
109
|
+
def incoming?
|
110
|
+
direction == :incoming
|
111
|
+
end
|
112
|
+
alias in? incoming?
|
113
|
+
|
114
|
+
def outgoing?
|
115
|
+
direction == :outgoing
|
116
|
+
end
|
117
|
+
alias out? outgoing?
|
118
|
+
|
119
|
+
def data
|
120
|
+
message['data']
|
121
|
+
end
|
122
|
+
|
123
|
+
def data?
|
124
|
+
!!data
|
125
|
+
end
|
126
|
+
|
127
|
+
def client_id?(x=nil)
|
128
|
+
if !!x
|
129
|
+
message['client_id'] == x
|
130
|
+
else
|
131
|
+
!!message['client_id']
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def channel_matches?(glob,test)
|
136
|
+
File.fnmatch? glob, test
|
137
|
+
end
|
138
|
+
|
139
|
+
def subscription?(channel)
|
140
|
+
message['subscription'] && channel_matches?(channel, message['subscription'])
|
141
|
+
end
|
142
|
+
|
143
|
+
# Syntactic sugar around callback.call which passes
|
144
|
+
# back the original message unmodified.
|
145
|
+
def pass
|
146
|
+
return callback.call(original_message)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Syntactic sugar around callback.call which passes
|
150
|
+
# the passed argument back to Faye in place of the
|
151
|
+
# original message.
|
152
|
+
# @param new_message
|
153
|
+
# Replacement message to send back to Faye.
|
154
|
+
def modify(new_message)
|
155
|
+
return callback.call(new_message)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Syntactic sugar around callback.call which adds
|
159
|
+
# an error message to the message and passes it back
|
160
|
+
# to Faye, which will send back a rejection message to
|
161
|
+
# the sending client.
|
162
|
+
# @param reason
|
163
|
+
# The error message to be sent back to the client.
|
164
|
+
def block(reason="Message blocked by filter")
|
165
|
+
new_message = message
|
166
|
+
new_message['error'] = reason
|
167
|
+
return callback.call(new_message)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Syntactic sugar around callback.call which returns
|
171
|
+
# nil to Faye - effectively dropping the message.
|
172
|
+
def drop
|
173
|
+
return callback.call(nil)
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|