refactor(store): sharpen AX examples and comments
All checks were successful
Security Scan / security (push) Successful in 9s
Test / test (push) Successful in 1m39s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 16:13:55 +00:00
parent ead99906de
commit cdc4d5a11d
6 changed files with 27 additions and 57 deletions

View file

@ -31,15 +31,15 @@ func main() {
storeInstance.Set("config", "theme", "dark")
storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour)
value, err := storeInstance.Get("config", "theme")
fmt.Println(value, err)
themeValue, err := storeInstance.Get("config", "theme")
fmt.Println(themeValue, err)
// Watch for mutations
watcher := storeInstance.Watch("config", "*")
defer storeInstance.Unwatch(watcher)
go func() {
for event := range watcher.Events {
fmt.Println(event.Type, event.Key)
fmt.Println(event.Type, event.Group, event.Key, event.Value)
}
}()

View file

@ -103,8 +103,8 @@ Both return `NotFoundError` if the key does not exist or has expired.
```go
storeInstance.Set("miner", "pool", "pool.lthn.io:3333")
storeInstance.Set("miner", "wallet", "iz...")
out, _ := storeInstance.Render(`{"pool":"{{ .pool }}","wallet":"{{ .wallet }}"}`, "miner")
// out: {"pool":"pool.lthn.io:3333","wallet":"iz..."}
renderedTemplate, _ := storeInstance.Render(`{"pool":"{{ .pool }}","wallet":"{{ .wallet }}"}`, "miner")
// renderedTemplate: {"pool":"pool.lthn.io:3333","wallet":"iz..."}
```
Template parse errors and execution errors are both returned as wrapped errors with context (e.g., `store.Render: parse: ...` and `store.Render: exec: ...`).

View file

