familia 2.0.0.pre15 → 2.0.0.pre17
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/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_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 +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,6 +1,6 @@
|
|
1
1
|
# Familia v2.0.0-pre Series Technical Reference
|
2
2
|
|
3
|
-
**Familia** is a Ruby ORM for Redis
|
3
|
+
**Familia** is a Ruby ORM for Valkey/Redis providing object mapping, relationships, and advanced features like encryption, connection pooling, and permission systems. This technical reference covers the major classes, methods, and usage patterns introduced in the v2.0.0-pre series.
|
4
4
|
|
5
5
|
---
|
6
6
|
|
@@ -9,37 +9,37 @@
|
|
9
9
|
### Base Classes
|
10
10
|
|
11
11
|
#### `Familia::Horreum` - Primary ORM Base Class
|
12
|
-
The main base class for Redis-backed objects, similar to ActiveRecord models.
|
12
|
+
The main base class for Valkey/Redis-backed objects, similar to ActiveRecord models.
|
13
13
|
|
14
14
|
```ruby
|
15
15
|
class User < Familia::Horreum
|
16
16
|
# Basic field definitions
|
17
17
|
field :name, :email, :created_at
|
18
18
|
|
19
|
-
# Redis data types as instance variables
|
20
|
-
list :sessions # Redis list
|
21
|
-
set :tags # Redis set
|
22
|
-
sorted_set :scores # Redis sorted set
|
23
|
-
hash :settings # Redis hash
|
19
|
+
# Valkey/Redis data types as instance variables
|
20
|
+
list :sessions # Valkey/Redis list
|
21
|
+
set :tags # Valkey/Redis set
|
22
|
+
sorted_set :scores # Valkey/Redis sorted set
|
23
|
+
hash :settings # Valkey/Redis hash
|
24
24
|
end
|
25
25
|
```
|
26
26
|
|
27
27
|
**Key Methods:**
|
28
|
-
- `save` - Persist object to Redis
|
28
|
+
- `save` - Persist object to Valkey/Redis
|
29
29
|
- `save_if_not_exists` - Conditional persistence (v2.0.0-pre6)
|
30
|
-
- `load` - Load object from Redis
|
31
|
-
- `exists?` - Check if object exists in Redis
|
32
|
-
- `destroy` - Remove object from Redis
|
30
|
+
- `load` - Load object from Valkey/Redis
|
31
|
+
- `exists?` - Check if object exists in Valkey/Redis
|
32
|
+
- `destroy` - Remove object from Valkey/Redis
|
33
33
|
|
34
|
-
#### `Familia::DataType` - Redis Data Type Wrapper
|
35
|
-
Base class for Redis data type implementations.
|
34
|
+
#### `Familia::DataType` - Valkey/Redis Data Type Wrapper
|
35
|
+
Base class for Valkey/Redis data type implementations.
|
36
36
|
|
37
37
|
**Registered Types:**
|
38
|
-
- `String` - Redis strings
|
39
|
-
- `List` - Redis lists
|
40
|
-
- `
|
41
|
-
- `SortedSet` - Redis sorted sets
|
42
|
-
- `HashKey` - Redis hashes
|
38
|
+
- `String` - Valkey/Redis strings
|
39
|
+
- `List` - Valkey/Redis lists
|
40
|
+
- `UnsortedSet` - Valkey/Redis sets
|
41
|
+
- `SortedSet` - Valkey/Redis sorted sets
|
42
|
+
- `HashKey` - Valkey/Redis hashes
|
43
43
|
- `Counter` - Atomic counters
|
44
44
|
- `Lock` - Distributed locks
|
45
45
|
|
@@ -82,7 +82,7 @@ end
|
|
82
82
|
|
83
83
|
session = Session.new(user_id: 123, token: "abc123")
|
84
84
|
session.save
|
85
|
-
session.expire_in(1.hour) #
|
85
|
+
session.expire_in(1.hour) # UnsortedSet custom expiration
|
86
86
|
session.ttl # Check remaining time
|
87
87
|
```
|
88
88
|
|
@@ -152,7 +152,7 @@ class LoginForm < Familia::Horreum
|
|
152
152
|
feature :transient_fields
|
153
153
|
|
154
154
|
field :username # Persistent
|
155
|
-
transient_field :password # Never stored in Redis
|
155
|
+
transient_field :password # Never stored in Valkey/Redis
|
156
156
|
transient_field :csrf_token # Runtime only
|
157
157
|
end
|
158
158
|
|
@@ -188,7 +188,7 @@ class Customer < Familia::Horreum
|
|
188
188
|
indexed_by :email_lookup, field: :email
|
189
189
|
|
190
190
|
# Global tracking with scoring
|
191
|
-
|
191
|
+
participates_in :all_customers, type: :sorted_set, score: :created_at
|
192
192
|
end
|
193
193
|
|
194
194
|
class Domain < Familia::Horreum
|
@@ -201,8 +201,8 @@ class Domain < Familia::Horreum
|
|
201
201
|
member_of Customer, :domains, type: :set
|
202
202
|
|
203
203
|
# Conditional tracking with lambda scoring
|
204
|
-
|
205
|
-
score: ->(domain) { domain.status == 'active' ?
|
204
|
+
participates_in :active_domains, type: :sorted_set,
|
205
|
+
score: ->(domain) { domain.status == 'active' ? Familia.now.to_i : 0 }
|
206
206
|
end
|
207
207
|
```
|
208
208
|
|
@@ -228,7 +228,7 @@ found_id = Customer.email_lookup.get(customer.email) # O(1) lookup
|
|
228
228
|
# Global tracking
|
229
229
|
Customer.add_to_all_customers(customer)
|
230
230
|
recent = Customer.all_customers.range_by_score(
|
231
|
-
(
|
231
|
+
(Familia.now - 24.hours).to_i, '+inf'
|
232
232
|
)
|
233
233
|
```
|
234
234
|
|
@@ -240,7 +240,7 @@ recent = Customer.all_customers.range_by_score(
|
|
240
240
|
Flexible connection pooling with provider-based architecture.
|
241
241
|
|
242
242
|
```ruby
|
243
|
-
# Basic Redis connection
|
243
|
+
# Basic Valkey/Redis connection
|
244
244
|
Familia.configure do |config|
|
245
245
|
config.redis_uri = "redis://localhost:6379/0"
|
246
246
|
end
|
@@ -331,7 +331,7 @@ end
|
|
331
331
|
# Usage example
|
332
332
|
user_id = "user123"
|
333
333
|
doc_id = "doc456"
|
334
|
-
timestamp =
|
334
|
+
timestamp = Familia.now.to_i
|
335
335
|
|
336
336
|
# Grant read + write permissions
|
337
337
|
permissions = Document::READ | Document::WRITE # 3
|
@@ -356,25 +356,25 @@ class ActivityTracker < Familia::Horreum
|
|
356
356
|
feature :relationships
|
357
357
|
|
358
358
|
# Track user activities with timestamps
|
359
|
-
|
359
|
+
participates_in :user_activities, type: :sorted_set,
|
360
360
|
score: ->(activity) { activity.created_at }
|
361
361
|
|
362
362
|
# Track by activity type
|
363
|
-
|
363
|
+
participates_in :activity_by_type, type: :sorted_set,
|
364
364
|
score: ->(activity) { "#{activity.activity_type}:#{activity.created_at}".hash }
|
365
365
|
|
366
366
|
field :user_id, :activity_type, :data, :created_at
|
367
367
|
end
|
368
368
|
|
369
369
|
# Query recent activities (last hour)
|
370
|
-
hour_ago = (
|
370
|
+
hour_ago = (Familia.now - 1.hour).to_i
|
371
371
|
recent_activities = ActivityTracker.user_activities.range_by_score(
|
372
372
|
hour_ago, '+inf', limit: [0, 50]
|
373
373
|
)
|
374
374
|
|
375
375
|
# Get activities by type in time range
|
376
376
|
login_activities = ActivityTracker.activity_by_type.range_by_score(
|
377
|
-
"login:#{hour_ago}".hash, "login:#{
|
377
|
+
"login:#{hour_ago}".hash, "login:#{Familia.now.to_i}".hash
|
378
378
|
)
|
379
379
|
```
|
380
380
|
|
@@ -382,8 +382,8 @@ login_activities = ActivityTracker.activity_by_type.range_by_score(
|
|
382
382
|
|
383
383
|
## Data Type Usage Patterns
|
384
384
|
|
385
|
-
### Advanced Sorted
|
386
|
-
Leverage Redis sorted sets for rankings, time series, and scored data.
|
385
|
+
### Advanced Sorted UnsortedSet Operations
|
386
|
+
Leverage Valkey/Redis sorted sets for rankings, time series, and scored data.
|
387
387
|
|
388
388
|
```ruby
|
389
389
|
class Leaderboard < Familia::Horreum
|
@@ -414,7 +414,7 @@ leaderboard.scores.increment("player1", 100) # Add 100 to existing score
|
|
414
414
|
```
|
415
415
|
|
416
416
|
### List-Based Queues and Feeds
|
417
|
-
Use Redis lists for queues, feeds, and ordered data.
|
417
|
+
Use Valkey/Redis lists for queues, feeds, and ordered data.
|
418
418
|
|
419
419
|
```ruby
|
420
420
|
class TaskQueue < Familia::Horreum
|
@@ -447,7 +447,7 @@ task_data = JSON.parse(next_task) if next_task
|
|
447
447
|
feed = ActivityFeed.new(user_id: "user123")
|
448
448
|
|
449
449
|
# Add activity (keep last 100)
|
450
|
-
feed.activities.unshift("User logged in at #{
|
450
|
+
feed.activities.unshift("User logged in at #{Familia.now}")
|
451
451
|
feed.activities.trim(0, 99) # Keep only last 100 items
|
452
452
|
|
453
453
|
# Get recent activities
|
@@ -492,7 +492,7 @@ beta_enabled = prefs.feature_flags.get("beta_ui") == "true"
|
|
492
492
|
## Error Handling and Validation
|
493
493
|
|
494
494
|
### Connection Error Handling
|
495
|
-
Robust error handling for Redis connection issues.
|
495
|
+
Robust error handling for Valkey/Redis connection issues.
|
496
496
|
|
497
497
|
```ruby
|
498
498
|
class ResilientService < Familia::Horreum
|
@@ -508,7 +508,7 @@ class ResilientService < Familia::Horreum
|
|
508
508
|
sleep(0.1 * (4 - retries)) # Exponential backoff
|
509
509
|
retry
|
510
510
|
else
|
511
|
-
Familia.warn "Redis operation failed after retries: #{e.message}"
|
511
|
+
Familia.warn "Valkey/Redis operation failed after retries: #{e.message}"
|
512
512
|
nil # Return nil or handle gracefully
|
513
513
|
end
|
514
514
|
end
|
@@ -574,7 +574,7 @@ end
|
|
574
574
|
## Performance Optimization
|
575
575
|
|
576
576
|
### Batch Operations
|
577
|
-
Minimize Redis round trips with batch operations.
|
577
|
+
Minimize Valkey/Redis round trips with batch operations.
|
578
578
|
|
579
579
|
```ruby
|
580
580
|
# Instead of multiple individual operations
|
@@ -584,7 +584,7 @@ users = []
|
|
584
584
|
users << user
|
585
585
|
end
|
586
586
|
|
587
|
-
# Use Redis pipelining for batch saves
|
587
|
+
# Use Valkey/Redis pipelining for batch saves
|
588
588
|
User.transaction do |redis|
|
589
589
|
users.each do |user|
|
590
590
|
# All operations batched in transaction
|
@@ -720,9 +720,9 @@ Common patterns for testing Familia applications.
|
|
720
720
|
# test_helper.rb
|
721
721
|
require 'familia'
|
722
722
|
|
723
|
-
# Use separate Redis database for tests
|
723
|
+
# Use separate Valkey/Redis database for tests
|
724
724
|
Familia.configure do |config|
|
725
|
-
config.redis_uri = ENV.fetch('REDIS_TEST_URI', 'redis://localhost:
|
725
|
+
config.redis_uri = ENV.fetch('REDIS_TEST_URI', 'redis://localhost:2525/3')
|
726
726
|
end
|
727
727
|
|
728
728
|
module TestHelpers
|
@@ -739,7 +739,7 @@ module TestHelpers
|
|
739
739
|
User.new({
|
740
740
|
email: "test@example.com",
|
741
741
|
name: "Test User",
|
742
|
-
created_at:
|
742
|
+
created_at: Familia.now.to_i
|
743
743
|
}.merge(attrs))
|
744
744
|
end
|
745
745
|
end
|
@@ -787,7 +787,7 @@ Essential configuration options for Familia v2.0.0-pre.
|
|
787
787
|
|
788
788
|
```ruby
|
789
789
|
Familia.configure do |config|
|
790
|
-
# Basic Redis connection
|
790
|
+
# Basic Valkey/Redis connection
|
791
791
|
config.redis_uri = ENV['REDIS_URL'] || 'redis://localhost:6379/0'
|
792
792
|
|
793
793
|
# Connection provider for pooling (optional)
|
@@ -17,7 +17,7 @@
|
|
17
17
|
|
18
18
|
### Major API Modernization
|
19
19
|
- **Complete API redesign** for clarity and modern Ruby conventions
|
20
|
-
- **Valkey compatibility** alongside traditional Redis support
|
20
|
+
- **Valkey compatibility** alongside traditional Valkey/Redis support
|
21
21
|
- **Ruby 3.4+ modernization** with fiber and thread safety improvements
|
22
22
|
- **Connection pooling foundation** with provider pattern architecture
|
23
23
|
|
@@ -103,7 +103,7 @@ end
|
|
103
103
|
|
104
104
|
### Comprehensive Relationships System
|
105
105
|
- **Three relationship types** optimized for different use cases:
|
106
|
-
- `
|
106
|
+
- `participates_in` - Multi-presence tracking with score encoding
|
107
107
|
- `indexed_by` - O(1) hash-based lookups
|
108
108
|
- `member_of` - Bidirectional membership with collision-free naming
|
109
109
|
|
@@ -116,7 +116,7 @@ class Customer < Familia::Horreum
|
|
116
116
|
|
117
117
|
# Define collections
|
118
118
|
set :domains
|
119
|
-
|
119
|
+
participates_in :active_users, type: :sorted_set
|
120
120
|
end
|
121
121
|
|
122
122
|
class Domain < Familia::Horreum
|
data/docs/archive/README.md
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# Archived Documentation
|
2
2
|
|
3
|
-
This directory contains original documentation files that have been migrated to the new Scriv-based changelog system
|
3
|
+
This directory contains original documentation files that have either been migrated to the new Scriv-based changelog system or incorporated into other documents.
|
4
4
|
|
5
5
|
## Migration Date
|
6
|
-
**
|
6
|
+
**Sept 22, 1015** - Deprecated api-reference.md, to reduce surface area for stale documentation to hide.
|
7
|
+
**Sept 1, 2025** - As part of implementing [Issue #84: Scriv-based changelog system](https://github.com/delano/familia/issues/84)
|
7
8
|
|
8
9
|
## Archived Files
|
9
10
|
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# API Reference
|
2
2
|
|
3
|
+
> [!NOTE]
|
4
|
+
> This document is deprecated. For comprehensive encryption API documentation, see [`docs/reference/api-technical.md`](../reference/api-technical.md) which contains complete implementation details and examples.
|
5
|
+
|
3
6
|
## Class Methods
|
4
7
|
|
5
8
|
### encrypted_field
|
@@ -7,22 +10,22 @@
|
|
7
10
|
Defines an encrypted field on a Familia::Horreum class.
|
8
11
|
|
9
12
|
```ruby
|
10
|
-
encrypted_field(name, **options)
|
13
|
+
encrypted_field(name, aad_fields: [], **options)
|
11
14
|
```
|
12
15
|
|
13
16
|
**Parameters:**
|
14
17
|
- `name` (Symbol) - Field name
|
15
|
-
-
|
16
|
-
|
17
|
-
**Options:**
|
18
|
-
- `:as` - Custom accessor method name
|
19
|
-
- `:on_conflict` - Conflict resolution (always `:raise` for encrypted fields)
|
18
|
+
- `aad_fields` (Array<Symbol>) - Additional fields to include in authentication
|
19
|
+
- `**options` (Hash) - Standard field options
|
20
20
|
|
21
21
|
**Example:**
|
22
22
|
```ruby
|
23
23
|
class User < Familia::Horreum
|
24
|
+
feature :encrypted_fields
|
25
|
+
|
24
26
|
encrypted_field :favorite_snack
|
25
|
-
encrypted_field :api_key
|
27
|
+
encrypted_field :api_key
|
28
|
+
encrypted_field :notes, aad_fields: [:user_id, :email] # With tamper protection
|
26
29
|
end
|
27
30
|
```
|
28
31
|
|
@@ -38,63 +41,60 @@ User.encrypted_fields # => [:favorite_snack, :api_key]
|
|
38
41
|
|
39
42
|
### Field Accessors
|
40
43
|
|
41
|
-
Encrypted fields provide standard accessors:
|
44
|
+
Encrypted fields provide standard accessors that return ConcealedString objects:
|
42
45
|
|
43
46
|
```ruby
|
44
|
-
user.favorite_snack #
|
47
|
+
user.favorite_snack # Returns ConcealedString (safe for logging)
|
48
|
+
user.favorite_snack.reveal # Get actual decrypted value
|
45
49
|
user.favorite_snack = value # Set and encrypt value
|
46
50
|
user.favorite_snack! # Fast write (still encrypted)
|
47
51
|
```
|
48
52
|
|
49
|
-
|
50
|
-
|
53
|
+
**ConcealedString Methods:**
|
51
54
|
```ruby
|
52
|
-
|
53
|
-
|
55
|
+
concealed = user.favorite_snack
|
56
|
+
concealed.to_s # => "[CONCEALED]" (safe for logging)
|
57
|
+
concealed.reveal # => "actual value"
|
58
|
+
concealed.clear! # Clear from memory
|
59
|
+
concealed.cleared? # Check if cleared
|
54
60
|
```
|
55
61
|
|
56
62
|
## Familia::Encryption Module
|
57
63
|
|
58
|
-
###
|
64
|
+
### with_request_cache
|
59
65
|
|
60
|
-
|
66
|
+
Enables key derivation caching for performance optimization:
|
61
67
|
|
62
68
|
```ruby
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
69
|
+
Familia::Encryption.with_request_cache do
|
70
|
+
# Multiple encryption operations reuse derived keys
|
71
|
+
user.secret_one = "value1"
|
72
|
+
user.secret_two = "value2"
|
73
|
+
user.save
|
74
|
+
end
|
68
75
|
```
|
69
76
|
|
70
|
-
###
|
77
|
+
### clear_request_cache!
|
71
78
|
|
72
|
-
|
79
|
+
Manually clears the request-level key cache:
|
73
80
|
|
74
81
|
```ruby
|
75
|
-
Familia::Encryption.
|
76
|
-
context: "User:favorite_snack:user123",
|
77
|
-
additional_data: nil
|
78
|
-
)
|
82
|
+
Familia::Encryption.clear_request_cache!
|
79
83
|
```
|
80
84
|
|
81
|
-
###
|
85
|
+
### encrypt / decrypt
|
82
86
|
|
83
|
-
|
87
|
+
Low-level encryption methods (typically used internally):
|
84
88
|
|
85
89
|
```ruby
|
86
|
-
|
90
|
+
# Encrypt with context for key derivation
|
91
|
+
encrypted = Familia::Encryption.encrypt(plaintext,
|
87
92
|
context: "User:favorite_snack:user123",
|
88
93
|
additional_data: nil
|
89
94
|
)
|
90
|
-
```
|
91
95
|
|
92
|
-
|
93
|
-
|
94
|
-
Decrypts ciphertext (auto-detects algorithm from JSON).
|
95
|
-
|
96
|
-
```ruby
|
97
|
-
Familia::Encryption.decrypt(encrypted_json,
|
96
|
+
# Decrypt (auto-detects algorithm from JSON)
|
97
|
+
decrypted = Familia::Encryption.decrypt(encrypted_json,
|
98
98
|
context: "User:favorite_snack:user123",
|
99
99
|
additional_data: nil
|
100
100
|
)
|
@@ -200,16 +200,19 @@ Familia.configure do |config|
|
|
200
200
|
}
|
201
201
|
config.current_key_version = :v1
|
202
202
|
|
203
|
-
# Multi-version configuration
|
203
|
+
# Multi-version configuration for key rotation
|
204
204
|
config.encryption_keys = {
|
205
205
|
v1_2024: ENV['OLD_KEY'],
|
206
206
|
v2_2025: ENV['NEW_KEY']
|
207
207
|
}
|
208
208
|
config.current_key_version = :v2_2025
|
209
209
|
|
210
|
-
#
|
211
|
-
config.
|
210
|
+
# Optional personalization (XChaCha20-Poly1305 only)
|
211
|
+
config.encryption_personalization = 'MyApp-2024'
|
212
212
|
end
|
213
|
+
|
214
|
+
# Always validate configuration
|
215
|
+
Familia::Encryption.validate_configuration!
|
213
216
|
```
|
214
217
|
|
215
218
|
## Data Types
|
@@ -228,18 +231,30 @@ EncryptedData = Data.define(
|
|
228
231
|
)
|
229
232
|
```
|
230
233
|
|
231
|
-
###
|
234
|
+
### ConcealedString
|
232
235
|
|
233
|
-
String
|
236
|
+
String-like object that conceals sensitive data in output and provides memory safety.
|
234
237
|
|
235
238
|
```ruby
|
236
|
-
class
|
239
|
+
class ConcealedString
|
240
|
+
def reveal
|
241
|
+
# Returns actual decrypted string value
|
242
|
+
end
|
243
|
+
|
237
244
|
def to_s
|
238
|
-
'[
|
245
|
+
'[CONCEALED]'
|
239
246
|
end
|
240
247
|
|
241
248
|
def inspect
|
242
|
-
'[
|
249
|
+
'[CONCEALED]'
|
250
|
+
end
|
251
|
+
|
252
|
+
def clear!
|
253
|
+
# Best-effort memory wiping
|
254
|
+
end
|
255
|
+
|
256
|
+
def cleared?
|
257
|
+
# Returns true if cleared from memory
|
243
258
|
end
|
244
259
|
end
|
245
260
|
```
|
@@ -265,83 +280,54 @@ rescue Familia::EncryptionError => e
|
|
265
280
|
end
|
266
281
|
```
|
267
282
|
|
268
|
-
##
|
269
|
-
|
270
|
-
### Generate Key
|
283
|
+
## Instance Methods
|
271
284
|
|
272
|
-
|
273
|
-
$ familia encryption:generate_key [--bits 256]
|
274
|
-
# Outputs Base64-encoded key
|
275
|
-
```
|
285
|
+
### encrypted_data?
|
276
286
|
|
277
|
-
|
287
|
+
Check if any encrypted fields have values:
|
278
288
|
|
279
|
-
```
|
280
|
-
|
281
|
-
# Verifies field encryption is working
|
289
|
+
```ruby
|
290
|
+
user.encrypted_data? # => true if any encrypted fields have values
|
282
291
|
```
|
283
292
|
|
284
|
-
###
|
293
|
+
### clear_encrypted_fields!
|
294
|
+
|
295
|
+
Clear all encrypted field values from memory:
|
285
296
|
|
286
|
-
```
|
287
|
-
|
288
|
-
# Migrates encrypted fields to new key
|
297
|
+
```ruby
|
298
|
+
user.clear_encrypted_fields! # Clear all ConcealedString values
|
289
299
|
```
|
290
300
|
|
291
|
-
|
301
|
+
### encrypted_fields_cleared?
|
292
302
|
|
293
|
-
|
303
|
+
Check if all encrypted fields have been cleared:
|
294
304
|
|
295
305
|
```ruby
|
296
|
-
|
297
|
-
# Set up test encryption keys
|
298
|
-
def with_test_encryption_keys(&block)
|
299
|
-
|
300
|
-
# Verify field is encrypted in storage
|
301
|
-
def assert_field_encrypted(model, field)
|
302
|
-
|
303
|
-
# Verify decryption works
|
304
|
-
def assert_decryption_works(model, field, expected)
|
305
|
-
end
|
306
|
+
user.encrypted_fields_cleared? # => true if all cleared
|
306
307
|
```
|
307
308
|
|
308
|
-
###
|
309
|
-
|
310
|
-
```ruby
|
311
|
-
RSpec.describe User do
|
312
|
-
include Familia::EncryptionTestHelpers
|
309
|
+
### re_encrypt_fields!
|
313
310
|
|
314
|
-
|
315
|
-
with_test_encryption_keys do
|
316
|
-
user = User.create(favorite_snack: "chocolate chip cookies")
|
311
|
+
Re-encrypt all encrypted fields with current key version:
|
317
312
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
end
|
322
|
-
end
|
313
|
+
```ruby
|
314
|
+
user.re_encrypt_fields! # Uses current_key_version
|
315
|
+
user.save
|
323
316
|
```
|
324
317
|
|
325
|
-
|
318
|
+
### encrypted_fields_status
|
326
319
|
|
327
|
-
|
320
|
+
Get encryption status for debugging:
|
328
321
|
|
329
322
|
```ruby
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
Familia::Encryption.with_key_cache { yield }
|
336
|
-
end
|
337
|
-
end
|
323
|
+
user.encrypted_fields_status
|
324
|
+
# => {
|
325
|
+
# ssn: { encrypted: true, cleared: false },
|
326
|
+
# credit_card: { encrypted: true, cleared: true }
|
327
|
+
# }
|
338
328
|
```
|
339
329
|
|
340
|
-
|
330
|
+
---
|
341
331
|
|
342
|
-
|
343
|
-
|
344
|
-
User.batch_decrypt(:favorite_snack) do |users|
|
345
|
-
users.each { |u| process(u.favorite_snack) }
|
346
|
-
end
|
347
|
-
```
|
332
|
+
> [!IMPORTANT]
|
333
|
+
> For complete implementation details, configuration examples, and advanced usage patterns, see the comprehensive documentation in [`docs/reference/api-technical.md`](../reference/api-technical.md).
|
data/docs/conf.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Configuration file for the Sphinx documentation builder.
|
2
|
+
#
|
3
|
+
# For the full list of built-in configuration values, see the documentation:
|
4
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
5
|
+
|
6
|
+
# -- Project information -----------------------------------------------------
|
7
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
8
|
+
|
9
|
+
project = "Familia"
|
10
|
+
copyright = "2010-ongoing delano, contributors"
|
11
|
+
author = "Delano Mandelbaum"
|
12
|
+
|
13
|
+
# -- General configuration ---------------------------------------------------
|
14
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
15
|
+
|
16
|
+
extensions = [
|
17
|
+
"sphinx.ext.autodoc",
|
18
|
+
"sphinx.ext.viewcode",
|
19
|
+
"sphinx.ext.napoleon",
|
20
|
+
]
|
21
|
+
|
22
|
+
templates_path = ["_templates"]
|
23
|
+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
24
|
+
|
25
|
+
# -- Options for HTML output -------------------------------------------------
|
26
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
27
|
+
|
28
|
+
html_theme = "alabaster"
|
29
|
+
html_static_path = ["_static"]
|
@@ -376,10 +376,10 @@ profile = UserProfile.new(user_id: 'user_123')
|
|
376
376
|
profile.save
|
377
377
|
|
378
378
|
# Regular setter: updates instance variable only
|
379
|
-
profile.last_login_at =
|
379
|
+
profile.last_login_at = Familia.now # Not yet in database
|
380
380
|
|
381
381
|
# Fast method: immediate database write
|
382
|
-
profile.last_login_at!(
|
382
|
+
profile.last_login_at!(Familia.now) # Written to database immediately
|
383
383
|
|
384
384
|
# Reading from database
|
385
385
|
profile.last_login_at # => reads from instance variable
|
@@ -417,8 +417,8 @@ class AuditedFieldType < Familia::FieldType
|
|
417
417
|
field: field_name,
|
418
418
|
old_value: old_value,
|
419
419
|
new_value: value,
|
420
|
-
changed_at:
|
421
|
-
changed_by:
|
420
|
+
changed_at: Familia.now,
|
421
|
+
changed_by: Fiber[:current_user]&.id
|
422
422
|
}
|
423
423
|
|
424
424
|
# Store audit trail
|
@@ -449,7 +449,7 @@ end
|
|
449
449
|
doc = AuditedDocument.new(doc_id: 'doc_123')
|
450
450
|
doc.save
|
451
451
|
|
452
|
-
|
452
|
+
Fiber[:current_user] = OpenStruct.new(id: 'user_456')
|
453
453
|
doc.content!("Initial content") # Audited change
|
454
454
|
doc.status!("draft") # Audited change
|
455
455
|
|
@@ -705,22 +705,22 @@ RSpec.describe TimestampFieldType do
|
|
705
705
|
instance.created_at = "2023-06-15 14:30:00"
|
706
706
|
expect(instance.created_at).to be_a(Time)
|
707
707
|
|
708
|
-
instance.created_at =
|
708
|
+
instance.created_at = Familia.now
|
709
709
|
expect(instance.created_at).to be_a(Time)
|
710
710
|
|
711
|
-
instance.created_at =
|
711
|
+
instance.created_at = Familia.now.to_i
|
712
712
|
expect(instance.created_at).to be_a(Time)
|
713
713
|
end
|
714
714
|
|
715
715
|
it "serializes to integer" do
|
716
|
-
time_value =
|
716
|
+
time_value = Familia.now
|
717
717
|
serialized = field_type.serialize(time_value)
|
718
718
|
expect(serialized).to be_a(Integer)
|
719
719
|
expect(serialized).to eq(time_value.to_i)
|
720
720
|
end
|
721
721
|
|
722
722
|
it "deserializes from integer" do
|
723
|
-
timestamp =
|
723
|
+
timestamp = Familia.now.to_i
|
724
724
|
deserialized = field_type.deserialize(timestamp)
|
725
725
|
expect(deserialized).to be_a(Time)
|
726
726
|
expect(deserialized.to_i).to eq(timestamp)
|