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
data/README.rdoc
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
= Primer
|
2
|
+
|
3
|
+
This is an experiment to bring automatic cache expiry and regeneration to Rails.
|
4
|
+
At Songkick[http://www.songkick.com], we have a ton of code that deals with caches
|
5
|
+
and denormalization and messages and offline processing and it's hard to maintain.
|
6
|
+
I want to get rid of it. All of it.
|
7
|
+
|
8
|
+
|
9
|
+
== What?
|
10
|
+
|
11
|
+
Inspired by LunaScript[http://asana.com/luna] and Fun[http://marcuswest.in/read/fun-intro/],
|
12
|
+
I figured Ruby could figure out which values a computation uses, and use that to
|
13
|
+
expire caches for you without having to write any expiry code. This turns out to be
|
14
|
+
possible, at least for typical ActiveRecord usage, and Primer includes an engine
|
15
|
+
for that.
|
16
|
+
|
17
|
+
Primer currently lets you do the following:
|
18
|
+
|
19
|
+
* Mark up ERB templates with cache keys on Rails and Sinatra
|
20
|
+
* Monitor what attributes a cache value depends on
|
21
|
+
* Automatically expire a cache when its dependencies change
|
22
|
+
* Declare how caches should be recalculated for eager cache population
|
23
|
+
* Update pages in real time when their data is updated
|
24
|
+
|
25
|
+
It does all this without you having to write a single cache sweeper. You
|
26
|
+
just declare how to render your site, Primer does the rest.
|
27
|
+
|
28
|
+
|
29
|
+
== Enough waffle, show me the code!
|
30
|
+
|
31
|
+
The following is the minimal, 'hello world' use case. Get your ActiveRecord
|
32
|
+
model, put a mixn in it (<em>after</em> declaring the model's associations):
|
33
|
+
|
34
|
+
class BlogPost < ActiveRecord::Base
|
35
|
+
include Primer::Watcher
|
36
|
+
end
|
37
|
+
|
38
|
+
Set up a cache for your app (you need Redis[http://code.google.com/p/redis/]):
|
39
|
+
|
40
|
+
Primer.cache = Primer::Cache::Redis.new(:host => "10.0.1.1", :port => 6380)
|
41
|
+
|
42
|
+
Throw a helper in your views:
|
43
|
+
|
44
|
+
# Rails
|
45
|
+
module ApplicationHelper
|
46
|
+
include Primer::Helpers::ERB
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sinatra
|
50
|
+
helpers { include Primer::Helpers::ERB }
|
51
|
+
|
52
|
+
Wrap cache blocks around your markup for expensive bits:
|
53
|
+
|
54
|
+
# views/posts/show.html.erb
|
55
|
+
|
56
|
+
<% primer "/posts/#{@post.id}/title" do %>
|
57
|
+
<%= @post.title.upcase %>
|
58
|
+
<% end %>
|
59
|
+
|
60
|
+
The output of the block gets cached to Redis using the given key. Once the
|
61
|
+
output is cached, the block will not be called again. The cache is invalidated
|
62
|
+
when (and only when) the title of <tt>@post</tt> changes; Primer figures this
|
63
|
+
out and you don't need to write any cache sweeping code.
|
64
|
+
|
65
|
+
Finally you need to bind the cache to the event bus, unless you want to run the
|
66
|
+
cache monitoring work in a background process (see below):
|
67
|
+
|
68
|
+
Primer.cache.bind_to_bus
|
69
|
+
|
70
|
+
# If you're using ActiveRecord
|
71
|
+
Primer::Worker::ActiveRecordAgent.bind_to_bus
|
72
|
+
|
73
|
+
|
74
|
+
=== Declaring cache generators
|
75
|
+
|
76
|
+
You may have noticed that Primer forces the use of path-style keys for your
|
77
|
+
cache. Instead of wrapping code you want to memoize in a block, you can
|
78
|
+
declare how to calculate it elsewhere and use a router to map cache keys
|
79
|
+
to calculations. For example we could rewrite our post title example like
|
80
|
+
this:
|
81
|
+
|
82
|
+
# views/posts/show.html.erb
|
83
|
+
# note '=' sign here, not used with block form
|
84
|
+
|
85
|
+
<%= primer "/posts/#{@post.id}/title" %>
|
86
|
+
|
87
|
+
Then you can declare how to calculate this in a router attached to your
|
88
|
+
cache object:
|
89
|
+
|
90
|
+
Primer.cache.routes do
|
91
|
+
get "/posts/:id/title" do
|
92
|
+
post = BlogPost.find(params[:id])
|
93
|
+
post.title.upcase
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
The advantage of this is that the cache now has a way to generate cache values
|
98
|
+
outside of your rendering stack, meaning that instead of just invalidating the
|
99
|
+
cache it can actually calculate the new value so the cache is always ready
|
100
|
+
for incoming requests.
|
101
|
+
|
102
|
+
It also means you can generate cache content offline; running the following
|
103
|
+
will generate the cache of the first post's title:
|
104
|
+
|
105
|
+
Primer.cache.compute("/posts/1/title")
|
106
|
+
|
107
|
+
|
108
|
+
=== Throttling
|
109
|
+
|
110
|
+
Let's say you have a cache value that depends on many little bits of model
|
111
|
+
data, a common situation in web front-ends. For example:
|
112
|
+
|
113
|
+
Primer.cache.routes do
|
114
|
+
get "/posts/:id/summary" do
|
115
|
+
post = BlogPost.find(params[:id])
|
116
|
+
<<-HTML
|
117
|
+
<div class="post-summary">
|
118
|
+
<h2>#{ post.title }</h2>
|
119
|
+
<ul class="post-meta">
|
120
|
+
<li>Posted #{ post.strftime('%A %e %B %Y') } by #{ post.author.name }</li>
|
121
|
+
<li>Tagged with #{ post.tags.map { |t| link_to(t.name, t) }.join(', ') }</li>
|
122
|
+
</ul>
|
123
|
+
</div>
|
124
|
+
HTML
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
We've got a few domain objects in use here: the post itself, its author, the
|
129
|
+
tags attached to the post. We'd want this value regenerating whenever any of
|
130
|
+
this data changes, but what if many values that affect this template change at
|
131
|
+
around the same time? We might not want to regenerate it for every single change,
|
132
|
+
we just want to make sure it looks okay after all the changes have been applied.
|
133
|
+
Primer lets you throttle cache regeneration, for example this makes sure each
|
134
|
+
key is never regenerated twice within a 5-second interval:
|
135
|
+
|
136
|
+
Primer.cache.throttle = 5
|
137
|
+
|
138
|
+
When a value affecting a key changes, Primer will wait 5 seconds before
|
139
|
+
regenerating it, allowing other data changes to accrue before we update the
|
140
|
+
cache.
|
141
|
+
|
142
|
+
|
143
|
+
=== Background workers
|
144
|
+
|
145
|
+
You'll probably want to move a lot of the work Primer does out of your front-end
|
146
|
+
process. Primer includes an AMQP message bus to support this, and setting it up
|
147
|
+
is easy - put this somewhere in your app's setup:
|
148
|
+
|
149
|
+
Primer.bus = Primer::Bus::AMQP.new(:queue => 'my_app_events')
|
150
|
+
|
151
|
+
To make a background worker, you just need a file like this:
|
152
|
+
|
153
|
+
# worker.rb
|
154
|
+
|
155
|
+
# load your models, config, Primer routes etc
|
156
|
+
require 'path/to/app/environment'
|
157
|
+
|
158
|
+
Primer.worker!
|
159
|
+
|
160
|
+
Running <tt>ruby worker.rb</tt> will start a process in the shell that listens
|
161
|
+
for change notifications and updates the cache for you. You can start as many
|
162
|
+
of these as you like to spread the load out.
|
163
|
+
|
164
|
+
|
165
|
+
=== Real-time page updates
|
166
|
+
|
167
|
+
If you want to be properly web-scale, you'll need to be updating your pages
|
168
|
+
in real time as your data changes. Primer lets you update any fragment generated
|
169
|
+
by a block-less <tt>primer</tt> call in your view automatically.
|
170
|
+
|
171
|
+
All you need to do is place some middleware in your Rack config:
|
172
|
+
|
173
|
+
# config.ru
|
174
|
+
|
175
|
+
require 'path/to/sinatra/app'
|
176
|
+
|
177
|
+
use Primer::RealTime
|
178
|
+
run Sinatra::Application
|
179
|
+
|
180
|
+
Add the client-side script to your templates (this must be in the <tt>HEAD</tt>):
|
181
|
+
|
182
|
+
<script type="text/javascript" src="/primer.js"></script>
|
183
|
+
|
184
|
+
Then configure it wherever your data model gets used to tell it you want to
|
185
|
+
use real-time updates and where the messaging server is. You should also set
|
186
|
+
a password - this will stop third parties being able to publish to the message
|
187
|
+
bus and inject arbitrary HTML into your pages.
|
188
|
+
|
189
|
+
Primer.real_time = true
|
190
|
+
Primer::RealTime.bayeux_server = 'http://localhost:9292'
|
191
|
+
Primer::RealTime.password = 'super_secret_password'
|
192
|
+
|
193
|
+
|
194
|
+
== Examples
|
195
|
+
|
196
|
+
See <tt>example/README.rdoc</tt>, a little Sinatra blog with a Redis cache,
|
197
|
+
offline cache workers and real-time page updates.
|
198
|
+
|
199
|
+
|
200
|
+
== Anything else?
|
201
|
+
|
202
|
+
I've tested it on Ruby 1.8.7 and 1.9.2 with Rails 3 and Sinatra 1.1. I've
|
203
|
+
briefly tried using it in Rails 2.2 and it looked okay-ish.
|
204
|
+
|
205
|
+
I'm NOT using this in production, and neither should you. Ideas and feedback
|
206
|
+
welcome, pull requests considered, bug reports likely to gather dust.
|
207
|
+
|
208
|
+
|
209
|
+
== License
|
210
|
+
|
211
|
+
Copyright (c) 2010 Songkick.com, James Coglan. Named by the inimitable
|
212
|
+
grillpanda[http://github.com/grillpanda]. Released under the MIT license.
|
213
|
+
|
data/example/README.rdoc
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
= Primer example app
|
2
|
+
|
3
|
+
This is a simple app that demonstrates how you can use Primer. It's based on
|
4
|
+
ActiveRecord for the model and Sinatra for the front-end. To start it up,
|
5
|
+
install some gems, build the database then use Rack:
|
6
|
+
|
7
|
+
gem install redis amqp faye sinatra activerecord sqlite3-ruby
|
8
|
+
ruby script/setup_database.rb
|
9
|
+
rackup -s thin -E production config.ru
|
10
|
+
|
11
|
+
|
12
|
+
=== models
|
13
|
+
|
14
|
+
Let's take a look at the internals. In the +models+ directory, you'll see a
|
15
|
+
simple BlogPost model:
|
16
|
+
|
17
|
+
class BlogPost < ActiveRecord::Base
|
18
|
+
include Primer::Watcher
|
19
|
+
end
|
20
|
+
|
21
|
+
That's all we need for Primer to monitor how our data changes.
|
22
|
+
|
23
|
+
|
24
|
+
=== public, views
|
25
|
+
|
26
|
+
The +public+ and +views+ directories are used by the Sinatra app, and should
|
27
|
+
be pretty self-explanatory. Note which bits of the views are cached, and which
|
28
|
+
parts don't use blocks - we set these keys up in <tt>environment.rb</tt>.
|
29
|
+
|
30
|
+
|
31
|
+
=== application.rb
|
32
|
+
|
33
|
+
The <tt>application.rb</tt> file is a simple Sinatra app - it loads the
|
34
|
+
model from <tt>environment.rb</tt> and just sets up a couple of pages.
|
35
|
+
|
36
|
+
|
37
|
+
=== environment.rb
|
38
|
+
|
39
|
+
The meat of the system is in <tt>environment.rb</tt>. This file loads the
|
40
|
+
gems we need, loads our models, and configures Primer. See that file for more
|
41
|
+
details, but note that it uses an AMQP bus - this means we can compute cache
|
42
|
+
changes outside the app process. You'll need to start <tt>worker.rb</tt> so
|
43
|
+
that your changes get processed.
|
44
|
+
|
45
|
+
|
46
|
+
=== worker.rb
|
47
|
+
|
48
|
+
This file just loads <tt>environment.rb</tt>, then tells Primer to start a
|
49
|
+
worker. The call to <tt>Primer.worker!</tt> is blocking, and makes Primer
|
50
|
+
pick up messages from the AMQP bus and process them. You can start as many
|
51
|
+
of these as you like to distribute the work. Just run:
|
52
|
+
|
53
|
+
ruby worker.rb
|
54
|
+
|
55
|
+
If you don't run any of these, messages will build up in AMQP until you start
|
56
|
+
a worker. Each data change message goes to only one worker process so you
|
57
|
+
won't duplicate any work.
|
58
|
+
|
59
|
+
|
60
|
+
=== console
|
61
|
+
|
62
|
+
You can interact with the model by starting IRB with the app's environment:
|
63
|
+
|
64
|
+
irb -r ./environment.rb
|
65
|
+
|
66
|
+
Go and create posts and change their data to see how the front-end reacts.
|
67
|
+
To get the real-time updates to work you must have a worker running to
|
68
|
+
update caches; this is not done by the web app process.
|
69
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra'
|
3
|
+
|
4
|
+
class Application < Sinatra::Base
|
5
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require ROOT + '/environment'
|
8
|
+
|
9
|
+
set :reload_templates, true
|
10
|
+
set :static, true
|
11
|
+
set :public, ROOT + '/public'
|
12
|
+
set :views, ROOT + '/views'
|
13
|
+
|
14
|
+
helpers { include Primer::Helpers::ERB }
|
15
|
+
|
16
|
+
get '/' do
|
17
|
+
@posts = BlogPost.all
|
18
|
+
erb :index
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/posts/:id' do
|
22
|
+
@post = BlogPost.find(params[:id])
|
23
|
+
erb :show
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
data/example/config.ru
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
# Load gems: ActiveRecord and Primer
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_record'
|
6
|
+
require dir + '/../lib/primer'
|
7
|
+
|
8
|
+
# Load database config and models
|
9
|
+
require dir + '/models/connection'
|
10
|
+
require dir + '/models/blog_post'
|
11
|
+
|
12
|
+
# Configure Primer with a Redis cache and AMQP bus
|
13
|
+
Primer.cache = Primer::Cache::Redis.new
|
14
|
+
Primer.bus = Primer::Bus::AMQP.new(:queue => 'blog_changes')
|
15
|
+
|
16
|
+
# Enable real-time page updates
|
17
|
+
Primer.real_time = true
|
18
|
+
Primer::RealTime.bayeux_server = 'http://0.0.0.0:9292'
|
19
|
+
Primer::RealTime.password = 'omg_rofl_scale'
|
20
|
+
|
21
|
+
# Set up cache generation routes
|
22
|
+
Primer.cache.routes do
|
23
|
+
get '/posts/:id/date' do
|
24
|
+
BlogPost.find(params[:id]).created_at.strftime('%A %e %B %Y')
|
25
|
+
end
|
26
|
+
|
27
|
+
get '/posts/:id/title' do
|
28
|
+
BlogPost.find(params[:id]).title.upcase
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
FileUtils.mkdir_p(dir + '/../db')
|
8
|
+
dbfile = dir + '/../db/blog.sqlite3'
|
9
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => dbfile)
|
10
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
body {
|
2
|
+
font: 16px/1.4 FreeSans, Helvetica, Arial, sans-serif;
|
3
|
+
background: #353e4b;
|
4
|
+
}
|
5
|
+
|
6
|
+
.sub {
|
7
|
+
width: 800px;
|
8
|
+
margin: 0 auto;
|
9
|
+
padding: 1em 2em;
|
10
|
+
}
|
11
|
+
|
12
|
+
.header {
|
13
|
+
text-shadow: #23282e 0px -2px 0px;
|
14
|
+
}
|
15
|
+
|
16
|
+
.header h1 {
|
17
|
+
color: #e5dec7;
|
18
|
+
font-size: 4em;
|
19
|
+
letter-spacing: -0.06em;
|
20
|
+
margin: 0;
|
21
|
+
}
|
22
|
+
|
23
|
+
.header h2 {
|
24
|
+
color: #b3a784;
|
25
|
+
font-size: 1.5em;
|
26
|
+
font-weight: normal;
|
27
|
+
letter-spacing: -0.06em;
|
28
|
+
margin: 0 0 0.5em;
|
29
|
+
}
|
30
|
+
|
31
|
+
.content .sub {
|
32
|
+
background: #fff;
|
33
|
+
color: #333;
|
34
|
+
-webkit-border-radius: 16px;
|
35
|
+
-moz-border-radius: 16px;
|
36
|
+
border-radius: 16px;
|
37
|
+
}
|
38
|
+
|
39
|
+
ul {
|
40
|
+
font-size: 1.5em;
|
41
|
+
list-style: none;
|
42
|
+
}
|
43
|
+
|
44
|
+
ul .date {
|
45
|
+
font-size: 0.8em;
|
46
|
+
letter-spacing: -0.06em;
|
47
|
+
color: #888;
|
48
|
+
}
|
49
|
+
|
50
|
+
h3 {
|
51
|
+
color: #9ba749;
|
52
|
+
font-size: 3em;
|
53
|
+
letter-spacing: -0.06em;
|
54
|
+
margin: 0;
|
55
|
+
}
|
56
|
+
|
57
|
+
h4 {
|
58
|
+
color: #888;
|
59
|
+
font-size: 1.5em;
|
60
|
+
font-weight: normal;
|
61
|
+
margin: 0 0 1em;
|
62
|
+
}
|
63
|
+
|
64
|
+
a {
|
65
|
+
color: #9ba749;
|
66
|
+
font-weight: bold;
|
67
|
+
text-decoration: none;
|
68
|
+
}
|
69
|
+
|
70
|
+
.footer {
|
71
|
+
font-size: 0.8em;
|
72
|
+
color: #999;
|
73
|
+
text-shadow: #23282e 0px -1px 0px;
|
74
|
+
}
|
75
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<p>Look at all the awesome posts omg</p>
|
2
|
+
|
3
|
+
<ul>
|
4
|
+
<% @posts.each do |post| %>
|
5
|
+
<li>
|
6
|
+
<span class="date"><%= post.created_at.strftime '%d %b %Y' %></span>
|
7
|
+
<a href="/posts/<%= post.id %>">
|
8
|
+
<%= primer "/posts/#{post.id}/title", :span %>
|
9
|
+
</a>
|
10
|
+
</li>
|
11
|
+
<% end %>
|
12
|
+
</ul>
|
13
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
5
|
+
<title>My ROFLscale blog</title>
|
6
|
+
<link rel="stylesheet" href="/style.css">
|
7
|
+
<script type="text/javascript" src="/primer.js"></script>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<div class="header"><div class="sub">
|
12
|
+
<h1>My ROFLscale blog</h1>
|
13
|
+
<h2>If you can’t store it in <tt>/dev/null</tt> it isn’t worth reading</h2>
|
14
|
+
</div></div>
|
15
|
+
|
16
|
+
<div class="content"><div class="sub">
|
17
|
+
<%= yield %>
|
18
|
+
</div></div>
|
19
|
+
|
20
|
+
<div class="footer"><div class="sub">
|
21
|
+
<p>Copyright © 2010 That’s What She Said</p>
|
22
|
+
</div></div>
|
23
|
+
|
24
|
+
</body>
|
25
|
+
</html>
|
26
|
+
|
data/example/worker.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
(function() {
|
2
|
+
window.PRIMER_CHANNELS = window.PRIMER_CHANNELS || [];
|
3
|
+
|
4
|
+
var BAYEUX_CLIENT = null;
|
5
|
+
|
6
|
+
var connect = function() {
|
7
|
+
BAYEUX_CLIENT = new Faye.Client('/primer/bayeux');
|
8
|
+
var i = PRIMER_CHANNELS.length;
|
9
|
+
while (i--) listen(PRIMER_CHANNELS.pop());
|
10
|
+
PRIMER_CHANNELS = {push: listen};
|
11
|
+
};
|
12
|
+
|
13
|
+
var listen = function(channel) {
|
14
|
+
BAYEUX_CLIENT.subscribe(channel, function(message) {
|
15
|
+
var node = document.getElementById(message.dom_id);
|
16
|
+
if (node) node.innerHTML = message.content;
|
17
|
+
});
|
18
|
+
};
|
19
|
+
|
20
|
+
var script = document.createElement('script'),
|
21
|
+
head = document.getElementsByTagName('head')[0];
|
22
|
+
|
23
|
+
script.type = 'text/javascript';
|
24
|
+
script.src = '/primer/bayeux.js';
|
25
|
+
|
26
|
+
script.onload = script.onreadystatechange = function() {
|
27
|
+
var state = script.readyState;
|
28
|
+
if (!state || state === 'loaded' || state === 'complete') {
|
29
|
+
script.onload = script.onreadystatechange = null;
|
30
|
+
head.removeChild(script);
|
31
|
+
connect();
|
32
|
+
}
|
33
|
+
};
|
34
|
+
head.appendChild(script);
|
35
|
+
})();
|
36
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'mq'
|
2
|
+
|
3
|
+
module Primer
|
4
|
+
class Bus
|
5
|
+
|
6
|
+
class AMQP < Bus
|
7
|
+
def initialize(config = {})
|
8
|
+
@config = config
|
9
|
+
super()
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish(topic, message)
|
13
|
+
tuple = [topic, message]
|
14
|
+
queue.publish(YAML.dump(tuple))
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe(*args, &block)
|
18
|
+
bind
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def queue
|
25
|
+
Faye.ensure_reactor_running!
|
26
|
+
return @queue if defined?(@queue)
|
27
|
+
raise "I need a queue name!" unless @config[:queue]
|
28
|
+
@queue = MQ.new.queue(@config[:queue])
|
29
|
+
end
|
30
|
+
|
31
|
+
def bind
|
32
|
+
return if @bound
|
33
|
+
queue.subscribe do |message|
|
34
|
+
data = YAML.load(message)
|
35
|
+
distribute(*data)
|
36
|
+
end
|
37
|
+
@bound = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
data/lib/primer/bus.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Primer
|
2
|
+
class Bus
|
3
|
+
|
4
|
+
autoload :Memory, ROOT + '/primer/bus/memory'
|
5
|
+
autoload :AMQP, ROOT + '/primer/bus/amqp'
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
unsubscribe_all
|
9
|
+
end
|
10
|
+
|
11
|
+
def distribute(topic, message)
|
12
|
+
return unless @listeners.has_key?(topic)
|
13
|
+
@listeners[topic].each { |cb| cb.call(message) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def subscribe(topic, &listener)
|
17
|
+
@listeners[topic].add(listener)
|
18
|
+
end
|
19
|
+
|
20
|
+
def unsubscribe(topic, &listener)
|
21
|
+
@listeners[topic].delete(listener)
|
22
|
+
end
|
23
|
+
|
24
|
+
def unsubscribe_all
|
25
|
+
@listeners = Hash.new { |h,k| h[k] = Set.new }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Primer
|
2
|
+
class Cache
|
3
|
+
|
4
|
+
class Memory < Cache
|
5
|
+
def initialize
|
6
|
+
clear
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear
|
10
|
+
@data_store = {}
|
11
|
+
@relations = {}
|
12
|
+
@dependencies = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def put(cache_key, value)
|
16
|
+
validate_key(cache_key)
|
17
|
+
@data_store[cache_key] = value
|
18
|
+
publish_change(cache_key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(cache_key)
|
22
|
+
validate_key(cache_key)
|
23
|
+
@data_store[cache_key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_key?(cache_key)
|
27
|
+
@data_store.has_key?(cache_key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def invalidate(cache_key)
|
31
|
+
@data_store.delete(cache_key)
|
32
|
+
return unless deps = @dependencies[cache_key]
|
33
|
+
deps.each do |attribute|
|
34
|
+
@relations[attribute].delete(cache_key)
|
35
|
+
end
|
36
|
+
@dependencies.delete(cache_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def relate(cache_key, attributes)
|
40
|
+
deps = @dependencies[cache_key] ||= Set.new
|
41
|
+
attributes.each do |attribute|
|
42
|
+
deps.add(attribute)
|
43
|
+
list = @relations[attribute] ||= Set.new
|
44
|
+
list.add(cache_key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def keys_for_attribute(attribute)
|
49
|
+
return [] unless keys = @relations[attribute]
|
50
|
+
keys.to_a
|
51
|
+
end
|
52
|
+
|
53
|
+
def timeout(cache_key, &block)
|
54
|
+
add_timeout(cache_key, @throttle, &block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|