Documentation
¶
Overview ¶
Package store provides a generic, type-safe keyed collection (Table[V]) that replaces the per-backend map + Init/Reset/Snapshot/Restore boilerplate that every service.InMemoryBackend hand-rolls today (EC2 alone repeats it for ~180 maps). It keeps the existing O(1) partitioned-map design services already use — this package does not introduce a database, a memdb, or a pointer graph; those were evaluated and rejected (14-79x slower, and a referential-integrity burden respectively). It only collapses the boilerplate around a plain map[string]*V into one generic type.
Locking ¶
Table and Index perform NO internal locking. This is deliberate and documented loudly: gopherstack's locking rule is that lock granularity follows INVARIANT granularity, not data-structure granularity (see .claude/memories/pkgs-catalog.md). A service backend's operations are cross-map transactions (create validates foreign keys + writes a resource + updates secondary indexes atomically; Snapshot needs one consistent view), so the single coarse github.com/blackbirdworks/gopherstack/pkgs/lockmetrics.RWMutex already held by the owning backend is the correct lock boundary. A Table is meant to be a field inside that backend, guarded by the backend's existing mutex exactly as the raw map it replaces was. Using a Table (or Index) concurrently without an external lock is a data race, just as it would be with a bare map.
Typical usage ¶
A backend declares one Table per resource collection and registers each one with a Registry exactly once at construction time:
type Queue struct {
Name string
// ...
}
type InMemoryBackend struct {
mu *lockmetrics.RWMutex
queues *store.Table[Queue]
registry *store.Registry
}
func NewInMemoryBackend() *InMemoryBackend {
b := &InMemoryBackend{
mu: lockmetrics.New("sqs"),
registry: store.NewRegistry(),
}
b.queues = store.Register(b.registry, "queues", store.New(func(q *Queue) string { return q.Name }))
return b
}
Every backend operation still takes b.mu itself (store performs no locking), but Reset/Snapshot/Restore across every registered table — regardless of how many different value types they hold — collapse to one Registry call instead of one hand-written block per map.
Index ¶
- type Index
- type Registry
- type Table
- func (t *Table[V]) AddIndex(name string, keyFn func(*V) string) *Index[V]
- func (t *Table[V]) All() []*V
- func (t *Table[V]) Delete(id string) bool
- func (t *Table[V]) Get(id string) (*V, bool)
- func (t *Table[V]) Has(id string) bool
- func (t *Table[V]) Len() int
- func (t *Table[V]) Put(v *V)
- func (t *Table[V]) Range(f func(*V) bool)
- func (t *Table[V]) Reset()
- func (t *Table[V]) Restore(items []*V)
- func (t *Table[V]) Snapshot() []*V
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Index ¶
type Index[V any] struct { // contains filtered or unexported fields }
Index is an opt-in secondary index over a Table's values, grouping them by a caller-supplied key function. It exists for real filter hot paths — e.g. "all SQS messages in queue X" — where a linear scan of Table.All would otherwise be required on every call.
The zero value is not usable; always create via Table.AddIndex.
Like Table, Index performs no locking of its own; it is maintained in-line by Table.Put, Table.Delete, and Table.Restore under whatever external lock the owning backend already holds.
func (*Index[V]) Get ¶
Get returns the values currently grouped under key. Iteration order within the group is insertion order, not any table-defined order. The returned slice is owned by the index — the caller must not mutate it, and must not retain it across a subsequent Table.Put/Table.Delete/Table.Restore call, which may reuse or invalidate it. Copy the slice first if you need to hold onto it past the current lock scope.
func (*Index[V]) Name ¶
Name returns the identifier passed to Table.AddIndex when this index was created.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is a lifecycle registry for a backend's [Table]s. A backend registers each of its tables once at construction time (via Register); afterward the whole backend's lifecycle — reset, snapshot, restore — collapses to one Registry call each, regardless of how many differently typed tables the backend owns. This is what eliminates the per-map Init/Reset/Snapshot/Restore boilerplate described in the package doc.
Registry performs no locking of its own, matching Table and Index: it is meant to be driven from within the backend's own coarse lock.
The zero value is not usable; always create via NewRegistry.
func (*Registry) ResetAll ¶
func (r *Registry) ResetAll()
ResetAll clears every registered table (and every index on it), returning each to the empty state it was in immediately after New.
func (*Registry) RestoreAll ¶
func (r *Registry) RestoreAll(data map[string]json.RawMessage) error
RestoreAll loads every registered table from data, keyed the same way Registry.SnapshotAll produced it. A registered table whose name is absent from data is reset to empty rather than left untouched, so RestoreAll always produces the same backend state a fresh NewRegistry plus SnapshotAll's input would — never a stale mix of old and new state.
func (*Registry) SnapshotAll ¶
func (r *Registry) SnapshotAll() (map[string]json.RawMessage, error)
SnapshotAll captures every registered table's contents as JSON, keyed by the name each was registered under. Each table's own encoding is already deterministic (see Table.Snapshot); marshaling a Go map with string keys additionally sorts those keys, so the overall result — and any JSON built from it — is byte-for-byte stable across calls for the same backend state.
type Table ¶
type Table[V any] struct { // contains filtered or unexported fields }
Table is a generic keyed collection over map[string]*V. The string key is the universal primary-key shape for AWS resources (names, ARNs, IDs, ...).
The zero value is not usable; always create via New.
Table performs no locking of its own — see the package doc for why. Every method below assumes the caller (typically a service backend) already holds whatever lock protects the surrounding invariant.
func New ¶
New creates an empty Table. keyFn extracts the primary key from a value; it is called on every Table.Put and Table.Restore to place the value in the underlying map, so it must be a pure function of the value's identity (e.g. a resource name or ARN) and must not change across the value's lifetime — see Table.AddIndex for the same rule applied to secondary keys.
func Register ¶
Register adds t to r under name and returns t unchanged, so it can be used inline in a field initializer:
b.queues = store.Register(b.registry, "queues", store.New(queueKeyFn))
Register is a free function rather than a Registry method because Go does not allow a generic method (Table's value type V) on a non-generic receiver (Registry); the erasure this requires — storing *Table[V] behind the unexported tableSnapshotter interface — happens entirely inside this function. Every call site remains fully typed: t keeps its concrete *Table[V] type both before and after this call, so no interface{}/any is ever visible to the caller.
name must be unique within r; registering a second table under a name already in use panics, since that always indicates a construction-time bug (e.g. a copy-pasted registration) rather than a runtime condition a backend could reasonably recover from.
func (*Table[V]) AddIndex ¶
AddIndex creates and registers a secondary Index on t, keyed by keyFn. name identifies the index for debugging/error messages (e.g. "queue"); it plays no role in lookup. The index is populated immediately from every value already in the table, then kept consistent by Table.Put, Table.Delete, and Table.Restore for the rest of the table's lifetime.
A Table with no indexes pays no cost for this feature: Table.Put and Table.Delete range over t.indexes, which is a nil slice until AddIndex is first called, so the loop is zero iterations and zero allocations.
keyFn must be a pure function of the value's current field(s); if a value is mutated in place through a pointer obtained from Table.Get such that keyFn's result would change, the index becomes stale for that value until the caller re-inserts it with Table.Put. This is the same caveat every secondary index carries (relational or otherwise) and is not specific to this package.
func (*Table[V]) All ¶
func (t *Table[V]) All() []*V
All returns every value in the table. Iteration order is UNSPECIFIED (Go map order); callers that need a stable order for an AWS list operation must sort the result themselves (e.g. by name/ARN, matching how the real AWS API they are emulating orders its own responses).
func (*Table[V]) Delete ¶
Delete removes the entry stored under id, reporting whether it existed. Any registered Index is kept consistent by removing the deleted value.
func (*Table[V]) Put ¶
func (t *Table[V]) Put(v *V)
Put inserts or replaces the value under the key derived from v via the table's keyFn. A nil v is a documented no-op (nil-safe): there is no well-formed key to derive from a nil value, so Put silently ignores it rather than panicking inside a caller-supplied keyFn.
If an entry already exists at that key, it is first removed from every registered Index so the index reflects only the new value; the new value is then added to each index under its (possibly different) index key.
func (*Table[V]) Range ¶
Range calls f for each value in the table in unspecified order, stopping early if f returns false. Unlike Table.All it performs no allocation, making it the preferred form for a read-only scan over a large table.
func (*Table[V]) Reset ¶
func (t *Table[V]) Reset()
Reset removes every entry from the table and clears every registered Index, returning it to the state it was in immediately after New.
func (*Table[V]) Restore ¶
func (t *Table[V]) Restore(items []*V)
Restore replaces the table's contents with items, rebuilding the primary map and every registered Index from scratch via keyFn. It is the inverse of Table.Snapshot. A nil or empty items leaves the table empty, matching Table.Reset.
func (*Table[V]) Snapshot ¶
func (t *Table[V]) Snapshot() []*V
Snapshot returns every value in the table ordered by key, ascending. The fixed order (rather than Table.All's unspecified map order) is what makes Table.Restore round-trips and JSON-marshaled snapshots byte-for-byte deterministic across runs, which keeps snapshot-based tests and diffs stable.