render_sync 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +153 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +521 -0
  6. data/Rakefile +9 -0
  7. data/app/assets/javascripts/sync.coffee +355 -0
  8. data/app/controllers/sync/refetches_controller.rb +56 -0
  9. data/app/helpers/render_sync/config_helper.rb +15 -0
  10. data/config/routes.rb +3 -0
  11. data/config/sync.yml +21 -0
  12. data/lib/generators/render_sync/install_generator.rb +14 -0
  13. data/lib/generators/render_sync/templates/sync.ru +14 -0
  14. data/lib/generators/render_sync/templates/sync.yml +34 -0
  15. data/lib/render_sync.rb +174 -0
  16. data/lib/render_sync/action.rb +39 -0
  17. data/lib/render_sync/actions.rb +114 -0
  18. data/lib/render_sync/channel.rb +23 -0
  19. data/lib/render_sync/clients/dummy.rb +22 -0
  20. data/lib/render_sync/clients/faye.rb +104 -0
  21. data/lib/render_sync/clients/pusher.rb +77 -0
  22. data/lib/render_sync/controller_helpers.rb +33 -0
  23. data/lib/render_sync/engine.rb +24 -0
  24. data/lib/render_sync/erb_tracker.rb +49 -0
  25. data/lib/render_sync/faye_extension.rb +45 -0
  26. data/lib/render_sync/model.rb +174 -0
  27. data/lib/render_sync/model_actions.rb +60 -0
  28. data/lib/render_sync/model_change_tracking.rb +97 -0
  29. data/lib/render_sync/model_syncing.rb +65 -0
  30. data/lib/render_sync/model_touching.rb +35 -0
  31. data/lib/render_sync/partial.rb +112 -0
  32. data/lib/render_sync/partial_creator.rb +47 -0
  33. data/lib/render_sync/reactor.rb +48 -0
  34. data/lib/render_sync/refetch_model.rb +21 -0
  35. data/lib/render_sync/refetch_partial.rb +43 -0
  36. data/lib/render_sync/refetch_partial_creator.rb +21 -0
  37. data/lib/render_sync/renderer.rb +19 -0
  38. data/lib/render_sync/resource.rb +115 -0
  39. data/lib/render_sync/scope.rb +113 -0
  40. data/lib/render_sync/scope_definition.rb +30 -0
  41. data/lib/render_sync/view_helpers.rb +106 -0
  42. data/test/dummy/README.rdoc +28 -0
  43. data/test/dummy/Rakefile +6 -0
  44. data/test/dummy/app/assets/javascripts/application.js +13 -0
  45. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  46. data/test/dummy/app/controllers/application_controller.rb +5 -0
  47. data/test/dummy/app/helpers/application_helper.rb +2 -0
  48. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/test/dummy/app/views/sync/users/_show.html.erb +1 -0
  50. data/test/dummy/app/views/sync/users/refetch/_show.html.erb +1 -0
  51. data/test/dummy/bin/bundle +3 -0
  52. data/test/dummy/bin/rails +4 -0
  53. data/test/dummy/bin/rake +4 -0
  54. data/test/dummy/config.ru +4 -0
  55. data/test/dummy/config/application.rb +22 -0
  56. data/test/dummy/config/boot.rb +5 -0
  57. data/test/dummy/config/database.yml +8 -0
  58. data/test/dummy/config/environment.rb +5 -0
  59. data/test/dummy/config/environments/development.rb +29 -0
  60. data/test/dummy/config/environments/production.rb +80 -0
  61. data/test/dummy/config/environments/test.rb +36 -0
  62. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  64. data/test/dummy/config/initializers/inflections.rb +16 -0
  65. data/test/dummy/config/initializers/mime_types.rb +5 -0
  66. data/test/dummy/config/initializers/secret_token.rb +12 -0
  67. data/test/dummy/config/initializers/session_store.rb +3 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/en.yml +23 -0
  70. data/test/dummy/config/routes.rb +56 -0
  71. data/test/dummy/log/test.log +626 -0
  72. data/test/dummy/public/404.html +58 -0
  73. data/test/dummy/public/422.html +58 -0
  74. data/test/dummy/public/500.html +57 -0
  75. data/test/dummy/public/favicon.ico +0 -0
  76. data/test/em_minitest_spec.rb +100 -0
  77. data/test/fixtures/sync_auth_token_missing.yml +6 -0
  78. data/test/fixtures/sync_erb.yml +7 -0
  79. data/test/fixtures/sync_faye.yml +7 -0
  80. data/test/fixtures/sync_pusher.yml +8 -0
  81. data/test/models/group.rb +3 -0
  82. data/test/models/project.rb +2 -0
  83. data/test/models/todo.rb +8 -0
  84. data/test/models/user.rb +82 -0
  85. data/test/sync/abstract_controller.rb +3 -0
  86. data/test/sync/action_test.rb +82 -0
  87. data/test/sync/channel_test.rb +15 -0
  88. data/test/sync/config_test.rb +25 -0
  89. data/test/sync/erb_tracker_test.rb +72 -0
  90. data/test/sync/faye_extension_test.rb +87 -0
  91. data/test/sync/message_test.rb +159 -0
  92. data/test/sync/model_test.rb +315 -0
  93. data/test/sync/partial_creator_test.rb +35 -0
  94. data/test/sync/partial_test.rb +107 -0
  95. data/test/sync/protected_attributes_test.rb +39 -0
  96. data/test/sync/reactor_test.rb +18 -0
  97. data/test/sync/refetch_model_test.rb +26 -0
  98. data/test/sync/refetch_partial_creator_test.rb +16 -0
  99. data/test/sync/refetch_partial_test.rb +74 -0
  100. data/test/sync/renderer_test.rb +19 -0
  101. data/test/sync/resource_test.rb +181 -0
  102. data/test/sync/scope_definition_test.rb +39 -0
  103. data/test/sync/scope_test.rb +113 -0
  104. data/test/test_helper.rb +66 -0
  105. data/test/travis/sync.ru +14 -0
  106. data/test/travis/sync.yml +21 -0
  107. metadata +317 -0
