Class: ViewModel::ActiveRecord::UpdateContext

Inherits:
Object
  • Object
show all
Defined in:
lib/view_model/active_record/update_context.rb

Defined Under Namespace

Classes: ReleaseEntry, ReleasePool

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeUpdateContext

Returns a new instance of UpdateContext.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/view_model/active_record/update_context.rb', line 75

def initialize
  @root_update_operations       = [] # The subject(s) of this update
  @referenced_update_operations = {} # data updates to other root models, referred to by a ref hash

  # Set of ViewModel::Reference used to assert only a single update is
  # present for each viewmodel
  @updated_viewmodel_references = Set.new

  # hash of { ViewModel::Reference => deferred UpdateOperation }
  # for linked partially-constructed node updates
  @worklist = {}

  @release_pool = ReleasePool.new
end

Class Method Details

.build!(root_update_data, referenced_update_data, root_type: nil) ⇒ Object



59
60
61
62
63
64
65
66
67
# File 'lib/view_model/active_record/update_context.rb', line 59

def self.build!(root_update_data, referenced_update_data, root_type: nil)
  if root_type.present? && (bad_types = root_update_data.map(&:viewmodel_class).to_set.delete(root_type)).present?
    raise ViewModel::DeserializationError::InvalidViewType.new(root_type.view_name, bad_types.map { |t| ViewModel::Reference.new(t, nil) })
  end

  self.new
    .build_root_update_operations(root_update_data, referenced_update_data)
    .assemble_update_tree
end

Instance Method Details

#assemble_update_treeObject



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/view_model/active_record/update_context.rb', line 170

def assemble_update_tree
  @root_update_operations.each do |root_update|
    root_update.build!(self)
  end

  while @worklist.present?
    key = @worklist.keys.detect { |k| @release_pool.include?(k) }
    if key.nil?
      raise ViewModel::DeserializationError::ParentNotFound.new(@worklist.keys)
    end

    deferred_update    = @worklist.delete(key)
    released_viewmodel = @release_pool.claim_from_pool(key)

    if deferred_update.viewmodel
      # Deferred reference updates already have a viewmodel: ensure it
      # matches the tree
      unless deferred_update.viewmodel == released_viewmodel
        raise ViewModel::DeserializationError::Internal.new(
                "Released viewmodel doesn't match reference update", blame_reference)
      end
    else
      deferred_update.viewmodel = released_viewmodel
    end

    deferred_update.build!(self)
  end

  dangling_references = @referenced_update_operations.reject { |ref, upd| upd.built? }.map { |ref, upd| upd.viewmodel.to_reference }
  if dangling_references.present?
    raise ViewModel::DeserializationError::InvalidStructure.new("References not referred to from roots", dangling_references)
  end

  self
end

#build_root_update_operations(root_updates, referenced_updates) ⇒ Object

Processes parsed (UpdateData) root updates and referenced updates into



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/view_model/active_record/update_context.rb', line 92

def build_root_update_operations(root_updates, referenced_updates)
  # Look up viewmodel classes for each tree with eager_includes. Note this
  # won't yet include through a polymorphic boundary: for now we become
  # lazy-loading and slow every time that happens.

  # Combine our root and referenced updates, and separate by viewmodel type
  updates_by_viewmodel_class =
    root_updates.lazily
      .map { |root_update| [nil, root_update] }
      .concat(referenced_updates)
      .group_by { |_, update_data| update_data.viewmodel_class }

  # For each viewmodel type, look up referenced models and construct viewmodels to update
  updates_by_viewmodel_class.each do |viewmodel_class, updates|
    dependencies = updates.map { |_, upd| upd.preload_dependencies }
                   .inject { |acc, deps| acc.merge!(deps) }

    model_ids = updates.map { |_, update_data| update_data.id unless update_data.new? }.compact

    existing_models =
      if model_ids.present?
        model_class = viewmodel_class.model_class
        models = model_class.where(model_class.primary_key => model_ids).to_a

        if models.size < model_ids.size
          missing_model_ids = model_ids - models.map(&:id)
          missing_viewmodel_refs = missing_model_ids.map  { |id| ViewModel::Reference.new(viewmodel_class, id) }
          raise ViewModel::DeserializationError::NotFound.new(missing_viewmodel_refs)
        end

        DeepPreloader.preload(models, dependencies)
        models.index_by(&:id)
      else
        {}
      end

    updates.each do |ref, update_data|
      viewmodel =
        if update_data.new?
          viewmodel_class.for_new_model(id: update_data.id)
        else
          viewmodel_class.new(existing_models[update_data.id])
        end

      update_op = new_update(viewmodel, update_data)

      if ref.nil?
        @root_update_operations << update_op
      else
        # TODO make sure that referenced subtree hashes are unique and provide a decent error message
        # not strictly necessary, but will save confusion
        @referenced_update_operations[ref] = update_op
      end
    end
  end

  self
