Class: LittleGhost::SessionStore

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/session_store.rb

Overview

SessionStore connects LittleGhost conversations to application persistence. Subclass it to keep sessions in a database, remote service, or other durable store.

class DatabaseSessionStore < LittleGhost::SessionStore
def load(id, actor_id: nil)
  Conversation.find_by(external_id: id, actor_id:)&.snapshot
end

def append(id, messages:, state:, metadata:, expected_count:, actor_id: nil)
  Conversation.append!(
    id, messages:, state:, metadata:, expected_count:, actor_id:
  )
end

def replace(id, messages:, state:, metadata:, actor_id: nil)
  Conversation.replace!(id, messages:, state:, metadata:, actor_id:)
end
end

A snapshot contains :messages, :state, and :metadata. State and metadata cross this boundary as deeply string-keyed JSON mappings. Sessions expose the same data through DataMap, which accepts String or Symbol keys. Implementations provide #load, #append, and #replace; #append must check expected_count atomically so two writers cannot silently lose a turn.

Actor identity always comes from the caller. A store must not infer it from ambient process state.

Instance Method Summary collapse

Constructor Details

#initializeSessionStore

Prepares the per-session synchronization used by #synchronize.



34
35
36
37
# File 'lib/little_ghost/session_store.rb', line 34

def initialize
  @session_locks = {}
  @session_locks_mutex = Mutex.new
end

Instance Method Details

#append(_id, messages:, state:, metadata:, expected_count:, actor_id: nil) ⇒ Object

Atomically appends sanitized messages and stores canonical JSON state and metadata. Implementations raise ProtocolError if the persisted message count differs from expected_count.



48
49
50
# File 'lib/little_ghost/session_store.rb', line 48

def append(_id, messages:, state:, metadata:, expected_count:, actor_id: nil)
  raise AbstractMethodError, "#{self.class} must implement #append"
end

#load(_id, actor_id: nil) ⇒ Object

Finds the snapshot for id, or returns nil when it does not exist.



40
41
42
# File 'lib/little_ghost/session_store.rb', line 40

def load(_id, actor_id: nil)
  raise AbstractMethodError, "#{self.class} must implement #load"
end

#project_conversation(_id, messages:, metadata:, actor_id: nil) ⇒ Object

Stores may expose a clean conversational view without changing the stored session transcript. The default implementation is a no-op.



60
61
62
# File 'lib/little_ghost/session_store.rb', line 60

def project_conversation(_id, messages:, metadata:, actor_id: nil)
  nil
end

#replace(_id, messages:, state:, metadata:, actor_id: nil) ⇒ Object

Replaces the complete snapshot for id atomically with canonical JSON state and metadata.



54
55
56
# File 'lib/little_ghost/session_store.rb', line 54

def replace(_id, messages:, state:, metadata:, actor_id: nil)
  raise AbstractMethodError, "#{self.class} must implement #replace"
end

#synchronize(id, actor_id: nil) ⇒ Object

Serializes work for one actor/session key within this store instance.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/little_ghost/session_store.rb', line 71

def synchronize(id, actor_id: nil)
  key = [actor_id&.to_s, id.to_s].freeze
  entry = @session_locks_mutex.synchronize do
    current = (@session_locks[key] ||= [Mutex.new, 0])
    current[1] += 1
    current
  end
  entry.first.synchronize { yield }
ensure
  if entry
    @session_locks_mutex.synchronize do
      entry[1] -= 1
      @session_locks.delete(key) if entry[1].zero?
    end
  end
end

#with_operation_context(_operation_id) ⇒ Object

Wraps a store operation with an optional telemetry parent operation. Custom stores may override this while preserving the block's return value.



66
67
68
# File 'lib/little_ghost/session_store.rb', line 66

def with_operation_context(_operation_id)
  yield
end