refactor(store): tighten AX public comments
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
cdc4d5a11d
commit
0bda91f0bd
4 changed files with 13 additions and 41 deletions
8
doc.go
8
doc.go
|
|
@ -1,18 +1,14 @@
|
|||
// Package store provides a SQLite-backed key-value store with group namespaces,
|
||||
// Package store provides SQLite-backed key-value storage with group namespaces,
|
||||
// TTL expiry, quota-enforced scoped views, and reactive change notifications.
|
||||
//
|
||||
// Usage example:
|
||||
//
|
||||
// storeInstance, _ := store.New(":memory:")
|
||||
// defer storeInstance.Close()
|
||||
// storeInstance.Set("config", "theme", "dark")
|
||||
// _ = storeInstance.Set("config", "theme", "dark")
|
||||
// themeValue, _ := storeInstance.Get("config", "theme")
|
||||
// scopedStore, _ := store.NewScoped(storeInstance, "tenant-a")
|
||||
// _ = scopedStore.Set("config", "theme", "dark")
|
||||
// quotaScopedStore, _ := store.NewScopedWithQuota(storeInstance, "tenant-b", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10})
|
||||
// _ = quotaScopedStore.Set("prefs", "locale", "en-GB")
|
||||
//
|
||||
// Use New to open a store, then Set/Get for CRUD operations. Use
|
||||
// NewScoped/NewScopedWithQuota when group names need tenant isolation or
|
||||
// per-namespace quotas.
|
||||
package store
|
||||
|
|
|
|||
11
events.go
11
events.go
|
|
@ -11,13 +11,10 @@ import (
|
|||
type EventType int
|
||||
|
||||
const (
|
||||
// EventSet indicates a key was created or updated.
|
||||
// Usage example: `if event.Type == store.EventSet { return }`
|
||||
EventSet EventType = iota
|
||||
// EventDelete indicates a single key was removed.
|
||||
// Usage example: `if event.Type == store.EventDelete { return }`
|
||||
EventDelete
|
||||
// EventDeleteGroup indicates all keys in a group were removed.
|
||||
// Usage example: `if event.Type == store.EventDeleteGroup { return }`
|
||||
EventDeleteGroup
|
||||
)
|
||||
|
|
@ -38,7 +35,6 @@ func (t EventType) String() string {
|
|||
|
||||
// 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.
|
||||
type Event struct {
|
||||
Type EventType
|
||||
Group string
|
||||
|
|
@ -69,8 +65,7 @@ type changeCallbackRegistration struct {
|
|||
// watcherEventBufferCapacity is the capacity of each watcher's buffered channel.
|
||||
const watcherEventBufferCapacity = 16
|
||||
|
||||
// Usage example: `watcher := storeInstance.Watch("config", "*")`
|
||||
// `("*", "*")` matches every mutation and the watcher buffer holds 16 events.
|
||||
// Usage example: `watcher := storeInstance.Watch("*", "*")`
|
||||
func (storeInstance *Store) Watch(group, key string) *Watcher {
|
||||
eventChannel := make(chan Event, watcherEventBufferCapacity)
|
||||
watcher := &Watcher{
|
||||
|
|
@ -89,7 +84,6 @@ func (storeInstance *Store) Watch(group, key string) *Watcher {
|
|||
}
|
||||
|
||||
// Usage example: `storeInstance.Unwatch(watcher)`
|
||||
// Safe to call multiple times; subsequent calls are no-ops.
|
||||
func (storeInstance *Store) Unwatch(watcher *Watcher) {
|
||||
if watcher == nil {
|
||||
return
|
||||
|
|
@ -107,8 +101,7 @@ func (storeInstance *Store) Unwatch(watcher *Watcher) {
|
|||
})
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Usage example: `events := make(chan store.Event, 1); unregister := storeInstance.OnChange(func(event store.Event) { events <- event }); defer unregister()`
|
||||
func (storeInstance *Store) OnChange(callback func(Event)) func() {
|
||||
registrationID := atomic.AddUint64(&storeInstance.nextRegistrationID, 1)
|
||||
callbackRegistration := changeCallbackRegistration{id: registrationID, callback: callback}
|
||||
|
|
|
|||
15
scope.go
15
scope.go
|
|
@ -11,14 +11,12 @@ import (
|
|||
// validNamespace matches alphanumeric characters and hyphens (non-empty).
|
||||
var validNamespace = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
||||
|
||||
// Zero values mean unlimited.
|
||||
// Usage example: `quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}`
|
||||
type QuotaConfig struct {
|
||||
MaxKeys int // maximum total keys across all groups in the namespace
|
||||
MaxGroups int // maximum distinct groups in the namespace
|
||||
}
|
||||
|
||||
// Group names are prefixed with namespace + ":" before reaching the underlying store.
|
||||
// Usage example: `scopedStore, _ := store.NewScoped(storeInstance, "tenant-a"); _ = scopedStore.Set("config", "theme", "dark")`
|
||||
type ScopedStore struct {
|
||||
storeInstance *Store
|
||||
|
|
@ -26,7 +24,6 @@ type ScopedStore struct {
|
|||
quota QuotaConfig
|
||||
}
|
||||
|
||||
// Namespaces must be non-empty and contain only alphanumeric characters and hyphens.
|
||||
// Usage example: `scopedStore, _ := store.NewScoped(storeInstance, "tenant-a")`
|
||||
func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
|
||||
if !validNamespace.MatchString(namespace) {
|
||||
|
|
@ -36,7 +33,6 @@ func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
|
|||
return scopedStore, nil
|
||||
}
|
||||
|
||||
// Quotas are checked before new keys or groups are created.
|
||||
// Usage example: `scopedStore, _ := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10})`
|
||||
func NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfig) (*ScopedStore, error) {
|
||||
scopedStore, err := NewScoped(storeInstance, namespace)
|
||||
|
|
@ -47,7 +43,6 @@ func NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfi
|
|||
return scopedStore, nil
|
||||
}
|
||||
|
||||
// namespacedGroup returns the group name with the namespace prefix applied.
|
||||
func (scopedStore *ScopedStore) namespacedGroup(group string) string {
|
||||
return scopedStore.namespace + ":" + group
|
||||
}
|
||||
|
|
@ -62,8 +57,7 @@ func (scopedStore *ScopedStore) Get(group, key string) (string, error) {
|
|||
return scopedStore.storeInstance.Get(scopedStore.namespacedGroup(group), key)
|
||||
}
|
||||
|
||||
// Quota checks happen before inserting new keys or groups.
|
||||
// Usage example: `err := scopedStore.Set("config", "theme", "dark")`
|
||||
// Usage example: `_ = scopedStore.Set("config", "theme", "dark")`
|
||||
func (scopedStore *ScopedStore) Set(group, key, value string) error {
|
||||
if err := scopedStore.checkQuota(group, key); err != nil {
|
||||
return err
|
||||
|
|
@ -71,8 +65,7 @@ func (scopedStore *ScopedStore) Set(group, key, value string) error {
|
|||
return scopedStore.storeInstance.Set(scopedStore.namespacedGroup(group), key, value)
|
||||
}
|
||||
|
||||
// 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)`
|
||||
// Usage example: `_ = scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour)`
|
||||
func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, ttl time.Duration) error {
|
||||
if err := scopedStore.checkQuota(group, key); err != nil {
|
||||
return err
|
||||
|
|
@ -80,12 +73,12 @@ func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, ttl time.Du
|
|||
return scopedStore.storeInstance.SetWithTTL(scopedStore.namespacedGroup(group), key, value, ttl)
|
||||
}
|
||||
|
||||
// Usage example: `err := scopedStore.Delete("config", "theme")`
|
||||
// Usage example: `_ = scopedStore.Delete("config", "theme")`
|
||||
func (scopedStore *ScopedStore) Delete(group, key string) error {
|
||||
return scopedStore.storeInstance.Delete(scopedStore.namespacedGroup(group), key)
|
||||
}
|
||||
|
||||
// Usage example: `err := scopedStore.DeleteGroup("cache")`
|
||||
// Usage example: `_ = scopedStore.DeleteGroup("cache")`
|
||||
func (scopedStore *ScopedStore) DeleteGroup(group string) error {
|
||||
return scopedStore.storeInstance.DeleteGroup(scopedStore.namespacedGroup(group))
|
||||
}
|
||||
|
|
|
|||
20
store.go
20
store.go
|
|
@ -13,11 +13,9 @@ import (
|
|||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// NotFoundError is returned when a key does not exist in the store.
|
||||
// Usage example: `if core.Is(err, store.NotFoundError) { return }`
|
||||
var NotFoundError = core.E("store", "not found", nil)
|
||||
|
||||
// QuotaExceededError is returned when a namespace quota limit is reached.
|
||||
// Usage example: `if core.Is(err, store.QuotaExceededError) { return }`
|
||||
var QuotaExceededError = core.E("store", "quota exceeded", nil)
|
||||
|
||||
|
|
@ -29,7 +27,6 @@ const (
|
|||
entryValueColumn = "entry_value"
|
||||
)
|
||||
|
||||
// Store is a group-namespaced key-value store backed by SQLite.
|
||||
// Usage example: `storeInstance, _ := store.New(":memory:"); _ = storeInstance.Set("config", "theme", "dark")`
|
||||
type Store struct {
|
||||
database *sql.DB
|
||||
|
|
@ -45,8 +42,7 @@ type Store struct {
|
|||
nextRegistrationID uint64 // monotonic ID for watchers and callbacks
|
||||
}
|
||||
|
||||
// Use ":memory:" for ephemeral data or a file path for persistence.
|
||||
// Usage example: `storeInstance, _ := store.New(":memory:"); defer storeInstance.Close()`
|
||||
// Usage example: `storeInstance, _ := store.New(":memory:")`
|
||||
func New(databasePath string) (*Store, error) {
|
||||
sqliteDatabase, err := sql.Open("sqlite", databasePath)
|
||||
if err != nil {
|
||||
|
|
@ -76,7 +72,6 @@ func New(databasePath string) (*Store, error) {
|
|||
return storeInstance, nil
|
||||
}
|
||||
|
||||
// Stops the background purge goroutine and closes the database.
|
||||
// Usage example: `storeInstance, _ := store.New(":memory:"); defer storeInstance.Close()`
|
||||
func (storeInstance *Store) Close() error {
|
||||
storeInstance.cancelPurge()
|
||||
|
|
@ -84,7 +79,6 @@ func (storeInstance *Store) Close() error {
|
|||
return storeInstance.database.Close()
|
||||
}
|
||||
|
||||
// Expired keys are lazily removed and treated as not found.
|
||||
// Usage example: `themeValue, err := storeInstance.Get("config", "theme")`
|
||||
func (storeInstance *Store) Get(group, key string) (string, error) {
|
||||
var value string
|
||||
|
|
@ -106,7 +100,7 @@ func (storeInstance *Store) Get(group, key string) (string, error) {
|
|||
return value, nil
|
||||
}
|
||||
|
||||
// Usage example: `err := storeInstance.Set("config", "theme", "dark")`
|
||||
// Usage example: `_ = storeInstance.Set("config", "theme", "dark")`
|
||||
func (storeInstance *Store) Set(group, key, value string) error {
|
||||
_, err := storeInstance.database.Exec(
|
||||
"INSERT INTO "+entriesTableName+" ("+entryGroupColumn+", "+entryKeyColumn+", "+entryValueColumn+", expires_at) VALUES (?, ?, ?, NULL) "+
|
||||
|
|
@ -120,8 +114,7 @@ func (storeInstance *Store) Set(group, key, value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// The key expires after ttl and is removed on the next Get or by purge.
|
||||
// Usage example: `err := storeInstance.SetWithTTL("session", "token", "abc", time.Hour)`
|
||||
// Usage example: `_ = storeInstance.SetWithTTL("session", "token", "abc123", time.Minute)`
|
||||
func (storeInstance *Store) SetWithTTL(group, key, value string, ttl time.Duration) error {
|
||||
expiresAt := time.Now().Add(ttl).UnixMilli()
|
||||
_, err := storeInstance.database.Exec(
|
||||
|
|
@ -136,7 +129,7 @@ func (storeInstance *Store) SetWithTTL(group, key, value string, ttl time.Durati
|
|||
return nil
|
||||
}
|
||||
|
||||
// Usage example: `err := storeInstance.Delete("config", "theme")`
|
||||
// Usage example: `_ = 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)
|
||||
if err != nil {
|
||||
|
|
@ -159,7 +152,7 @@ func (storeInstance *Store) Count(group string) (int, error) {
|
|||
return count, nil
|
||||
}
|
||||
|
||||
// Usage example: `err := storeInstance.DeleteGroup("cache")`
|
||||
// Usage example: `_ = storeInstance.DeleteGroup("cache")`
|
||||
func (storeInstance *Store) DeleteGroup(group string) error {
|
||||
_, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ?", group)
|
||||
if err != nil {
|
||||
|
|
@ -217,7 +210,6 @@ func (storeInstance *Store) All(group string) iter.Seq2[KeyValue, error] {
|
|||
}
|
||||
}
|
||||
|
||||
// Splits the stored value by a custom separator.
|
||||
// Usage example: `parts, _ := storeInstance.GetSplit("config", "hosts", ",")`
|
||||
func (storeInstance *Store) GetSplit(group, key, separator string) (iter.Seq[string], error) {
|
||||
value, err := storeInstance.Get(group, key)
|
||||
|
|
@ -227,7 +219,6 @@ func (storeInstance *Store) GetSplit(group, key, separator string) (iter.Seq[str
|
|||
return splitSeq(value, separator), nil
|
||||
}
|
||||
|
||||
// Splits the stored value on whitespace.
|
||||
// Usage example: `fields, _ := storeInstance.GetFields("config", "flags")`
|
||||
func (storeInstance *Store) GetFields(group, key string) (iter.Seq[string], error) {
|
||||
value, err := storeInstance.Get(group, key)
|
||||
|
|
@ -340,7 +331,6 @@ func escapeLike(text string) string {
|
|||
return text
|
||||
}
|
||||
|
||||
// Deletes expired keys across all groups.
|
||||
// Usage example: `removed, err := storeInstance.PurgeExpired()`
|
||||
func (storeInstance *Store) PurgeExpired() (int64, error) {
|
||||
deleteResult, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE expires_at IS NOT NULL AND expires_at <= ?",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue