Documentation
¶
Overview ¶
Package memstore implements an in-memory, concurrency-safe port.SessionStore. It is the default store: fast and offline, suitable for single-process use and tests. Sessions are keyed by session.SessionID in a mutex-guarded map.
Save and Load deep-copy the session through a sessnap snapshot round-trip, so a stored session cannot be mutated through a reference the caller still holds (and vice versa). This keeps the store's copy authoritative and isolated.
Index ¶
- Variables
- type EventLog
- func (l *EventLog) Append(ctx context.Context, id session.SessionID, ev session.Event) error
- func (l *EventLog) AppendEvent(_ context.Context, id session.SessionID, ev session.Event) (port.Cursor, error)
- func (l *EventLog) AppendGap(_ context.Context, id session.SessionID, reason string) (port.Cursor, error)
- func (l *EventLog) Read(_ context.Context, id session.SessionID) iter.Seq2[session.Event, error]
- func (l *EventLog) ReadAfter(ctx context.Context, id session.SessionID, after port.Cursor, ...) iter.Seq2[port.LogRecord, error]
- func (l *EventLog) Reset(id session.SessionID)
- type Lease
- type Option
- type Store
- func (st *Store) Create(_ context.Context, s *session.Session) error
- func (st *Store) Delete(_ context.Context, id session.SessionID) error
- func (st *Store) DeleteSessionIfUnchanged(_ context.Context, expected port.SessionDiscoveryMeta) (bool, error)
- func (st *Store) List(_ context.Context) ([]port.StoredSession, error)
- func (st *Store) Load(_ context.Context, id session.SessionID) (*session.Session, error)
- func (st *Store) PageSessionMetadata(_ context.Context, request port.SessionMetadataPageRequest) (port.SessionMetadataPage, error)
- func (st *Store) ReadSessionLineage(_ context.Context, query port.SessionLineageQuery) (port.SessionLineageResult, error)
- func (st *Store) Save(_ context.Context, s *session.Session) error
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = fmt.Errorf("memstore: session not found: %w", port.ErrSessionNotFound)
ErrNotFound is returned by Load when no session is stored under the given id. It wraps port.ErrSessionNotFound so a consumer that may not import this adapter (e.g. engine/agent) can distinguish not-found from an infra failure via errors.Is.
Functions ¶
This section is empty.
Types ¶
type EventLog ¶
type EventLog struct {
// contains filtered or unexported fields
}
EventLog is an in-memory, concurrency-safe port.EventLog — the durable-event sibling of the in-memory Store. It is the no-store-dir default the composition layer wires when SessionStore is memstore (so the event-log seam is never nil), and the mockable seam offline tests assert against: Append records each event per session id, Read replays them in append order.
It also satisfies port.CursorEventLog. In-memory is the one backend where "durable" is a fiction, so the cursor half is here for CONTRACT coverage rather than deployment: it lets the shared conformance suite pin cursor semantics without a Redis or a temp dir, and lets an offline test exercise a follower. Cross-process follow is by definition out of reach here — a second process shares no memory — which is exactly why the cross-process obligation is proved against Redis and JSONL instead.
It does NOT round-trip through a serialization (the relay already hands it a value Event and the loop never mutates a past event), so the recorded events are stored by value directly.
func NewEventLog ¶
func NewEventLog() *EventLog
NewEventLog constructs an empty in-memory event log.
func (*EventLog) AppendEvent ¶
func (l *EventLog) AppendEvent(_ context.Context, id session.SessionID, ev session.Event) (port.Cursor, error)
AppendEvent records ev and returns the cursor positioned after it.
func (*EventLog) AppendGap ¶
func (l *EventLog) AppendGap(_ context.Context, id session.SessionID, reason string) (port.Cursor, error)
AppendGap records a gap marker and returns the cursor positioned after it.
func (*EventLog) Read ¶
Read yields the EVENTS recorded under id in append order. A miss (no events) yields an empty sequence (absence is data). Gap markers are SKIPPED: this is port.EventLog, whose shipped contract is that it returns events, and widening it to emit a non-event would break every existing consumer — including the event-sourced fold.
The slice is copied under the lock so a concurrent Append cannot race the iteration.
func (*EventLog) ReadAfter ¶
func (l *EventLog) ReadAfter(ctx context.Context, id session.SessionID, after port.Cursor, opts port.ReadOptions) iter.Seq2[port.LogRecord, error]
ReadAfter yields records strictly after the cursor, optionally following the tail until ctx is done.
func (*EventLog) Reset ¶
Reset discards the log recorded under id and mints a NEW generation, so every outstanding cursor for that id expires rather than silently addressing a different record. It is the in-memory analogue of deleting a session's log.
It closes the outgoing log's broadcast channel so a parked follower wakes and observes the expiry immediately instead of blocking until its context ends.
type Lease ¶
type Lease struct {
// contains filtered or unexported fields
}
Lease is an in-memory, concurrency-safe port.SessionLease — the cross-process single-writer sibling of the in-memory Store, the same shape as the EventLog sibling. It exists so the type-assert discovery path (composition asserting a configured SessionStore for port.SessionLease) is exercised offline: a memstore deployment can opt INTO leasing by flag, and this is the lease it gets.
It is NOT auto-wired by Store: composition wires a lease only when an operator selects a backend, so the default no-flag path stays byte-identical. The Store itself deliberately does NOT implement SessionLease — leasing is a separate, flag-selected concern, so this is its own type constructed on demand.
Records expire against an injected clock (default time.Now), so tests advance a fake clock past the TTL to exercise expiry/takeover without real sleeps.
func NewLease ¶
NewLease constructs an in-memory lease with the given TTL. A nil now defaults to time.Now; a non-positive ttl defaults to 30s.
type Option ¶
type Option func(*Store)
Option configures a Store at construction.
func WithDeleteFailure ¶
WithDeleteFailure scripts a deterministic Delete failure for the reference adapter. It exists for offline conformance of retryable maintenance paths.
func WithNow ¶
WithNow injects the clock Save uses to stamp each snapshot's ModifiedAt (List's ordering input). Tests inject a fake for deterministic retention assertions; production keeps the default time.Now. A nil now is ignored.
(A plain func rather than port.Clock keeps the option dependency-free for callers; wrap a port.Clock as clock.Now where one is already in hand.)
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a concurrency-safe in-memory SessionStore.
func (*Store) Delete ¶
Delete removes the session stored under id. It is idempotent: an unknown id is success (port.PrunableStore contract).
func (*Store) DeleteSessionIfUnchanged ¶
func (st *Store) DeleteSessionIfUnchanged(_ context.Context, expected port.SessionDiscoveryMeta) (bool, error)
DeleteSessionIfUnchanged atomically revalidates metadata and deletes the in-memory family while holding the store mutex.
func (*Store) List ¶
List returns every stored session's id and last Save time, in no guaranteed order. It satisfies the optional port.PrunableStore retention seam.
func (*Store) Load ¶
Load returns a freshly reconstructed copy of the session stored under id. It returns ErrNotFound if no such session exists.
func (*Store) PageSessionMetadata ¶
func (st *Store) PageSessionMetadata(_ context.Context, request port.SessionMetadataPageRequest) (port.SessionMetadataPage, error)
PageSessionMetadata returns one owner-filtered keyset page from a consistent in-memory snapshot of the store maps.
func (*Store) ReadSessionLineage ¶
func (st *Store) ReadSessionLineage(_ context.Context, query port.SessionLineageQuery) (port.SessionLineageResult, error)
ReadSessionLineage returns the root and its direct children from the content-free index.