Package memory (import path github.com/coder/chat/state/memory) is an
in-process implementation of the chat.State interface from
github.com/coder/chat. The package comment describes it as state "for tests
and local development" [doc.go:memory]. All data lives in Go maps guarded by
one mutex. Nothing survives a process restart, and nothing is shared between
processes. It is part of the root Go module, not a separate module.
State[memory.go:State]: in-process state. Unexported fields:mu(sync.Mutex),subscribed,events,locksmaps,closed(atomic.Bool), andnow(clock func).var _ chat.State = (*State)(nil)[memory.go:State]: compile-time check that*Stateimplementschat.State.func New() *State[memory.go:New]: returns aStatewith empty maps andnowset totime.Now. There are no options.(*State).IsThreadSubscribed(ctx, id chat.ThreadID) (bool, error)[memory.go:State.IsThreadSubscribed](*State).SubscribeThread(ctx, id chat.ThreadID) error[memory.go:State.SubscribeThread](*State).UnsubscribeThread(ctx, id chat.ThreadID) error[memory.go:State.UnsubscribeThread](*State).MarkEvent(ctx, id string, ttl time.Duration) (bool, error)[memory.go:State.MarkEvent]: returnstruethe first time an ID is seen within its TTL.(*State).AcquireLock(ctx, key string, ttl time.Duration) (chat.LockLease, bool, error)[memory.go:State.AcquireLock](*State).ExtendLock(ctx, lease chat.LockLease, ttl time.Duration) (bool, error)[memory.go:State.ExtendLock](*State).ReleaseLock(ctx, lease chat.LockLease) (bool, error)[memory.go:State.ReleaseLock](*State).Shutdown(context.Context) error[memory.go:State.Shutdown]
- Every method except
ShutdowncallscheckOperation. It returns"memory state: nil state"for a nil receiver,ctx.Err()for a done context, and"memory state: closed"afterShutdown[memory.go:State.checkOperation]. lockOperationAfterCheckrunscheckOperationagain after it takesmu. An operation that waits on the mutex whileShutdownruns fails with"memory state: closed"and does not mutate state [memory.go:State.lockOperationAfterCheck].SubscribeThreadandUnsubscribeThreadreject an empty ID with"memory state: thread id is required"[memory.go:State.SubscribeThread] [memory.go:State.UnsubscribeThread].IsThreadSubscribeddoes not check for an empty ID [memory.go:State.IsThreadSubscribed].- Subscriptions have no expiry [memory.go:State.SubscribeThread].
MarkEventrejects an empty ID with"memory state: event id is required"andttl <= 0with"memory state: dedupe ttl must be positive"[memory.go:State.MarkEvent].MarkEventon an unexpired ID returns(false, nil)and does not refresh its expiry [memory.go:State.MarkEvent].AcquireLockrejects an empty key with"memory state: lock key is required"andttl <= 0with"memory state: lock ttl must be positive". A token error is wrapped as"memory state: create lock token: %w"[memory.go:State.AcquireLock].AcquireLockon a key with an unexpired lock returns(chat.LockLease{}, false, nil). At most one unexpired holder exists per key [memory.go:State.AcquireLock].ExtendLockandReleaseLockreject a lease with an emptyKeyorTokenwith"memory state: lock lease is required".ExtendLockalso rejectsttl <= 0with"memory state: lock ttl must be positive"[memory.go:State.ExtendLock] [memory.go:State.ReleaseLock].ExtendLockandReleaseLockact only when the stored token equalslease.Token. A missing, expired, or mismatched lock returns(false, nil), not an error [memory.go:State.ExtendLock] [memory.go:State.ReleaseLock].- An event or lock is expired when
!expiry.After(now), that isexpiry <= now[memory.go:State.pruneExpiredEvents] [memory.go:State.pruneExpiredLocks]. - All map reads and writes happen while
muis held. Theclosedflag is anatomic.Bool, socheckOperationreads it without the mutex [memory.go:State].
SubscribeThreadsetssubscribed[id] = true.UnsubscribeThreaddeletessubscribed[id][memory.go:State.SubscribeThread] [memory.go:State.UnsubscribeThread].MarkEventscans all events, deletes expired ones, then storesevents[id] = now + ttlfor a new ID [memory.go:State.MarkEvent] [memory.go:State.pruneExpiredEvents].AcquireLock,ExtendLock, andReleaseLockscan all locks and delete expired ones before they act [memory.go:State.pruneExpiredLocks].AcquireLockcreates a token withtokens.New()fromgithub.com/coder/chat/internal/tokens, then storeslocks[key] = lockRecord{token, now + ttl}[memory.go:State.AcquireLock].ExtendLockoverwrites the lock expiry withnow + ttl[memory.go:State.ExtendLock].ReleaseLockdeleteslocks[key][memory.go:State.ReleaseLock].Shutdownstoresclosed = true, then takes and releasesmu. It does not clear subscriptions, events, or locks, and always returnsnil[memory.go:State.Shutdown].- No goroutines, timers, or background cleanup exist. Expired records are
removed only during
MarkEventand the lock methods [memory.go:State].
- Run from the repository root:
go test ./state/memory/...