@ -35,21 +35,21 @@ func main() {
// Basic CRUD
storeInstance.Set("config", "theme", "dark")
value, _ := storeInstance.Get("config", "theme")
core.Println(value) // "dark"
themeValue, _ := storeInstance.Get("config", "theme")
core.Println(themeValue) // "dark"
// TTL expiry -- key disappears after the duration elapses
storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour)
// Fetch all keys in a group
all, _ := storeInstance.GetAll("config")
core.Println(all) // map[theme:dark]
configEntries, _ := storeInstance.GetAll("config")
core.Println(configEntries) // map[theme:dark]
// Template rendering from stored values
storeInstance.Set("mail", "host", "smtp.example.com")
storeInstance.Set("mail", "port", "587")
out, _ := storeInstance.Render(`{{ .host }}:{{ .port }}`, "mail")
core.Println(out) // "smtp.example.com:587"
renderedTemplate, _ := storeInstance.Render(`{{ .host }}:{{ .port }}`, "mail")
core.Println(renderedTemplate) // "smtp.example.com:587"
// Namespace isolation for multi-tenant use
scopedStore, _ := store.NewScoped(storeInstance, "tenant-42")
@ -66,13 +66,13 @@ func main() {
defer storeInstance.Unwatch(watcher)
go func() {
for event := range watcher.Events {
core.Println("event", event.Type, event.Group, event.Key)
core.Println("event", event.Type, event.Group, event.Key, event.Value)
}
}()
// Or register a synchronous callback
unregister := storeInstance.OnChange(func(e store.Event) {
core.Println("changed", e.Key)
unregister := storeInstance.OnChange(func(event store.Event) {
core.Println("changed", event.Group, event.Key, event.Value)
})
defer unregister()
}

View file

@ -7,7 +7,6 @@ import (
"time"
)
// EventType labels the mutation stored in Event.Type.
// Usage example: `if event.Type == store.EventSet { return }`
type EventType int
@ -37,7 +36,6 @@ func (t EventType) String() string {
}
}
// Event records mutation details sent to watchers and callbacks.
// Usage example: `event := store.Event{Type: store.EventSet, Group: "config", Key: "theme", Value: "dark"}`
// Usage example: `event := store.Event{Type: store.EventDeleteGroup, Group: "config"}`
// EventDeleteGroup leaves Key and Value empty.
@ -49,7 +47,6 @@ type Event struct {
Timestamp time.Time
}
// Watcher exposes the buffered event channel for a group/key filter.
// Usage example: `watcher := storeInstance.Watch("config", "*"); defer storeInstance.Unwatch(watcher); for event := range watcher.Events { _ = event }`
type Watcher struct {
// Usage example: `for event := range watcher.Events { _ = event }`
@ -72,7 +69,6 @@ type changeCallbackRegistration struct {
// watcherEventBufferCapacity is the capacity of each watcher's buffered channel.
const watcherEventBufferCapacity = 16
// Watch creates a buffered subscription for a group/key filter.
// Usage example: `watcher := storeInstance.Watch("config", "*")`
// `("*", "*")` matches every mutation and the watcher buffer holds 16 events.
func (storeInstance *Store) Watch(group, key string) *Watcher {
@ -92,7 +88,6 @@ func (storeInstance *Store) Watch(group, key string) *Watcher {
return watcher
}
// Unwatch removes a watcher and closes its channel.
// Usage example: `storeInstance.Unwatch(watcher)`
// Safe to call multiple times; subsequent calls are no-ops.
func (storeInstance *Store) Unwatch(watcher *Watcher) {
@ -112,7 +107,6 @@ func (storeInstance *Store) Unwatch(watcher *Watcher) {
})
}
// OnChange registers a synchronous callback for every mutation.
// Usage example: `unregister := storeInstance.OnChange(func(event store.Event) { hub.SendToChannel("store-events", event) }); defer unregister()`
// Callbacks run synchronously in the writer goroutine, so keep heavy work out of the handler.
func (storeInstance *Store) OnChange(callback func(Event)) func() {

View file

@ -52,20 +52,17 @@ func (scopedStore *ScopedStore) namespacedGroup(group string) string {
return scopedStore.namespace + ":" + group
}
// Returns the namespace string for this scoped store.
// Usage example: `scopedStore, _ := store.NewScoped(storeInstance, "tenant-a"); namespace := scopedStore.Namespace()`
func (scopedStore *ScopedStore) Namespace() string {
return scopedStore.namespace
}
// Get retrieves a value by group and key within the namespace.
// Usage example: `value, err := scopedStore.Get("config", "theme")`
// Usage example: `themeValue, err := scopedStore.Get("config", "theme")`
func (scopedStore *ScopedStore) Get(group, key string) (string, error) {
return scopedStore.storeInstance.Get(scopedStore.namespacedGroup(group), key)
}
// Set stores a value by group and key within the namespace. If quotas are
// configured, they are checked before inserting new keys or groups.
// Quota checks happen before inserting new keys or groups.
// Usage example: `err := scopedStore.Set("config", "theme", "dark")`
func (scopedStore *ScopedStore) Set(group, key, value string) error {
if err := scopedStore.checkQuota(group, key); err != nil {
@ -74,8 +71,7 @@ func (scopedStore *ScopedStore) Set(group, key, value string) error {
return scopedStore.storeInstance.Set(scopedStore.namespacedGroup(group), key, value)
}
// SetWithTTL stores a value with a time-to-live within the namespace. Quota
// checks are applied for new keys and groups.
// Quota checks happen before inserting new keys or groups, even when the value expires later.
// Usage example: `err := scopedStore.SetWithTTL("sessions", "token", "abc", time.Hour)`
func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, ttl time.Duration) error {
if err := scopedStore.checkQuota(group, key); err != nil {
@ -84,41 +80,32 @@ func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, ttl time.Du
return scopedStore.storeInstance.SetWithTTL(scopedStore.namespacedGroup(group), key, value, ttl)
}
// Delete removes a single key from a group within the namespace.
// Usage example: `err := scopedStore.Delete("config", "theme")`
func (scopedStore *ScopedStore) Delete(group, key string) error {
return scopedStore.storeInstance.Delete(scopedStore.namespacedGroup(group), key)
}
// DeleteGroup removes all keys in a group within the namespace.
// Usage example: `err := scopedStore.DeleteGroup("cache")`
func (scopedStore *ScopedStore) DeleteGroup(group string) error {
return scopedStore.storeInstance.DeleteGroup(scopedStore.namespacedGroup(group))
}
// GetAll returns all non-expired key-value pairs in a group within the
// namespace.
// Usage example: `entries, err := scopedStore.GetAll("config")`
// Usage example: `configEntries, err := scopedStore.GetAll("config")`
func (scopedStore *ScopedStore) GetAll(group string) (map[string]string, error) {
return scopedStore.storeInstance.GetAll(scopedStore.namespacedGroup(group))
}
// All returns an iterator over all non-expired key-value pairs in a group
// within the namespace.
// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; _ = entry }`
func (scopedStore *ScopedStore) All(group string) iter.Seq2[KeyValue, error] {
return scopedStore.storeInstance.All(scopedStore.namespacedGroup(group))
}
// Count returns the number of non-expired keys in a group within the namespace.
// Usage example: `count, err := scopedStore.Count("config")`
// Usage example: `keyCount, err := scopedStore.Count("config")`
func (scopedStore *ScopedStore) Count(group string) (int, error) {
return scopedStore.storeInstance.Count(scopedStore.namespacedGroup(group))
}
// Render loads all non-expired key-value pairs from a namespaced group and
// renders a Go template.
// Usage example: `output, err := scopedStore.Render("Hello {{ .name }}", "user")`
// Usage example: `renderedTemplate, err := scopedStore.Render("Hello {{ .name }}", "user")`
func (scopedStore *ScopedStore) Render(templateSource, group string) (string, error) {
return scopedStore.storeInstance.Render(templateSource, scopedStore.namespacedGroup(group))
}

View file

@ -85,7 +85,7 @@ func (storeInstance *Store) Close() error {
}
// Expired keys are lazily removed and treated as not found.
// Usage example: `value, err := storeInstance.Get("config", "theme")`
// Usage example: `themeValue, err := storeInstance.Get("config", "theme")`
func (storeInstance *Store) Get(group, key string) (string, error) {
var value string
var expiresAt sql.NullInt64
@ -106,7 +106,6 @@ func (storeInstance *Store) Get(group, key string) (string, error) {
return value, nil
}
// Overwrites any existing row and clears its expiry.
// Usage example: `err := storeInstance.Set("config", "theme", "dark")`
func (storeInstance *Store) Set(group, key, value string) error {
_, err := storeInstance.database.Exec(
@ -137,7 +136,6 @@ func (storeInstance *Store) SetWithTTL(group, key, value string, ttl time.Durati
return nil
}
// Removes a single key from a group.
// Usage example: `err := storeInstance.Delete("config", "theme")`
func (storeInstance *Store) Delete(group, key string) error {
_, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ?", group, key)
@ -148,8 +146,7 @@ func (storeInstance *Store) Delete(group, key string) error {
return nil
}
// Counts live keys only.
// Usage example: `count, err := storeInstance.Count("config")`
// Usage example: `keyCount, err := storeInstance.Count("config")`
func (storeInstance *Store) Count(group string) (int, error) {
var count int
err := storeInstance.database.QueryRow(
@ -162,7 +159,6 @@ func (storeInstance *Store) Count(group string) (int, error) {
return count, nil
}
// Removes all keys from a group.
// Usage example: `err := storeInstance.DeleteGroup("cache")`
func (storeInstance *Store) DeleteGroup(group string) error {
_, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ?", group)
@ -173,14 +169,12 @@ func (storeInstance *Store) DeleteGroup(group string) error {
return nil
}
// KeyValue represents a key-value pair.
// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; _ = entry }`
type KeyValue struct {
Key, Value string
}
// Returns live key-value pairs only.
// Usage example: `entries, err := storeInstance.GetAll("config")`
// Usage example: `configEntries, err := storeInstance.GetAll("config")`
func (storeInstance *Store) GetAll(group string) (map[string]string, error) {
entriesByKey := make(map[string]string)
for entry, err := range storeInstance.All(group) {
@ -192,7 +186,6 @@ func (storeInstance *Store) GetAll(group string) (map[string]string, error) {
return entriesByKey, nil
}
// Returns live key-value pairs only.
// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; _ = entry }`
func (storeInstance *Store) All(group string) iter.Seq2[KeyValue, error] {
return func(yield func(KeyValue, error) bool) {
@ -244,8 +237,7 @@ func (storeInstance *Store) GetFields(group, key string) (iter.Seq[string], erro
return fieldsSeq(value), nil
}
// Renders a Go template from live key-value pairs.
// Usage example: `out, err := storeInstance.Render("Hello {{ .name }}", "user")`
// Usage example: `renderedTemplate, err := storeInstance.Render("Hello {{ .name }}", "user")`
func (storeInstance *Store) Render(templateSource, group string) (string, error) {
templateData := make(map[string]string)
for entry, err := range storeInstance.All(group) {
@ -266,8 +258,7 @@ func (storeInstance *Store) Render(templateSource, group string) (string, error)
return builder.String(), nil
}
// Counts live keys across groups with the given prefix.
// Usage example: `count, err := storeInstance.CountAll("tenant-a:")`
// Usage example: `tenantKeyCount, err := storeInstance.CountAll("tenant-a:")`
func (storeInstance *Store) CountAll(groupPrefix string) (int, error) {
var count int
var err error
@ -288,8 +279,7 @@ func (storeInstance *Store) CountAll(groupPrefix string) (int, error) {
return count, nil
}
// Returns distinct live group names with the given prefix.
// Usage example: `groupNames, err := storeInstance.Groups("tenant-a:")`
// Usage example: `tenantGroupNames, err := storeInstance.Groups("tenant-a:")`
func (storeInstance *Store) Groups(groupPrefix string) ([]string, error) {
var groupNames []string
for groupName, err := range storeInstance.GroupsSeq(groupPrefix) {
@ -301,8 +291,7 @@ func (storeInstance *Store) Groups(groupPrefix string) ([]string, error) {
return groupNames, nil
}
// Returns distinct live group names with the given prefix.
// Usage example: `for groupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; _ = groupName }`
// Usage example: `for tenantGroupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; _ = tenantGroupName }`
func (storeInstance *Store) GroupsSeq(groupPrefix string) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
var rows *sql.Rows