end

#check_deferred_constraints!(model_class) ⇒ Object

Immediately enforce any deferred database constraints (when using Postgres) and convert them to DeserializationErrors.

Note that there’s no effective way to tie such a failure back to the individual node that caused it, without attempting to parse Postgres’ human-readable error details.



262
263
264
265
266
267
268
# File 'lib/view_model/active_record/update_context.rb', line 262

def check_deferred_constraints!(model_class)
  if model_class.connection.adapter_name == "PostgreSQL"
    model_class.connection.execute("SET CONSTRAINTS ALL IMMEDIATE")
  end
rescue ::ActiveRecord::StatementInvalid => ex
  raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex)
end

#check_unique_update!(vm_ref) ⇒ Object



236
237
238
239
240
# File 'lib/view_model/active_record/update_context.rb', line 236

def check_unique_update!(vm_ref)
  unless @updated_viewmodel_references.add?(vm_ref)
    raise ViewModel::DeserializationError::DuplicateNodes.new(vm_ref.viewmodel_class.view_name, vm_ref)
  end
end

#defer_update(viewmodel_reference, update_operation) ⇒ Object

Defer an existing update: used if we need to ensure that an owned reference has been freed before we use it.



221
222
223
# File 'lib/view_model/active_record/update_context.rb', line 221

def defer_update(viewmodel_reference, update_operation)
  @worklist[viewmodel_reference] = update_operation
end

#new_deferred_update(viewmodel_reference, update_data, reparent_to: nil, reposition_to: nil) ⇒ Object

We require the updates to be recorded in the context so we can enforce the property that each viewmodel is in the tree at most once. To avoid mistakes, we require construction to go via methods that do this tracking.



212
213
214
215
216
217
# File 'lib/view_model/active_record/update_context.rb', line 212

def new_deferred_update(viewmodel_reference, update_data, reparent_to: nil, reposition_to: nil)
  update_operation = ViewModel::ActiveRecord::UpdateOperation.new(
    nil, update_data, reparent_to: reparent_to, reposition_to: reposition_to)
  check_unique_update!(viewmodel_reference)
  defer_update(viewmodel_reference, update_operation)
end

#new_update(viewmodel, update_data, reparent_to: nil, reposition_to: nil) ⇒ Object



225
226
227
228
229
230
231
232
233
234
# File 'lib/view_model/active_record/update_context.rb', line 225

def new_update(viewmodel, update_data, reparent_to: nil, reposition_to: nil)
  update = ViewModel::ActiveRecord::UpdateOperation.new(
    viewmodel, update_data, reparent_to: reparent_to, reposition_to: reposition_to)

  if (vm_ref = update.viewmodel_reference).present?
    check_unique_update!(vm_ref)
  end

  update
end

#release_viewmodel(viewmodel, association_data) ⇒ Object



252
253
254
# File 'lib/view_model/active_record/update_context.rb', line 252

def release_viewmodel(viewmodel, association_data)
  @release_pool.release_to_pool(viewmodel, association_data)
end

#resolve_reference(ref, blame_reference) ⇒ Object



242
243
244
245
246
# File 'lib/view_model/active_record/update_context.rb', line 242

def resolve_reference(ref, blame_reference)
  @referenced_update_operations.fetch(ref) do
    raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
  end
end

#root_updatesObject

TODO an unfortunate abstraction violation. The ‘append` case constructs an update tree and later injects the context of parent and position.



71
72
73
# File 'lib/view_model/active_record/update_context.rb', line 71

def root_updates
  @root_update_operations
end

#run!(deserialize_context:) ⇒ Object

Applies updates and subsequently releases. Returns the updated viewmodels.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/view_model/active_record/update_context.rb', line 152

def run!(deserialize_context:)
  updated_viewmodels = @root_update_operations.map do |root_update|
    root_update.run!(deserialize_context: deserialize_context)
  end

  @release_pool.release_all!

  if updated_viewmodels.present?
    # Deferred database constraints may have been violated by changes during
    # deserialization. VM::AR promises that any errors during deserialization
    # will be raised as a ViewModel::DeserializationError, so check constraints
    # and raise before exit.
    check_deferred_constraints!(updated_viewmodels.first.model.class)
  end

  updated_viewmodels
end

#try_take_released_viewmodel(vm_ref) ⇒ Object



248
249
250
# File 'lib/view_model/active_record/update_context.rb', line 248

def try_take_released_viewmodel(vm_ref)
  @release_pool.claim_from_pool(vm_ref)
end