class ViewModel::ActiveRecord::Cache::CacheWorker

Constants

SENTINEL
WorklistEntry

Attributes

migration_versions[R]
resolved_references[R]
serialize_context[R]

Public Class Methods

new(migration_versions:, serialize_context:) click to toggle source
# File lib/view_model/active_record/cache.rb, line 81
def initialize(migration_versions:, serialize_context:)
  @worklist                = {} # Hash[type_name, Hash[id, WorklistEntry]]
  @resolved_references     = {} # Hash[refname, json]
  @migration_versions      = migration_versions
  @migrated_cache_versions = {}
  @serialize_context       = serialize_context
end

Public Instance Methods

add_refs_to_worklist(cacheable_references) click to toggle source
# File lib/view_model/active_record/cache.rb, line 297
def add_refs_to_worklist(cacheable_references)
  cacheable_references.each do |ref_name, (type, id)|
    next if resolved_references.has_key?(ref_name)

    (@worklist[type] ||= {})[id] = WorklistEntry.new(ref_name, nil)
  end
end
add_viewmodels_to_worklist(referenced_viewmodels) click to toggle source
# File lib/view_model/active_record/cache.rb, line 305
def add_viewmodels_to_worklist(referenced_viewmodels)
  referenced_viewmodels.each do |ref_name, viewmodel|
    next if resolved_references.has_key?(ref_name)

    (@worklist[viewmodel.class.view_name] ||= {})[viewmodel.id] = WorklistEntry.new(ref_name, viewmodel)
  end
end
cacheable_reference(viewmodel) click to toggle source

Store VM references in the cache as viewmodel name + id pairs.

# File lib/view_model/active_record/cache.rb, line 293
def cacheable_reference(viewmodel)
  [viewmodel.class.view_name, viewmodel.id]
end
find_and_preload_viewmodels(viewmodel_class, ids, available_viewmodels: nil) click to toggle source

Resolves viewmodels for the provided ids from the database or available_viewmodels and shallowly preloads them.

# File lib/view_model/active_record/cache.rb, line 267
def find_and_preload_viewmodels(viewmodel_class, ids, available_viewmodels: nil)
  viewmodels = []

  if available_viewmodels.present?
    ids = ids.reject do |id|
      if (vm = available_viewmodels[id])
        viewmodels << vm
      end
    end
  end

  if ids.present?
    found = viewmodel_class.find(ids,
                                 eager_include: false,
                                 lock: 'FOR SHARE')
    viewmodels.concat(found)
  end

  ViewModel.preload_for_serialization(viewmodels,
                                      include_referenced: false,
                                      lock: 'FOR SHARE')

  viewmodels
end
load_from_cache(viewmodel_cache, ids) click to toggle source

Loads the specified entities from the cache and returns a hash of {id=>serialized_view}. Any references encountered are added to the worklist.

# File lib/view_model/active_record/cache.rb, line 198
def load_from_cache(viewmodel_cache, ids)
  cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache))

  cached_serializations.each_with_object({}) do |(id, cached_serialization), result|
    add_refs_to_worklist(cached_serialization[:ref_cache])
    result[id] = cached_serialization[:data]
  end
end
migrated_cache_version(viewmodel_cache) click to toggle source
# File lib/view_model/active_record/cache.rb, line 191
def migrated_cache_version(viewmodel_cache)
  @migrated_cache_versions[viewmodel_cache] ||= viewmodel_cache.migrated_cache_version(migration_versions)
