lazy_init 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,549 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LazyInit
4
+ # Provides class-level methods for defining lazy attributes with various optimization strategies.
5
+ #
6
+ # This module is automatically extended when a class includes or extends LazyInit.
7
+ # It analyzes attribute configuration and selects the most efficient implementation:
8
+ # simple inline methods for basic cases, optimized dependency methods for single
9
+ # dependencies, and full LazyValue wrappers for complex scenarios.
10
+ #
11
+ # The module generates three methods for each lazy attribute:
12
+ # - `attribute_name` - the main accessor method
13
+ # - `attribute_name_computed?` - predicate to check computation state
14
+ # - `reset_attribute_name!` - method to reset and allow recomputation
15
+ #
16
+ # @example Basic lazy attribute
17
+ # class ApiClient
18
+ # extend LazyInit
19
+ #
20
+ # lazy_attr_reader :connection do
21
+ # HTTPClient.new(api_url)
22
+ # end
23
+ # end
24
+ #
25
+ # @example Lazy attribute with dependencies
26
+ # class DatabaseService
27
+ # extend LazyInit
28
+ #
29
+ # lazy_attr_reader :config do
30
+ # load_configuration
31
+ # end
32
+ #
33
+ # lazy_attr_reader :connection, depends_on: [:config] do
34
+ # Database.connect(config.database_url)
35
+ # end
36
+ # end
37
+ #
38
+ # @since 0.1.0
39
+ module ClassMethods
40
+ # Set up necessary infrastructure when LazyInit is extended by a class.
41
+ #
42
+ # This is an internal Ruby hook method that's automatically called when a class
43
+ # extends LazyInit. Users should never call this method directly - it's part of
44
+ # Ruby's module extension mechanism.
45
+ #
46
+ # Initializes thread-safe mutex and dependency resolver for the target class.
47
+ # This ensures each class has its own isolated dependency management.
48
+ #
49
+ # @param base [Class] the class being extended with LazyInit
50
+ # @return [void]
51
+ # @api private
52
+ def self.extended(base)
53
+ base.instance_variable_set(:@lazy_init_class_mutex, Mutex.new)
54
+ base.instance_variable_set(:@dependency_resolver, DependencyResolver.new(base))
55
+ end
56
+
57
+ # Access the registry of all lazy initializers defined on this class.
58
+ #
59
+ # Used internally for introspection and debugging. Each entry contains
60
+ # the configuration (block, timeout, dependencies) for a lazy attribute.
61
+ #
62
+ # @return [Hash<Symbol, Hash>] mapping of attribute names to their configuration
63
+ def lazy_initializers
64
+ @lazy_initializers ||= {}
65
+ end
66
+
67
+ # Access the dependency resolver for this class.
68
+ #
69
+ # Handles dependency graph management and resolution order computation.
70
+ # Creates a new resolver if one doesn't exist.
71
+ #
72
+ # @return [DependencyResolver] the resolver instance for this class
73
+ def dependency_resolver
74
+ @dependency_resolver ||= DependencyResolver.new(self)
75
+ end
76
+
77
+ # Define a thread-safe lazy-initialized instance attribute.
78
+ #
79
+ # The attribute will be computed only once per instance when first accessed.
80
+ # Subsequent calls return the cached value. The implementation is automatically
81
+ # optimized based on complexity: simple cases use inline variables, single
82
+ # dependencies use optimized resolution, complex cases use full LazyValue.
83
+ #
84
+ # @param name [Symbol, String] the attribute name
85
+ # @param timeout [Numeric, nil] timeout in seconds for the computation
86
+ # @param depends_on [Array<Symbol>, Symbol, nil] other attributes this depends on
87
+ # @param block [Proc] the computation block
88
+ # @return [void]
89
+ # @raise [ArgumentError] if no block is provided
90
+ # @raise [InvalidAttributeNameError] if the attribute name is invalid
91
+ #
92
+ # @example Simple lazy attribute
93
+ # lazy_attr_reader :expensive_data do
94
+ # fetch_from_external_api
95
+ # end
96
+ #
97
+ # @example With dependencies
98
+ # lazy_attr_reader :database, depends_on: [:config] do
99
+ # Database.connect(config.database_url)
100
+ # end
101
+ #
102
+ # @example With timeout protection
103
+ # lazy_attr_reader :slow_service, timeout: 10 do
104
+ # SlowExternalService.connect
105
+ # end
106
+ def lazy_attr_reader(name, timeout: nil, depends_on: nil, &block)
107
+ validate_attribute_name!(name)
108
+ raise ArgumentError, 'Block is required' unless block
109
+
110
+ # store configuration for introspection and debugging
111
+ config = {
112
+ block: block,
113
+ timeout: timeout || LazyInit.configuration.default_timeout,
114
+ depends_on: depends_on
115
+ }
116
+ lazy_initializers[name] = config
117
+
118
+ # register dependencies with resolver if present
119
+ dependency_resolver.add_dependency(name, depends_on) if depends_on
120
+
121
+ # select optimal implementation strategy based on complexity
122
+ if enhanced_simple_case?(timeout, depends_on)
123
+ if simple_dependency_case?(depends_on)
124
+ generate_simple_dependency_method(name, depends_on, block)
125
+ else
126
+ generate_simple_inline_method(name, block)
127
+ end
128
+ else
129
+ generate_complex_lazyvalue_method(name, config)
130
+ end
131
+
132
+ # generate helper methods for all implementation types
133
+ generate_predicate_method(name)
134
+ generate_reset_method(name)
135
+ end
136
+
137
+ # Define a thread-safe lazy-initialized class variable shared across all instances.
138
+ #
139
+ # The variable will be computed only once per class when first accessed.
140
+ # All instances share the same computed value. Class variables are always
141
+ # implemented using LazyValue for full thread safety and feature support.
142
+ #
143
+ # @param name [Symbol, String] the class variable name
144
+ # @param timeout [Numeric, nil] timeout in seconds for the computation
145
+ # @param depends_on [Array<Symbol>, Symbol, nil] other attributes this depends on
146
+ # @param block [Proc] the computation block
147
+ # @return [void]
148
+ # @raise [ArgumentError] if no block is provided
149
+ # @raise [InvalidAttributeNameError] if the attribute name is invalid
150
+ #
151
+ # @example Shared connection pool
152
+ # lazy_class_variable :connection_pool do
153
+ # ConnectionPool.new(size: 20)
154
+ # end
155
+ def lazy_class_variable(name, timeout: nil, depends_on: nil, &block)
156
+ validate_attribute_name!(name)
157
+ raise ArgumentError, 'Block is required' unless block
158
+
159
+ class_variable_name = "@@#{name}_lazy_value"
160
+
161
+ # register dependencies for class-level attributes too
162
+ dependency_resolver.add_dependency(name, depends_on) if depends_on
163
+
164
+ # cache configuration for use in generated methods
165
+ cached_timeout = timeout
166
+ cached_depends_on = depends_on
167
+ cached_block = block
168
+
169
+ # generate class-level accessor with full thread safety
170
+ define_singleton_method(name) do
171
+ @lazy_init_class_mutex.synchronize do
172
+ return class_variable_get(class_variable_name).value if class_variable_defined?(class_variable_name)
173
+
174
+ # resolve dependencies using temporary instance if needed
175
+ if cached_depends_on
176
+ temp_instance = begin
177
+ new
178
+ rescue StandardError
179
+ # fallback for classes that can't be instantiated normally
180
+ Object.new.tap { |obj| obj.extend(self) }
181
+ end
182
+ dependency_resolver.resolve_dependencies(name, temp_instance)
183
+ end
184
+
185
+ # create and store the lazy value wrapper
186
+ lazy_value = LazyValue.new(timeout: cached_timeout, &cached_block)
187
+ class_variable_set(class_variable_name, lazy_value)
188
+ lazy_value.value
189
+ end
190
+ end
191
+
192
+ # generate class-level predicate method
193
+ define_singleton_method("#{name}_computed?") do
194
+ if class_variable_defined?(class_variable_name)
195
+ class_variable_get(class_variable_name).computed?
196
+ else
197
+ false
198
+ end
199
+ end
200
+
201
+ # generate class-level reset method
202
+ define_singleton_method("reset_#{name}!") do
203
+ if class_variable_defined?(class_variable_name)
204
+ lazy_value = class_variable_get(class_variable_name)
205
+ lazy_value.reset!
206
+ remove_class_variable(class_variable_name)
207
+ end
208
+ end
209
+
210
+ # generate instance-level delegation methods for convenience
211
+ define_method(name) { self.class.send(name) }
212
+ define_method("#{name}_computed?") { self.class.send("#{name}_computed?") }
213
+ define_method("reset_#{name}!") { self.class.send("reset_#{name}!") }
214
+ end
215
+
216
+ private
217
+
218
+ # Determine if an attribute qualifies for simple optimization.
219
+ #
220
+ # Simple cases avoid LazyValue overhead by using direct instance variables.
221
+ # This includes attributes with no timeout and either no dependencies or
222
+ # a single simple dependency that can be inlined.
223
+ #
224
+ # @param timeout [Object] timeout configuration
225
+ # @param depends_on [Object] dependency configuration
226
+ # @return [Boolean] true if simple implementation should be used
227
+ def enhanced_simple_case?(timeout, depends_on)
228
+ # timeout requires LazyValue for proper handling
229
+ return false unless timeout.nil?
230
+
231
+ # categorize dependency complexity
232
+ case depends_on
233
+ when nil, []
234
+ true # no dependencies are always simple
235
+ when Array
236
+ depends_on.size == 1 # single dependency can be optimized
237
+ when Symbol, String
238
+ true # single dependency in simple form
239
+ else
240
+ false
241
+ end
242
+ end
243
+
244
+ # Check if dependencies qualify for simple dependency optimization.
245
+ #
246
+ # Single dependencies can use an optimized resolution strategy that
247
+ # avoids the full dependency resolver overhead.
248
+ #
249
+ # @param depends_on [Object] dependency configuration
250
+ # @return [Boolean] true if simple dependency method should be used
251
+ def simple_dependency_case?(depends_on)
252
+ return false if depends_on.nil? || depends_on.empty?
253
+
254
+ deps = Array(depends_on)
255
+ deps.size == 1 # any single dependency qualifies for optimization
256
+ end
257
+
258
+ # Generate an optimized method for attributes with single dependencies.
259
+ #
260
+ # This creates a method that uses inline variables for storage and
261
+ # optimized dependency resolution that avoids LazyValue overhead.
262
+ # Includes circular dependency detection and thread-safe error caching.
263
+ #
264
+ # @param name [Symbol] the attribute name
265
+ # @param depends_on [Array, Symbol] the single dependency
266
+ # @param block [Proc] the computation block
267
+ # @return [void]
268
+ def generate_simple_dependency_method(name, depends_on, block)
269
+ dep_name = Array(depends_on).first
270
+ computed_var = "@#{name}_computed"
271
+ value_var = "@#{name}_value"
272
+ exception_var = "@#{name}_exception"
273
+
274
+ # cache references to avoid repeated lookups in generated method
275
+ cached_block = block
276
+ cached_dep_name = dep_name
277
+
278
+ define_method(name) do
279
+ # fast path: return cached result including cached errors
280
+ if instance_variable_get(computed_var)
281
+ stored_exception = instance_variable_get(exception_var)
282
+ raise stored_exception if stored_exception
283
+
284
+ return instance_variable_get(value_var)
285
+ end
286
+
287
+ # circular dependency protection using shared resolution stack
288
+ resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
289
+ if resolution_stack.include?(name)
290
+ circular_error = LazyInit::DependencyError.new(
291
+ "Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
292
+ )
293
+
294
+ # thread-safe error caching so all threads see the same error
295
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex) || Mutex.new
296
+ unless self.class.instance_variable_get(:@lazy_init_simple_mutex)
297
+ self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
298
+ end
299
+
300
+ mutex.synchronize do
301
+ instance_variable_set(exception_var, circular_error)
302
+ instance_variable_set(computed_var, true)
303
+ end
304
+
305
+ raise circular_error
306
+ end
307
+
308
+ # ensure we have a mutex for thread-safe computation
309
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
310
+ unless mutex
311
+ mutex = Mutex.new
312
+ self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
313
+ end
314
+
315
+ # track this attribute in resolution stack
316
+ resolution_stack.push(name)
317
+
318
+ begin
319
+ mutex.synchronize do
320
+ # double-check pattern after acquiring lock
321
+ if instance_variable_get(computed_var)
322
+ stored_exception = instance_variable_get(exception_var)
323
+ raise stored_exception if stored_exception
324
+
325
+ return instance_variable_get(value_var)
326
+ end
327
+
328
+ begin
329
+ # ensure dependency is computed first using optimized approach
330
+ unless send("#{cached_dep_name}_computed?")
331
+ # temporarily release lock to avoid deadlocks during dependency resolution
332
+ mutex.unlock
333
+ begin
334
+ send(cached_dep_name) # uses same shared resolution_stack for circular detection
335
+ ensure
336
+ mutex.lock
337
+ end
338
+
339
+ # check if we got computed while lock was released
340
+ if instance_variable_get(computed_var)
341
+ stored_exception = instance_variable_get(exception_var)
342
+ raise stored_exception if stored_exception
343
+
344
+ return instance_variable_get(value_var)
345
+ end
346
+ end
347
+
348
+ # perform the actual computation with dependency available
349
+ result = instance_eval(&cached_block)
350
+ instance_variable_set(value_var, result)
351
+ instance_variable_set(computed_var, true)
352
+ result
353
+ rescue StandardError => e
354
+ # cache exceptions for consistent behavior across threads
355
+ instance_variable_set(exception_var, e)
356
+ instance_variable_set(computed_var, true)
357
+ raise
358
+ end
359
+ end
360
+ ensure
361
+ # always clean up resolution stack to prevent leaks
362
+ resolution_stack.pop
363
+ Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
364
+ end
365
+ end
366
+ end
367
+
368
+ # Generate a simple inline method for attributes with no dependencies.
369
+ #
370
+ # Uses direct instance variables for maximum performance while maintaining
371
+ # thread safety through mutex synchronization. This is the fastest
372
+ # implementation strategy available.
373
+ #
374
+ # @param name [Symbol] the attribute name
375
+ # @param block [Proc] the computation block
376
+ # @return [void]
377
+ def generate_simple_inline_method(name, block)
378
+ computed_var = "@#{name}_computed"
379
+ value_var = "@#{name}_value"
380
+ exception_var = "@#{name}_exception"
381
+
382
+ # cache block reference to avoid lookup in generated method
383
+ cached_block = block
384
+
385
+ define_method(name) do
386
+ # fast path: return cached value immediately if available
387
+ return instance_variable_get(value_var) if instance_variable_get(computed_var)
388
+
389
+ # ensure we have a shared mutex for thread safety
390
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
391
+ unless mutex
392
+ mutex = Mutex.new
393
+ self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
394
+ end
395
+
396
+ mutex.synchronize do
397
+ # double-check pattern: another thread might have computed while we waited
398
+ if instance_variable_get(computed_var)
399
+ stored_exception = instance_variable_get(exception_var)
400
+ raise stored_exception if stored_exception
401
+
402
+ return instance_variable_get(value_var)
403
+ end
404
+
405
+ begin
406
+ # perform computation and cache result
407
+ result = instance_eval(&cached_block)
408
+ instance_variable_set(value_var, result)
409
+ instance_variable_set(computed_var, true)
410
+ result
411
+ rescue StandardError => e
412
+ # cache exceptions to ensure consistent error behavior
413
+ instance_variable_set(exception_var, e)
414
+ instance_variable_set(computed_var, true)
415
+ raise
416
+ end
417
+ end
418
+ end
419
+ end
420
+
421
+ # Generate a method using full LazyValue for complex scenarios.
422
+ #
423
+ # This handles timeouts, complex dependencies, and other advanced features
424
+ # that require the full LazyValue implementation. Used when simple
425
+ # optimizations aren't applicable.
426
+ #
427
+ # @param name [Symbol] the attribute name
428
+ # @param config [Hash] the attribute configuration
429
+ # @return [void]
430
+ def generate_complex_lazyvalue_method(name, config)
431
+ # cache configuration to avoid hash lookups in generated method
432
+ cached_timeout = config[:timeout]
433
+ cached_depends_on = config[:depends_on]
434
+ cached_block = config[:block]
435
+
436
+ define_method(name) do
437
+ # resolve dependencies using full dependency resolver if needed
438
+ self.class.dependency_resolver.resolve_dependencies(name, self) if cached_depends_on
439
+
440
+ # lazy creation of LazyValue wrapper
441
+ ivar_name = "@#{name}_lazy_value"
442
+ lazy_value = instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
443
+
444
+ unless lazy_value
445
+ lazy_value = LazyValue.new(timeout: cached_timeout) do
446
+ instance_eval(&cached_block)
447
+ end
448
+ instance_variable_set(ivar_name, lazy_value)
449
+ end
450
+
451
+ lazy_value.value
452
+ end
453
+ end
454
+
455
+ # Generate predicate method to check if attribute has been computed.
456
+ #
457
+ # Handles both simple (inline variables) and complex (LazyValue) cases.
458
+ # Returns false for exceptions to maintain consistent behavior.
459
+ #
460
+ # @param name [Symbol] the attribute name
461
+ # @return [void]
462
+ def generate_predicate_method(name)
463
+ define_method("#{name}_computed?") do
464
+ # check simple implementation first (most common after optimization)
465
+ computed_var = "@#{name}_computed"
466
+ exception_var = "@#{name}_exception"
467
+
468
+ if instance_variable_defined?(computed_var)
469
+ # simple implementation: computed but not if there's a cached exception
470
+ return instance_variable_get(computed_var) && !instance_variable_get(exception_var)
471
+ end
472
+
473
+ # check complex implementation (LazyValue wrapper)
474
+ lazy_var = "@#{name}_lazy_value"
475
+ if instance_variable_defined?(lazy_var)
476
+ lazy_value = instance_variable_get(lazy_var)
477
+ return lazy_value&.computed? || false
478
+ end
479
+
480
+ # not computed yet
481
+ false
482
+ end
483
+ end
484
+
485
+ # Generate reset method to clear computed state and allow recomputation.
486
+ #
487
+ # Handles both simple and complex implementations, ensuring proper
488
+ # cleanup of all associated state including cached exceptions.
489
+ #
490
+ # @param name [Symbol] the attribute name
491
+ # @return [void]
492
+ def generate_reset_method(name)
493
+ define_method("reset_#{name}!") do
494
+ # handle simple implementation reset
495
+ computed_var = "@#{name}_computed"
496
+ value_var = "@#{name}_value"
497
+ exception_var = "@#{name}_exception"
498
+
499
+ if instance_variable_defined?(computed_var)
500
+ # use mutex if available for thread safety during reset
501
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
502
+ if mutex
503
+ mutex.synchronize do
504
+ instance_variable_set(computed_var, false)
505
+ remove_instance_variable(value_var) if instance_variable_defined?(value_var)
506
+ remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
507
+ end
508
+ else
509
+ # no mutex means no concurrent access, safe to reset directly
510
+ instance_variable_set(computed_var, false)
511
+ remove_instance_variable(value_var) if instance_variable_defined?(value_var)
512
+ remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
513
+ end
514
+ return
515
+ end
516
+
517
+ # handle complex implementation reset (LazyValue)
518
+ lazy_var = "@#{name}_lazy_value"
519
+ if instance_variable_defined?(lazy_var)
520
+ lazy_value = instance_variable_get(lazy_var)
521
+ lazy_value&.reset!
522
+ remove_instance_variable(lazy_var)
523
+ end
524
+ end
525
+ end
526
+
527
+ # Validate that the attribute name is suitable for method generation.
528
+ #
529
+ # Ensures the name follows Ruby method naming conventions and won't
530
+ # cause issues when used to generate accessor methods.
531
+ #
532
+ # @param name [Object] the proposed attribute name
533
+ # @return [void]
534
+ # @raise [InvalidAttributeNameError] if the name is invalid
535
+ def validate_attribute_name!(name)
536
+ raise InvalidAttributeNameError, 'Attribute name cannot be nil' if name.nil?
537
+ raise InvalidAttributeNameError, 'Attribute name cannot be empty' if name.to_s.strip.empty?
538
+
539
+ unless name.is_a?(Symbol) || name.is_a?(String)
540
+ raise InvalidAttributeNameError, 'Attribute name must be a symbol or string'
541
+ end
542
+
543
+ name_str = name.to_s
544
+ return if name_str.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
545
+
546
+ raise InvalidAttributeNameError, "Invalid attribute name: #{name_str}"
547
+ end
548
+ end
549
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LazyInit
4
+ # Global configuration for LazyInit gem behavior.
5
+ #
6
+ # Provides centralized configuration for timeout defaults and memory management settings.
7
+ #
8
+ # @example Basic configuration
9
+ # LazyInit.configure do |config|
10
+ # config.default_timeout = 30
11
+ # config.max_lazy_once_entries = 5000
12
+ # end
13
+ #
14
+ # @since 0.1.0
15
+ class Configuration
16
+ # Default timeout in seconds for all lazy attributes
17
+ # @return [Numeric, nil] timeout value (default: nil)
18
+ attr_accessor :default_timeout
19
+
20
+ # Maximum entries in lazy_once cache
21
+ # @return [Integer] maximum cache entries (default: 1000)
22
+ attr_accessor :max_lazy_once_entries
23
+
24
+ # Time-to-live for lazy_once entries in seconds
25
+ # @return [Numeric, nil] TTL value (default: nil)
26
+ attr_accessor :lazy_once_ttl
27
+
28
+ # Initializes configuration with default values.
29
+ def initialize
30
+ @default_timeout = nil
31
+ @max_lazy_once_entries = 1000
32
+ @lazy_once_ttl = nil
33
+ end
34
+ end
35
+
36
+ # Returns the global configuration instance.
37
+ #
38
+ # @return [Configuration] the current configuration object
39
+ def self.configuration
40
+ @configuration ||= Configuration.new
41
+ end
42
+
43
+ # Configures LazyInit global settings.
44
+ #
45
+ # @yield [Configuration] the configuration object to modify
46
+ # @return [Configuration] the updated configuration
47
+ #
48
+ # @example Environment-specific configuration
49
+ # LazyInit.configure do |config|
50
+ # config.default_timeout = 10
51
+ # config.max_lazy_once_entries = 5000
52
+ # config.lazy_once_ttl = 1.hour
53
+ # end
54
+ def self.configure
55
+ yield(configuration)
56
+ end
57
+ end