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.



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

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



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

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



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/view_model/active_record/update_context.rb', line 185

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



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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/view_model/active_record/update_context.rb', line 94

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.
  # Sort by id where possible to obtain as close to possible a deterministic
  # update order to avoid database write deadlocks. This can't be entirely
  # comprehensive, since we can't control the order that shared references
  # are referred to from roots (and therefore visited).
  updates_by_viewmodel_class =
    root_updates.lazily
      .map { |root_update| [nil, root_update] }
      .concat(referenced_updates)
      .sort_by { |_, update_data| update_data..id.to_s }
      .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.auto_child_update?
          raise ViewModel::DeserializationError::InvalidStructure.new(
                  'Cannot make an automatic child update to a root node',
                  ViewModel::Reference.new(update_data.viewmodel_class, nil))
        elsif update_data.child_update?
          raise ViewModel::DeserializationError::InvalidStructure.new(
                  'Cannot update an existing root node without a specified id',
                  ViewModel::Reference.new(update_data.viewmodel_class, nil))
        elsif 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.



277
278
279
280
281
282
283
# File 'lib/view_model/active_record/update_context.rb', line 277

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



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

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.



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

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.



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

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



240
241
242
243
244
245
246
247
248
249
# File 'lib/view_model/active_record/update_context.rb', line 240

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



267
268
269
# File 'lib/view_model/active_record/update_context.rb', line 267

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

#resolve_reference(ref, blame_reference) ⇒ Object



257
258
259
260
261
# File 'lib/view_model/active_record/update_context.rb', line 257

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.



73
74
75
# File 'lib/view_model/active_record/update_context.rb', line 73

def root_updates
  @root_update_operations
end

#run!(deserialize_context:) ⇒ Object

Applies updates and subsequently releases. Returns the updated viewmodels.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/view_model/active_record/update_context.rb', line 167

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? && deserialize_context.validate_deferred_constraints?
    # 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



263
264
265
# File 'lib/view_model/active_record/update_context.rb', line 263

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