[agent/codex:gpt-5.4-mini] Update the code against the AX design principles in docs/RFC... #23
3 changed files with 27 additions and 11 deletions
28
events.go
28
events.go
|
|
@ -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
|
||||
|
|
|
|||
5
scope.go
5
scope.go
|
|
@ -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
|
||||
|
|
|
|||
5
store.go
5
store.go
|
|
@ -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 }`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue