module Scrivito # This class represents a single instance of a content state. # Basically a content state has three important characteristics: cache with obj datas, # changes representing changed objs and ways they has been changed and an ancestor content state. class ContentState < Struct.new(:content_state_id, :changes, :changes_index, :from_content_state_id) class << self private :new # Creates a new content state with given changes and ancestor (optional). def create(attributes) new(attributes).tap do |content_state| content_state.index_changes! CmsCacheStorage.write_content_state(content_state.content_state_id, content_state.to_hash) end end # Finds a previously saved content state. # Returns nil if not found. def find(content_state_id) if content_state_data = CmsCacheStorage.read_content_state(content_state_id) new(content_state_data) end end # Fetches an existing workspace. # If not found creates a new one and returns it. def find_or_create(content_state_id) find(content_state_id) || create(content_state_id: content_state_id) end end def initialize(attributes) super(*attributes.symbolize_keys.values_at(:content_state_id, :changes, :changes_index, :from_content_state_id)) end # Stores arbitrary data in cache. # Cache key is build from given index and key. def save_obj_data(index, key, data) CmsCacheStorage.write_obj_data(content_state_id, index, key, data) end # Fetches previously stored arbitrary data from cache. # Returns nil if nothing found. # Cache key is build from given index and key. def find_obj_data(index, key) CmsCacheStorage.read_obj_data(content_state_id, index, key) end # Fetches and caches the ancestor. # Returns nil if there is no ancestor. def from_content_state @from_content_state ||= self.class.find(from_content_state_id) end # Determines whether given data is still up-to-date for given index and key. def has_changes_for?(index, key, data) case index when 'id' id_index.include?(key) when 'path' path_index.include?(key) || data.first && id_index.include?(data.first['_id'].first) when 'ppath' ppath_index.include?(key) || data.find { |d| id_index.include?(d['_id'].first) } end end # Computes for a given changes feed a set of access-efficient indexes. def index_changes! id_index, path_index, ppath_index = Set.new, Set.new, Set.new if changes.present? changes.each do |hash| id_index.add(hash['id']) if path = hash['modified_path'] path_index.add(path) ppath_index.add(path.gsub(/\/[^\/]+$/, '').presence || '/') if path != '/' end end end self.changes = nil self.changes_index = {'id' => id_index, 'path' => path_index, 'ppath' => ppath_index} end # Returns a hash representation of a content state for serialization purpose. def to_hash { content_state_id: content_state_id, changes_index: changes_index, from_content_state_id: from_content_state_id } end private def id_index @id_index ||= changes_index['id'] end def path_index @path_index ||= changes_index['path'] end def ppath_index @ppath_index ||= changes_index['ppath'] end end end