memstore

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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) Append

func (l *EventLog) Append(ctx context.Context, id session.SessionID, ev session.Event) error

Append records ev under id in append order. It never fails (in-memory).

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

ReadAfter yields records strictly after the cursor, optionally following the tail until ctx is done.

func (*EventLog) Reset

func (l *EventLog) Reset(id session.SessionID)

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

func NewLease(ttl time.Duration, now func() time.Time) *Lease

NewLease constructs an in-memory lease with the given TTL. A nil now defaults to time.Now; a non-positive ttl defaults to 30s.

func (*Lease) Acquire

func (l *Lease) Acquire(_ context.Context, id session.SessionID, owner string) (port.Lease, error)

Acquire grants the lease when free/expired/same-owner; else ErrLeaseHeld.

func (*Lease) Release

func (l *Lease) Release(_ context.Context, in port.Lease) error

Release drops the caller's own hold; idempotent.

func (*Lease) Renew

func (l *Lease) Renew(_ context.Context, in port.Lease) (port.Lease, error)

Renew extends a held lease (same token, new expiry); else ErrLeaseHeld.

type Option

type Option func(*Store)

Option configures a Store at construction.

func WithDeleteFailure

func WithDeleteFailure(id session.SessionID, err error) Option

WithDeleteFailure scripts a deterministic Delete failure for the reference adapter. It exists for offline conformance of retryable maintenance paths.

func WithNow

func WithNow(now func() time.Time) Option

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 New

func New(opts ...Option) *Store

New constructs an empty in-memory Store.

func (*Store) Create

func (st *Store) Create(_ context.Context, s *session.Session) error

Create atomically publishes a deep copy of s only when its ID is absent.

func (*Store) Delete

func (st *Store) Delete(_ context.Context, id session.SessionID) error

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

func (st *Store) List(_ context.Context) ([]port.StoredSession, error)

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

func (st *Store) Load(_ context.Context, id session.SessionID) (*session.Session, error)

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.

func (*Store) Save

func (st *Store) Save(_ context.Context, s *session.Session) error

Save persists a deep copy of s under s.ID, overwriting any prior state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL