Merge pull request '[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/store/RFC.md fully. Find features d...' (#115) from agent/read---spec-code-core-go-store-rfc-md-fu into dev
This commit is contained in:
commit
fb973d81bf
3 changed files with 294 additions and 0 deletions
|
|
@ -209,6 +209,8 @@ Namespace strings must match `^[a-zA-Z0-9-]+$`. Invalid namespaces are rejected
|
|||
|
||||
`ScopedStore` exposes the same read helpers as `Store` for `Get`, `Set`, `SetWithTTL`, `Delete`, `DeleteGroup`, `DeletePrefix`, `GetAll`, `All`, `Count`, `CountAll`, `Groups`, `GroupsSeq`, `GetSplit`, `GetFields`, `Render`, and `PurgeExpired`. Methods that return group names strip the namespace prefix before returning results. The `Namespace()` method returns the namespace string.
|
||||
|
||||
`ScopedStore.Transaction` exposes the same transaction helpers through `ScopedStoreTransaction`, so callers can work inside a namespace without manually prefixing group names during a multi-step write.
|
||||
|
||||
### Quota Enforcement
|
||||
|
||||
`NewScopedWithQuota(store, namespace, QuotaConfig)` adds per-namespace limits. For example, `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` caps a namespace at 100 keys and 10 groups:
|
||||
|
|
|
|||
242
scope.go
242
scope.go
|
|
@ -35,6 +35,12 @@ type ScopedStore struct {
|
|||
scopedWatchers map[uintptr]*scopedWatcherBinding
|
||||
}
|
||||
|
||||
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("colour", "blue") })`
|
||||
type ScopedStoreTransaction struct {
|
||||
scopedStore *ScopedStore
|
||||
storeTransaction *StoreTransaction
|
||||
}
|
||||
|
||||
// Usage example: `config := store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}`
|
||||
type ScopedStoreConfig struct {
|
||||
// Usage example: `config := store.ScopedStoreConfig{Namespace: "tenant-a"}`
|
||||
|
|
@ -427,6 +433,242 @@ func (scopedStore *ScopedStore) PurgeExpired() (int64, error) {
|
|||
return removedRows, nil
|
||||
}
|
||||
|
||||
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.SetIn("config", "colour", "blue") })`
|
||||
func (scopedStore *ScopedStore) Transaction(operation func(*ScopedStoreTransaction) error) error {
|
||||
backingStore, err := scopedStore.resolvedStore("store.ScopedStore.Transaction")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if operation == nil {
|
||||
return core.E("store.ScopedStore.Transaction", "operation is nil", nil)
|
||||
}
|
||||
|
||||
return backingStore.Transaction(func(transaction *StoreTransaction) error {
|
||||
scopedTransaction := &ScopedStoreTransaction{
|
||||
scopedStore: scopedStore,
|
||||
storeTransaction: transaction,
|
||||
}
|
||||
return operation(scopedTransaction)
|
||||
})
|
||||
}
|
||||
|
||||
func (scopedTransaction *ScopedStoreTransaction) resolvedTransaction(operation string) (*StoreTransaction, error) {
|
||||
if scopedTransaction == nil {
|
||||
return nil, core.E(operation, "scoped transaction is nil", nil)
|
||||
}
|
||||
if scopedTransaction.scopedStore == nil {
|
||||
return nil, core.E(operation, "scoped store is nil", nil)
|
||||
}
|
||||
if scopedTransaction.storeTransaction == nil {
|
||||
return nil, core.E(operation, "transaction is nil", nil)
|
||||
}
|
||||
if _, err := scopedTransaction.scopedStore.resolvedStore(operation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scopedTransaction.storeTransaction, nil
|
||||
}
|
||||
|
||||
// Usage example: `value, err := transaction.Get("colour")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) Get(key string) (string, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.Get")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return storeTransaction.Get(scopedTransaction.scopedStore.namespacedGroup(defaultScopedGroupName), key)
|
||||
}
|
||||
|
||||
// Usage example: `value, err := transaction.GetFrom("config", "colour")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) GetFrom(group, key string) (string, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.GetFrom")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return storeTransaction.Get(scopedTransaction.scopedStore.namespacedGroup(group), key)
|
||||
}
|
||||
|
||||
// Usage example: `if err := transaction.Set("colour", "blue"); err != nil { return err }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) Set(key, value string) error {
|
||||
return scopedTransaction.SetIn(defaultScopedGroupName, key, value)
|
||||
}
|
||||
|
||||
// Usage example: `if err := transaction.SetIn("config", "colour", "blue"); err != nil { return err }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) SetIn(group, key, value string) error {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.SetIn")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storeTransaction.Set(scopedTransaction.scopedStore.namespacedGroup(group), key, value)
|
||||
}
|
||||
|
||||
// Usage example: `if err := transaction.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return err }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) SetWithTTL(group, key, value string, timeToLive time.Duration) error {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.SetWithTTL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storeTransaction.SetWithTTL(scopedTransaction.scopedStore.namespacedGroup(group), key, value, timeToLive)
|
||||
}
|
||||
|
||||
// Usage example: `if err := transaction.Delete("config", "colour"); err != nil { return err }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) Delete(group, key string) error {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.Delete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storeTransaction.Delete(scopedTransaction.scopedStore.namespacedGroup(group), key)
|
||||
}
|
||||
|
||||
// Usage example: `if err := transaction.DeleteGroup("cache"); err != nil { return err }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) DeleteGroup(group string) error {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.DeleteGroup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storeTransaction.DeleteGroup(scopedTransaction.scopedStore.namespacedGroup(group))
|
||||
}
|
||||
|
||||
// Usage example: `if err := transaction.DeletePrefix("config"); err != nil { return err }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) DeletePrefix(groupPrefix string) error {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.DeletePrefix")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storeTransaction.DeletePrefix(scopedTransaction.scopedStore.namespacedGroup(groupPrefix))
|
||||
}
|
||||
|
||||
// Usage example: `entries, err := transaction.GetAll("config")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) GetAll(group string) (map[string]string, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.GetAll")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeTransaction.GetAll(scopedTransaction.scopedStore.namespacedGroup(group))
|
||||
}
|
||||
|
||||
// Usage example: `page, err := transaction.GetPage("config", 0, 25)`
|
||||
func (scopedTransaction *ScopedStoreTransaction) GetPage(group string, offset, limit int) ([]KeyValue, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.GetPage")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeTransaction.GetPage(scopedTransaction.scopedStore.namespacedGroup(group), offset, limit)
|
||||
}
|
||||
|
||||
// Usage example: `for entry, err := range transaction.All("config") { if err != nil { return }; fmt.Println(entry.Key, entry.Value) }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) All(group string) iter.Seq2[KeyValue, error] {
|
||||
return scopedTransaction.AllSeq(group)
|
||||
}
|
||||
|
||||
// Usage example: `for entry, err := range transaction.AllSeq("config") { if err != nil { return }; fmt.Println(entry.Key, entry.Value) }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) AllSeq(group string) iter.Seq2[KeyValue, error] {
|
||||
return func(yield func(KeyValue, error) bool) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.AllSeq")
|
||||
if err != nil {
|
||||
yield(KeyValue{}, err)
|
||||
return
|
||||
}
|
||||
for entry, iterationErr := range storeTransaction.AllSeq(scopedTransaction.scopedStore.namespacedGroup(group)) {
|
||||
if iterationErr != nil {
|
||||
if !yield(KeyValue{}, iterationErr) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !yield(entry, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example: `count, err := transaction.Count("config")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) Count(group string) (int, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.Count")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return storeTransaction.Count(scopedTransaction.scopedStore.namespacedGroup(group))
|
||||
}
|
||||
|
||||
// Usage example: `count, err := transaction.CountAll("config")`
|
||||
// Usage example: `count, err := transaction.CountAll()`
|
||||
func (scopedTransaction *ScopedStoreTransaction) CountAll(groupPrefix ...string) (int, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.CountAll")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return storeTransaction.CountAll(scopedTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
||||
}
|
||||
|
||||
// Usage example: `groups, err := transaction.Groups("config")`
|
||||
// Usage example: `groups, err := transaction.Groups()`
|
||||
func (scopedTransaction *ScopedStoreTransaction) Groups(groupPrefix ...string) ([]string, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.Groups")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupNames, err := storeTransaction.Groups(scopedTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, groupName := range groupNames {
|
||||
groupNames[index] = scopedTransaction.scopedStore.trimNamespacePrefix(groupName)
|
||||
}
|
||||
return groupNames, nil
|
||||
}
|
||||
|
||||
// Usage example: `for groupName, err := range transaction.GroupsSeq("config") { if err != nil { return }; fmt.Println(groupName) }`
|
||||
// Usage example: `for groupName, err := range transaction.GroupsSeq() { if err != nil { return }; fmt.Println(groupName) }`
|
||||
func (scopedTransaction *ScopedStoreTransaction) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] {
|
||||
return func(yield func(string, error) bool) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.GroupsSeq")
|
||||
if err != nil {
|
||||
yield("", err)
|
||||
return
|
||||
}
|
||||
namespacePrefix := scopedTransaction.scopedStore.namespacePrefix()
|
||||
for groupName, iterationErr := range storeTransaction.GroupsSeq(scopedTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) {
|
||||
if iterationErr != nil {
|
||||
if !yield("", iterationErr) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !yield(core.TrimPrefix(groupName, namespacePrefix), nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example: `renderedTemplate, err := transaction.Render("Hello {{ .name }}", "user")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) Render(templateSource, group string) (string, error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.Render")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return storeTransaction.Render(templateSource, scopedTransaction.scopedStore.namespacedGroup(group))
|
||||
}
|
||||
|
||||
// Usage example: `parts, err := transaction.GetSplit("config", "hosts", ",")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) GetSplit(group, key, separator string) (iter.Seq[string], error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.GetSplit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeTransaction.GetSplit(scopedTransaction.scopedStore.namespacedGroup(group), key, separator)
|
||||
}
|
||||
|
||||
// Usage example: `fields, err := transaction.GetFields("config", "flags")`
|
||||
func (scopedTransaction *ScopedStoreTransaction) GetFields(group, key string) (iter.Seq[string], error) {
|
||||
storeTransaction, err := scopedTransaction.resolvedTransaction("store.ScopedStoreTransaction.GetFields")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeTransaction.GetFields(scopedTransaction.scopedStore.namespacedGroup(group), key)
|
||||
}
|
||||
|
||||
func (scopedStore *ScopedStore) forgetScopedWatcher(events <-chan Event) {
|
||||
if scopedStore == nil || events == nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -587,6 +587,56 @@ func TestScope_ScopedStore_Good_OnChange(t *testing.T) {
|
|||
assert.Equal(t, "dark", seen[0].Value)
|
||||
}
|
||||
|
||||
func TestScope_ScopedStoreTransaction_Good_PrefixesAndReadsPendingWrites(t *testing.T) {
|
||||
storeInstance, _ := New(":memory:")
|
||||
defer storeInstance.Close()
|
||||
|
||||
scopedStore := mustScoped(t, storeInstance, "tenant-a")
|
||||
events := storeInstance.Watch("*")
|
||||
defer storeInstance.Unwatch("*", events)
|
||||
|
||||
err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
||||
require.NoError(t, transaction.Set("theme", "dark"))
|
||||
require.NoError(t, transaction.SetIn("config", "colour", "blue"))
|
||||
|
||||
value, err := transaction.Get("theme")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "dark", value)
|
||||
|
||||
entriesByKey, err := transaction.GetAll("config")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"colour": "blue"}, entriesByKey)
|
||||
|
||||
count, err := transaction.CountAll("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
groupNames, err := transaction.Groups()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"config", "default"}, groupNames)
|
||||
|
||||
renderedTemplate, err := transaction.Render("{{ .theme }} / {{ .colour }}", "default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "dark / <no value>", renderedTemplate)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
value, err := storeInstance.Get("tenant-a:default", "theme")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "dark", value)
|
||||
|
||||
value, err = storeInstance.Get("tenant-a:config", "colour")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "blue", value)
|
||||
|
||||
received := drainEvents(events, 2, time.Second)
|
||||
require.Len(t, received, 2)
|
||||
assert.Equal(t, "tenant-a:default", received[0].Group)
|
||||
assert.Equal(t, "tenant-a:config", received[1].Group)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quota enforcement — MaxKeys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue