familia 2.0.0.pre17 → 2.0.0.pre19
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/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,281 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/datatype_standalone.rb
|
3
|
+
|
4
|
+
# Demonstration: Familia::StringKey for Session Storage with Atomic Transactions
|
5
|
+
#
|
6
|
+
# This example shows how to use Familia's DataType classes independently
|
7
|
+
# without inheriting from Familia::Horreum. It implements a Rack-compatible
|
8
|
+
# session store using Familia::StringKey for secure, TTL-managed storage.
|
9
|
+
#
|
10
|
+
# Key Familia Features Demonstrated:
|
11
|
+
# - Standalone DataType usage (no parent model required)
|
12
|
+
# - Atomic transactions for multi-operation consistency
|
13
|
+
# - TTL management for automatic expiration
|
14
|
+
# - JSON serialization for complex data structures
|
15
|
+
# - Direct Redis access through DataType objects
|
16
|
+
|
17
|
+
require 'rack/session/abstract/id'
|
18
|
+
require 'securerandom'
|
19
|
+
|
20
|
+
require 'base64'
|
21
|
+
require 'openssl'
|
22
|
+
|
23
|
+
# Load local development version of Familia (not the gem)
|
24
|
+
begin
|
25
|
+
require_relative '../lib/familia'
|
26
|
+
rescue LoadError
|
27
|
+
# Fall back to installed gem
|
28
|
+
require 'familia'
|
29
|
+
end
|
30
|
+
|
31
|
+
# SecureSessionStore - a rack-session compatible session store using Familia::StringKey
|
32
|
+
#
|
33
|
+
# Usage:
|
34
|
+
# ruby examples/datatype_standalone.rb
|
35
|
+
# # Or in your Rack app:
|
36
|
+
# use SecureSessionStore, secret: 'your-secret-key', expire_after: 3600
|
37
|
+
#
|
38
|
+
# @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/abstract/id.rb
|
39
|
+
# @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/encryptor.rb
|
40
|
+
#
|
41
|
+
class SecureSessionStore < Rack::Session::Abstract::PersistedSecure
|
42
|
+
unless defined?(DEFAULT_OPTIONS)
|
43
|
+
DEFAULT_OPTIONS = {
|
44
|
+
key: 'project.session',
|
45
|
+
expire_after: 86_400, # 24 hours default
|
46
|
+
namespace: 'session',
|
47
|
+
sidbits: 256, # Required by Rack::Session::Abstract::Persisted
|
48
|
+
dbclient: nil,
|
49
|
+
}.freeze
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :dbclient
|
53
|
+
|
54
|
+
def initialize(app, options = {})
|
55
|
+
# Require a secret for security
|
56
|
+
raise ArgumentError, 'Secret required for secure sessions' unless options[:secret]
|
57
|
+
|
58
|
+
# Merge options with defaults
|
59
|
+
options = DEFAULT_OPTIONS.merge(options)
|
60
|
+
|
61
|
+
# Configure Familia connection if redis_uri provided
|
62
|
+
@dbclient = options[:dbclient] || Familia.dbclient
|
63
|
+
|
64
|
+
super
|
65
|
+
|
66
|
+
@secret = options[:secret]
|
67
|
+
@expire_after = options[:expire_after]
|
68
|
+
@namespace = options[:namespace] || 'session'
|
69
|
+
|
70
|
+
# Derive different keys for different purposes
|
71
|
+
@hmac_key = derive_key('hmac')
|
72
|
+
@encryption_key = derive_key('encryption')
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Create a StringKey instance for a session ID
|
78
|
+
def get_stringkey(sid)
|
79
|
+
return nil if sid.to_s.empty?
|
80
|
+
|
81
|
+
key = Familia.join(@namespace, sid)
|
82
|
+
Familia::StringKey.new(key,
|
83
|
+
ttl: @expire_after,
|
84
|
+
default: nil)
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete_session(_request, sid, _options)
|
88
|
+
# Extract string ID from SessionId object if needed
|
89
|
+
sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
|
90
|
+
|
91
|
+
get_stringkey(sid_string)&.del
|
92
|
+
|
93
|
+
generate_sid
|
94
|
+
end
|
95
|
+
|
96
|
+
def valid_session_id?(sid)
|
97
|
+
return false if sid.to_s.empty?
|
98
|
+
return false unless sid.match?(/\A[a-f0-9]{64,}\z/)
|
99
|
+
|
100
|
+
# Additional security checks could go here
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid_hmac?(data, hmac)
|
105
|
+
expected = compute_hmac(data)
|
106
|
+
return false unless hmac.is_a?(String) && expected.is_a?(String) && hmac.bytesize == expected.bytesize
|
107
|
+
|
108
|
+
Rack::Utils.secure_compare(expected, hmac)
|
109
|
+
end
|
110
|
+
|
111
|
+
def derive_key(purpose)
|
112
|
+
OpenSSL::HMAC.hexdigest('SHA256', @secret, "session-#{purpose}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def compute_hmac(data)
|
116
|
+
OpenSSL::HMAC.hexdigest('SHA256', @hmac_key, data)
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_session(_request, sid)
|
120
|
+
# Parent class already extracts sid from cookies
|
121
|
+
# sid may be a SessionId object or nil
|
122
|
+
sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
|
123
|
+
|
124
|
+
# Only generate new sid if none provided or invalid
|
125
|
+
return [generate_sid, {}] unless sid_string && valid_session_id?(sid_string)
|
126
|
+
|
127
|
+
begin
|
128
|
+
stringkey = get_stringkey(sid_string)
|
129
|
+
stored_data = stringkey.value if stringkey
|
130
|
+
|
131
|
+
# If no data stored, return empty session
|
132
|
+
return [sid, {}] unless stored_data
|
133
|
+
|
134
|
+
# Verify HMAC before deserializing
|
135
|
+
data, hmac = stored_data.split('--', 2)
|
136
|
+
|
137
|
+
# If no HMAC or invalid format, create new session
|
138
|
+
unless hmac && valid_hmac?(data, hmac)
|
139
|
+
# Session tampered with - create new session
|
140
|
+
return [generate_sid, {}]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Decode and parse the session data
|
144
|
+
session_data = Familia::JsonSerializer.parse(Base64.decode64(data))
|
145
|
+
|
146
|
+
[sid, session_data]
|
147
|
+
rescue Familia::PersistenceError => e
|
148
|
+
# Log error in development/debugging
|
149
|
+
Familia.ld "[Session] Error reading session #{sid_string}: #{e.message}"
|
150
|
+
|
151
|
+
# Return new session on any error
|
152
|
+
[generate_sid, {}]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def write_session(_request, sid, session_data, _options)
|
157
|
+
# Extract string ID from SessionId object if needed
|
158
|
+
sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
|
159
|
+
|
160
|
+
# Serialize and sign the data
|
161
|
+
encoded = Base64.encode64(Familia::JsonSerializer.dump(session_data)).delete("\n")
|
162
|
+
hmac = compute_hmac(encoded)
|
163
|
+
signed_data = "#{encoded}--#{hmac}"
|
164
|
+
|
165
|
+
# Get or create StringKey for this session
|
166
|
+
stringkey = get_stringkey(sid_string)
|
167
|
+
|
168
|
+
# ATOMIC TRANSACTION: Ensures both operations succeed or both fail
|
169
|
+
#
|
170
|
+
# Before DataType transaction support (PR #160), these operations were not atomic:
|
171
|
+
# stringkey.set(signed_data)
|
172
|
+
# stringkey.update_expiration(expiration: @expire_after)
|
173
|
+
#
|
174
|
+
# With transaction support, we guarantee atomicity - critical for session storage
|
175
|
+
# where partial writes could lead to sessions without TTL (memory leaks) or
|
176
|
+
# expired sessions with stale data (security issues).
|
177
|
+
#
|
178
|
+
# RECOMMENDED PATTERN: Use DataType methods inside transaction blocks
|
179
|
+
# The transaction block automatically handles the atomic MULTI/EXEC wrapping.
|
180
|
+
# DataType methods handle key generation and provide clean, expressive syntax.
|
181
|
+
stringkey.transaction do
|
182
|
+
stringkey.set(signed_data)
|
183
|
+
stringkey.update_expiration(expiration: @expire_after) if @expire_after&.positive?
|
184
|
+
end
|
185
|
+
|
186
|
+
# ADVANCED: The block yields the Redis connection for low-level access when needed
|
187
|
+
# This is useful for operations that require direct Redis command access or
|
188
|
+
# when working with multiple DataTypes in a single transaction.
|
189
|
+
#
|
190
|
+
# stringkey.transaction do |conn|
|
191
|
+
# conn.set(stringkey.dbkey, signed_data)
|
192
|
+
# conn.expire(stringkey.dbkey, @expire_after) if @expire_after&.positive?
|
193
|
+
# end
|
194
|
+
|
195
|
+
# Return the original sid (may be SessionId object)
|
196
|
+
sid
|
197
|
+
rescue Familia::PersistenceError => e
|
198
|
+
# Log error in development/debugging
|
199
|
+
Familia.ld "[Session] Error writing session #{sid_string}: #{e.message}"
|
200
|
+
|
201
|
+
# Return false to indicate failure
|
202
|
+
false
|
203
|
+
end
|
204
|
+
|
205
|
+
# Clean up expired sessions (optional, can be called periodically)
|
206
|
+
def cleanup_expired_sessions
|
207
|
+
# This would typically be handled by Redis TTL automatically
|
208
|
+
# but you could implement manual cleanup if needed
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Demo application showing session store in action
|
213
|
+
class DemoApp
|
214
|
+
def initialize
|
215
|
+
@store = SecureSessionStore.new(
|
216
|
+
proc { |_env| [200, {}, ['Demo App']] },
|
217
|
+
secret: 'demo-secret-key-change-in-production',
|
218
|
+
expire_after: 300, # 5 minutes for demo
|
219
|
+
)
|
220
|
+
end
|
221
|
+
|
222
|
+
def call(env)
|
223
|
+
puts "\n=== Familia::StringKey Session Demo ==="
|
224
|
+
|
225
|
+
# Mock Rack environment
|
226
|
+
env['rack.session'] ||= {}
|
227
|
+
env['HTTP_COOKIE'] ||= ''
|
228
|
+
|
229
|
+
# Simulate session operations
|
230
|
+
session_id = SecureRandom.hex(32)
|
231
|
+
session_data = {
|
232
|
+
'user_id' => '12345',
|
233
|
+
'username' => 'demo_user',
|
234
|
+
'login_time' => Time.now.to_i,
|
235
|
+
'preferences' => { 'theme' => 'dark', 'lang' => 'en' },
|
236
|
+
}
|
237
|
+
|
238
|
+
puts 'Writing session data...'
|
239
|
+
result = @store.send(:write_session, nil, session_id, session_data, {})
|
240
|
+
puts " Result: #{result ? 'Success' : 'Failed'}"
|
241
|
+
|
242
|
+
puts "\nReading session data..."
|
243
|
+
found_id, found_data = @store.send(:find_session, nil, session_id)
|
244
|
+
puts " Session ID: #{found_id}"
|
245
|
+
puts " Data: #{found_data}"
|
246
|
+
|
247
|
+
puts "\nDeleting session..."
|
248
|
+
@store.send(:delete_session, nil, session_id, {})
|
249
|
+
|
250
|
+
puts "\nVerifying deletion..."
|
251
|
+
deleted_id, deleted_data = @store.send(:find_session, nil, session_id)
|
252
|
+
puts " Data after deletion: #{deleted_data}"
|
253
|
+
puts " New session ID: #{deleted_id == session_id ? 'Same' : 'Generated'}"
|
254
|
+
|
255
|
+
puts "\n✅ Demo complete!"
|
256
|
+
puts "\nKey Familia Features Used:"
|
257
|
+
puts '• Familia::StringKey for typed Redis storage'
|
258
|
+
puts '• Automatic TTL management'
|
259
|
+
puts '• Direct Redis operations (set, get, del)'
|
260
|
+
puts '• JSON serialization support'
|
261
|
+
puts '• No Horreum inheritance required'
|
262
|
+
|
263
|
+
[200, { 'Content-Type' => 'text/plain' }, ['Familia StringKey Demo - Check console output']]
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Run demo if executed directly
|
268
|
+
if __FILE__ == $0
|
269
|
+
# Ensure Redis is available
|
270
|
+
begin
|
271
|
+
Familia.dbclient.ping
|
272
|
+
rescue Familia::PersistenceError => e
|
273
|
+
puts "❌ Redis connection failed: #{e.message}"
|
274
|
+
puts ' Please ensure Redis is running on localhost:6379'
|
275
|
+
exit 1
|
276
|
+
end
|
277
|
+
|
278
|
+
# Run the demo
|
279
|
+
app = DemoApp.new
|
280
|
+
app.call({})
|
281
|
+
end
|
data/lib/familia/base.rb
CHANGED
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/familia/connection/behavior.rb
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Connection
|
7
|
+
# Shared connection behavior for both Horreum and DataType classes
|
8
|
+
#
|
9
|
+
# This module extracts common connection management functionality that was
|
10
|
+
# previously duplicated between Horreum::Connection and DataType::Connection.
|
11
|
+
# It provides:
|
12
|
+
#
|
13
|
+
# * URI normalization with logical_database support
|
14
|
+
# * Connection creation methods
|
15
|
+
# * Transaction and pipeline execution methods
|
16
|
+
# * Consistent connection API across object types
|
17
|
+
#
|
18
|
+
# Classes including this module must implement:
|
19
|
+
# * `dbclient(uri = nil)` - Connection resolution method
|
20
|
+
# * `build_connection_chain` (private) - Chain of Responsibility setup
|
21
|
+
#
|
22
|
+
# @example Basic usage in a class
|
23
|
+
# class MyDataStore
|
24
|
+
# include Familia::Connection::Behavior
|
25
|
+
#
|
26
|
+
# def dbclient(uri = nil)
|
27
|
+
# @connection_chain ||= build_connection_chain
|
28
|
+
# @connection_chain.handle(uri)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# private
|
32
|
+
#
|
33
|
+
# def build_connection_chain
|
34
|
+
# # ... handler setup ...
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
module Behavior
|
39
|
+
def self.included(base)
|
40
|
+
base.class_eval do
|
41
|
+
attr_writer :dbclient
|
42
|
+
attr_reader :uri
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Normalizes various URI formats to a consistent URI object
|
47
|
+
#
|
48
|
+
# Handles multiple input types and considers the logical_database setting
|
49
|
+
# when uri is nil or Integer. This method is public so connection handlers
|
50
|
+
# can use it for consistent URI processing.
|
51
|
+
#
|
52
|
+
# @param uri [Integer, String, URI, nil] The URI to normalize
|
53
|
+
# @return [URI] Normalized URI object
|
54
|
+
# @raise [ArgumentError] If URI type is invalid
|
55
|
+
#
|
56
|
+
# @example Integer database number
|
57
|
+
# normalize_uri(2) # => URI with db=2 on default server
|
58
|
+
#
|
59
|
+
# @example String URI
|
60
|
+
# normalize_uri('redis://localhost:6379/1')
|
61
|
+
#
|
62
|
+
# @example nil with logical_database
|
63
|
+
# class MyModel
|
64
|
+
# include Familia::Connection::Behavior
|
65
|
+
# attr_accessor :logical_database
|
66
|
+
# end
|
67
|
+
# model = MyModel.new
|
68
|
+
# model.logical_database = 3
|
69
|
+
# model.normalize_uri(nil) # => URI with db=3
|
70
|
+
#
|
71
|
+
def normalize_uri(uri)
|
72
|
+
case uri
|
73
|
+
when Integer
|
74
|
+
new_uri = Familia.uri.dup
|
75
|
+
new_uri.db = uri
|
76
|
+
new_uri
|
77
|
+
when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
|
78
|
+
URI.parse(uri)
|
79
|
+
when URI
|
80
|
+
uri
|
81
|
+
when nil
|
82
|
+
# Use logical_database if available, otherwise fall back to Familia.uri
|
83
|
+
if respond_to?(:logical_database) && logical_database
|
84
|
+
new_uri = Familia.uri.dup
|
85
|
+
new_uri.db = logical_database
|
86
|
+
new_uri
|
87
|
+
else
|
88
|
+
Familia.uri
|
89
|
+
end
|
90
|
+
else
|
91
|
+
raise ArgumentError, "Invalid URI type: #{uri.class.name}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates a new Database connection instance
|
96
|
+
#
|
97
|
+
# This method always creates a fresh connection and does not use caching.
|
98
|
+
# Each call returns a new Redis client instance that you are responsible
|
99
|
+
# for managing and closing when done.
|
100
|
+
#
|
101
|
+
# @param uri [String, URI, Integer, nil] The URI of the Database server
|
102
|
+
# @return [Redis] A new Database client connection
|
103
|
+
#
|
104
|
+
# @example Creating a new connection
|
105
|
+
# client = create_dbclient('redis://localhost:6379/1')
|
106
|
+
# client.ping
|
107
|
+
# client.close
|
108
|
+
#
|
109
|
+
def create_dbclient(uri = nil)
|
110
|
+
parsed_uri = normalize_uri(uri)
|
111
|
+
Familia.create_dbclient(parsed_uri)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Alias for create_dbclient (backward compatibility)
|
115
|
+
def connect(*)
|
116
|
+
create_dbclient(*)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Sets the URI for this object's database connection
|
120
|
+
#
|
121
|
+
# @param uri [String, URI, Integer] The new URI
|
122
|
+
# @return [URI] The normalized URI
|
123
|
+
#
|
124
|
+
def uri=(uri)
|
125
|
+
@uri = normalize_uri(uri)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Alias for uri (backward compatibility)
|
129
|
+
def url
|
130
|
+
uri
|
131
|
+
end
|
132
|
+
|
133
|
+
# Alias for uri= (backward compatibility)
|
134
|
+
def url=(uri)
|
135
|
+
self.uri = uri
|
136
|
+
end
|
137
|
+
|
138
|
+
# Executes a Redis transaction (MULTI/EXEC) using this object's connection context
|
139
|
+
#
|
140
|
+
# Provides atomic execution of multiple Redis commands with automatic connection
|
141
|
+
# management and operation mode enforcement. Uses the object's database and
|
142
|
+
# connection settings. Returns a MultiResult object for consistency.
|
143
|
+
#
|
144
|
+
# @yield [Redis] conn The Redis connection configured for transaction mode
|
145
|
+
# @return [MultiResult] Result object with success status and command results
|
146
|
+
#
|
147
|
+
# @raise [Familia::OperationModeError] When called with incompatible connection handlers
|
148
|
+
#
|
149
|
+
# @example Basic transaction
|
150
|
+
# obj.transaction do |conn|
|
151
|
+
# conn.set('key1', 'value1')
|
152
|
+
# conn.set('key2', 'value2')
|
153
|
+
# conn.get('key1')
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# @example Reentrant behavior
|
157
|
+
# obj.transaction do |conn|
|
158
|
+
# conn.set('outer', 'value')
|
159
|
+
#
|
160
|
+
# # Nested transaction reuses same connection
|
161
|
+
# obj.transaction do |inner_conn|
|
162
|
+
# inner_conn.set('inner', 'value')
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# @note Connection Inheritance:
|
167
|
+
# - Uses object's logical_database setting if configured
|
168
|
+
# - Inherits class-level database settings
|
169
|
+
# - Falls back to instance-level dbclient if set
|
170
|
+
# - Uses global connection chain as final fallback
|
171
|
+
#
|
172
|
+
# @note Transaction Context:
|
173
|
+
# - When called outside global transaction: Creates local MultiResult
|
174
|
+
# - When called inside global transaction: Yields to existing transaction
|
175
|
+
# - Maintains proper Fiber-local state for nested calls
|
176
|
+
#
|
177
|
+
# @see Familia.transaction For global transaction method
|
178
|
+
# @see MultiResult For details on the return value structure
|
179
|
+
#
|
180
|
+
def transaction(&)
|
181
|
+
ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
|
182
|
+
Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Alias for transaction (alternate naming)
|
186
|
+
def multi(&)
|
187
|
+
transaction(&)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Executes Redis commands in a pipeline using this object's connection context
|
191
|
+
#
|
192
|
+
# Batches multiple Redis commands together and sends them in a single network
|
193
|
+
# round-trip for improved performance. Uses the object's database and connection
|
194
|
+
# settings. Returns a MultiResult object for consistency.
|
195
|
+
#
|
196
|
+
# @yield [Redis] conn The Redis connection configured for pipelined mode
|
197
|
+
# @return [MultiResult] Result object with success status and command results
|
198
|
+
#
|
199
|
+
# @raise [Familia::OperationModeError] When called with incompatible connection handlers
|
200
|
+
#
|
201
|
+
# @example Basic pipeline
|
202
|
+
# obj.pipelined do |conn|
|
203
|
+
# conn.set('key1', 'value1')
|
204
|
+
# conn.incr('counter')
|
205
|
+
# conn.get('key1')
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# @example Performance optimization
|
209
|
+
# # Instead of multiple round-trips:
|
210
|
+
# obj.save # Round-trip 1
|
211
|
+
# obj.increment_count # Round-trip 2
|
212
|
+
# obj.update_timestamp # Round-trip 3
|
213
|
+
#
|
214
|
+
# # Use pipeline for single round-trip:
|
215
|
+
# obj.pipelined do |conn|
|
216
|
+
# conn.hmset(obj.dbkey, obj.to_h)
|
217
|
+
# conn.hincrby(obj.dbkey, 'count', 1)
|
218
|
+
# conn.hset(obj.dbkey, 'updated_at', Time.now.to_i)
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# @note Connection Inheritance:
|
222
|
+
# - Uses object's logical_database setting if configured
|
223
|
+
# - Inherits class-level database settings
|
224
|
+
# - Falls back to instance-level dbclient if set
|
225
|
+
# - Uses global connection chain as final fallback
|
226
|
+
#
|
227
|
+
# @note Pipeline Context:
|
228
|
+
# - When called outside global pipeline: Creates local MultiResult
|
229
|
+
# - When called inside global pipeline: Yields to existing pipeline
|
230
|
+
# - Maintains proper Fiber-local state for nested calls
|
231
|
+
#
|
232
|
+
# @note Performance Considerations:
|
233
|
+
# - Best for multiple independent operations
|
234
|
+
# - Reduces network latency by batching commands
|
235
|
+
# - Commands execute independently (some may succeed, others fail)
|
236
|
+
#
|
237
|
+
# @see Familia.pipelined For global pipeline method
|
238
|
+
# @see MultiResult For details on the return value structure
|
239
|
+
# @see #transaction For atomic command execution
|
240
|
+
#
|
241
|
+
def pipelined(&block)
|
242
|
+
ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
|
243
|
+
Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Alias for pipelined (alternate naming)
|
247
|
+
def pipeline(&block)
|
248
|
+
pipelined(&block)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -219,5 +219,100 @@ module Familia
|
|
219
219
|
dbclient
|
220
220
|
end
|
221
221
|
end
|
222
|
+
|
223
|
+
# Handler for delegating connection resolution to parent object
|
224
|
+
#
|
225
|
+
# Used by DataType objects that are attached to a parent (Horreum instance or class).
|
226
|
+
# Delegates the connection resolution to the parent's dbclient method, which allows
|
227
|
+
# DataType objects to inherit connection settings, logical_database, and transaction
|
228
|
+
# context from their parent.
|
229
|
+
#
|
230
|
+
# This preserves the existing architectural pattern where DataType objects owned by
|
231
|
+
# Horreum models use the parent's connection chain. This is the primary behavior
|
232
|
+
# for DataType objects in typical usage.
|
233
|
+
#
|
234
|
+
# @example Instance-level DataType with parent
|
235
|
+
# user = User.new(userid: 'user_123')
|
236
|
+
# user.tags # DataType that delegates to user.dbclient
|
237
|
+
#
|
238
|
+
# @example Class-level DataType with parent
|
239
|
+
# User.global_users # DataType that delegates to User.dbclient
|
240
|
+
#
|
241
|
+
class ParentDelegationHandler < BaseConnectionHandler
|
242
|
+
@allows_transaction = true
|
243
|
+
@allows_pipelined = true
|
244
|
+
|
245
|
+
def initialize(data_type)
|
246
|
+
@data_type = data_type
|
247
|
+
end
|
248
|
+
|
249
|
+
def handle(uri)
|
250
|
+
return nil unless @data_type.parent
|
251
|
+
|
252
|
+
# Delegate to parent's connection chain
|
253
|
+
# Parent can be either a Horreum class or instance
|
254
|
+
parent_connection = @data_type.parent.dbclient(uri)
|
255
|
+
|
256
|
+
if parent_connection
|
257
|
+
Familia.trace :DBCLIENT_PARENT_DELEGATION, @data_type.dbkey,
|
258
|
+
"Using parent connection from #{@data_type.parent.class}"
|
259
|
+
end
|
260
|
+
|
261
|
+
parent_connection
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Handler for standalone DataType objects without a parent
|
266
|
+
#
|
267
|
+
# Provides connection resolution for DataType objects that are created independently
|
268
|
+
# rather than being attached to a Horreum model. Checks for instance-level @dbclient
|
269
|
+
# first, then falls back to creating a connection based on logical_database option
|
270
|
+
# or global Familia connection.
|
271
|
+
#
|
272
|
+
# This enables standalone DataType usage patterns like Rack::Session implementations
|
273
|
+
# where DataType objects need independent connection management and transaction support.
|
274
|
+
#
|
275
|
+
# @example Standalone DataType with custom connection
|
276
|
+
# leaderboard = Familia::SortedSet.new('game:leaderboard')
|
277
|
+
# leaderboard.dbclient = ConnectionPool.new { Redis.new }
|
278
|
+
#
|
279
|
+
# @example Standalone DataType with logical_database option
|
280
|
+
# cache = Familia::HashKey.new('app:cache', logical_database: 2)
|
281
|
+
#
|
282
|
+
class StandaloneConnectionHandler < BaseConnectionHandler
|
283
|
+
@allows_transaction = true
|
284
|
+
@allows_pipelined = true
|
285
|
+
|
286
|
+
def initialize(data_type)
|
287
|
+
@data_type = data_type
|
288
|
+
end
|
289
|
+
|
290
|
+
def handle(uri)
|
291
|
+
# If a specific URI is provided, always use it to get a connection.
|
292
|
+
if uri
|
293
|
+
connection = Familia.dbclient(uri)
|
294
|
+
Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
|
295
|
+
"Created standalone connection for specific URI: #{uri}"
|
296
|
+
return connection
|
297
|
+
end
|
298
|
+
|
299
|
+
# Use instance @dbclient if explicitly set and no URI was passed
|
300
|
+
instance_dbclient = @data_type.instance_variable_get(:@dbclient)
|
301
|
+
if instance_dbclient
|
302
|
+
Familia.trace :DBCLIENT_DATATYPE_INSTANCE, @data_type.dbkey,
|
303
|
+
'Using DataType instance @dbclient'
|
304
|
+
return instance_dbclient
|
305
|
+
end
|
306
|
+
|
307
|
+
# Fall back to creating connection based on opts or global
|
308
|
+
target_uri = @data_type.opts[:logical_database]
|
309
|
+
connection = Familia.dbclient(target_uri)
|
310
|
+
|
311
|
+
Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
|
312
|
+
"Created standalone connection for #{target_uri || 'default'}"
|
313
|
+
|
314
|
+
connection
|
315
|
+
end
|
316
|
+
end
|
222
317
|
end
|
223
318
|
end
|