Module: Familia::Horreum::DirtyTracking
- Included in:
- Familia::Horreum
- Defined in:
- lib/familia/horreum/dirty_tracking.rb
Overview
DirtyTracking - Tracks in-memory field changes since last save/refresh.
Provides a minimal ActiveModel::Dirty-inspired API for detecting which scalar fields have been modified. This is useful for:
- Knowing whether a save is needed
- Warning when collection writes happen with unsaved scalar changes
- Inspecting what changed and the old/new values
Fields are marked dirty automatically by the setter defined in FieldType. Dirty state is cleared after save, commit_fields, and refresh operations.
Uses Concurrent::Map for thread-safe access to the dirty fields tracker without requiring explicit mutex locks. The map is eagerly initialized in Horreum#initialize and the allocate-based load paths so that no lazy ||= race exists under normal usage. The ||= fallbacks in each method are a safety net for subclasses that override initialize without calling super (a documented anti-pattern).
Instance Method Summary collapse
-
#changed_fields ⇒ Hash{Symbol => Array(Object, Object)}
Returns a hash of changed fields with [old_value, new_value] pairs.
-
#clear_dirty!(*field_names) ⇒ void
Clears dirty tracking state for all or specific fields.
-
#dirty?(field = nil) ⇒ Boolean
Whether any fields (or a specific field) have unsaved changes.
-
#dirty_fields ⇒ Array<Symbol>
Returns the set of field names that have been modified.
-
#mark_dirty!(field_name, old_value) ⇒ void
Mark a field as dirty, recording its old value before the change.
Instance Method Details
#changed_fields ⇒ Hash{Symbol => Array(Object, Object)}
Returns a hash of changed fields with [old_value, new_value] pairs.
The old value is captured at the time of the first change since the last clear. The new value is read from the current instance variable.
85 86 87 88 89 90 91 92 93 |
# File 'lib/familia/horreum/dirty_tracking.rb', line 85 def changed_fields @dirty_fields ||= Concurrent::Map.new result = {} @dirty_fields.each_pair do |field_name, old_value| current_value = instance_variable_get(:"@#{field_name}") result[field_name] = [old_value, current_value] end result end |
#clear_dirty!(*field_names) ⇒ void
This method returns an undefined value.
Clears dirty tracking state for all or specific fields.
Called automatically after save, commit_fields, and refresh. When field names are provided, only those fields are cleared, preserving dirty state for fields that were not persisted.
105 106 107 108 109 110 111 112 |
# File 'lib/familia/horreum/dirty_tracking.rb', line 105 def clear_dirty!(*field_names) @dirty_fields ||= Concurrent::Map.new if field_names.empty? @dirty_fields.clear else field_names.each { |f| @dirty_fields.delete(f.to_sym) } end end |
#dirty?(field = nil) ⇒ Boolean
Whether any fields (or a specific field) have unsaved changes.
60 61 62 63 64 65 66 67 |
# File 'lib/familia/horreum/dirty_tracking.rb', line 60 def dirty?(field = nil) @dirty_fields ||= Concurrent::Map.new if field @dirty_fields.key?(field.to_sym) else !@dirty_fields.empty? end end |
#dirty_fields ⇒ Array<Symbol>
Returns the set of field names that have been modified.
73 74 75 76 |
# File 'lib/familia/horreum/dirty_tracking.rb', line 73 def dirty_fields @dirty_fields ||= Concurrent::Map.new @dirty_fields.keys end |
#mark_dirty!(field_name, old_value) ⇒ void
This method returns an undefined value.
Mark a field as dirty, recording its old value before the change.
Called by the field setter in FieldType#define_setter. Only records the original value on the first change (subsequent changes update the current value but preserve the original baseline).
48 49 50 51 52 53 |
# File 'lib/familia/horreum/dirty_tracking.rb', line 48 def mark_dirty!(field_name, old_value) # Safety net for subclasses that override initialize without calling super @dirty_fields ||= Concurrent::Map.new # Atomic: only stores old_value if field_sym is not already tracked. @dirty_fields.put_if_absent(field_name.to_sym, old_value) end |