diff --git a/specs/RFC.md b/specs/RFC.md new file mode 100644 index 0000000..09983ed --- /dev/null +++ b/specs/RFC.md @@ -0,0 +1,131 @@ +# store + +**Import:** `dappco.re/go/core/store` + +**Files:** 4 + +`store` provides a SQLite-backed key-value store with group namespaces, TTL expiry, quota-enforced scoped views, and reactive change notifications. The package also exports the sentinel errors `ErrNotFound` and `ErrQuotaExceeded`. + +## Types + +This package exports structs and one defined integer type. It exports no interfaces or type aliases. + +### `EventType` + +`type EventType int` + +Describes the kind of store mutation that occurred. + +Exported constants: +- `EventSet`: a key was created or updated. +- `EventDelete`: a single key was removed. +- `EventDeleteGroup`: all keys in a group were removed. + +### `Event` + +`type Event struct` + +Describes a single store mutation. `Key` is empty for `EventDeleteGroup`. `Value` is only populated for `EventSet`. + +Fields: +- `Type EventType`: the mutation kind. +- `Group string`: the group that changed. +- `Key string`: the key that changed, or `""` for group deletion. +- `Value string`: the new value for set events. +- `Timestamp time.Time`: when the event was emitted. + +### `Watcher` + +`type Watcher struct` + +Receives events matching a group/key filter. Create one with `(*Store).Watch` and stop delivery with `(*Store).Unwatch`. + +Fields: +- `Ch <-chan Event`: the public read-only event channel consumers select on. + +### `KV` + +`type KV struct` + +Represents a key-value pair yielded by store iterators. + +Fields: +- `Key string`: the stored key. +- `Value string`: the stored value. + +### `QuotaConfig` + +`type QuotaConfig struct` + +Defines optional limits for a `ScopedStore` namespace. Zero values mean unlimited. + +Fields: +- `MaxKeys int`: maximum total keys across all groups in the namespace. +- `MaxGroups int`: maximum distinct groups in the namespace. + +### `ScopedStore` + +`type ScopedStore struct` + +Wraps a `*Store` and prefixes all group names with a namespace to prevent collisions across tenants. Quotas, when configured, are enforced on new keys and groups. + +### `Store` + +`type Store struct` + +Group-namespaced key-value store backed by SQLite. It owns the SQLite connection, starts a background purge loop for expired entries, and fans out mutation notifications to watchers and change callbacks. + +## Functions + +### Package functions + +| Signature | Description | +| --- | --- | +| `func New(dbPath string) (*Store, error)` | Creates a `Store` at the given SQLite path. `":memory:"` is valid for tests. The implementation opens SQLite, forces a single open connection, enables WAL mode, sets `busy_timeout=5000`, ensures the `kv` table exists, applies the `expires_at` migration if needed, and starts the background expiry purge loop. | +| `func NewScoped(store *Store, namespace string) (*ScopedStore, error)` | Creates a `ScopedStore` that prefixes every group with `namespace:`. The namespace must be non-empty and match `^[a-zA-Z0-9-]+$`. | +| `func NewScopedWithQuota(store *Store, namespace string, quota QuotaConfig) (*ScopedStore, error)` | Creates a `ScopedStore` with the same namespace rules as `NewScoped`, then attaches quota enforcement used by `Set` and `SetWithTTL` for new keys and new groups. | + +### `EventType` methods + +| Signature | Description | +| --- | --- | +| `func (t EventType) String() string` | Returns a human-readable label for the event type: `set`, `delete`, `delete_group`, or `unknown`. | + +### `Store` methods + +| Signature | Description | +| --- | --- | +| `func (s *Store) All(group string) iter.Seq2[KV, error]` | Returns an iterator over all non-expired key-value pairs in `group`. Query, scan, and row errors are yielded through the second iterator value. | +| `func (s *Store) Close() error` | Stops the background purge goroutine and closes the underlying database connection. | +| `func (s *Store) Count(group string) (int, error)` | Returns the number of non-expired keys in `group`. | +| `func (s *Store) CountAll(prefix string) (int, error)` | Returns the total number of non-expired keys across all groups whose names start with `prefix`. Passing `""` counts all non-expired keys. Prefix matching is implemented with escaped SQLite `LIKE` patterns. | +| `func (s *Store) Delete(group, key string) error` | Removes a single key from a group and emits an `EventDelete` notification after a successful write. | +| `func (s *Store) DeleteGroup(group string) error` | Removes all keys in a group and emits an `EventDeleteGroup` notification after a successful write. | +| `func (s *Store) Get(group, key string) (string, error)` | Retrieves a value by group and key. Expired entries are treated as missing, are lazily deleted on read, and return `ErrNotFound` wrapped with `store.Get` context. | +| `func (s *Store) GetAll(group string) (map[string]string, error)` | Collects all non-expired key-value pairs in `group` into a `map[string]string` by consuming `All`. | +| `func (s *Store) GetFields(group, key string) (iter.Seq[string], error)` | Retrieves a value and returns an iterator over whitespace-delimited fields. | +| `func (s *Store) GetSplit(group, key, sep string) (iter.Seq[string], error)` | Retrieves a value and returns an iterator over substrings split by `sep`. | +| `func (s *Store) Groups(prefix string) ([]string, error)` | Returns the distinct names of groups containing non-expired keys. If `prefix` is non-empty, only matching group names are returned. | +| `func (s *Store) GroupsSeq(prefix string) iter.Seq2[string, error]` | Returns an iterator over the distinct names of groups containing non-expired keys, optionally filtered by `prefix`. Query, scan, and row errors are yielded through the second iterator value. | +| `func (s *Store) OnChange(fn func(Event)) func()` | Registers a callback invoked on every store mutation. Callbacks run synchronously in the goroutine that performed the write. The returned function unregisters the callback and is idempotent. | +| `func (s *Store) PurgeExpired() (int64, error)` | Deletes all expired keys across all groups and returns the number of removed rows. | +| `func (s *Store) Render(tmplStr, group string) (string, error)` | Loads all non-expired key-value pairs from `group`, parses `tmplStr` with Go's `text/template`, and executes the template with the group's key-value map as data. | +| `func (s *Store) Set(group, key, value string) error` | Inserts or updates a key with no expiry. Existing rows are overwritten and any previous TTL is cleared. A successful write emits an `EventSet`. | +| `func (s *Store) SetWithTTL(group, key, value string, ttl time.Duration) error` | Inserts or updates a key with an expiry time of `time.Now().Add(ttl)`. A successful write emits an `EventSet`. | +| `func (s *Store) Unwatch(w *Watcher)` | Removes a watcher and closes its channel. Calling `Unwatch` more than once for the same watcher is a no-op. | +| `func (s *Store) Watch(group, key string) *Watcher` | Creates a watcher that receives events matching `group` and `key`. `*` acts as a wildcard, the returned channel is buffered to 16 events, and sends are non-blocking, so events are dropped if the consumer falls behind. | + +### `ScopedStore` methods + +| Signature | Description | +| --- | --- | +| `func (s *ScopedStore) All(group string) iter.Seq2[KV, error]` | Returns the same iterator as `Store.All`, but against the namespace-prefixed group. | +| `func (s *ScopedStore) Count(group string) (int, error)` | Returns the number of non-expired keys in the namespace-prefixed group. | +| `func (s *ScopedStore) Delete(group, key string) error` | Removes a single key from the namespace-prefixed group. | +| `func (s *ScopedStore) DeleteGroup(group string) error` | Removes all keys from the namespace-prefixed group. | +| `func (s *ScopedStore) Get(group, key string) (string, error)` | Retrieves a value from the namespace-prefixed group. | +| `func (s *ScopedStore) GetAll(group string) (map[string]string, error)` | Returns all non-expired key-value pairs from the namespace-prefixed group. | +| `func (s *ScopedStore) Namespace() string` | Returns the namespace string used to prefix groups. | +| `func (s *ScopedStore) Render(tmplStr, group string) (string, error)` | Renders a Go template with the key-value map loaded from the namespace-prefixed group. | +| `func (s *ScopedStore) Set(group, key, value string) error` | Stores a value in the namespace-prefixed group. When quotas are configured, new keys and new groups are checked before writing; upserts bypass the quota limit checks. | +| `func (s *ScopedStore) SetWithTTL(group, key, value string, ttl time.Duration) error` | Stores a TTL-bound value in the namespace-prefixed group with the same quota enforcement rules as `Set`. |