11# frozen_string_literal: true
22
33require 'set'
4+ require_relative 'registry/index_store'
5+ require_relative 'registry/query_cache'
6+ require_relative 'registry/method_watcher'
47
58class Registry < Set
9+ include RegistryIndexStore
10+ include RegistryQueryCache
11+ include RegistryMethodWatcher
12+
613 # Exception classes for better error handling
714 class RegistryError < StandardError ; end
815 class MoreThanOneRecordFound < RegistryError ; end
@@ -14,15 +21,14 @@ class MissingAttributeError < RegistryError; end
1421 DEFAULT_INDEX = :object_id
1522
1623 def initialize ( *args , indexes : [ ] , thread_safe : false )
17- @indexed = { }
1824 @thread_safe = thread_safe
1925 @mutex = Mutex . new if @thread_safe
20- @watched_objects = Set . new # Track objects with watched methods for cleanup
21- @method_cache = { } # Cache setter method lookups
22- @batch_mode = false # For optimizing bulk operations
23- @query_cache = { } # Cache frequent query results
24- @cache_hits = 0 # Performance tracking
25- @cache_misses = 0
26+
27+ # Initialize module-specific state
28+ initialize_index_store
29+ initialize_query_cache
30+ initialize_method_watcher
31+
2632 super ( *args )
2733 reindex! ( indexes )
2834 end
@@ -35,10 +41,6 @@ def to_h
3541 @indexed
3642 end
3743
38- def indexes
39- @indexed . keys - [ :object_id ]
40- end
41-
4244 def delete ( item )
4345 @indexed . each do |idx , store |
4446 ignore_setter ( item , idx ) if include? ( item )
@@ -52,9 +54,8 @@ def delete(item)
5254 'is missing or not accessible.'
5355 end
5456 end
55- @watched_objects . delete ( item )
56- # Invalidate query cache when registry changes
57- @query_cache . clear
57+ remove_from_watched_objects ( item )
58+ invalidate_cache
5859 super
5960 end
6061
@@ -69,9 +70,8 @@ def add(item)
6970 "Item #{ item . inspect } cannot be added because indexable attribute '#{ idx } ' is missing or not accessible."
7071 end
7172 end
72- @watched_objects . add ( item ) unless include? ( item )
73- # Invalidate query cache when registry changes
74- @query_cache . clear
73+ add_to_watched_objects ( item ) unless include? ( item )
74+ invalidate_cache
7575 super
7676 end
7777 alias << add
@@ -85,148 +85,33 @@ def find(search_criteria)
8585 end
8686
8787 def where ( search_criteria )
88- # Check cache first for frequent queries
89- cache_key = [ :where , search_criteria . sort ]
90- if @query_cache . key? ( cache_key )
91- @cache_hits += 1
92- cached_items = @query_cache [ cache_key ]
93- return Registry . new ( cached_items . to_a , indexes : indexes , thread_safe : @thread_safe )
94- end
95- @cache_misses += 1
96-
97- # Fast path for single criteria - avoid array creation and reduce operations
98- if search_criteria . size == 1
99- idx , value = search_criteria . first
100- unless @indexed . include? ( idx )
101- raise IndexNotFound ,
102- "Index '#{ idx } ' not found. Available indexes: #{ indexes . inspect } . Add it with '.index(:#{ idx } )'"
103- end
104-
105- result_set = @indexed . dig ( idx , value ) || Set . new
106- # Cache the result set for future queries
107- @query_cache [ cache_key ] = result_set . dup
108- return Registry . new ( result_set . to_a , indexes : indexes , thread_safe : @thread_safe )
109- end
110-
111- # Multi-criteria path - optimize intersection logic
112- result_set = nil
113- search_criteria . each do |idx , value |
114- unless @indexed . include? ( idx )
115- raise IndexNotFound ,
116- "Index '#{ idx } ' not found. Available indexes: #{ indexes . inspect } . Add it with '.index(:#{ idx } )'"
117- end
118-
119- current_set = @indexed . dig ( idx , value ) || Set . new
120- result_set = result_set ? ( result_set & current_set ) : current_set
121-
122- # Early exit if no intersection possible
123- break if result_set . empty?
124- end
125-
126- final_result = result_set || Set . new
127- # Cache the result set for future queries (limit cache size)
128- if @query_cache . size < 1000
129- @query_cache [ cache_key ] = final_result . dup
88+ with_thread_safety do
89+ cache_key = [ :where , search_criteria . sort ]
90+ cached_result = check_cache ( cache_key )
91+ return new_registry_from_set ( cached_result ) if cached_result
92+
93+ result_set = if search_criteria . size == 1
94+ single_criteria_search ( search_criteria )
95+ else
96+ multi_criteria_search ( search_criteria )
97+ end
98+ store_in_cache ( cache_key , result_set )
99+ new_registry_from_set ( result_set )
130100 end
131-
132- Registry . new ( final_result . to_a , indexes : indexes , thread_safe : @thread_safe )
133101 end
134102
135103 # Check if any items exist matching the criteria
136104 def exists? ( search_criteria )
137105 with_thread_safety do
138- # Fast path for single criteria
139- if search_criteria . size == 1
140- idx , value = search_criteria . first
141- raise IndexNotFound ,
142- "Index '#{ idx } ' not found. Available indexes: #{ indexes . inspect } . " \
143- "Add it with '.index(:#{ idx } )'" unless @indexed . include? ( idx )
144-
145- return @indexed . dig ( idx , value ) &.any? || false
146- end
147-
148- # Multi-criteria path with intersection logic
149- result_set = nil
150- search_criteria . each do |idx , value |
151- raise IndexNotFound ,
152- "Index '#{ idx } ' not found. Available indexes: #{ indexes . inspect } . " \
153- "Add it with '.index(:#{ idx } )'" unless @indexed . include? ( idx )
154-
155- current_set = @indexed . dig ( idx , value )
156- return false if current_set . nil? || current_set . empty?
157-
158- result_set = result_set ? ( result_set & current_set ) : current_set
159- return false if result_set . empty?
160- end
161-
162- true
163- end
164- end
165-
166- def index ( *indexes )
167- indexes . each do |idx |
168- warn "Index #{ idx } already exists!" and next if @indexed . key? ( idx )
169-
170- # Optimize: Build index hash directly instead of using group_by + transformation
171- index_hash = { }
172- each do |item |
173- watch_setter ( item , idx )
174- @watched_objects . add ( item ) # Track watched objects
175-
176- # Get the index value and build the index in one pass
177- begin
178- idx_value = item . send ( idx )
179- ( index_hash [ idx_value ] ||= Set . new ) << item
180- rescue NoMethodError
181- raise MissingAttributeError ,
182- "Item #{ item . inspect } cannot be indexed because attribute '#{ idx } ' is missing or not accessible."
183- end
184- end
185- @indexed [ idx ] = index_hash
106+ search_criteria . size == 1 ? single_criteria_exists? ( search_criteria ) : multi_criteria_exists? ( search_criteria )
186107 end
187108 end
188109
189- def reindex! ( indexes = [ ] )
110+ def reindex! ( new_indexes = [ ] )
190111 cleanup_watched_methods # Clean up before reindexing
191112 @indexed = { }
192- index ( *( [ DEFAULT_INDEX ] | indexes ) )
193- end
194-
195- # Clean up method watching for memory management
196- def cleanup_watched_methods
197- @watched_objects . each do |item |
198- @indexed . each_key { |idx | ignore_setter ( item , idx ) }
199- end
200- @watched_objects . clear
201- end
202-
203- # Manual cleanup method for long-lived registries
204- def cleanup!
205- cleanup_watched_methods
206- end
207-
208- # Cache statistics for performance monitoring
209- def cache_stats
210- total_queries = @cache_hits + @cache_misses
211- return { hits : 0 , misses : 0 , hit_rate : 0.0 , total_queries : 0 } if total_queries == 0
212-
213- {
214- hits : @cache_hits ,
215- misses : @cache_misses ,
216- hit_rate : ( @cache_hits . to_f / total_queries * 100 ) . round ( 2 ) ,
217- total_queries : total_queries
218- }
219- end
220-
221- protected
222-
223- def reindex ( idx , item , old_value , new_value )
224- return unless new_value != old_value
225-
226- @indexed [ idx ] [ old_value ] . delete item
227- ( @indexed [ idx ] [ new_value ] ||= Set . new ) . add item
228- # Invalidate query cache when items change
229- @query_cache . clear
113+ @indexes = [ ]
114+ index ( *( [ DEFAULT_INDEX ] | new_indexes ) )
230115 end
231116
232117 private
@@ -237,67 +122,57 @@ def _find(search_criteria)
237122 results . first
238123 end
239124
240- def watch_setter ( item , idx )
241- return if item . frozen?
125+ def new_registry_from_set ( set )
126+ Registry . new ( set . to_a , indexes : indexes , thread_safe : @thread_safe )
127+ end
242128
243- # Use cached method lookup
244- item_class = item . class
245- cache_key = [ item_class , idx ]
246-
247- setter_method = @method_cache [ cache_key ] ||= begin
248- method_name = :"#{ idx } ="
249- item_class . instance_methods . include? ( method_name ) ? method_name : nil
250- end
251-
252- return unless setter_method
253-
254- watched_method = :"__watched_#{ setter_method } "
255- return if item . methods . include? ( watched_method )
129+ def validate_index_exists! ( idx )
130+ return if index_exists? ( idx )
256131
257- # Optimize: Reduce closure overhead by storing registry reference directly on item
258- item . instance_variable_set ( :@__registry__ , self ) unless item . instance_variable_defined? ( :@__registry__ )
259- original_method = setter_method
260- renamed_method = :"__unwatched_#{ original_method } "
132+ raise IndexNotFound ,
133+ "Index '#{ idx } ' not found. Available indexes: #{ indexes . inspect } . Add it with '.index(:#{ idx } )'"
134+ end
261135
262- item . singleton_class . class_eval do
263- define_method ( watched_method ) do |*args |
264- old_value = send ( idx ) # Use direct send instead of item.send
265- send ( renamed_method , *args ) . tap do |new_value |
266- instance_variable_get ( :@__registry__ ) . send ( :reindex , idx , self , old_value , new_value )
267- end
268- end
269- alias_method renamed_method , original_method
270- alias_method original_method , watched_method
271- end
136+ def single_criteria_search ( search_criteria )
137+ idx , value = search_criteria . first
138+ validate_index_exists! ( idx )
139+ lookup_index ( idx , value )
272140 end
273141
274- def ignore_setter ( item , idx )
275- return if item . frozen?
142+ def multi_criteria_search ( search_criteria )
143+ result_set = nil
144+ search_criteria . each do |idx , value |
145+ validate_index_exists! ( idx )
146+ current_set = lookup_index ( idx , value )
147+ result_set = result_set ? ( result_set & current_set ) : current_set
148+ break if result_set . empty?
149+ end
150+ result_set || Set . new
151+ end
276152
277- # Use cached method lookup
278- item_class = item . class
279- cache_key = [ item_class , idx ]
280- setter_method = @method_cache [ cache_key ]
281-
282- return unless setter_method
153+ def single_criteria_exists? ( search_criteria )
154+ idx , value = search_criteria . first
155+ validate_index_exists! ( idx )
156+ lookup_index ( idx , value ) . any?
157+ end
283158
284- original_method = setter_method
285- watched_method = :"__watched_#{ original_method } "
286- renamed_method = :"__unwatched_#{ original_method } "
287-
288- return unless item . methods . include? ( watched_method )
159+ def multi_criteria_exists? ( search_criteria )
160+ result_set = nil
161+ search_criteria . each do |idx , value |
162+ validate_index_exists! ( idx )
163+ current_set = lookup_index ( idx , value )
164+ return false if current_set . nil? || current_set . empty?
289165
290- item . singleton_class . class_eval do
291- alias_method original_method , renamed_method
292- remove_method ( watched_method )
293- remove_method ( renamed_method )
166+ result_set = result_set ? ( result_set & current_set ) : current_set
167+ return false if result_set . empty?
294168 end
169+ true
295170 end
296171
297172 # Thread safety wrapper
298- def with_thread_safety
173+ def with_thread_safety ( & block )
299174 if @thread_safe
300- @mutex . synchronize { yield }
175+ @mutex . synchronize ( & block )
301176 else
302177 yield
303178 end
0 commit comments