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
isValidDataStorechecks 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
StandardErrorfrom a user-supplied store and log it; a raising store degrades tonil(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 —
{"bucketing" => {...}, "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
isValidDataStorecontract — 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
valueunderkey. -
#visitor_key(account_id, project_id, visitor_id) ⇒ String
Build the visitor-data store key — byte-identical to the JS
getStoreKeyformat<code>${accountId}-${projectId}-${visitorId}</code>.
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 <code>${accountId}-${projectId}-${visitorId}</code>. 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 |