render_sync 0.5.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.
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