class Webgen::ItemTracker

Namespace for all item trackers.

About

This extension manager class is used to track various “items”. Such items can be added as a dependency to a node and later be checked if they have changed. This allows webgen to conditionally render a node.

An item can basically be anything, there only has to be an item tracker extension that knows how to handle it. Each item tracker extension is uniquely identified by its name (e.g. :node_content, :node_meta_info, …).

Implementing an item tracker.

An item tracker extension class must respond to the following methods:

initialize(website)

Initializes the extension and provides the website object which can be used to resolve the item ID to the referenced item or item data itself.

item_id(*item)

Return the unique ID for the given item. The returned ID has to be unique for this item tracker extension

item_data(*item)

Return the data for the item so that it can be correctly checked later if it has changed. If data for the given item cannot be computer anymore because the item got invalid, raise an exception.

item_changed?(item_id, old_data)

Return true if the item identified by its unique ID has changed. The parameter old_data contains the last known data of the item.

node_referenced?(item_id, old_data, node_alcn)

Return true if the node identified by node_alcn is referenced in the old data identified by the unique ID.

item_description(item_id, data)

Return a string or an array of strings which describe the item (identified by its unique ID) and the given item data. This is used for display purposes and should therefore include a nice, human readable interpretation of the item.

The parameter item for the methods item_id and item_data contains the information needed to identify the item and is depdendent on the specific item tracker extension class. Therefore you need to look at the documentation for an item tracker extension to see what it expects as the item.

Since these methods are invoked multiple times for different items, these methods should have no side effects.

Sample item tracker

The following sample item tracker tracks changes in configuration values. It needs the configuration option name as item.

class ConfigTracker

  def initialize(website)
    @website = website
  end

  def item_id(config_key)
    config_key
  end

  def item_data(config_key)
    @website.config[config_key]
  end

  def item_changed?(config_key, old_val)
    @website.config[config_key] != old_val
  end

  def node_referenced?(config_key, config_value, node_alcn)
    false
  end

  def item_description(config_key, old_val)
    "The website configuration option '#{config_key}'"
  end

end

website.ext.item_tracker.register ConfigTracker, name: :config

Public Class Methods

new(website) click to toggle source

Create a new item tracker for the given website.

Calls superclass method Webgen::ExtensionManager::new
    # File lib/webgen/item_tracker.rb
 97 def initialize(website)
 98   super()
 99   @instances = {}
100   @node_dependencies = Hash.new {|h,k| h[k] = Set.new}
101   @item_data = {}
102   @cached = {:node_dependencies => {}, :item_data => {}}
103   @written_nodes = []
104   @checked_nodes = Set.new
105 
106   @website = website
107 
108   @item_changed = {}
109 
110   @website.blackboard.add_listener(:website_initialized, 'item_tracker') do
111     @cached = @website.cache[:item_tracker_data] || @cached
112   end
113 
114   @website.blackboard.add_listener(:after_tree_populated, 'item_tracker') do |node|
115     @item_data.keys.each do |uid|
116       @item_data[uid] = item_tracker(uid.first).item_data(*uid.last)
117     end
118   end
119 
120   @website.blackboard.add_listener(:before_all_nodes_written, 'item_tracker') do
121     # make all used item data from the previous pass current again if applicable and remove
122     # invalid UIDs
123     uids_to_update = @written_nodes.each_with_object(Set.new) do |node, set|
124       next unless @website.tree[node.alcn]
125       set.merge(@node_dependencies[node.alcn])
126     end
127     uids_to_update.each do |uid|
128       begin
129         @item_data[uid] = item_tracker(uid.first).item_data(*uid.last)
130       rescue Exception
131         @item_data.delete(uid)
132         @cached[:item_data].delete(uid)
133         @written_nodes.each do |node|
134           if @node_dependencies[node.alcn].include?(uid)
135             @node_dependencies[node.alcn].delete(uid)
136             @cached[:node_dependencies][node.alcn].delete(uid)
137             node.node_info[:item_tracker_changed_once] = true
138           end
139         end
140       end
141     end
142 
143     @written_nodes = []
144     @item_changed = {}
145   end
146 
147   @website.blackboard.add_listener(:after_node_written, 'item_tracker') do |node|
148     @written_nodes << node
149   end
150 
151   @website.blackboard.add_listener(:after_all_nodes_written, 'item_tracker') do
152     # update cached data with data from the run
153     uids_to_update = Set.new
154     @written_nodes.each do |node|
155       next unless @website.tree[node.alcn]
156       node.node_info.delete(:item_tracker_changed_once)
157       @cached[:node_dependencies][node.alcn] = @node_dependencies[node.alcn]
158       uids_to_update.merge(@node_dependencies[node.alcn])
159     end
160     uids_to_update.each {|uid| @cached[:item_data][uid] = @item_data[uid]}
161   end
162 
163   @website.blackboard.add_listener(:website_generated, 'item_tracker') do
164     @cached[:node_dependencies].reject! {|alcn, data| !@website.tree[alcn]}
165 
166     used_uids = @cached[:node_dependencies].each_with_object(Set.new) {|(_, uids), obj| obj.merge(uids)}
167     @cached[:item_data].merge!(@item_data)
168     @cached[:item_data].reject! {|uid, _| !used_uids.include?(uid)}
169 
170     @website.cache[:item_tracker_data] = @cached
171   end
172 end

