diff --git a/doc.go b/doc.go index bb1f2c4..dbfeede 100644 --- a/doc.go +++ b/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 diff --git a/events.go b/events.go index 0350c06..927cef7 100644 --- a/events.go +++ b/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} diff --git a/scope.go b/scope.go index ac6083d..ece0ca5 100644 --- a/scope.go +++ b/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)) } diff --git a/store.go b/store.go index af43666..da5f578 100644 --- a/store.go +++ b/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 <= ?",