require "memoist" module Scrivito module Backend class ContentStateNode < Struct.new(:content_state_id) extend Memoist # rationale: # with at most 16 changes, the resulting files should not exceed 1 KB # note that this is not a hard limit, but a best effort limit. # there may be nodes with more changes. MAX_CHANGES_PER_NODE = 16 def self.find(csid) raise InternalError unless csid new(csid) end def create_successor(successor_content_state_id, change_index) write_successor(successor_content_state_id, change_index) self.class.new(successor_content_state_id) end # used as special return values CACHE_MISS = Object.new NOT_FOUND = Object.new MAX_TRAVERSAL_DEPTH = 20 def obj_from_changes(obj_id, destination_csid, up_to_depth = MAX_TRAVERSAL_DEPTH) # reached the destination, did not find a change return NOT_FOUND if destination_csid == content_state_id return CACHE_MISS if up_to_depth == 0 # recorded history ends without reaching destination return CACHE_MISS unless predecessor changes[obj_id] || predecessor.obj_from_changes(obj_id, destination_csid, up_to_depth - 1) end def change_index_for(destination_csid, up_to_depth = MAX_TRAVERSAL_DEPTH) return {} if destination_csid == content_state_id return CACHE_MISS if up_to_depth == 0 # recorded history ends without reaching destination return CACHE_MISS unless predecessor pred_index = predecessor.change_index_for(destination_csid, up_to_depth - 1) return CACHE_MISS if pred_index == CACHE_MISS pred_index.merge(changes) end def predecessor self.class.find(data.first) if data end memoize :predecessor # the most recent nodes are considered unstable, # since they may "disappear" from the tree when nodes are compacted. # nodes without predecessor are always considered stable, since they # cannot be compacted. def next_stable_node predecessor || self end private def write_successor(successor_content_state_id, successor_changes) if predecessor # try to built a compacted node, # i.e. create a new node by mering the new changes and the # changes from this node (similar to git "squashing" commits) compacted_changes = changes.merge(successor_changes) if compacted_changes.size <= MAX_CHANGES_PER_NODE write_node( successor_content_state_id, predecessor.content_state_id, compacted_changes) return end end write_node( successor_content_state_id, content_state_id, successor_changes) end def write_node(csid, pred_csid, changes) CmsDataCache.write_content_state_node(csid, [pred_csid, changes]) end # changes is a hash from Obj-ID to Data-Tag (see CmsDataCache) def changes unless data raise InternalError, "access to changes without predecessor" end data.second end memoize :changes def data CmsDataCache.read_content_state_node(content_state_id) end memoize :data end end end