fragmentary 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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +611 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fragmentary.gemspec +31 -0
- data/lib/fragmentary.rb +11 -0
- data/lib/fragmentary/dispatcher.rb +19 -0
- data/lib/fragmentary/fragment.rb +549 -0
- data/lib/fragmentary/fragments_helper.rb +62 -0
- data/lib/fragmentary/handler.rb +31 -0
- data/lib/fragmentary/publisher.rb +82 -0
- data/lib/fragmentary/request.rb +30 -0
- data/lib/fragmentary/request_queue.rb +139 -0
- data/lib/fragmentary/subscriber.rb +36 -0
- data/lib/fragmentary/subscription.rb +85 -0
- data/lib/fragmentary/user_session.rb +53 -0
- data/lib/fragmentary/version.rb +3 -0
- data/lib/fragmentary/widget.rb +56 -0
- data/lib/fragmentary/widget_parser.rb +46 -0
- metadata +158 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
module FragmentsHelper
|
4
|
+
|
5
|
+
def cache_fragment(options)
|
6
|
+
no_cache = options.delete(:no_cache)
|
7
|
+
options.reverse_merge!(:user => current_user) if respond_to?(:current_user)
|
8
|
+
fragment = options.delete(:fragment) || Fragmentary::Fragment.base_class.root(options)
|
9
|
+
builder = CacheBuilder.new(fragment, template = self)
|
10
|
+
unless no_cache
|
11
|
+
cache fragment, :skip_digest => true do
|
12
|
+
yield(builder)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
yield(builder)
|
16
|
+
end
|
17
|
+
self.output_buffer = WidgetParser.new(self).parse_buffer
|
18
|
+
end
|
19
|
+
|
20
|
+
def fragment_builder(options)
|
21
|
+
template = options.delete(:template)
|
22
|
+
options.reverse_merge!(:user => current_user) if respond_to?(:current_user)
|
23
|
+
CacheBuilder.new(Fragmentary::Fragment.base_class.existing(options), template)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class CacheBuilder
|
28
|
+
include ::ActionView::Helpers::CacheHelper
|
29
|
+
include ::ActionView::Helpers::TextHelper
|
30
|
+
|
31
|
+
attr_accessor :fragment, :template
|
32
|
+
|
33
|
+
def initialize(fragment, template)
|
34
|
+
@fragment = fragment
|
35
|
+
@template = template
|
36
|
+
end
|
37
|
+
|
38
|
+
def cache_child(options)
|
39
|
+
no_cache = options.delete(:no_cache)
|
40
|
+
insert_widgets = options.delete(:insert_widgets)
|
41
|
+
options.reverse_merge!(:user => template.current_user) if template.respond_to?(:current_user)
|
42
|
+
child = options.delete(:child) || fragment.child(options)
|
43
|
+
builder = CacheBuilder.new(child, template)
|
44
|
+
unless no_cache
|
45
|
+
template.cache child, :skip_digest => true do
|
46
|
+
yield(builder)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
yield(builder)
|
50
|
+
end
|
51
|
+
template.output_buffer = WidgetParser.new(template).parse_buffer if insert_widgets
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_missing(method, *args)
|
55
|
+
fragment.send(method, *args)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
class Handler
|
3
|
+
def self.all
|
4
|
+
@@all
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.clear
|
8
|
+
@@all = []
|
9
|
+
end
|
10
|
+
self.clear
|
11
|
+
|
12
|
+
def self.create(**args)
|
13
|
+
@@all << (handler = self.new(args))
|
14
|
+
handler
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(**args)
|
18
|
+
@args = args
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
raise "Method 'call' not defined."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class ActiveRecord::Base
|
28
|
+
def to_h
|
29
|
+
attributes.symbolize_keys
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'wisper/active_record'
|
2
|
+
|
3
|
+
module Fragmentary
|
4
|
+
|
5
|
+
module Publisher
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.instance_eval do
|
9
|
+
@class_registrations ||= Set.new
|
10
|
+
include Wisper.model
|
11
|
+
# ensures we override Wisper's definitions
|
12
|
+
include InstanceMethods
|
13
|
+
extend ClassMethods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def registrations
|
22
|
+
local_registrations + class_registrations + global_registrations + temporary_registrations
|
23
|
+
end
|
24
|
+
|
25
|
+
def class_registrations
|
26
|
+
self.class.registrations
|
27
|
+
end
|
28
|
+
|
29
|
+
def after_create_broadcast
|
30
|
+
Rails.logger.info "\n***** #{start = Time.now} broadcasting :after_create from #{self.class.name} #{self.id}\n"
|
31
|
+
broadcast(:after_create, self)
|
32
|
+
Rails.logger.info "\n***** #{Time.now} broadcast :after_create from #{self.class.name} #{self.id} took #{(Time.now - start) * 1000} ms\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
def after_update_broadcast
|
36
|
+
broadcast(:after_update, self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def after_destroy_broadcast
|
40
|
+
broadcast(:after_destroy, self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def after_commit_broadcast
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def subscribe(listener, options = {})
|
49
|
+
@class_registrations << ::Wisper::ObjectRegistration.new(listener, options.merge(:scope => self))
|
50
|
+
end
|
51
|
+
|
52
|
+
def registrations
|
53
|
+
@class_registrations + (superclass.try(:registrations) || [])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
module Wisper
|
61
|
+
class ObjectRegistration
|
62
|
+
|
63
|
+
# For the registration to broadcast a specified event we require:
|
64
|
+
# - 'should_broadcast?' - If the listener susbcribed with an ':on' option, ensure that the event
|
65
|
+
# is included in the 'on' list.
|
66
|
+
# - 'listener.respond_to?' - The listener contains a handler for the event
|
67
|
+
# - 'publisher_in_scope' - If the listener subscribed with a ':scope' option, ensure that the
|
68
|
+
# publisher's class is included in the 'scope' list.
|
69
|
+
def broadcast(event, publisher, *args)
|
70
|
+
method_to_call = map_event_to_method(event)
|
71
|
+
if should_broadcast?(event) && listener.respond_to?(method_to_call) && publisher_in_scope?(publisher)
|
72
|
+
broadcaster.broadcast(listener, publisher, method_to_call, args)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def publisher_in_scope?(publisher)
|
79
|
+
allowed_classes.empty? || (allowed_classes.include? publisher.class.name)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
class Request
|
4
|
+
attr_reader :method, :path, :options, :parameters
|
5
|
+
|
6
|
+
def initialize(method, path, parameters=nil, options=nil)
|
7
|
+
@method, @path, @parameters, @options = method, path, parameters, options
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
method == other.method and path == other.path and parameters == other.parameters and options == other.options
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_proc
|
15
|
+
method = @method; path = @path; parameters = @parameters; options = @options.try :dup
|
16
|
+
if @options.try(:[], :xhr)
|
17
|
+
Proc.new do
|
18
|
+
puts " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
|
19
|
+
send(:xhr, method, path, parameters, options)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
Proc.new do
|
23
|
+
puts " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
|
24
|
+
send(method, path, parameters, options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'fragmentary/user_session'
|
2
|
+
|
3
|
+
module Fragmentary
|
4
|
+
|
5
|
+
class RequestQueue
|
6
|
+
|
7
|
+
@@all = []
|
8
|
+
|
9
|
+
def self.all
|
10
|
+
@@all
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :requests, :user_type, :sender
|
14
|
+
|
15
|
+
def initialize(user_type)
|
16
|
+
@user_type = user_type
|
17
|
+
@requests = []
|
18
|
+
@sender = Sender.new(self)
|
19
|
+
@@all << self
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(request)
|
23
|
+
unless @requests.find{|r| r == request}
|
24
|
+
@requests << request
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
@requests.size
|
31
|
+
end
|
32
|
+
|
33
|
+
def session
|
34
|
+
@session ||= new_session
|
35
|
+
end
|
36
|
+
|
37
|
+
def new_session
|
38
|
+
case user_type
|
39
|
+
when 'signed_in'
|
40
|
+
UserSession.new('Bob')
|
41
|
+
when 'admin'
|
42
|
+
UserSession.new('Alice', :admin => true)
|
43
|
+
else
|
44
|
+
UserSession.new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def next_request
|
49
|
+
@requests.shift
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear
|
53
|
+
@requests = []
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear_session
|
57
|
+
@session = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_path(path)
|
61
|
+
requests.delete_if{|r| r.path == path}
|
62
|
+
end
|
63
|
+
|
64
|
+
def send(**args)
|
65
|
+
sender.start(args)
|
66
|
+
end
|
67
|
+
|
68
|
+
def method_missing(method, *args)
|
69
|
+
sender.send(method, *args)
|
70
|
+
end
|
71
|
+
|
72
|
+
class Sender
|
73
|
+
class << self
|
74
|
+
def jobs
|
75
|
+
::Delayed::Job.where("(handler LIKE ?) OR (handler LIKE ?)", "--- !ruby/object:#{name} %", "--- !ruby/object:#{name}\n%")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_reader :queue
|
80
|
+
|
81
|
+
def initialize(queue)
|
82
|
+
@queue = queue
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_request
|
86
|
+
queue.next_request.to_proc
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_next_request
|
90
|
+
if queue.size > 0
|
91
|
+
queue.session.instance_exec(&(next_request))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def send_all_requests
|
96
|
+
while queue.size > 0
|
97
|
+
send_next_request
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Send all requests, either directly or by schedule
|
102
|
+
def start(delay: nil, between: nil)
|
103
|
+
Rails.logger.info "\n***** Processing request queue for user_type '#{queue.user_type}'\n"
|
104
|
+
@delay = delay; @between = between
|
105
|
+
if @delay or @between
|
106
|
+
schedule_requests(@delay)
|
107
|
+
# sending requests by schedule makes a copy of the sender and queue objects for
|
108
|
+
# asynchronous execution, so we have to manually clear out the original queue.
|
109
|
+
queue.clear
|
110
|
+
else
|
111
|
+
send_all_requests
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def perform
|
116
|
+
Rails.logger.info "\n***** Processing request queue for user_type '#{queue.user_type}'\n"
|
117
|
+
@between ? send_next_request : send_all_requests
|
118
|
+
end
|
119
|
+
|
120
|
+
def success
|
121
|
+
schedule_requests(@between) if queue.size > 0
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def schedule_requests(delay=0.seconds)
|
127
|
+
if queue.size > 0
|
128
|
+
queue.clear_session
|
129
|
+
Delayed::Job.transaction do
|
130
|
+
self.class.jobs.destroy_all
|
131
|
+
Delayed::Job.enqueue self, :run_at => delay.from_now
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'fragmentary/subscription'
|
2
|
+
|
3
|
+
module Fragmentary
|
4
|
+
|
5
|
+
# Each fragment subclass has a unique Subscriber instance reponsible for handling subscriptions
|
6
|
+
# to publishers. Each subscriber maintains a hash of Subscriptions, one for each publisher it
|
7
|
+
# subscribes to. The 'subscribe_to' method instantiates each new Subscription in turn and executes
|
8
|
+
# its block against against the Subscriber in order to define handlers for each publisher event
|
9
|
+
# of interest. Any other method invoked within a handler is delegated to the client, i.e. the
|
10
|
+
# fragment subclass that the subscriber is reponsible for.
|
11
|
+
class Subscriber
|
12
|
+
attr_reader :client, :subscriptions
|
13
|
+
|
14
|
+
def initialize(client)
|
15
|
+
@client = client
|
16
|
+
@subscriptions = Hash.new do |h, key|
|
17
|
+
if Object.const_defined?(key) and (publisher = key.constantize) < ActiveRecord::Base
|
18
|
+
h[key] = Subscription.new(publisher, self)
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscribe_to(publisher, block)
|
26
|
+
if subscriptions[publisher.name]
|
27
|
+
instance_exec(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(method, *args)
|
32
|
+
@client.send(method, *args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
class Subscription
|
4
|
+
|
5
|
+
class Proxy
|
6
|
+
|
7
|
+
# Allow only one proxy per publisher; the proxy is responsible for subscribing
|
8
|
+
# to the publisher on behalf of individual subscriptions and calling handlers
|
9
|
+
# on each of them whenever the publisher broadcasts.
|
10
|
+
@@all = Hash.new do |h, key|
|
11
|
+
h[key] = Proxy.new(:publisher => key.constantize)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :publisher
|
15
|
+
|
16
|
+
def self.fetch(key)
|
17
|
+
@@all[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def register(subscription)
|
21
|
+
subscriptions << subscription if subscription.is_a? Subscription
|
22
|
+
end
|
23
|
+
|
24
|
+
['create', 'update', 'destroy'].each do |event|
|
25
|
+
class_eval <<-HEREDOC
|
26
|
+
def after_#{event}(record)
|
27
|
+
subscriptions.each do |subscription|
|
28
|
+
subscription.after_#{event}(record)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
HEREDOC
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def initialize(publisher:)
|
36
|
+
@publisher = publisher
|
37
|
+
@publisher.subscribe(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def subscriptions
|
41
|
+
@subscriptions ||= Set.new
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
include ActiveSupport::Callbacks
|
47
|
+
define_callbacks :after_destroy
|
48
|
+
|
49
|
+
attr_reader :subscriber
|
50
|
+
attr_accessor :record
|
51
|
+
|
52
|
+
def initialize(publisher, subscriber)
|
53
|
+
@subscriber = subscriber
|
54
|
+
Proxy.fetch(publisher.name).register(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def after_create(record)
|
58
|
+
call_method(:"create_#{record.class.model_name.param_key}_successful", record)
|
59
|
+
end
|
60
|
+
|
61
|
+
def after_update(record)
|
62
|
+
call_method(:"update_#{record.class.model_name.param_key}_successful", record)
|
63
|
+
end
|
64
|
+
|
65
|
+
def after_destroy(record)
|
66
|
+
# An ActiveSupport::Callbacks :after_destroy callback is set on the eigenclass of each individual
|
67
|
+
# subscription in Fragment.set_record_type in order to clean up fragments whose AR records are destroyed.
|
68
|
+
run_callbacks :after_destroy do
|
69
|
+
@record = record
|
70
|
+
call_method(:"destroy_#{record.class.model_name.param_key}_successful", record)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def call_method(method, record)
|
76
|
+
Rails.logger.info "***** Calling #{method.inspect} on #{subscriber.client.name} with record #{record.class.name} #{record.id}"
|
77
|
+
start = Time.now
|
78
|
+
subscriber.public_send(method, record) if subscriber.respond_to? method
|
79
|
+
finish = Time.now
|
80
|
+
Rails.logger.info "***** #{method.inspect} duration: #{(finish - start) * 1000}ms\n\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|