Public Instance Methods

add(node, name, *item) click to toggle source

Add the given item that is handled by the item tracker extension name as a dependency to the node.

    # File lib/webgen/item_tracker.rb
201 def add(node, name, *item)
202   uid = unique_id(name, item)
203   @node_dependencies[node.alcn] << uid
204   @item_data[uid] ||= item_tracker(name).item_data(*uid.last)
205 end
cached_items(include_default=true) click to toggle source

Return a hash with mappings from node ALCNs to their used items (items are converted to a human readable representation by using the item_description method).

If the parameter include_default is set to false, the default items added to each node so that it is possible to correctly detect changes, are not included.

The cached data, not the current data, is used. So this information is only useful after a website has been generated.

    # File lib/webgen/item_tracker.rb
246 def cached_items(include_default=true)
247   @cached[:node_dependencies].each_with_object({}) do |(alcn, uids), h|
248     h[alcn] = uids.sort {|a,b| a.first <=> b.first }.map do |uid|
249       next if !include_default && ((uid.first == :node_meta_info && uid.last.first == alcn )||
250                                    (uid.first == :node_content && uid.last == alcn))
251       item_tracker(uid.first).item_description(uid.last, @cached[:item_data][uid])
252     end.compact
253   end
254 end
item_changed?(name, *item) click to toggle source

Return true if the given item that is handled by the item tracker extension name has changed.

    # File lib/webgen/item_tracker.rb
234 def item_changed?(name, *item)
235   item_changed_by_uid?(unique_id(name, item))
236 end
node_changed?(node) click to toggle source

Return true if the given node has changed.

    # File lib/webgen/item_tracker.rb
208 def node_changed?(node)
209   return false if @checked_nodes.include?(node)
210   @checked_nodes << node
211   node.node_info[:item_tracker_changed_once] ||
212     !@cached[:node_dependencies].has_key?(node.alcn) ||
213     @cached[:node_dependencies][node.alcn].any? {|uid| item_changed_by_uid?(uid)}
214 ensure
215   @checked_nodes.delete(node)
216 end
node_referenced?(node) click to toggle source

Return true if the given node has been referenced by any item tracker extension.

    # File lib/webgen/item_tracker.rb
219 def node_referenced?(node)
220   if (cached = @website.cache.volatile[:item_tracker_referenced_nodes]).nil?
221     cached = @website.cache.volatile[:item_tracker_referenced_nodes] = Set.new
222     @cached[:node_dependencies].each do |alcn, uids|
223       uids.each do |uid|
224         cached.merge(item_tracker(uid.first).referenced_nodes(uid.last, @cached[:item_data][uid]) - [alcn])
225       end
226     end
227   end
228 
229   cached.include?(node.alcn)
230 end
register(klass, options={}, &block) click to toggle source

Register an item tracker.

The parameter klass has to contain the name of the item tracker class or the class object itself. If the class is located under this namespace, only the class name without the hierarchy part is needed, otherwise the full class name including parent module/class names is needed.

Options:

:name

The name for the item tracker class. If not set, it defaults to the snake-case version (i.e. FileSystem → file_system) of the class name (without the hierarchy part). It should only contain letters.

Examples:

item_tracker.register('Node')   # registers Webgen::ItemTracker::Node

item_tracker.register('::Node') # registers Node !!!

item_tracker.register('MyModule::Doit', name: 'infos')
    # File lib/webgen/item_tracker.rb
195 def register(klass, options={}, &block)
196   do_register(klass, options, false, &block)
197 end

Private Instance Methods

item_changed_by_uid?(uid) click to toggle source

Return true if the given item has changed. See add for a description of the item parameters.

    # File lib/webgen/item_tracker.rb
262 def item_changed_by_uid?(uid)
263   return @item_changed[uid] if @item_changed.has_key?(uid)
264   @item_changed[uid] = if !@cached[:item_data].has_key?(uid)
265                          true
266                        else
267                          item_tracker(uid.first).item_changed?(uid.last, @cached[:item_data][uid])
268                        end
269 end
item_tracker(name) click to toggle source

Return the item tracker extension object called name.

    # File lib/webgen/item_tracker.rb
278 def item_tracker(name)
279   @instances[name] ||= extension(name).new(@website)
280 end
unique_id(name, item) click to toggle source

Return the unique ID for the given item handled by the item tracker extension object specified by name.

    # File lib/webgen/item_tracker.rb
273 def unique_id(name, item)
274   [name.to_sym, item_tracker(name).item_id(*item)]
275 end