primer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|