familia 2.0.0.pre6 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -13
- data/Gemfile +2 -2
- data/Gemfile.lock +2 -2
- data/docs/wiki/Feature-System-Guide.md +36 -5
- data/docs/wiki/Home.md +30 -20
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type.rb +7 -4
- data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +1 -1
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +3 -1
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +3 -0
- data/try/features/encryption_fields/context_isolation_try.rb +1 -0
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/helpers/test_helpers.rb +1 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
- data/try/horreum/relations_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +32 -4
- data/docs/wiki/RelatableObjects-Guide.md +0 -563
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,273 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Basic Relationships Example
|
4
|
+
# This example demonstrates the core features of Familia's relationships system
|
5
|
+
|
6
|
+
require_relative '../lib/familia'
|
7
|
+
|
8
|
+
# Configure Familia for the example
|
9
|
+
Familia.configure do |config|
|
10
|
+
config.redis_uri = ENV.fetch('REDIS_URI', 'redis://localhost:6379/15')
|
11
|
+
end
|
12
|
+
|
13
|
+
puts "=== Familia Relationships Basic Example ==="
|
14
|
+
puts
|
15
|
+
|
16
|
+
# Define our model classes
|
17
|
+
class Customer < Familia::Horreum
|
18
|
+
feature :relationships
|
19
|
+
|
20
|
+
identifier_field :custid
|
21
|
+
field :custid, :name, :email, :plan
|
22
|
+
|
23
|
+
# Define collections for tracking relationships
|
24
|
+
set :domains # Simple set of domain IDs
|
25
|
+
list :projects # Ordered list of project IDs
|
26
|
+
sorted_set :activity # Activity feed with timestamps
|
27
|
+
|
28
|
+
# Create indexes for fast lookups
|
29
|
+
indexed_by :email_lookup, field: :email
|
30
|
+
indexed_by :plan_lookup, field: :plan
|
31
|
+
|
32
|
+
# Track in global collections
|
33
|
+
tracked_in :all_customers, type: :sorted_set, score: :created_at
|
34
|
+
|
35
|
+
def created_at
|
36
|
+
Time.now.to_i
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Domain < Familia::Horreum
|
41
|
+
feature :relationships
|
42
|
+
|
43
|
+
identifier_field :domain_id
|
44
|
+
field :domain_id, :name, :dns_zone, :status
|
45
|
+
|
46
|
+
# Declare membership in customer collections
|
47
|
+
member_of Customer, :domains, type: :set
|
48
|
+
|
49
|
+
# Track domains by status
|
50
|
+
tracked_in :active_domains, type: :sorted_set,
|
51
|
+
score: ->(domain) { domain.status == 'active' ? Time.now.to_i : 0 }
|
52
|
+
end
|
53
|
+
|
54
|
+
class Project < Familia::Horreum
|
55
|
+
feature :relationships
|
56
|
+
|
57
|
+
identifier_field :project_id
|
58
|
+
field :project_id, :name, :priority
|
59
|
+
|
60
|
+
# Member of customer projects list (ordered)
|
61
|
+
member_of Customer, :projects, type: :list
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "=== 1. Basic Object Creation ==="
|
65
|
+
|
66
|
+
# Create some sample objects
|
67
|
+
customer = Customer.new(
|
68
|
+
custid: "cust_#{SecureRandom.hex(4)}",
|
69
|
+
name: "Acme Corporation",
|
70
|
+
email: "admin@acme.com",
|
71
|
+
plan: "enterprise"
|
72
|
+
)
|
73
|
+
|
74
|
+
domain1 = Domain.new(
|
75
|
+
domain_id: "dom_#{SecureRandom.hex(4)}",
|
76
|
+
name: "acme.com",
|
77
|
+
dns_zone: "acme.com.",
|
78
|
+
status: "active"
|
79
|
+
)
|
80
|
+
|
81
|
+
domain2 = Domain.new(
|
82
|
+
domain_id: "dom_#{SecureRandom.hex(4)}",
|
83
|
+
name: "staging.acme.com",
|
84
|
+
dns_zone: "staging.acme.com.",
|
85
|
+
status: "active"
|
86
|
+
)
|
87
|
+
|
88
|
+
project = Project.new(
|
89
|
+
project_id: "proj_#{SecureRandom.hex(4)}",
|
90
|
+
name: "Website Redesign",
|
91
|
+
priority: "high"
|
92
|
+
)
|
93
|
+
|
94
|
+
puts "✓ Created customer: #{customer.name} (#{customer.custid})"
|
95
|
+
puts "✓ Created domains: #{domain1.name}, #{domain2.name}"
|
96
|
+
puts "✓ Created project: #{project.name}"
|
97
|
+
puts
|
98
|
+
|
99
|
+
puts "=== 2. Establishing Relationships ==="
|
100
|
+
|
101
|
+
# Add objects to indexed lookups
|
102
|
+
Customer.add_to_email_lookup(customer)
|
103
|
+
Customer.add_to_plan_lookup(customer)
|
104
|
+
puts "✓ Added customer to email and plan indexes"
|
105
|
+
|
106
|
+
# Add customer to global tracking
|
107
|
+
Customer.add_to_all_customers(customer)
|
108
|
+
puts "✓ Added customer to global customer tracking"
|
109
|
+
|
110
|
+
# Establish member_of relationships (bidirectional)
|
111
|
+
domain1.add_to_customer_domains(customer.custid)
|
112
|
+
customer.domains.add(domain1.identifier)
|
113
|
+
|
114
|
+
domain2.add_to_customer_domains(customer.custid)
|
115
|
+
customer.domains.add(domain2.identifier)
|
116
|
+
|
117
|
+
project.add_to_customer_projects(customer.custid)
|
118
|
+
customer.projects.add(project.identifier)
|
119
|
+
|
120
|
+
puts "✓ Established domain ownership relationships"
|
121
|
+
puts "✓ Established project ownership relationships"
|
122
|
+
|
123
|
+
# Track domains in status collections
|
124
|
+
Domain.add_to_active_domains(domain1)
|
125
|
+
Domain.add_to_active_domains(domain2)
|
126
|
+
puts "✓ Added domains to active tracking"
|
127
|
+
puts
|
128
|
+
|
129
|
+
puts "=== 3. Querying Relationships ==="
|
130
|
+
|
131
|
+
# Test indexed lookups
|
132
|
+
found_customer_id = Customer.email_lookup.get(customer.email)
|
133
|
+
puts "Email lookup for #{customer.email}: #{found_customer_id}"
|
134
|
+
|
135
|
+
enterprise_customers = Customer.plan_lookup.get("enterprise")
|
136
|
+
puts "Enterprise customer found: #{enterprise_customers}"
|
137
|
+
|
138
|
+
# Test membership queries
|
139
|
+
puts "\nDomain membership checks:"
|
140
|
+
puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer.custid)}"
|
141
|
+
puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer.custid)}"
|
142
|
+
|
143
|
+
puts "\nCustomer collections:"
|
144
|
+
puts " Customer has #{customer.domains.size} domains"
|
145
|
+
puts " Customer has #{customer.projects.size} projects"
|
146
|
+
puts " Domain IDs: #{customer.domains.members}"
|
147
|
+
puts " Project IDs: #{customer.projects.members}"
|
148
|
+
|
149
|
+
# Test tracked_in collections
|
150
|
+
all_customers_count = Customer.all_customers.size
|
151
|
+
puts "\nGlobal tracking:"
|
152
|
+
puts " Total customers in system: #{all_customers_count}"
|
153
|
+
|
154
|
+
active_domains_count = Domain.active_domains.size
|
155
|
+
puts " Active domains in system: #{active_domains_count}"
|
156
|
+
puts
|
157
|
+
|
158
|
+
puts "=== 4. Range Queries ==="
|
159
|
+
|
160
|
+
# Get recent customers (last 24 hours)
|
161
|
+
yesterday = (Time.now - 24.hours).to_i
|
162
|
+
recent_customers = Customer.all_customers.range_by_score(yesterday, '+inf')
|
163
|
+
puts "Recent customers (last 24h): #{recent_customers.size}"
|
164
|
+
|
165
|
+
# Get all active domains by score
|
166
|
+
active_domain_scores = Domain.active_domains.range_by_score(1, '+inf', with_scores: true)
|
167
|
+
puts "Active domains with timestamps:"
|
168
|
+
active_domain_scores.each do |domain_id, timestamp|
|
169
|
+
puts " #{domain_id}: active since #{Time.at(timestamp.to_i)}"
|
170
|
+
end
|
171
|
+
puts
|
172
|
+
|
173
|
+
puts "=== 5. Batch Operations ==="
|
174
|
+
|
175
|
+
# Create additional test data
|
176
|
+
additional_customers = []
|
177
|
+
3.times do |i|
|
178
|
+
cust = Customer.new(
|
179
|
+
custid: "batch_cust_#{i}",
|
180
|
+
name: "Customer #{i}",
|
181
|
+
email: "customer#{i}@example.com",
|
182
|
+
plan: i.even? ? "basic" : "premium"
|
183
|
+
)
|
184
|
+
additional_customers << cust
|
185
|
+
|
186
|
+
# Add to indexes and tracking
|
187
|
+
Customer.add_to_email_lookup(cust)
|
188
|
+
Customer.add_to_plan_lookup(cust)
|
189
|
+
Customer.add_to_all_customers(cust)
|
190
|
+
end
|
191
|
+
|
192
|
+
puts "✓ Created and indexed #{additional_customers.size} additional customers"
|
193
|
+
|
194
|
+
# Query by plan
|
195
|
+
basic_customers = Customer.plan_lookup.get("basic")
|
196
|
+
premium_customers = Customer.plan_lookup.get("premium")
|
197
|
+
enterprise_customers = Customer.plan_lookup.get("enterprise")
|
198
|
+
|
199
|
+
puts "\nCustomer distribution by plan:"
|
200
|
+
puts " Basic: #{basic_customers ? 1 : 0} customers"
|
201
|
+
puts " Premium: #{premium_customers ? 1 : 0} customers"
|
202
|
+
puts " Enterprise: #{enterprise_customers ? 1 : 0} customers"
|
203
|
+
puts
|
204
|
+
|
205
|
+
puts "=== 6. Relationship Cleanup ==="
|
206
|
+
|
207
|
+
# Remove relationships
|
208
|
+
puts "Cleaning up relationships..."
|
209
|
+
|
210
|
+
# Remove from member_of relationships
|
211
|
+
domain1.remove_from_customer_domains(customer.custid)
|
212
|
+
customer.domains.remove(domain1.identifier)
|
213
|
+
puts "✓ Removed #{domain1.name} from customer domains"
|
214
|
+
|
215
|
+
# Remove from tracking collections
|
216
|
+
Domain.active_domains.remove(domain2.identifier)
|
217
|
+
puts "✓ Removed #{domain2.name} from active domains"
|
218
|
+
|
219
|
+
# Verify cleanup
|
220
|
+
puts "\nAfter cleanup:"
|
221
|
+
puts " Customer domains: #{customer.domains.size}"
|
222
|
+
puts " Active domains: #{Domain.active_domains.size}"
|
223
|
+
puts
|
224
|
+
|
225
|
+
puts "=== 7. Advanced Usage - Permission Encoding ==="
|
226
|
+
|
227
|
+
# Demonstrate basic score encoding for permissions
|
228
|
+
class DocumentAccess
|
229
|
+
# Permission flags (powers of 2)
|
230
|
+
READ = 1
|
231
|
+
WRITE = 2
|
232
|
+
DELETE = 4
|
233
|
+
ADMIN = 8
|
234
|
+
|
235
|
+
def self.encode_permissions(timestamp, permissions)
|
236
|
+
"#{timestamp}.#{permissions}".to_f
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.decode_permissions(score)
|
240
|
+
parts = score.to_s.split('.')
|
241
|
+
timestamp = parts[0].to_i
|
242
|
+
permissions = parts[1] ? parts[1].to_i : 0
|
243
|
+
[timestamp, permissions]
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Example permission encoding
|
248
|
+
now = Time.now.to_i
|
249
|
+
read_write_permissions = DocumentAccess::READ | DocumentAccess::WRITE # 3
|
250
|
+
admin_permissions = DocumentAccess::ADMIN # 8
|
251
|
+
|
252
|
+
encoded_score = DocumentAccess.encode_permissions(now, read_write_permissions)
|
253
|
+
timestamp, permissions = DocumentAccess.decode_permissions(encoded_score)
|
254
|
+
|
255
|
+
puts "Permission encoding example:"
|
256
|
+
puts " Original: timestamp=#{now}, permissions=#{read_write_permissions}"
|
257
|
+
puts " Encoded score: #{encoded_score}"
|
258
|
+
puts " Decoded: timestamp=#{timestamp}, permissions=#{permissions}"
|
259
|
+
puts " Has read access: #{(permissions & DocumentAccess::READ) != 0}"
|
260
|
+
puts " Has write access: #{(permissions & DocumentAccess::WRITE) != 0}"
|
261
|
+
puts " Has delete access: #{(permissions & DocumentAccess::DELETE) != 0}"
|
262
|
+
puts
|
263
|
+
|
264
|
+
puts "=== Example Complete! ==="
|
265
|
+
puts
|
266
|
+
puts "Key takeaways:"
|
267
|
+
puts "• tracked_in: Use for activity feeds, leaderboards, time-series data"
|
268
|
+
puts "• indexed_by: Use for fast O(1) lookups by field values"
|
269
|
+
puts "• member_of: Use for bidirectional ownership/membership"
|
270
|
+
puts "• Score encoding: Combine timestamps with metadata for rich queries"
|
271
|
+
puts "• Batch operations: Use Redis pipelines for efficiency"
|
272
|
+
puts
|
273
|
+
puts "See docs/wiki/Relationships-Guide.md for comprehensive documentation"
|
data/lib/familia/connection.rb
CHANGED
@@ -171,7 +171,7 @@ module Familia
|
|
171
171
|
result = dbclient.multi do |conn|
|
172
172
|
Fiber[:familia_transaction] = conn
|
173
173
|
begin
|
174
|
-
block_result = yield(conn)
|
174
|
+
block_result = yield(conn)
|
175
175
|
ensure
|
176
176
|
Fiber[:familia_transaction] = nil # cleanup reference
|
177
177
|
end
|
@@ -220,7 +220,7 @@ module Familia
|
|
220
220
|
result = dbclient.pipelined do |conn|
|
221
221
|
Fiber[:familia_pipeline] = conn
|
222
222
|
begin
|
223
|
-
block_result = yield(conn)
|
223
|
+
block_result = yield(conn)
|
224
224
|
ensure
|
225
225
|
Fiber[:familia_pipeline] = nil # cleanup reference
|
226
226
|
end
|
@@ -244,7 +244,7 @@ module Familia
|
|
244
244
|
# conn.expire("custom_key", 3600)
|
245
245
|
# end
|
246
246
|
#
|
247
|
-
def with_connection(&
|
247
|
+
def with_connection(&)
|
248
248
|
yield dbclient
|
249
249
|
end
|
250
250
|
|
data/lib/familia/data_type.rb
CHANGED
@@ -27,6 +27,8 @@ module Familia
|
|
27
27
|
attr_writer :logical_database, :uri
|
28
28
|
end
|
29
29
|
|
30
|
+
# DataType::ClassMethods
|
31
|
+
#
|
30
32
|
module ClassMethods
|
31
33
|
# To be called inside every class that inherits DataType
|
32
34
|
# +methname+ is the term used for the class and instance methods
|
@@ -57,16 +59,16 @@ module Familia
|
|
57
59
|
end
|
58
60
|
|
59
61
|
def valid_keys_only(opts)
|
60
|
-
opts.
|
62
|
+
opts.slice(*DataType.valid_options)
|
61
63
|
end
|
62
64
|
|
63
|
-
def
|
64
|
-
@has_relations ||= false
|
65
|
+
def relations?
|
66
|
+
@has_relations ||= false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
65
67
|
end
|
66
68
|
end
|
67
69
|
extend ClassMethods
|
68
70
|
|
69
|
-
attr_reader :keystring, :
|
71
|
+
attr_reader :keystring, :opts
|
70
72
|
attr_writer :dump_method, :load_method
|
71
73
|
|
72
74
|
# +keystring+: If parent is set, this will be used as the suffix
|
@@ -199,6 +201,7 @@ module Familia
|
|
199
201
|
@opts[:parent]
|
200
202
|
end
|
201
203
|
|
204
|
+
|
202
205
|
def logical_database
|
203
206
|
@opts[:logical_database] || self.class.logical_database
|
204
207
|
end
|
@@ -105,7 +105,7 @@ class ConcealedString
|
|
105
105
|
def belongs_to_context?(expected_record, expected_field_name)
|
106
106
|
return false if @record.nil? || @field_type.nil?
|
107
107
|
|
108
|
-
@record.
|
108
|
+
@record.instance_of?(expected_record.class) &&
|
109
109
|
@record.identifier == expected_record.identifier &&
|
110
110
|
@field_type.instance_variable_get(:@name) == expected_field_name
|
111
111
|
end
|
@@ -169,7 +169,6 @@ class ConcealedString
|
|
169
169
|
'[CONCEALED]'
|
170
170
|
end
|
171
171
|
|
172
|
-
|
173
172
|
# String methods that should return safe concealed values
|
174
173
|
def upcase
|
175
174
|
'[CONCEALED]'
|
@@ -180,7 +179,7 @@ class ConcealedString
|
|
180
179
|
end
|
181
180
|
|
182
181
|
def length
|
183
|
-
11
|
182
|
+
11 # Fixed concealed length to match '[CONCEALED]' length
|
184
183
|
end
|
185
184
|
|
186
185
|
def size
|
@@ -188,19 +187,19 @@ class ConcealedString
|
|
188
187
|
end
|
189
188
|
|
190
189
|
def present?
|
191
|
-
true
|
190
|
+
true # Always return true since encrypted data exists
|
192
191
|
end
|
193
192
|
|
194
193
|
def blank?
|
195
|
-
false
|
194
|
+
false # Never blank if encrypted data exists
|
196
195
|
end
|
197
196
|
|
198
197
|
# String concatenation operations return concealed result
|
199
|
-
def +(
|
198
|
+
def +(_other)
|
200
199
|
'[CONCEALED]'
|
201
200
|
end
|
202
201
|
|
203
|
-
def concat(
|
202
|
+
def concat(_other)
|
204
203
|
'[CONCEALED]'
|
205
204
|
end
|
206
205
|
|
@@ -218,12 +217,12 @@ class ConcealedString
|
|
218
217
|
'[CONCEALED]'
|
219
218
|
end
|
220
219
|
|
221
|
-
def gsub(*
|
220
|
+
def gsub(*)
|
222
221
|
'[CONCEALED]'
|
223
222
|
end
|
224
223
|
|
225
|
-
def include?(
|
226
|
-
false
|
224
|
+
def include?(_substring)
|
225
|
+
false # Never reveal substring presence
|
227
226
|
end
|
228
227
|
|
229
228
|
# Enumerable methods for safety
|
@@ -261,20 +260,29 @@ class ConcealedString
|
|
261
260
|
['[CONCEALED]']
|
262
261
|
end
|
263
262
|
|
264
|
-
def deconstruct_keys(
|
263
|
+
def deconstruct_keys(*)
|
265
264
|
{ concealed: true }
|
266
265
|
end
|
267
266
|
|
268
267
|
# Prevent exposure in JSON serialization
|
269
|
-
def to_json(*
|
268
|
+
def to_json(*)
|
270
269
|
'"[CONCEALED]"'
|
271
270
|
end
|
272
271
|
|
273
272
|
# Prevent exposure in Rails serialization (as_json -> to_json)
|
274
|
-
def as_json(*
|
273
|
+
def as_json(*)
|
275
274
|
'[CONCEALED]'
|
276
275
|
end
|
277
276
|
|
277
|
+
# Finalizer to attempt memory cleanup
|
278
|
+
def self.finalizer_proc(encrypted_data)
|
279
|
+
proc do
|
280
|
+
# Best effort cleanup - Ruby doesn't guarantee memory security
|
281
|
+
# Only clear if not frozen to avoid FrozenError
|
282
|
+
encrypted_data.clear if encrypted_data.respond_to?(:clear) && !encrypted_data.frozen?
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
278
286
|
private
|
279
287
|
|
280
288
|
# Check if a string looks like encrypted JSON data
|
@@ -282,14 +290,4 @@ class ConcealedString
|
|
282
290
|
Familia::Encryption::EncryptedData.valid?(data)
|
283
291
|
end
|
284
292
|
|
285
|
-
# Finalizer to attempt memory cleanup
|
286
|
-
def self.finalizer_proc(encrypted_data)
|
287
|
-
proc do |id|
|
288
|
-
# Best effort cleanup - Ruby doesn't guarantee memory security
|
289
|
-
# Only clear if not frozen to avoid FrozenError
|
290
|
-
if encrypted_data&.respond_to?(:clear) && !encrypted_data.frozen?
|
291
|
-
encrypted_data.clear
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
295
293
|
end
|