familia 2.0.0.pre5 → 2.0.0.pre7

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.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,466 @@
1
+ # lib/familia/features/relationships.rb
2
+
3
+ require 'securerandom'
4
+ require_relative 'relationships/score_encoding'
5
+ require_relative 'relationships/redis_operations'
6
+ require_relative 'relationships/tracking'
7
+ require_relative 'relationships/indexing'
8
+ require_relative 'relationships/membership'
9
+ require_relative 'relationships/cascading'
10
+ require_relative 'relationships/querying'
11
+ require_relative 'relationships/permission_management'
12
+
13
+ module Familia
14
+ module Features
15
+ # Unified Relationships feature for Familia v2
16
+ #
17
+ # This feature merges the functionality of relatable_objects and relationships
18
+ # into a single, Redis-native implementation that embraces the "where does this appear?"
19
+ # philosophy rather than "who owns this?".
20
+ #
21
+ # Key improvements in v2:
22
+ # - Multi-presence: Objects can exist in multiple collections simultaneously
23
+ # - Score encoding: Metadata embedded in Redis scores for efficiency
24
+ # - Collision-free: Method names include collection names to prevent conflicts
25
+ # - Redis-native: All operations use Redis commands, no Ruby iteration
26
+ # - Atomic operations: Multi-collection updates happen atomically
27
+ #
28
+ # Breaking changes from v1:
29
+ # - Single feature: Use `feature :relationships` instead of separate features
30
+ # - Simplified identifier: Use `identifier :field` instead of `identifier_field :field`
31
+ # - No ownership concept: Remove `owned_by`, use multi-presence instead
32
+ # - Method naming: Generated methods include collection names for uniqueness
33
+ # - Score encoding: Scores can carry metadata like permissions
34
+ #
35
+ # @example Basic usage
36
+ # class Domain < Familia::Horreum
37
+ # feature :relationships
38
+ #
39
+ # identifier :domain_id
40
+ # field :domain_id
41
+ # field :display_name
42
+ # field :created_at
43
+ # field :permission_bits
44
+ #
45
+ # # Multi-presence tracking with score encoding
46
+ # tracked_in Customer, :domains,
47
+ # score: -> { permission_encode(created_at, permission_bits) }
48
+ # tracked_in Team, :domains, score: :added_at
49
+ # tracked_in Organization, :all_domains, score: :created_at
50
+ #
51
+ # # O(1) lookups with Redis hashes
52
+ # indexed_by :display_name, in: Customer, index_name: :domain_index
53
+ # indexed_by :display_name, in: :global, index_name: :global_domain_index
54
+ #
55
+ # # Context-aware membership (no method collisions)
56
+ # member_of Customer, :domains
57
+ # member_of Team, :domains
58
+ # member_of Organization, :domains
59
+ # end
60
+ #
61
+ # @example Generated methods (collision-free)
62
+ # # Tracking methods
63
+ # Customer.domains # => Familia::SortedSet
64
+ # Customer.add_domain(domain, score) # Add to customer's domains
65
+ # domain.in_customer_domains?(customer) # Check membership
66
+ #
67
+ # # Indexing methods
68
+ # Customer.find_by_display_name(name) # O(1) lookup
69
+ # Domain.find_by_display_name_globally(name) # Global lookup
70
+ #
71
+ # # Membership methods (collision-free naming)
72
+ # domain.add_to_customer_domains(customer) # Specific collection
73
+ # domain.add_to_team_domains(team) # Different collection
74
+ # domain.in_customer_domains?(customer) # Check specific membership
75
+ #
76
+ # @example Score encoding for permissions
77
+ # # Encode permission in score
78
+ # score = domain.permission_encode(Time.now, :write)
79
+ # # => 1704067200.004 (timestamp + permission bits)
80
+ #
81
+ # # Decode permission from score
82
+ # decoded = domain.permission_decode(score)
83
+ # # => { timestamp: 1704067200, permissions: 4, permission_list: [:write] }
84
+ #
85
+ # # Query with permission filtering
86
+ # Customer.domains_with_permission(:read)
87
+ #
88
+ # @example Multi-collection operations
89
+ # # Atomic updates across multiple collections
90
+ # domain.update_multiple_presence([
91
+ # { key: "customer:123:domains", score: current_score },
92
+ # { key: "team:456:domains", score: permission_encode(Time.now, :read) }
93
+ # ], :add, domain.identifier)
94
+ #
95
+ # # Set operations on collections
96
+ # accessible = Domain.union_collections([
97
+ # { owner: customer, collection: :domains },
98
+ # { owner: team, collection: :domains }
99
+ # ], min_permission: :read)
100
+ module Relationships
101
+ # Feature initialization
102
+ def self.included(base)
103
+ puts "[DEBUG] Relationships included in #{base}"
104
+ base.extend ClassMethods
105
+ base.include InstanceMethods
106
+
107
+ # Include all relationship submodules and their class methods
108
+ base.include ScoreEncoding
109
+ base.include RedisOperations
110
+
111
+ puts '[DEBUG] Including Tracking module'
112
+ base.include Tracking
113
+ puts '[DEBUG] Extending with Tracking::ClassMethods'
114
+ base.extend Tracking::ClassMethods
115
+ puts "[DEBUG] Base now responds to tracked_in: #{base.respond_to?(:tracked_in)}"
116
+
117
+ base.include Indexing
118
+ base.extend Indexing::ClassMethods
119
+
120
+ base.include Membership
121
+ base.extend Membership::ClassMethods
122
+
123
+ base.include Cascading
124
+ base.extend Cascading::ClassMethods
125
+
126
+ base.include Querying
127
+ base.extend Querying::ClassMethods
128
+ end
129
+
130
+ # Error classes
131
+ class RelationshipError < StandardError; end
132
+ class InvalidIdentifierError < RelationshipError; end
133
+ class InvalidScoreError < RelationshipError; end
134
+ class CascadeError < RelationshipError; end
135
+
136
+ module ClassMethods
137
+ # Define the identifier for this class (replaces identifier_field)
138
+ # This is a compatibility wrapper around the existing identifier_field method
139
+ #
140
+ # @param field [Symbol] The field to use as identifier
141
+ # @return [Symbol] The identifier field
142
+ #
143
+ # @example
144
+ # identifier :domain_id
145
+ def identifier(field = nil)
146
+ return identifier_field(field) if field
147
+
148
+ identifier_field
149
+ end
150
+
151
+ # Generate a secure temporary identifier
152
+ def generate_identifier
153
+ SecureRandom.hex(8)
154
+ end
155
+
156
+ # Get all relationship configurations for this class
157
+ def relationship_configs
158
+ configs = {}
159
+
160
+ configs[:tracking] = tracking_relationships if respond_to?(:tracking_relationships)
161
+ configs[:indexing] = indexing_relationships if respond_to?(:indexing_relationships)
162
+ configs[:membership] = membership_relationships if respond_to?(:membership_relationships)
163
+
164
+ configs
165
+ end
166
+
167
+ # Validate relationship configurations
168
+ def validate_relationships!
169
+ errors = []
170
+
171
+ # Check for method name collisions
172
+ method_names = []
173
+
174
+ if respond_to?(:tracking_relationships)
175
+ tracking_relationships.each do |config|
176
+ context_name = config[:context_class_name].downcase
177
+ collection_name = config[:collection_name]
178
+
179
+ method_names << "in_#{context_name}_#{collection_name}?"
180
+ method_names << "add_to_#{context_name}_#{collection_name}"
181
+ method_names << "remove_from_#{context_name}_#{collection_name}"
182
+ end
183
+ end
184
+
185
+ if respond_to?(:membership_relationships)
186
+ membership_relationships.each do |config|
187
+ owner_name = config[:owner_class_name].downcase
188
+ collection_name = config[:collection_name]
189
+
190
+ method_names << "in_#{owner_name}_#{collection_name}?"
191
+ method_names << "add_to_#{owner_name}_#{collection_name}"
192
+ method_names << "remove_from_#{owner_name}_#{collection_name}"
193
+ end
194
+ end
195
+
196
+ # Check for duplicates
197
+ duplicates = method_names.group_by(&:itself).select { |_, v| v.size > 1 }.keys
198
+ errors << "Method name collisions detected: #{duplicates.join(', ')}" if duplicates.any?
199
+
200
+ # Validate identifier field exists
201
+ id_field = identifier
202
+ unless instance_methods.include?(id_field) || method_defined?(id_field)
203
+ errors << "Identifier field '#{id_field}' is not defined"
204
+ end
205
+
206
+ raise RelationshipError, "Relationship validation failed: #{errors.join('; ')}" if errors.any?
207
+
208
+ true
209
+ end
210
+
211
+ # Create a new instance with relationships initialized
212
+ def create_with_relationships(attributes = {})
213
+ instance = new(attributes)
214
+ instance.initialize_relationships
215
+ instance
216
+ end
217
+
218
+ # Class method wrapper for create_temp_key
219
+ def create_temp_key(base_name, ttl = 300)
220
+ timestamp = Time.now.to_i
221
+ random_suffix = SecureRandom.hex(3)
222
+ temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
223
+
224
+ # Set immediate expiry to ensure cleanup even if operation fails
225
+ if respond_to?(:dbclient)
226
+ dbclient.expire(temp_key, ttl)
227
+ else
228
+ Familia.dbclient.expire(temp_key, ttl)
229
+ end
230
+
231
+ temp_key
232
+ end
233
+
234
+ # Include core score encoding methods at class level
235
+ include ScoreEncoding
236
+
237
+ private
238
+
239
+ # Simple constantize method to convert string to constant
240
+ def constantize_class_name(class_name)
241
+ class_name.split('::').reduce(Object) { |mod, name| mod.const_get(name) }
242
+ rescue NameError
243
+ # If the class doesn't exist, return nil
244
+ nil
245
+ end
246
+ end
247
+
248
+ module InstanceMethods
249
+ # Get the identifier value for this instance
250
+ # Uses the existing Horreum identifier infrastructure
251
+ def identifier
252
+ id_field = self.class.identifier_field
253
+ send(id_field) if respond_to?(id_field)
254
+ end
255
+
256
+ # Set the identifier value for this instance
257
+ def identifier=(value)
258
+ id_field = self.class.identifier_field
259
+ send("#{id_field}=", value) if respond_to?("#{id_field}=")
260
+ end
261
+
262
+ # Initialize relationships (called after object creation)
263
+ def initialize_relationships
264
+ # This can be overridden by subclasses to set up initial relationships
265
+ end
266
+
267
+ # Override save to update relationships
268
+ def save(update_expiration: true)
269
+ result = super
270
+
271
+ if result && respond_to?(:update_all_indexes)
272
+ # Update all indexes with current field values
273
+ update_all_indexes
274
+
275
+ # NOTE: Tracking and membership updates are typically done explicitly
276
+ # since we need to know which specific collections this object should be in
277
+ end
278
+
279
+ result
280
+ end
281
+
282
+ # Override destroy to handle cascade operations
283
+ def destroy!
284
+ # Execute cascade operations before destroying the object
285
+ execute_cascade_operations if respond_to?(:execute_cascade_operations)
286
+
287
+ super
288
+ end
289
+
290
+ # Get comprehensive relationship status for this object
291
+ def relationship_status
292
+ status = {
293
+ identifier: identifier,
294
+ tracking_memberships: [],
295
+ membership_collections: [],
296
+ index_memberships: []
297
+ }
298
+
299
+ # Get tracking memberships
300
+ if respond_to?(:tracking_collections_membership)
301
+ status[:tracking_memberships] = tracking_collections_membership
302
+ end
303
+
304
+ # Get membership collections
305
+ status[:membership_collections] = membership_collections if respond_to?(:membership_collections)
306
+
307
+ # Get index memberships
308
+ status[:index_memberships] = indexing_memberships if respond_to?(:indexing_memberships)
309
+
310
+ status
311
+ end
312
+
313
+ # Comprehensive cleanup - remove from all relationships
314
+ def cleanup_all_relationships!
315
+ # Remove from tracking collections
316
+ remove_from_all_tracking_collections if respond_to?(:remove_from_all_tracking_collections)
317
+
318
+ # Remove from membership collections
319
+ remove_from_all_memberships if respond_to?(:remove_from_all_memberships)
320
+
321
+ # Remove from indexes
322
+ remove_from_all_indexes if respond_to?(:remove_from_all_indexes)
323
+ end
324
+
325
+ # Dry run for relationship cleanup (preview what would be affected)
326
+ def cleanup_preview
327
+ preview = {
328
+ tracking_collections: [],
329
+ membership_collections: [],
330
+ index_entries: []
331
+ }
332
+
333
+ if respond_to?(:cascade_dry_run)
334
+ cascade_preview = cascade_dry_run
335
+ preview.merge!(cascade_preview)
336
+ end
337
+
338
+ preview
339
+ end
340
+
341
+ # Validate that this object's relationships are consistent
342
+ def validate_relationships!
343
+ errors = []
344
+
345
+ # Validate identifier exists
346
+ errors << 'Object identifier is nil' unless identifier
347
+
348
+ # Validate tracking memberships
349
+ if respond_to?(:tracking_collections_membership)
350
+ tracking_collections_membership.each do |membership|
351
+ score = membership[:score]
352
+ errors << "Invalid score in tracking membership: #{membership}" if score && !score.is_a?(Numeric)
353
+ end
354
+ end
355
+
356
+ raise RelationshipError, "Relationship validation failed for #{self}: #{errors.join('; ')}" if errors.any?
357
+
358
+ true
359
+ end
360
+
361
+ # Refresh relationship data from Redis (useful after external changes)
362
+ def refresh_relationships!
363
+ # Clear any cached relationship data
364
+ @relationship_status = nil
365
+ @tracking_memberships = nil
366
+ @membership_collections = nil
367
+ @index_memberships = nil
368
+
369
+ # Reload fresh data
370
+ relationship_status
371
+ end
372
+
373
+ # Create a snapshot of current relationship state (for debugging)
374
+ def relationship_snapshot
375
+ {
376
+ timestamp: Time.now,
377
+ identifier: identifier,
378
+ class: self.class.name,
379
+ status: relationship_status,
380
+ redis_keys: find_related_redis_keys
381
+ }
382
+ end
383
+
384
+ # Direct Redis access for instance methods
385
+ def redis
386
+ self.class.dbclient
387
+ end
388
+
389
+ # Instance method wrapper for create_temp_key
390
+ def create_temp_key(base_name, ttl = 300)
391
+ timestamp = Time.now.to_i
392
+ random_suffix = SecureRandom.hex(3)
393
+ temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
394
+
395
+ # Set immediate expiry to ensure cleanup even if operation fails
396
+ redis.expire(temp_key, ttl)
397
+
398
+ temp_key
399
+ end
400
+
401
+ # Instance method wrapper for cleanup_temp_keys
402
+ def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100)
403
+ cursor = 0
404
+
405
+ loop do
406
+ cursor, keys = redis.scan(cursor, match: pattern, count: batch_size)
407
+
408
+ if keys.any?
409
+ # Check TTL and remove keys that should have expired
410
+ keys.each_slice(batch_size) do |key_batch|
411
+ redis.pipelined do |pipeline|
412
+ key_batch.each do |key|
413
+ ttl = redis.ttl(key)
414
+ pipeline.del(key) if ttl == -1 # Key exists but has no TTL
415
+ end
416
+ end
417
+ end
418
+ end
419
+
420
+ break if cursor.zero?
421
+ end
422
+ end
423
+
424
+ private
425
+
426
+ # Find all Redis keys related to this object
427
+ def find_related_redis_keys
428
+ related_keys = []
429
+ id = identifier
430
+ return related_keys unless id
431
+
432
+ # Scan for keys that might contain this object
433
+ patterns = [
434
+ '*:*:*', # General pattern for relationship keys
435
+ "*#{id}*" # Keys containing the identifier
436
+ ]
437
+
438
+ patterns.each do |pattern|
439
+ redis.scan_each(match: pattern, count: 100) do |key|
440
+ # Check if this key actually contains our object
441
+ key_type = redis.type(key)
442
+
443
+ case key_type
444
+ when 'zset'
445
+ related_keys << key if redis.zscore(key, id)
446
+ when 'set'
447
+ related_keys << key if redis.sismember(key, id)
448
+ when 'list'
449
+ related_keys << key if redis.lpos(key, id)
450
+ when 'hash'
451
+ # For hash keys, check if any field values match our identifier
452
+ hash_values = redis.hvals(key)
453
+ related_keys << key if hash_values.include?(id.to_s)
454
+ end
455
+ end
456
+ end
457
+
458
+ related_keys.uniq
459
+ end
460
+ end
461
+
462
+ # Register the feature with Familia
463
+ Familia::Base.add_feature Relationships, :relationships
464
+ end
465
+ end
466
+ end
@@ -54,7 +54,7 @@ module Familia::Features
54
54
  @safe_dump_field_map = {}
55
55
 
56
56
  def self.included(base)
57
- Familia.ld "[#{self}] Enabled in #{base}"
57
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
58
58
  base.extend ClassMethods
59
59
 
60
60
  # Optionally define safe_dump_fields in the class to make
@@ -141,7 +141,7 @@ class RedactedString
141
141
  def inspect = to_s
142
142
  def cleared? = @cleared
143
143
 
144
- # Returns true when it's literally the same object, otherwsie false.
144
+ # Returns true when it's literally the same object, otherwise false.
145
145
  # This prevents timing attacks where an attacker could potentially
146
146
  # infer information about the secret value through comparison timing
147
147
  def ==(other)