require_relative './event_collection' # How EventStore and Clockwork manager events are kept in sync... # # The normal Clockwork::Manager is responsible for keeping track of # Clockwork events, and ensuring they are scheduled at the correct time. # It has an @events array for this purpose. # For keeping track of Database-backed events though, we need to keep # track of more information about the events, e.g. the block which should # be triggered when they are run, which model the event comes from, the # model ID it relates to etc. Therefore, we devised a separate mechanism # for keeping track of these database-backed events: the per-model EventStore. # Having two classes responsible for keeping track of events though leads to # a slight quirk, in that these two have to be kept in sync. The way this is # done is by letting the EventStore largely defer to the Clockwork Manager. # 1. When the EventStore wishes to recreate events: # - it asks the Clockwork.manager to do this for it # - by calling Clockwork.manager.every # 2. When the DatabaseEvents::Manager creates events (via its #register) # - it creates a new DatabaseEvents::Event # - DatabaseEvents::Event#initialize registers it with the EventStore module Clockwork module DatabaseEvents class EventStore def initialize(block_to_perform_on_event_trigger) @related_events = {} @block_to_perform_on_event_trigger = block_to_perform_on_event_trigger end # DatabaseEvents::Manager#register creates a new DatabaseEvents::Event, whose # #initialize method registers the new database event with the EventStore by # calling this method. def register(event, model) related_events_for(model.id).add(event) end def update(current_model_objects) unregister_all_except(current_model_objects) update_registered_models(current_model_objects) register_new_models(current_model_objects) end def unregister_all_except(model_objects) ids = model_objects.collect(&:id) (@related_events.keys - ids).each{|id| unregister(id) } end def update_registered_models(model_objects) registered_models(model_objects).each do |model| if has_changed?(model) unregister(model.id) register_with_manager(model) end end end def register_new_models(model_objects) unregistered_models(model_objects).each do |new_model_object| register_with_manager(new_model_object) end end private attr_reader :related_events def registered?(model) related_events_for(model.id) != nil end def has_changed?(model) related_events_for(model.id).has_changed?(model) end def related_events_for(id) related_events[id] ||= EventCollection.new end def registered_models(model_objects) model_objects.select{|m| registered?(m) } end def unregistered_models(model_objects) model_objects.select{|m| !registered?(m) } end def unregister(id) related_events_for(id).unregister related_events.delete(id) end # When re-creating events, the Clockwork.manager must be used to # create them, as it is ultimately responsible for ensuring that # the events actually get run when they should. We call its #every # method, which will result in DatabaseEvent::Manager#register being # called, which creates a new DatabaseEvent::Event, which will be # registered with the EventStore on #initialize. def register_with_manager(model) Clockwork.manager. every(model.frequency, model, options(model), &@block_to_perform_on_event_trigger) end def options(model) options = { :from_database => true, :synchronizer => self, :ignored_attributes => [], } options[:at] = at_strings_for(model) if model.respond_to?(:at) options[:if] = ->(time){ model.if?(time) } if model.respond_to?(:if?) options[:tz] = model.tz if model.respond_to?(:tz) options[:ignored_attributes] = model.ignored_attributes if model.respond_to?(:ignored_attributes) options[:skip_first_run] = model.skip_first_run if model.respond_to?(:skip_first_run) # store the state of the model at time of registering so we can # easily compare and determine if state has changed later options[:model_attributes] = model.attributes.select do |k, v| not options[:ignored_attributes].include?(k.to_sym) end options end def at_strings_for(model) return nil if model.at.to_s.empty? model.at.split(',').map(&:strip) end end end end