end
render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false) click to toggle source
# File lib/view_model/active_record/cache.rb, line 89
def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false)
  viewmodel_class.transaction do
    root_serializations = Array.new(ids.size)

    # Collect input array positions for each id, allowing duplicates
    positions = ids.each_with_index.with_object({}) do |(id, i), h|
      (h[id] ||= []) << i
    end

    # If duplicates are specified, fetch each only once
    ids = positions.keys

    ids_to_render = ids.to_set

    if viewmodel_class < CacheableView
      # Load existing serializations from the cache
      cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, ids)
      cached_serializations.each do |id, data|
        positions[id].each do |idx|
          root_serializations[idx] = data
        end
      end

      ids_to_render.subtract(cached_serializations.keys)

      # If initial root viewmodels were provided, call hooks on any
      # viewmodels which were rendered from the cache to ensure that the
      # root is visible (in isolation). Other than this, no traversal
      # callbacks are performed for cache-rendered views. This particularly
      # requires care for references: if a visible view may refer to
      # non-visible cacheable views, those referenced views will not be
      # access control checked.
      initial_viewmodels&.each do |v|
        next unless cached_serializations.has_key?(v.id)
        serialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeVisit, v)
        serialize_context.run_callback(ViewModel::Callbacks::Hook::AfterVisit, v)
      end
    end

    # Render remaining views. If initial viewmodels have been locked, we may
    # use them to serialize from, otherwise we must reload with share lock
    # in find_and_preload.
    available_viewmodels =
      if locked
        initial_viewmodels&.each_with_object({}) do |vm, h|
          h[vm.id] = vm if ids_to_render.include?(vm.id)
        end
      end

    viewmodels = find_and_preload_viewmodels(viewmodel_class, ids_to_render.to_a,
                                             available_viewmodels: available_viewmodels)

    loaded_serializations = serialize_and_cache(viewmodels)

    loaded_serializations.each do |id, data|
      positions[id].each do |idx|
        root_serializations[idx] = data
      end
    end

    # recursively resolve referenced views
    self.resolve_references!

    [root_serializations, self.resolved_references]
  end
end
resolve_references!() click to toggle source
# File lib/view_model/active_record/cache.rb, line 156
def resolve_references!
  @serialize_context = serialize_context.for_references

  while @worklist.present?
    type_name, required_entries = @worklist.shift
    viewmodel_class = ViewModel::Registry.for_view_name(type_name)

    required_entries.each do |_id, entry|
      @resolved_references[entry.ref_name] = SENTINEL
    end

    if viewmodel_class < CacheableView
      cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, required_entries.keys)
      cached_serializations.each do |id, data|
        ref_name = required_entries.delete(id).ref_name
        @resolved_references[ref_name] = data
      end
    end

    # Load remaining entries from database
    available_viewmodels = required_entries.each_with_object({}) do |(id, entry), h|
      h[id] = entry.viewmodel if entry.viewmodel
    end

    viewmodels = find_and_preload_viewmodels(viewmodel_class, required_entries.keys,
                                             available_viewmodels: available_viewmodels)

    loaded_serializations = serialize_and_cache(viewmodels)
    loaded_serializations.each do |id, data|
      ref_name = required_entries[id].ref_name
      @resolved_references[ref_name] = data
    end
  end
end
serialize_and_cache(viewmodels) click to toggle source

Serializes the specified preloaded viewmodels and returns a hash of {id=>serialized_view}. If the viewmodel type is cacheable, it will be added to the cache. Any references encountered during serialization are added to the worklist.

# File lib/view_model/active_record/cache.rb, line 211
def serialize_and_cache(viewmodels)
  viewmodels.each_with_object({}) do |viewmodel, result|
    builder = Jbuilder.new do |json|
      ViewModel.serialize(viewmodel, json, serialize_context: serialize_context)
    end

    # viewmodels referenced from roots
    referenced_viewmodels = serialize_context.extract_referenced_views!

    if migration_versions.present?
      migrator = ViewModel::DownMigrator.new(migration_versions)

      # This migration isn't able to affect the contents of referenced
      # views, only their presence. The references will be themselves
      # rendered (and migrated) independently later. We mark the dummy
      # references provided to exclude their partial contents from being
      # themselves migrated.
      dummy_references = referenced_viewmodels.transform_values do |ref_vm|
        {
          ViewModel::TYPE_ATTRIBUTE    => ref_vm.class.view_name,
          ViewModel::VERSION_ATTRIBUTE => ref_vm.class.schema_version,
          ViewModel::ID_ATTRIBUTE      => ref_vm.id,
          ViewModel::Migrator::EXCLUDE_FROM_MIGRATION => true,
        }.freeze
      end

      migrator.migrate!({ 'data' => builder.attributes!, 'references' => dummy_references })

      # Removed dummy references can be removed from referenced_viewmodels.
      referenced_viewmodels.keep_if { |k, _| dummy_references.has_key?(k) }

      # Introduced dummy references cannot be handled.
      if dummy_references.keys != referenced_viewmodels.keys
        version = migration_versions[viewmodel.class]
        raise ViewModel::Error.new(
                status: 500,
                detail: "Down-migration for cacheable view #{viewmodel.class} to v#{version} must not introduce new shared references")
      end
    end

    data_serialization = builder.target!

    add_viewmodels_to_worklist(referenced_viewmodels)

    if viewmodel.class < CacheableView
      cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) }
      target_cache = viewmodel.class.viewmodel_cache
      target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references)
    end

    result[viewmodel.id] = data_serialization
  end
end