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.
-
#record_dirty_warning!(signature) ⇒ Boolean
Records that a dirty-write warning has been emitted for +signature+ within the current dirty window, returning whether this is the first time that signature has been seen.
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 113 114 115 116 117 118 |
# 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 # Reset the dedup window on every clear_dirty! call. After a real clear # the dirty set has changed, so previously-warned signatures are stale. # On a no-op clear (a field name that was never dirty) the window still # resets, which at worst re-warns one already-seen signature -- harmless, # and not worth a branch to avoid. See record_dirty_warning!. @warned_dirty_signatures&.clear 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 |
#record_dirty_warning!(signature) ⇒ Boolean
Records that a dirty-write warning has been emitted for +signature+ within the current dirty window, returning whether this is the first time that signature has been seen.
A "dirty window" spans from the first +mark_dirty!+ to the next +clear_dirty!+ (called by save, commit_fields, batch_update, refresh). +Familia::DataType#warn_if_dirty!+ uses this to dedupe warnings in :once mode -- warning once per distinct set of unsaved fields rather than once per collection write.
134 135 136 137 138 139 140 |
# File 'lib/familia/horreum/dirty_tracking.rb', line 134 def record_dirty_warning!(signature) # Safety net for subclasses that override initialize without calling super. @warned_dirty_signatures ||= Concurrent::Map.new # Atomic: put_if_absent returns nil when the key was absent (and was # just stored), or the existing value otherwise. nil => first time. @warned_dirty_signatures.put_if_absent(signature, true).nil? end |