class ViewModel::ActiveRecord::Cache::CacheWorker

def add_refs_to_worklist(cacheable_references)

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

def add_viewmodels_to_worklist(referenced_viewmodels)

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

def cacheable_reference(viewmodel)

Store VM references in the cache as viewmodel name + id pairs.
def cacheable_reference(viewmodel)
  [viewmodel.class.view_name, viewmodel.id]
end

def find_and_preload_viewmodels(viewmodel_class, ids, available_viewmodels: nil)

available_viewmodels and shallowly preloads them.
Resolves viewmodels for the provided ids from the database or
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

def initialize(migration_versions:, serialize_context:)

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

def load_from_cache(viewmodel_cache, ids)

worklist.
{id=>serialized_view}. Any references encountered are added to the
Loads the specified entities from the cache and returns a hash of
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

def migrated_cache_version(viewmodel_cache)

def migrated_cache_version(viewmodel_cache)
  @migrated_cache_versions[viewmodel_cache] ||= viewmodel_cache.migrated_cache_version(migration_versions)
end

def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false)

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

def resolve_references!

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

def serialize_and_cache(viewmodels)

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