@@ -0,0 +1,97 @@
1
+ module RenderSync
2
+ module ModelChangeTracking
3
+ private
4
+ # Set up callback to store record and sync scope states prior
5
+ # the update action
6
+ def self.included(base)
7
+ base.class_eval do
8
+ before_update :store_state_before_update, if: -> { RenderSync::Model.enabled? }
9
+ end
10
+ end
11
+
12
+ # Stores the current state of the record with its attributes
13
+ # and all sync relations in an instance variable BEFORE the update
14
+ # command to later be able to check if the record has been
15
+ # added/removed from sync scopes.
16
+ #
17
+ # Uses ActiveModel::Dirty to track attribute changes
18
+ # (triggered by AR Callback before_update)
19
+ #
20
+ def store_state_before_update
21
+ record = self.dup
22
+ changed_attributes.each do |key, value|
23
+ record.send("#{key}=", value)
24
+ end
25
+ record.send("#{self.class.primary_key}=", self.send(self.class.primary_key))
26
+
27
+ @record_before_update = record
28
+
29
+ @scopes_before_update = {}
30
+ sync_scope_definitions.each do |definition|
31
+ scope = RenderSync::Scope.new_from_model(definition, record)
32
+ @scopes_before_update[definition.name] = {
33
+ scope: scope,
34
+ contains_record: scope.contains?(record)
35
+ }
36
+ end
37
+ end
38
+
39
+ # Checks if this record has left the old scope defined by the passed scope
40
+ # definition throughout the update process
41
+ #
42
+ def left_old_scope?(definition)
43
+ scope_before_update(definition).valid? \
44
+ && old_record_in_old_scope?(definition) \
45
+ && !new_record_in_old_scope?(definition)
46
+ end
47
+
48
+ # Checks if this record has entered the new (possibly changed) scope
49
+ # defined by the passed scope definition throughout the update process
50
+ #
51
+ def entered_new_scope?(definition)
52
+ scope_after_update(definition).valid? \
53
+ && new_record_in_new_scope?(definition) \
54
+ && !remained_in_old_scope?(definition)
55
+ end
56
+
57
+ # Return the instance (state) of this record from before the update
58
+ # (which was previously stored by #store_state_before_update)
59
+ #
60
+ def record_before_update
61
+ @record_before_update
62
+ end
63
+
64
+ def record_after_update
65
+ self
66
+ end
67
+
68
+ def remained_in_old_scope?(definition)
69
+ old_record_in_old_scope?(definition) && new_record_in_old_scope?(definition)
70
+ end
71
+
72
+ def scope_before_update(definition)
73
+ @scopes_before_update[definition.name][:scope]
74
+ end
75
+
76
+ def scope_after_update(definition)
77
+ RenderSync::Scope.new_from_model(definition, record_after_update)
78
+ end
79
+
80
+ def old_record_in_old_scope?(definition)
81
+ @scopes_before_update[definition.name][:contains_record]
82
+ end
83
+
84
+ def old_record_in_new_scope?(definition)
85
+ scope_after_update(definition).contains?(record_before_update)
86
+ end
87
+
88
+ def new_record_in_new_scope?(definition)
89
+ scope_after_update(definition).contains?(record_after_update)
90
+ end
91
+
92
+ def new_record_in_old_scope?(defintion)
93
+ scope_before_update(defintion).contains?(record_after_update)
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,65 @@
1
+ module RenderSync
2
+ module ModelRenderSyncing
3
+
4
+ private
5
+
6
+ def prepare_sync_create
7
+ add_sync_action(:new, self, default_scope: sync_default_scope)
8
+
9
+ sync_scope_definitions.each do |definition|
10
+ scope = RenderSync::Scope.new_from_model(definition, self)
11
+ if scope.contains?(self)
12
+ add_sync_action :new, self, scope: scope, default_scope: sync_default_scope
13
+ end
14
+ end
15
+ end
16
+
17
+ def prepare_sync_update
18
+ add_sync_action :update, self
19
+
20
+ sync_scope_definitions.each do |definition|
21
+ prepare_sync_update_scope(definition)
22
+ end
23
+ end
24
+
25
+ def prepare_sync_destroy
26
+ add_sync_action :destroy, self, default_scope: sync_default_scope
27
+
28
+ sync_scope_definitions.each do |definition|
29
+ scope = RenderSync::Scope.new_from_model(definition, self)
30
+ if scope.valid?
31
+ add_sync_action :destroy, self,
32
+ scope: scope,
33
+ default_scope: sync_default_scope
34
+ end
35
+ end
36
+ end
37
+
38
+ # Creates update actions for subscribers on the sync scope defined by
39
+ # the passed sync scope definition.
40
+ #
41
+ # It compares the state of the record in context of the sync scope before
42
+ # and after the update. If the record has been added to a scope, it
43
+ # publishes a new partial to the subscribers of that scope. It also sends
44
+ # a destroy action to subscribers of the scope, if the record has been
45
+ # removed from it.
46
+ #
47
+ def prepare_sync_update_scope(scope_definition)
48
+ # Add destroy action for the old scope (scope_before_update)
49
+ # if this record has left it
50
+ if left_old_scope?(scope_definition)
51
+ add_sync_action :destroy, record_before_update,
52
+ scope: scope_before_update(scope_definition),
53
+ default_scope: sync_default_scope
54
+ end
55
+
56
+ # Add new action for the new scope (scope_after_update) if this record has entered it
57
+ if entered_new_scope?(scope_definition)
58
+ add_sync_action :new, record_after_update,
59
+ scope: scope_after_update(scope_definition),
60
+ default_scope: sync_default_scope
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ module RenderSync
2
+ module ModelTouching
3
+
4
+ private
5
+
6
+ def prepare_sync_touches
7
+ sync_touches.each do |touch_association|
8
+ add_sync_action :update, touch_association
9
+ end
10
+ end
11
+
12
+ # Return the associations to be touched after a record change
13
+ # Takes into account that an association itself may have changed during
14
+ # an update call (e.g. project_id has changed). To accomplish this, it
15
+ # uses the stored record from before the update (@record_before_update)
16
+ # and touches that as well as the current association
17
+ #
18
+ def sync_touches
19
+ sync_associations = []
20
+
21
+ self.class.sync_touches.each do |touch|
22
+ current = send(touch)
23
+ sync_associations.push(current.reload) if current.present?
24
+
25
+ if @record_before_update.present?
26
+ previous = @record_before_update.send(touch)
27
+ sync_associations.push(previous.reload) if previous.present?
28
+ end
29
+ end
30
+
31
+ sync_associations.uniq.compact
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,112 @@
1
+ module RenderSync
2
+ class Partial
3
+ attr_accessor :name, :resource, :context
4
+
5
+ def self.all(model, context, scope = nil)
6
+ resource = Resource.new(model, scope)
7
+
8
+ Dir["#{RenderSync.views_root}/#{resource.plural_name}/_*.*"].map do |partial|
9
+ partial_name = File.basename(partial)
10
+ Partial.new(partial_name[1...partial_name.index('.')], resource.model, scope, context)
11
+ end
12
+ end
13
+
14
+ def self.find(model, partial_name, context)
15
+ resource = Resource.new(model)
16
+ plural_name = resource.plural_name
17
+ partial = Dir["app/views/sync/#{plural_name}/_#{partial_name}.*"].first
18
+ return unless partial
19
+ Partial.new(partial_name, resource.model, nil, context)
20
+ end
21
+
22
+ def initialize(name, resource, scope, context)
23
+ self.name = name
24
+ self.resource = Resource.new(resource, scope)
25
+ self.context = context
26
+ end
27
+
28
+ def render_to_string
29
+ context.render_to_string(partial: path, locals: locals, formats: [:html])
30
+ end
31
+
32
+ def render
33
+ context.render(partial: path, locals: locals, formats: [:html])
34
+ end
35
+
36
+ def sync(action)
37
+ message(action).publish
38
+ end
39
+
40
+ def message(action)
41
+ RenderSync.client.build_message channel_for_action(action),
42
+ html: (render_to_string unless action.to_s == "destroy")
43
+ end
44
+
45
+ def authorized?(auth_token)
46
+ self.auth_token == auth_token
47
+ end
48
+
49
+ def auth_token
50
+ @auth_token ||= Channel.new("#{polymorphic_path}-_#{name}").to_s
51
+ end
52
+
53
+ # For the refetch feature we need an auth_token that wasn't created
54
+ # with scopes, because the scope information is not available on the
55
+ # refetch-request. So we create a refetch_auth_token which is based
56
+ # only on model_name and id plus the name of this partial
57
+ #
58
+ def refetch_auth_token
59
+ @refetch_auth_token ||= Channel.new("#{model_path}-_#{name}").to_s
60
+ end
61
+
62
+ def channel_prefix
63
+ @channel_prefix ||= auth_token
64
+ end
65
+
66
+ def update_channel_prefix
67
+ @update_channel_prefix ||= refetch_auth_token
68
+ end
69
+
70
+ def channel_for_action(action)
71
+ case action
72
+ when :update
73
+ "#{update_channel_prefix}-#{action}"
74
+ else
75
+ "#{channel_prefix}-#{action}"
76
+ end
77
+ end
78
+
79
+ def selector_start
80
+ "#{channel_prefix}-start"
81
+ end
82
+
83
+ def selector_end
84
+ "#{channel_prefix}-end"
85
+ end
86
+
87
+ def creator_for_scope(scope)
88
+ PartialCreator.new(name, resource.model, scope, context)
89
+ end
90
+
91
+
92
+ private
93
+
94
+ def path
95
+ "sync/#{resource.plural_name}/#{name}"
96
+ end
97
+
98
+ def locals
99
+ locals_hash = {}
100
+ locals_hash[resource.base_name.to_sym] = resource.model
101
+ locals_hash
102
+ end
103
+
104
+ def model_path
105
+ resource.model_path
106
+ end
107
+
108
+ def polymorphic_path
109
+ resource.polymorphic_path
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,47 @@
1
+ module RenderSync
2
+ class PartialCreator
3
+ attr_accessor :name, :resource, :context, :partial
4
+
5
+ def initialize(name, resource, scopes, context)
6
+ self.name = name
7
+ self.resource = Resource.new(resource, scopes)
8
+ self.context = context
9
+ self.partial = Partial.new(name, self.resource.model, scopes, context)
10
+ end
11
+
12
+ def auth_token
13
+ @auth_token ||= Channel.new("#{polymorphic_path}-_#{name}").to_s
14
+ end
15
+
16
+ def channel
17
+ @channel ||= auth_token
18
+ end
19
+
20
+ def selector
21
+ "#{channel}"
22
+ end
23
+
24
+ def sync_new
25
+ message.publish
26
+ end
27
+
28
+ def message
29
+ RenderSync.client.build_message(channel,
30
+ html: partial.render_to_string,
31
+ resourceId: resource.id,
32
+ authToken: partial.auth_token,
33
+ channelUpdate: partial.channel_for_action(:update),
34
+ channelDestroy: partial.channel_for_action(:destroy),
35
+ selectorStart: partial.selector_start,
36
+ selectorEnd: partial.selector_end
37
+ )
38
+ end
39
+
40
+
41
+ private
42
+
43
+ def polymorphic_path
44
+ resource.polymorphic_new_path
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ module RenderSync
2
+ class Reactor
3
+ include MonitorMixin
4
+
5
+ # Execute EventMachine bound code block, waiting for reactor to start if
6
+ # not yet started or reactor thread has gone away
7
+ def perform
8
+ return EM.next_tick{ yield } if running?
9
+ cleanly_shutdown_reactor
10
+ condition = new_cond
11
+ Thread.new do
12
+ EM.run do
13
+ EM.next_tick do
14
+ synchronize do
15
+ condition.signal
16
+ end
17
+ end
18
+ end
19
+ end
20
+ synchronize do
21
+ condition.wait_until { EM.reactor_running? }
22
+ EM.next_tick { yield }
23
+ end
24
+ end
25
+
26
+ def stop
27
+ EM.stop if running?
28
+ end
29
+
30
+ def running?
31
+ EM.reactor_running? && EM.reactor_thread.alive?
32
+ end
33
+
34
+ # If the reactor's thread died, EM still thinks it's running but it isn't.
35
+ # This will happen if we forked from a process that had the reator running.
36
+ # Tell EM it's dead. Stolen from the EM internals
37
+ #
38
+ # https://groups.google.com/forum/#!msg/ruby-amqp/zchM4QzbZRE/I43wIjbgIv4J
39
+ #
40
+ def cleanly_shutdown_reactor
41
+ if EM.reactor_running?
42
+ EM.stop_event_loop
43
+ EM.release_machine
44
+ EM.instance_variable_set '@reactor_running', false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ module RenderSync
2
+ class RefetchModel
3
+
4
+ def self.find_by_class_name_and_id(resource_name, id)
5
+ class_name = resource_name.to_s.classify
6
+ class_name.safe_constantize.find(id) if supported_classes.include?(class_name)
7
+ rescue
8
+ nil
9
+ end
10
+
11
+ def self.supported_classes
12
+ Thread.current["sync_refetch_classes"] = nil if Rails.env.development?
13
+
14
+ Thread.current["sync_refetch_classes"] ||= begin
15
+ Dir["app/views/sync/*/refetch"].collect{|path|
16
+ File.basename(path.gsub(/\/refetch$/, '')).classify
17
+ }.reject{|clazz| clazz.nil? }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ module RenderSync
2
+ class RefetchPartial < Partial
3
+
4
+ def self.all(model, context, scope = nil)
5
+ resource = Resource.new(model)
6
+
7
+ Dir["#{RenderSync.views_root}/#{resource.plural_name}/refetch/_*.*"].map do |partial|
8
+ partial_name = File.basename(partial)
9
+ RefetchPartial.new(partial_name[1...partial_name.index('.')], resource.model, scope, context)
10
+ end
11
+ end
12
+
13
+ def self.find(model, partial_name, context)
14
+ resource = Resource.new(model)
15
+ plural_name = resource.plural_name
16
+ partial = Dir["#{RenderSync.views_root}/#{plural_name}/refetch/_#{partial_name}.*"].first
17
+ return unless partial
18
+ RefetchPartial.new(partial_name, resource.model, nil, context)
19
+ end
20
+
21
+ def self.find_by_authorized_resource(model, partial_name, context, auth_token)
22
+ partial = find(model, partial_name, context)
23
+ return unless partial && partial.authorized?(auth_token)
24
+
25
+ partial
26
+ end
27
+
28
+ def message(action)
29
+ RenderSync.client.build_message channel_for_action(action), refetch: true
30
+ end
31
+
32
+ def creator_for_scope(scope)
33
+ RefetchPartialCreator.new(name, resource.model, scope, context)
34
+ end
35
+
36
+
37
+ private
38
+
39
+ def path
40
+ "sync/#{resource.plural_name}/refetch/#{name}"
41
+ end
42
+ end
43
+ end