primer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +213 -0
- data/example/README.rdoc +69 -0
- data/example/application.rb +26 -0
- data/example/config.ru +5 -0
- data/example/environment.rb +31 -0
- data/example/models/blog_post.rb +4 -0
- data/example/models/connection.rb +10 -0
- data/example/public/style.css +75 -0
- data/example/script/setup_database.rb +11 -0
- data/example/views/index.erb +13 -0
- data/example/views/layout.erb +26 -0
- data/example/views/show.erb +7 -0
- data/example/worker.rb +3 -0
- data/lib/javascript/primer.js +36 -0
- data/lib/primer/bus/amqp.rb +43 -0
- data/lib/primer/bus/memory.rb +12 -0
- data/lib/primer/bus.rb +30 -0
- data/lib/primer/cache/memory.rb +60 -0
- data/lib/primer/cache/redis.rb +70 -0
- data/lib/primer/cache.rb +84 -0
- data/lib/primer/enabler.rb +18 -0
- data/lib/primer/helpers.rb +66 -0
- data/lib/primer/real_time.rb +80 -0
- data/lib/primer/route_set.rb +50 -0
- data/lib/primer/watcher/active_record_macros.rb +70 -0
- data/lib/primer/watcher/macros.rb +70 -0
- data/lib/primer/watcher.rb +62 -0
- data/lib/primer/worker/active_record_agent.rb +120 -0
- data/lib/primer/worker.rb +34 -0
- data/lib/primer.rb +31 -0
- data/spec/models/artist.rb +10 -0
- data/spec/models/blog_post.rb +5 -0
- data/spec/models/calendar.rb +7 -0
- data/spec/models/concert.rb +6 -0
- data/spec/models/performance.rb +6 -0
- data/spec/models/person.rb +14 -0
- data/spec/models/watchable.rb +17 -0
- data/spec/primer/bus_spec.rb +31 -0
- data/spec/primer/cache_spec.rb +309 -0
- data/spec/primer/helpers/erb_spec.rb +89 -0
- data/spec/primer/watcher/active_record_spec.rb +189 -0
- data/spec/primer/watcher_spec.rb +101 -0
- data/spec/schema.rb +31 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/templates/page.erb +3 -0
- metadata +235 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Primer
|
4
|
+
class Cache
|
5
|
+
|
6
|
+
class Redis < Cache
|
7
|
+
REDIS_CONFIG = {:thread_safe => true}
|
8
|
+
|
9
|
+
def initialize(config = {})
|
10
|
+
config = REDIS_CONFIG.merge(config)
|
11
|
+
@redis = ::Redis.new(config)
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear
|
15
|
+
@redis.flushdb
|
16
|
+
end
|
17
|
+
|
18
|
+
def put(cache_key, value)
|
19
|
+
validate_key(cache_key)
|
20
|
+
@redis.set(cache_key, YAML.dump(value))
|
21
|
+
publish_change(cache_key)
|
22
|
+
RealTime.publish(cache_key, value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(cache_key)
|
26
|
+
validate_key(cache_key)
|
27
|
+
string = @redis.get(cache_key)
|
28
|
+
string ? YAML.load(string) : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_key?(cache_key)
|
32
|
+
@redis.exists(cache_key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def invalidate(cache_key)
|
36
|
+
@redis.del(cache_key)
|
37
|
+
|
38
|
+
return unless has_key?('deps' + cache_key)
|
39
|
+
@redis.smembers('deps' + cache_key).each do |attribute|
|
40
|
+
@redis.srem(attribute, cache_key)
|
41
|
+
end
|
42
|
+
@redis.del('deps' + cache_key)
|
43
|
+
end
|
44
|
+
|
45
|
+
def relate(cache_key, attributes)
|
46
|
+
attributes.each do |attribute|
|
47
|
+
serial = attribute.join('/')
|
48
|
+
@redis.sadd('deps' + cache_key, serial)
|
49
|
+
@redis.sadd(serial, cache_key)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def keys_for_attribute(attribute)
|
54
|
+
serial = attribute.join('/')
|
55
|
+
has_key?(serial) ? @redis.smembers(serial) : []
|
56
|
+
end
|
57
|
+
|
58
|
+
def timeout(cache_key, &block)
|
59
|
+
return if has_key?('timeouts' + cache_key)
|
60
|
+
@redis.set('timeouts' + cache_key, 'true')
|
61
|
+
add_timeout(cache_key, @throttle) do
|
62
|
+
block.call
|
63
|
+
@redis.del('timeouts' + cache_key)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/lib/primer/cache.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Primer
|
2
|
+
class Cache
|
3
|
+
|
4
|
+
autoload :Memory, ROOT + '/primer/cache/memory'
|
5
|
+
autoload :Redis, ROOT + '/primer/cache/redis'
|
6
|
+
|
7
|
+
include Watcher
|
8
|
+
watch_calls_to :get
|
9
|
+
|
10
|
+
include Faye::Timeouts
|
11
|
+
|
12
|
+
attr_accessor :throttle
|
13
|
+
attr_writer :routes
|
14
|
+
|
15
|
+
def primer_identifier
|
16
|
+
[Cache.name]
|
17
|
+
end
|
18
|
+
|
19
|
+
def routes(&block)
|
20
|
+
@routes ||= RouteSet.new
|
21
|
+
@routes.instance_eval(&block) if block_given?
|
22
|
+
@routes
|
23
|
+
end
|
24
|
+
|
25
|
+
def bind_to_bus
|
26
|
+
Primer.bus.subscribe(:changes) { |attribute| changed(attribute) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def compute(cache_key)
|
30
|
+
return get(cache_key) if has_key?(cache_key)
|
31
|
+
|
32
|
+
unless block_given? or @routes
|
33
|
+
message = "Cannot call Cache#compute(#{cache_key}) with no block: no routes have been configured"
|
34
|
+
raise RouteNotFound.new(message)
|
35
|
+
end
|
36
|
+
|
37
|
+
calls = []
|
38
|
+
result = Watcher.watching(calls) do
|
39
|
+
block_given? ? yield : @routes.evaluate(cache_key)
|
40
|
+
end
|
41
|
+
|
42
|
+
attributes = calls.map do |(receiver, method_name, args, block, return_value)|
|
43
|
+
receiver.primer_identifier + [method_name.to_s] + args
|
44
|
+
end
|
45
|
+
|
46
|
+
unless result.nil?
|
47
|
+
relate(cache_key, attributes)
|
48
|
+
put(cache_key, result)
|
49
|
+
end
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def changed(attribute)
|
55
|
+
keys_for_attribute(attribute).each do |cache_key|
|
56
|
+
block = lambda do
|
57
|
+
invalidate(cache_key)
|
58
|
+
regenerate(cache_key)
|
59
|
+
end
|
60
|
+
@throttle ? timeout(cache_key, &block) : block.call
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def publish_change(cache_key)
|
67
|
+
Primer.bus.publish(:changes, primer_identifier + ['get', cache_key])
|
68
|
+
end
|
69
|
+
|
70
|
+
def regenerate(cache_key)
|
71
|
+
compute(cache_key) rescue nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_key(cache_key)
|
75
|
+
raise InvalidKey.new(cache_key) unless Cache.valid_key?(cache_key)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.valid_key?(cache_key)
|
79
|
+
Faye::Channel.valid?(cache_key)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Primer
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
module ERB
|
5
|
+
def primer(cache_key, tag_name = :div, &block)
|
6
|
+
result = Primer.cache.compute(cache_key) do
|
7
|
+
block_given? ?
|
8
|
+
primer_capture_output(&block) :
|
9
|
+
Primer.cache.routes.evaluate(cache_key)
|
10
|
+
end
|
11
|
+
|
12
|
+
if Primer.real_time and not block_given?
|
13
|
+
result = primer_real_time(result, tag_name, cache_key)
|
14
|
+
end
|
15
|
+
|
16
|
+
return result unless block_given?
|
17
|
+
primer_detect_buffer.concat(result)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def primer_capture_output(&block)
|
24
|
+
return primer_capture_output_from_rails(&block) if primer_rails?
|
25
|
+
return primer_capture_output_from_sinatra(&block) if primer_sinatra?
|
26
|
+
end
|
27
|
+
|
28
|
+
def primer_real_time(fragment, tag_name, cache_key)
|
29
|
+
<<-HTML
|
30
|
+
<#{ tag_name } id="#{ RealTime.dom_id(cache_key) }">
|
31
|
+
#{ fragment }
|
32
|
+
</#{ tag_name }>
|
33
|
+
<script type="text/javascript">PRIMER_CHANNELS.push(#{ cache_key.inspect })</script>
|
34
|
+
HTML
|
35
|
+
end
|
36
|
+
|
37
|
+
def primer_detect_buffer
|
38
|
+
[@output_buffer, @_out_buf].compact.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def primer_rails?
|
42
|
+
return false unless respond_to?(:capture)
|
43
|
+
return true if defined?(ActionView::OutputBuffer)
|
44
|
+
String === @output_buffer
|
45
|
+
end
|
46
|
+
|
47
|
+
def primer_capture_output_from_rails(&block)
|
48
|
+
capture(&block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def primer_sinatra?
|
52
|
+
defined?(@_out_buf)
|
53
|
+
end
|
54
|
+
|
55
|
+
def primer_capture_output_from_sinatra(&block)
|
56
|
+
original_buffer = @_out_buf
|
57
|
+
result = @_out_buf = ''
|
58
|
+
block.call
|
59
|
+
@_out_buf = original_buffer
|
60
|
+
result.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Primer
|
2
|
+
class RealTime < Faye::RackAdapter
|
3
|
+
|
4
|
+
BAYEUX_CONFIG = {:mount => '/primer/bayeux', :timeout => 25}
|
5
|
+
SCRIPT_PATH = '/primer.js'
|
6
|
+
TYPE_SCRIPT = {'Content-Type' => 'text/javascript'}
|
7
|
+
SCRIPT_SOURCE = File.read(ROOT + '/javascript/primer.js')
|
8
|
+
|
9
|
+
class ServerAuth
|
10
|
+
def incoming(message, callback)
|
11
|
+
channel = message['channel']
|
12
|
+
return callback.call(message) if Faye::Channel.meta?(channel)
|
13
|
+
|
14
|
+
password = message['ext'] && message['ext']['password']
|
15
|
+
unless password == RealTime.password
|
16
|
+
message['error'] = Faye::Error.ext_mismatch
|
17
|
+
end
|
18
|
+
|
19
|
+
message['ext'].delete('password') if password
|
20
|
+
callback.call(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ClientAuth
|
25
|
+
def outgoing(message, callback)
|
26
|
+
channel = message['channel']
|
27
|
+
return callback.call(message) if Faye::Channel.meta?(channel)
|
28
|
+
|
29
|
+
message['ext'] ||= {}
|
30
|
+
message['ext']['password'] = RealTime.password
|
31
|
+
|
32
|
+
callback.call(message)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(app)
|
37
|
+
super(app, BAYEUX_CONFIG)
|
38
|
+
add_extension(ServerAuth.new)
|
39
|
+
end
|
40
|
+
|
41
|
+
def call(env)
|
42
|
+
request = Rack::Request.new(env)
|
43
|
+
return super unless request.path_info == SCRIPT_PATH
|
44
|
+
[200, TYPE_SCRIPT, [SCRIPT_SOURCE]]
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_accessor :bayeux_server, :password
|
49
|
+
|
50
|
+
def dom_id(cache_key)
|
51
|
+
"primer#{ ('-' + cache_key).gsub(/[^a-z0-9]+/i, '-') }"
|
52
|
+
end
|
53
|
+
|
54
|
+
def publish(cache_key, value)
|
55
|
+
return unless Primer.real_time
|
56
|
+
client.publish(cache_key,
|
57
|
+
:dom_id => dom_id(cache_key),
|
58
|
+
:content => value
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def client
|
65
|
+
raise NotConfigured.new unless bayeux_server
|
66
|
+
|
67
|
+
Faye.ensure_reactor_running!
|
68
|
+
return @client if @client
|
69
|
+
|
70
|
+
endpoint = "#{ bayeux_server }#{ BAYEUX_CONFIG[:mount] }"
|
71
|
+
@client = Faye::Client.new(endpoint)
|
72
|
+
@client.add_extension(ClientAuth.new)
|
73
|
+
|
74
|
+
@client
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module Primer
|
4
|
+
class RouteSet
|
5
|
+
def initialize(&routes)
|
6
|
+
@app = Class.new(Router)
|
7
|
+
instance_eval(&routes) if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(path, &block)
|
11
|
+
@app.get(path, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate(path)
|
15
|
+
@app.new(path).evaluate
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Router < Sinatra::Base
|
20
|
+
class Request
|
21
|
+
attr_reader :path_info
|
22
|
+
def initialize(path)
|
23
|
+
@path_info = path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Circumvent the fact that Sinatra::Base.new creates a Rack stack
|
28
|
+
def self.new(*args)
|
29
|
+
Class.instance_method(:new).bind(self).call(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(path)
|
33
|
+
@request = Request.new(path)
|
34
|
+
@original_params = indifferent_hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def evaluate
|
38
|
+
routes = self.class.routes['GET']
|
39
|
+
catch(:halt) {
|
40
|
+
routes.each do |pattern, keys, conditions, block|
|
41
|
+
process_route(pattern, keys, conditions) do
|
42
|
+
throw(:halt, instance_eval(&block))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
raise RouteNotFound.new("No route match found for key #{@request.path_info}")
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Primer
|
2
|
+
module Watcher
|
3
|
+
|
4
|
+
module ActiveRecordMacros
|
5
|
+
def self.extended(klass)
|
6
|
+
klass.watch_calls_to(*klass.attributes_watchable_by_primer)
|
7
|
+
klass.__send__(:include, InstanceMethods)
|
8
|
+
klass.after_create(:notify_primer_after_create)
|
9
|
+
klass.after_update(:notify_primer_after_update)
|
10
|
+
klass.after_destroy(:notify_primer_after_destroy)
|
11
|
+
end
|
12
|
+
|
13
|
+
def attributes_watchable_by_primer
|
14
|
+
attributes = columns + reflect_on_all_associations
|
15
|
+
attributes.map { |c| c.name.to_s }
|
16
|
+
end
|
17
|
+
|
18
|
+
def primer_foreign_key_mappings
|
19
|
+
return @primer_foreign_key_mappings if defined?(@primer_foreign_key_mappings)
|
20
|
+
|
21
|
+
foreign_keys = reflect_on_all_associations.
|
22
|
+
select { |a| a.macro == :belongs_to }.
|
23
|
+
map { |a| [a.primary_key_name.to_s, a.name] }
|
24
|
+
|
25
|
+
@primer_foreign_key_mappings = Hash[foreign_keys]
|
26
|
+
end
|
27
|
+
|
28
|
+
def patch_method_for_primer(method_name)
|
29
|
+
method = instance_method(method_name) rescue nil
|
30
|
+
return super if method
|
31
|
+
class_eval <<-RUBY
|
32
|
+
def #{method_name}
|
33
|
+
result = read_attribute(:#{method_name})
|
34
|
+
Primer::Watcher.log(self, :#{method_name}, [], nil, result)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
|
40
|
+
def unpatch_method_for_primer(method_name)
|
41
|
+
alias_name = Macros.alias_name(method_name)
|
42
|
+
method = instance_method(alias_name) rescue nil
|
43
|
+
return super if method
|
44
|
+
class_eval <<-RUBY
|
45
|
+
undef_method :#{method_name}
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
def primer_identifier
|
51
|
+
['ActiveRecord', self.class.name, read_attribute(self.class.primary_key)]
|
52
|
+
end
|
53
|
+
|
54
|
+
def notify_primer_after_create
|
55
|
+
Primer.bus.publish(:active_record, [:create, self.class.name, attributes])
|
56
|
+
end
|
57
|
+
|
58
|
+
def notify_primer_after_update
|
59
|
+
Primer.bus.publish(:active_record, [:update, self.class.name, attributes, changes])
|
60
|
+
end
|
61
|
+
|
62
|
+
def notify_primer_after_destroy
|
63
|
+
Primer.bus.publish(:active_record, [:destroy, self.class.name, attributes])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Primer
|
2
|
+
module Watcher
|
3
|
+
|
4
|
+
module Macros
|
5
|
+
attr_reader :primer_watched_calls
|
6
|
+
|
7
|
+
def self.extended(klass)
|
8
|
+
if defined?(ActiveRecord) and klass < ActiveRecord::Base
|
9
|
+
klass.extend(ActiveRecordMacros)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.alias_name(method_name)
|
14
|
+
method_name.to_s.gsub(/[^a-z0-9_]$/i, '') + '_before_primer_patch'
|
15
|
+
end
|
16
|
+
|
17
|
+
def watch_calls_to(*methods)
|
18
|
+
Watcher.register(self)
|
19
|
+
@primer_watched_calls ||= []
|
20
|
+
@primer_watched_calls += methods
|
21
|
+
end
|
22
|
+
|
23
|
+
def inherited(subclass)
|
24
|
+
super
|
25
|
+
calls = @primer_watched_calls || []
|
26
|
+
subclass.watch_calls_to(*calls)
|
27
|
+
end
|
28
|
+
|
29
|
+
def patch_for_primer!
|
30
|
+
return if @primer_watched_calls.nil? or @primer_patched
|
31
|
+
@primer_patched = true
|
32
|
+
@primer_watched_calls.each do |method_name|
|
33
|
+
patch_method_for_primer(method_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def patch_method_for_primer(method_name)
|
38
|
+
alias_name = Macros.alias_name(method_name)
|
39
|
+
return unless method = instance_method(method_name) rescue nil
|
40
|
+
class_eval <<-RUBY
|
41
|
+
alias :#{alias_name} :#{method_name}
|
42
|
+
def #{method_name}(*args, &block)
|
43
|
+
result = #{alias_name}(*args, &block)
|
44
|
+
Primer::Watcher.log(self, :#{method_name}, args, block, result)
|
45
|
+
result
|
46
|
+
end
|
47
|
+
RUBY
|
48
|
+
end
|
49
|
+
|
50
|
+
def unpatch_for_primer!
|
51
|
+
return if @primer_watched_calls.nil? or not @primer_patched
|
52
|
+
@primer_patched = false
|
53
|
+
@primer_watched_calls.each do |method_name|
|
54
|
+
unpatch_method_for_primer(method_name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def unpatch_method_for_primer(method_name)
|
59
|
+
alias_name = Macros.alias_name(method_name)
|
60
|
+
return unless method = instance_method(alias_name) rescue nil
|
61
|
+
class_eval <<-RUBY
|
62
|
+
alias :#{method_name} :#{alias_name}
|
63
|
+
undef_method :#{alias_name}
|
64
|
+
RUBY
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Primer
|
2
|
+
module Watcher
|
3
|
+
|
4
|
+
extend Enabler
|
5
|
+
autoload :Macros, ROOT + '/primer/watcher/macros'
|
6
|
+
autoload :ActiveRecordMacros, ROOT + '/primer/watcher/active_record_macros'
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.extend(Macros)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.register(klass)
|
13
|
+
@classes ||= Set.new
|
14
|
+
@classes.add(klass)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.reset!
|
18
|
+
Thread.current[:primer_call_log] = []
|
19
|
+
Thread.current[:primer_loggers] = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.call_log
|
23
|
+
Thread.current[:primer_call_log] ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.log(receiver, method_name, args, block, result)
|
27
|
+
call = [receiver, method_name, args, block, result]
|
28
|
+
loggers.each { |logger| logger << call }
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.loggers
|
32
|
+
Thread.current[:primer_loggers] ||= [call_log]
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.on_enable
|
36
|
+
@classes.each { |klass| klass.patch_for_primer! }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.on_disable
|
40
|
+
@classes.each { |klass| klass.unpatch_for_primer! }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.watching(calls = [])
|
44
|
+
@active_watching_blocks ||= 0
|
45
|
+
@active_watching_blocks += 1
|
46
|
+
was_enabled = enabled?
|
47
|
+
enable!
|
48
|
+
loggers << calls
|
49
|
+
result = yield
|
50
|
+
loggers.pop
|
51
|
+
@active_watching_blocks -= 1
|
52
|
+
disable! if @active_watching_blocks.zero? and not was_enabled
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
def primer_identifier
|
57
|
+
['Object', self.class.name, object_id]
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Primer
|
2
|
+
class Worker
|
3
|
+
|
4
|
+
class ActiveRecordAgent
|
5
|
+
def self.bind_to_bus
|
6
|
+
Primer.bus.subscribe :active_record do |event, class_name, attributes, changes|
|
7
|
+
model = class_name.constantize.new(attributes)
|
8
|
+
model.instance_eval do
|
9
|
+
@attributes = attributes
|
10
|
+
@changed_attributes = changes if changes
|
11
|
+
end
|
12
|
+
__send__("on_#{event}", model)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.macros
|
17
|
+
Watcher::ActiveRecordMacros
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.on_create(model)
|
21
|
+
notify_belongs_to_associations(model)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.on_update(model)
|
25
|
+
notify_attributes(model, model.changes)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.on_destroy(model)
|
29
|
+
notify_attributes(model, model.attributes)
|
30
|
+
notify_has_many_associations(model)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.notify_attributes(model, fields)
|
34
|
+
foreign_keys = model.class.primer_foreign_key_mappings
|
35
|
+
|
36
|
+
fields.each do |attribute, value|
|
37
|
+
Primer.bus.publish(:changes, model.primer_identifier + [attribute.to_s])
|
38
|
+
|
39
|
+
next unless assoc = foreign_keys[attribute.to_s]
|
40
|
+
Primer.bus.publish(:changes, model.primer_identifier + [assoc.to_s])
|
41
|
+
notify_belongs_to_association(model, assoc, value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.notify_belongs_to_associations(model)
|
46
|
+
model.class.reflect_on_all_associations.each do |assoc|
|
47
|
+
next unless assoc.macro == :belongs_to
|
48
|
+
notify_belongs_to_association(model, assoc.name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.notify_belongs_to_association(model, assoc_name, change = nil)
|
53
|
+
assoc = model.class.reflect_on_association(assoc_name)
|
54
|
+
owner_class = assoc.class_name.constantize
|
55
|
+
|
56
|
+
mirror = mirror_association(model.class, owner_class, :has_many)
|
57
|
+
|
58
|
+
if owner = model.__send__(assoc_name)
|
59
|
+
Primer.bus.publish(:changes, owner.primer_identifier + [mirror.name.to_s])
|
60
|
+
notify_has_many_through_association(owner, mirror.name)
|
61
|
+
end
|
62
|
+
|
63
|
+
return unless Array === change and change.first.any?
|
64
|
+
old_id = change.first.first
|
65
|
+
previous = owner_class.find(:first, :conditions => {owner_class.primary_key => old_id})
|
66
|
+
return unless previous
|
67
|
+
|
68
|
+
Primer.bus.publish(:changes, previous.primer_identifier + [mirror.name.to_s])
|
69
|
+
notify_has_many_through_association(previous, mirror.name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.notify_has_many_associations(model)
|
73
|
+
model.class.reflect_on_all_associations.each do |assoc|
|
74
|
+
next unless assoc.macro == :has_many
|
75
|
+
next if assoc.options[:dependent] == :destroy
|
76
|
+
|
77
|
+
model_id = model.__send__(model.class.primary_key)
|
78
|
+
klass = assoc.class_name.constantize
|
79
|
+
related = klass.find(:all, :conditions => {assoc.primary_key_name => model_id})
|
80
|
+
|
81
|
+
related.each do |object|
|
82
|
+
mirror = mirror_association(model.class, object.class, :belongs_to)
|
83
|
+
next unless mirror
|
84
|
+
|
85
|
+
Primer.bus.publish(:changes, object.primer_identifier + [mirror.name.to_s])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.notify_has_many_through_association(model, through_name)
|
91
|
+
model.class.reflect_on_all_associations.each do |assoc|
|
92
|
+
next unless assoc.macro == :has_many
|
93
|
+
|
94
|
+
if assoc.options[:through] == through_name
|
95
|
+
Primer.bus.publish(:changes, model.primer_identifier + [assoc.name.to_s])
|
96
|
+
end
|
97
|
+
|
98
|
+
assoc.class_name.constantize.reflect_on_all_associations.each do |secondary|
|
99
|
+
next unless secondary.macro == :has_many and secondary.options[:through] and
|
100
|
+
secondary.source_reflection.active_record == model.class and
|
101
|
+
secondary.source_reflection.name == through_name
|
102
|
+
|
103
|
+
model.__send__(assoc.name).each do |related|
|
104
|
+
Primer.bus.publish(:changes, related.primer_identifier + [secondary.name.to_s])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.mirror_association(object_class, related_class, macro)
|
111
|
+
related_class.reflect_on_all_associations.find do |mirror_assoc|
|
112
|
+
mirror_assoc.macro == macro and
|
113
|
+
mirror_assoc.class_name == object_class.name
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|