[agent/codex:gpt-5.4-mini] Update the code against the AX design principles in docs/RFC... #23

Merged
Virgil merged 1 commit from agent/update-the-code-against-the-ax-design-pr into dev 2026-03-30 19:06:17 +00:00
3 changed files with 27 additions and 11 deletions

View file

@ -7,6 +7,7 @@ import (
"time"
)
// EventType identifies the kind of mutation emitted by Store.
// Usage example: `if event.Type == store.EventSet { return }`
type EventType int
@ -33,6 +34,7 @@ func (t EventType) String() string {
}
}
// Event describes one mutation delivered to watchers and callbacks.
// Usage example: `event := store.Event{Type: store.EventSet, Group: "config", Key: "colour", Value: "blue"}`
// Usage example: `event := store.Event{Type: store.EventDeleteGroup, Group: "config"}`
type Event struct {
@ -48,16 +50,17 @@ type Event struct {
Timestamp time.Time
}
// Watcher exposes the read-only event stream returned by Watch.
// Usage example: `watcher := storeInstance.Watch("config", "*"); defer storeInstance.Unwatch(watcher); for event := range watcher.Events { if event.Type == EventDeleteGroup { return } }`
type Watcher struct {
// Usage example: `for event := range watcher.Events { if event.Key == "colour" { return } }`
Events <-chan Event
// eventChannel is the internal write channel (same underlying channel as Events).
eventChannel chan Event
// eventsChannel is the internal write channel (same underlying channel as Events).
eventsChannel chan Event
group string
key string
groupPattern string
keyPattern string
registrationID uint64
}
@ -72,14 +75,15 @@ type changeCallbackRegistration struct {
// start dropping new ones.
const watcherEventBufferCapacity = 16
// Watch registers a buffered subscription for matching mutations.
// Usage example: `watcher := storeInstance.Watch("*", "*")`
func (storeInstance *Store) Watch(group, key string) *Watcher {
eventChannel := make(chan Event, watcherEventBufferCapacity)
watcher := &Watcher{
Events: eventChannel,
eventChannel: eventChannel,
group: group,
key: key,
eventsChannel: eventChannel,
groupPattern: group,
keyPattern: key,
registrationID: atomic.AddUint64(&storeInstance.nextWatcherRegistrationID, 1),
}
@ -90,6 +94,7 @@ func (storeInstance *Store) Watch(group, key string) *Watcher {
return watcher
}
// Unwatch removes a watcher and closes its event stream.
// Usage example: `storeInstance.Unwatch(watcher)`
func (storeInstance *Store) Unwatch(watcher *Watcher) {
if watcher == nil {
@ -101,13 +106,14 @@ func (storeInstance *Store) Unwatch(watcher *Watcher) {
storeInstance.watchers = slices.DeleteFunc(storeInstance.watchers, func(existing *Watcher) bool {
if existing.registrationID == watcher.registrationID {
close(watcher.eventChannel)
close(watcher.eventsChannel)
return true
}
return false
})
}
// OnChange registers a synchronous mutation callback.
// 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.nextCallbackRegistrationID, 1)
@ -144,7 +150,7 @@ func (storeInstance *Store) notify(event Event) {
}
// Non-blocking send: drop the event rather than block the writer.
select {
case watcher.eventChannel <- event:
case watcher.eventsChannel <- event:
default:
}
}
@ -162,10 +168,10 @@ func (storeInstance *Store) notify(event Event) {
// watcherMatches reports whether Watch("config", "*") should receive
// Event{Group: "config", Key: "colour"}.
func watcherMatches(watcher *Watcher, event Event) bool {
if watcher.group != "*" && watcher.group != event.Group {
if watcher.groupPattern != "*" && watcher.groupPattern != event.Group {
return false
}
if watcher.key != "*" && watcher.key != event.Key {
if watcher.keyPattern != "*" && watcher.keyPattern != event.Key {
// EventDeleteGroup has an empty Key — only wildcard watchers or
// group-level watchers (key="*") should receive it.
return false

View file

@ -11,6 +11,7 @@ import (
// validNamespace.MatchString("tenant-a") is true; validNamespace.MatchString("tenant_a") is false.
var validNamespace = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
// QuotaConfig sets per-namespace key and group limits.
// Usage example: `quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}`
type QuotaConfig struct {
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 100 keys.
@ -19,6 +20,7 @@ type QuotaConfig struct {
MaxGroups int
}
// ScopedStore prefixes group names with namespace + ":" before delegating to Store.
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.Set("config", "colour", "blue"); err != nil { return }`
type ScopedStore struct {
storeInstance *Store
@ -26,6 +28,7 @@ type ScopedStore struct {
quota QuotaConfig
}
// NewScoped validates a namespace and prefixes groups with namespace + ":".
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }`
func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
if !validNamespace.MatchString(namespace) {
@ -35,6 +38,7 @@ func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
return scopedStore, nil
}
// NewScopedWithQuota adds per-namespace key and group limits.
// Usage example: `scopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}); if err != nil { return }`
func NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfig) (*ScopedStore, error) {
scopedStore, err := NewScoped(storeInstance, namespace)
@ -49,6 +53,7 @@ func (scopedStore *ScopedStore) namespacedGroup(group string) string {
return scopedStore.namespace + ":" + group
}
// Namespace returns the namespace string.
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; namespace := scopedStore.Namespace(); fmt.Println(namespace)`
func (scopedStore *ScopedStore) Namespace() string {
return scopedStore.namespace

View file

@ -13,9 +13,11 @@ import (
_ "modernc.org/sqlite"
)
// NotFoundError is returned when Get cannot find a live key.
// Usage example: `if core.Is(err, store.NotFoundError) { return }`
var NotFoundError = core.E("store", "not found", nil)
// QuotaExceededError is returned when a scoped write would exceed quota.
// Usage example: `if core.Is(err, store.QuotaExceededError) { return }`
var QuotaExceededError = core.E("store", "quota exceeded", nil)
@ -27,6 +29,8 @@ const (
entryValueColumn = "entry_value"
)
// Store provides SQLite-backed grouped entries with TTL expiry, namespace
// isolation, and reactive change notifications.
// Usage example: `storeInstance, err := store.New(":memory:"); if err != nil { return }; if err := storeInstance.Set("config", "colour", "blue"); err != nil { return }`
type Store struct {
database *sql.DB
@ -168,6 +172,7 @@ func (storeInstance *Store) DeleteGroup(group string) error {
return nil
}
// KeyValue is one item returned by All.
// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
type KeyValue struct {
// Usage example: `if entry.Key == "colour" { return }`