Class: ConvertSdk::DataStoreManager
- Inherits:
-
Object
- Object
- ConvertSdk::DataStoreManager
- Defined in:
- lib/convert_sdk/data_store_manager.rb
Overview
The single persistence port every manager flows through.
+DataStoreManager+ wraps a duck-typed store (anything responding to +#get(key)+ / +#set(key, value)+) and is the ONLY object that holds a raw store reference — managers (config caching in Story 2.7, sticky bucketing in 2.11, goal dedup in 4.3) never touch a store directly. This gives the SDK one place to enforce three guarantees:
Validation at wiring time. The supplied store is duck-type-checked once, at construction. A non-conforming store is rejected with a logged error and replaced by a Stores::MemoryStore — wiring NEVER raises and NEVER accepts a broken store. (The JS SDK's +isValidDataStore+ checks only that +get+/+set+ are functions, with no arity enforcement; this port matches that contract exactly. Unlike JS — which leaves its data store undefined on invalid input — this Ruby port intentionally falls back to a working MemoryStore, because a Ruby process must never crash on SDK wiring errors.)
Never-crash passthrough. #get / #set rescue +StandardError+ from a user-supplied store and log it; a raising store degrades to +nil+ (get) or a no-op (set) instead of crashing the host.
Atomic visitor-data merge. #merge_visitor_data runs the whole read-modify-write cycle inside a manager-level mutex, so a compound "read current state, decide, write" operation is atomic by construction. Goal dedup (Story 4.3) builds its check-then-mark on this guarantee.
== One store, two tenants
A single store instance backs both config caching and visitor data. Keys are namespaced so the two never collide: config entries use +convert_sdk.config.sdk_key+ (#config_key) and visitor entries use +account_id-project_id-visitor_id+ (#visitor_key, byte-identical to the JS +getStoreKey+ format). The two key shapes are structurally disjoint.
== StoreData
Visitor data is a string-keyed hash of the JS +StoreData+ shape — +=> {..., "segments" => ..., "goals" => ...}+ (plus +"locations"+). Everything stored is string-keyed (wire-world); no symbols appear in stored structures.
== Thread safety
The merge cycle is guarded by +@merge_mutex+. The default Stores::MemoryStore adds its own internal lock, so in-process merges are atomic. For external stores (e.g. +RedisStore+, Story 2.2) the same code path runs, but cross-process merge atomicity is store-dependent and must be provided by the backing store.
Constant Summary collapse
- REQUIRED_STORE_METHODS =
Methods a store must respond to (JS +isValidDataStore+ contract — presence only, no arity check).
%i[get set].freeze
Instance Attribute Summary collapse
-
#store ⇒ Object
readonly
The validated backing store (the supplied store, or a Stores::MemoryStore fallback).
Instance Method Summary collapse
-
#config_key(sdk_key) ⇒ String
Build the config-cache store key.
-
#get(key) ⇒ Object?
Read the value stored under +key+.
-
#initialize(log_manager:, store: nil) ⇒ DataStoreManager
constructor
A new instance of DataStoreManager.
-
#merge_visitor_data(account_id, project_id, visitor_id) {|current| ... } ⇒ Hash
Atomically read-modify-write a visitor's +StoreData+.
-
#set(key, value) ⇒ void
Store +value+ under +key+.
-
#visitor_key(account_id, project_id, visitor_id) ⇒ String
Build the visitor-data store key — byte-identical to the JS +getStoreKey+ format +
${accountId}-${projectId}-${visitorId}+.
Constructor Details
#initialize(log_manager:, store: nil) ⇒ DataStoreManager
Returns a new instance of DataStoreManager.
65 66 67 68 69 70 |
# File 'lib/convert_sdk/data_store_manager.rb', line 65 def initialize(log_manager:, store: nil) @log_manager = log_manager @store = resolve_store(store) # Thread safety: guarded by @merge_mutex. @merge_mutex = Thread::Mutex.new end |
Instance Attribute Details
#store ⇒ Object (readonly)
Returns the validated backing store (the supplied store, or a Stores::MemoryStore fallback).
59 60 61 |
# File 'lib/convert_sdk/data_store_manager.rb', line 59 def store @store end |
Instance Method Details
#config_key(sdk_key) ⇒ String
Build the config-cache store key. SINGLE construction site for config keys.
114 115 116 |
# File 'lib/convert_sdk/data_store_manager.rb', line 114 def config_key(sdk_key) "convert_sdk.config.#{sdk_key}" end |
#get(key) ⇒ Object?
Read the value stored under +key+. A raising store is contained: the error is logged and +nil+ is returned.
77 78 79 80 81 82 |
# File 'lib/convert_sdk/data_store_manager.rb', line 77 def get(key) @store.get(key) rescue StandardError => e @log_manager.error("DataStoreManager#get: store raised (#{e.})") nil end |
#merge_visitor_data(account_id, project_id, visitor_id) {|current| ... } ⇒ Hash
Atomically read-modify-write a visitor's +StoreData+.
The entire cycle — read current data, yield it to the block, deep-merge the block's returned partial, write the result — runs inside +@merge_mutex+, so it is atomic by construction. The block receives the current stored data (or +{}+ for a first write) and returns a +StoreData+ partial to merge in; this lets a caller inspect current state and decide what to write atomically (the substrate for Story 4.3's check-then-mark goal dedup).
Merge semantics match the JS +objectDeepMerge+: nested string-keyed hashes merge recursively, arrays union (deduped, new values first), and scalars from the partial win.
138 139 140 141 142 143 144 145 146 147 |
# File 'lib/convert_sdk/data_store_manager.rb', line 138 def merge_visitor_data(account_id, project_id, visitor_id) key = visitor_key(account_id, project_id, visitor_id) @merge_mutex.synchronize do current = get(key) || {} partial = yield(current) merged = deep_merge(current, partial || {}) set(key, merged) merged end end |
#set(key, value) ⇒ void
This method returns an undefined value.
Store +value+ under +key+. A raising store is contained: the error is logged and the call is a no-op.
90 91 92 93 94 95 96 |
# File 'lib/convert_sdk/data_store_manager.rb', line 90 def set(key, value) @store.set(key, value) nil rescue StandardError => e @log_manager.error("DataStoreManager#set: store raised (#{e.})") nil end |
#visitor_key(account_id, project_id, visitor_id) ⇒ String
Build the visitor-data store key — byte-identical to the JS
+getStoreKey+ format +${accountId}-${projectId}-${visitorId}+. This is
the SINGLE construction site for visitor keys.
106 107 108 |
# File 'lib/convert_sdk/data_store_manager.rb', line 106 def visitor_key(account_id, project_id, visitor_id) "#{account_id}-#{project_id}-#{visitor_id}" end |