2026-02-20 08:19:11 +00:00
package store
import (
2026-02-23 05:21:39 +00:00
"iter"
2026-02-20 08:19:11 +00:00
"regexp"
2026-04-03 07:00:35 +00:00
"sync"
2026-02-20 08:19:11 +00:00
"time"
2026-03-16 21:29:42 +00:00
2026-03-26 13:58:50 +00:00
core "dappco.re/go/core"
2026-02-20 08:19:11 +00:00
)
2026-03-30 17:37:50 +00:00
// validNamespace.MatchString("tenant-a") is true; validNamespace.MatchString("tenant_a") is false.
2026-02-20 08:19:11 +00:00
var validNamespace = regexp . MustCompile ( ` ^[a-zA-Z0-9-]+$ ` )
2026-03-30 20:46:43 +00:00
const defaultScopedGroupName = "default"
2026-03-26 19:17:11 +00:00
// Usage example: `quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}`
2026-02-20 08:19:11 +00:00
type QuotaConfig struct {
2026-03-30 16:54:02 +00:00
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 100 keys.
MaxKeys int
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 10 groups.
MaxGroups int
2026-02-20 08:19:11 +00:00
}
2026-04-04 12:08:14 +00:00
// Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }; _ = scopedStore.Set("colour", "blue")`
2026-04-04 11:30:13 +00:00
// ScopedStore keeps one namespace isolated behind helpers such as Set and
// GetFrom so callers do not repeat the `tenant-a:` prefix manually.
2026-02-20 08:19:11 +00:00
type ScopedStore struct {
2026-04-04 13:16:24 +00:00
store * Store
namespace string
2026-04-04 09:48:02 +00:00
// Usage example: `scopedStore.MaxKeys = 100`
MaxKeys int
// Usage example: `scopedStore.MaxGroups = 10`
MaxGroups int
2026-04-03 07:00:35 +00:00
scopedWatchersLock sync . Mutex
scopedWatchers map [ uintptr ] * scopedWatcherBinding
}
2026-04-04 11:30:13 +00:00
// ScopedStoreTransaction stages multiple namespace-prefixed writes in one
// SQLite transaction and only emits events after commit succeeds.
//
2026-04-04 10:03:41 +00:00
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("colour", "blue") })`
type ScopedStoreTransaction struct {
scopedStore * ScopedStore
storeTransaction * StoreTransaction
}
2026-04-04 09:34:54 +00:00
// 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"}`
Namespace string
// Usage example: `config := store.ScopedStoreConfig{Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}`
Quota QuotaConfig
}
2026-04-04 10:46:46 +00:00
// Usage example: `if err := (store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}).Validate(); err != nil { return }`
2026-04-04 11:26:16 +00:00
func ( scopedConfig ScopedStoreConfig ) Validate ( ) error {
if ! validNamespace . MatchString ( scopedConfig . Namespace ) {
2026-04-04 10:46:46 +00:00
return core . E (
"store.ScopedStoreConfig.Validate" ,
2026-04-04 11:26:16 +00:00
core . Sprintf ( "namespace %q is invalid; use names like %q or %q" , scopedConfig . Namespace , "tenant-a" , "tenant-42" ) ,
2026-04-04 10:46:46 +00:00
nil ,
)
}
2026-04-04 11:26:16 +00:00
if scopedConfig . Quota . MaxKeys < 0 || scopedConfig . Quota . MaxGroups < 0 {
2026-04-04 10:46:46 +00:00
return core . E (
"store.ScopedStoreConfig.Validate" ,
2026-04-04 11:26:16 +00:00
core . Sprintf ( "quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d" , scopedConfig . Quota . MaxKeys , scopedConfig . Quota . MaxGroups ) ,
2026-04-04 10:46:46 +00:00
nil ,
)
}
return nil
}
2026-04-03 07:00:35 +00:00
type scopedWatcherBinding struct {
2026-04-04 13:16:24 +00:00
store * Store
2026-04-03 07:00:35 +00:00
underlyingEvents <- chan Event
done chan struct { }
stop chan struct { }
stopOnce sync . Once
2026-02-20 08:19:11 +00:00
}
2026-04-03 08:36:40 +00:00
func ( scopedStore * ScopedStore ) resolvedStore ( operation string ) ( * Store , error ) {
2026-04-03 06:31:35 +00:00
if scopedStore == nil {
return nil , core . E ( operation , "scoped store is nil" , nil )
}
2026-04-04 13:16:24 +00:00
if scopedStore . store == nil {
2026-04-03 06:31:35 +00:00
return nil , core . E ( operation , "underlying store is nil" , nil )
}
2026-04-04 13:16:24 +00:00
if err := scopedStore . store . ensureReady ( operation ) ; err != nil {
2026-04-03 06:31:35 +00:00
return nil , err
}
2026-04-04 13:16:24 +00:00
return scopedStore . store , nil
2026-04-03 06:31:35 +00:00
}
2026-04-04 08:57:06 +00:00
// Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a"); if scopedStore == nil { return }`
2026-04-03 04:44:45 +00:00
func NewScoped ( storeInstance * Store , namespace string ) * ScopedStore {
2026-03-30 19:29:48 +00:00
if storeInstance == nil {
2026-04-03 04:44:45 +00:00
return nil
2026-03-30 19:29:48 +00:00
}
2026-02-20 08:19:11 +00:00
if ! validNamespace . MatchString ( namespace ) {
2026-04-03 04:44:45 +00:00
return nil
2026-02-20 08:19:11 +00:00
}
2026-04-04 13:16:24 +00:00
scopedStore := & ScopedStore { store : storeInstance , namespace : namespace }
2026-04-03 04:44:45 +00:00
return scopedStore
2026-02-20 08:19:11 +00:00
}
2026-04-04 09:34:54 +00:00
// Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }`
2026-04-04 11:26:16 +00:00
func NewScopedConfigured ( storeInstance * Store , scopedConfig ScopedStoreConfig ) ( * ScopedStore , error ) {
2026-04-04 10:46:46 +00:00
if storeInstance == nil {
return nil , core . E ( "store.NewScopedConfigured" , "store instance is nil" , nil )
2026-02-20 08:19:11 +00:00
}
2026-04-04 11:26:16 +00:00
if err := scopedConfig . Validate ( ) ; err != nil {
2026-04-04 10:46:46 +00:00
return nil , core . E ( "store.NewScopedConfigured" , "validate config" , err )
2026-03-30 20:03:19 +00:00
}
2026-04-04 11:26:16 +00:00
scopedStore := NewScoped ( storeInstance , scopedConfig . Namespace )
scopedStore . MaxKeys = scopedConfig . Quota . MaxKeys
scopedStore . MaxGroups = scopedConfig . Quota . MaxGroups
2026-03-30 14:54:34 +00:00
return scopedStore , nil
2026-02-20 08:19:11 +00:00
}
2026-04-04 09:34:54 +00:00
// 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 ) {
return NewScopedConfigured ( storeInstance , ScopedStoreConfig {
Namespace : namespace ,
Quota : quota ,
} )
}
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) namespacedGroup ( group string ) string {
return scopedStore . namespace + ":" + group
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
func ( scopedStore * ScopedStore ) namespacePrefix ( ) string {
return scopedStore . namespace + ":"
}
func ( scopedStore * ScopedStore ) trimNamespacePrefix ( groupName string ) string {
return core . TrimPrefix ( groupName , scopedStore . namespacePrefix ( ) )
}
2026-04-04 08:57:06 +00:00
// Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a"); if scopedStore == nil { return }; fmt.Println(scopedStore.Namespace())`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Namespace ( ) string {
2026-04-03 06:31:35 +00:00
if scopedStore == nil {
return ""
}
2026-03-30 15:02:28 +00:00
return scopedStore . namespace
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
// Usage example: `colourValue, err := scopedStore.Get("colour")`
2026-04-03 05:45:24 +00:00
func ( scopedStore * ScopedStore ) Get ( key string ) ( string , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Get" )
2026-04-03 06:31:35 +00:00
if err != nil {
return "" , err
}
2026-04-03 08:36:40 +00:00
return backingStore . Get ( scopedStore . namespacedGroup ( defaultScopedGroupName ) , key )
2026-04-03 05:13:46 +00:00
}
// Usage example: `colourValue, err := scopedStore.GetFrom("config", "colour")`
func ( scopedStore * ScopedStore ) GetFrom ( group , key string ) ( string , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.GetFrom" )
2026-04-03 06:31:35 +00:00
if err != nil {
return "" , err
}
2026-04-03 08:36:40 +00:00
return backingStore . Get ( scopedStore . namespacedGroup ( group ) , key )
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
// Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return }`
2026-04-03 05:45:24 +00:00
func ( scopedStore * ScopedStore ) Set ( key , value string ) error {
return scopedStore . SetIn ( defaultScopedGroupName , key , value )
2026-04-03 05:13:46 +00:00
}
// Usage example: `if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }`
func ( scopedStore * ScopedStore ) SetIn ( group , key , value string ) error {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.SetIn" )
2026-04-03 06:31:35 +00:00
if err != nil {
return err
}
2026-04-03 05:13:46 +00:00
if err := scopedStore . checkQuota ( "store.ScopedStore.SetIn" , group , key ) ; err != nil {
2026-02-20 08:19:11 +00:00
return err
}
2026-04-03 08:36:40 +00:00
return backingStore . Set ( scopedStore . namespacedGroup ( group ) , key , value )
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `if err := scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return }`
2026-03-30 18:37:07 +00:00
func ( scopedStore * ScopedStore ) SetWithTTL ( group , key , value string , timeToLive time . Duration ) error {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.SetWithTTL" )
2026-04-03 06:31:35 +00:00
if err != nil {
return err
}
2026-03-30 16:27:54 +00:00
if err := scopedStore . checkQuota ( "store.ScopedStore.SetWithTTL" , group , key ) ; err != nil {
2026-02-20 08:19:11 +00:00
return err
}
2026-04-03 08:36:40 +00:00
return backingStore . SetWithTTL ( scopedStore . namespacedGroup ( group ) , key , value , timeToLive )
2026-02-20 08:19:11 +00:00
}
2026-03-30 18:49:17 +00:00
// Usage example: `if err := scopedStore.Delete("config", "colour"); err != nil { return }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Delete ( group , key string ) error {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Delete" )
2026-04-03 06:31:35 +00:00
if err != nil {
return err
}
2026-04-03 08:36:40 +00:00
return backingStore . Delete ( scopedStore . namespacedGroup ( group ) , key )
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `if err := scopedStore.DeleteGroup("cache"); err != nil { return }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) DeleteGroup ( group string ) error {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.DeleteGroup" )
2026-04-03 06:31:35 +00:00
if err != nil {
return err
}
2026-04-03 08:36:40 +00:00
return backingStore . DeleteGroup ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-04-03 07:14:22 +00:00
// Usage example: `if err := scopedStore.DeletePrefix("config"); err != nil { return }`
func ( scopedStore * ScopedStore ) DeletePrefix ( groupPrefix string ) error {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.DeletePrefix" )
2026-04-03 07:14:22 +00:00
if err != nil {
return err
}
2026-04-03 08:36:40 +00:00
return backingStore . DeletePrefix ( scopedStore . namespacedGroup ( groupPrefix ) )
2026-04-03 07:14:22 +00:00
}
2026-03-30 18:49:17 +00:00
// Usage example: `colourEntries, err := scopedStore.GetAll("config")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) GetAll ( group string ) ( map [ string ] string , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.GetAll" )
2026-04-03 06:31:35 +00:00
if err != nil {
return nil , err
}
2026-04-03 08:36:40 +00:00
return backingStore . GetAll ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-04-03 08:32:35 +00:00
// Usage example: `page, err := scopedStore.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }`
func ( scopedStore * ScopedStore ) GetPage ( group string , offset , limit int ) ( [ ] KeyValue , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.GetPage" )
2026-04-03 08:32:35 +00:00
if err != nil {
return nil , err
}
2026-04-03 08:36:40 +00:00
return backingStore . GetPage ( scopedStore . namespacedGroup ( group ) , offset , limit )
2026-04-03 08:32:35 +00:00
}
2026-03-30 17:32:09 +00:00
// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) All ( group string ) iter . Seq2 [ KeyValue , error ] {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.All" )
2026-04-03 06:31:35 +00:00
if err != nil {
return func ( yield func ( KeyValue , error ) bool ) {
yield ( KeyValue { } , err )
}
}
2026-04-03 08:36:40 +00:00
return backingStore . All ( scopedStore . namespacedGroup ( group ) )
2026-02-23 05:21:39 +00:00
}
2026-04-03 05:13:46 +00:00
// Usage example: `for entry, err := range scopedStore.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
func ( scopedStore * ScopedStore ) AllSeq ( group string ) iter . Seq2 [ KeyValue , error ] {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.AllSeq" )
2026-04-03 06:31:35 +00:00
if err != nil {
return func ( yield func ( KeyValue , error ) bool ) {
yield ( KeyValue { } , err )
}
}
2026-04-03 08:36:40 +00:00
return backingStore . AllSeq ( scopedStore . namespacedGroup ( group ) )
2026-04-03 05:13:46 +00:00
}
2026-03-30 16:13:55 +00:00
// Usage example: `keyCount, err := scopedStore.Count("config")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Count ( group string ) ( int , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Count" )
2026-04-03 06:31:35 +00:00
if err != nil {
return 0 , err
}
2026-04-03 08:36:40 +00:00
return backingStore . Count ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
// Usage example: `keyCount, err := scopedStore.CountAll("config")`
2026-03-30 20:46:43 +00:00
// Usage example: `keyCount, err := scopedStore.CountAll()`
func ( scopedStore * ScopedStore ) CountAll ( groupPrefix ... string ) ( int , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.CountAll" )
2026-04-03 06:31:35 +00:00
if err != nil {
return 0 , err
}
2026-04-03 08:36:40 +00:00
return backingStore . CountAll ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-03-30 19:14:09 +00:00
}
// Usage example: `groupNames, err := scopedStore.Groups("config")`
2026-03-30 20:46:43 +00:00
// Usage example: `groupNames, err := scopedStore.Groups()`
func ( scopedStore * ScopedStore ) Groups ( groupPrefix ... string ) ( [ ] string , error ) {
2026-04-04 10:46:46 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Groups" )
2026-04-03 06:31:35 +00:00
if err != nil {
return nil , err
}
2026-04-03 08:36:40 +00:00
groupNames , err := backingStore . Groups ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-03-30 19:14:09 +00:00
if err != nil {
return nil , err
}
for i , groupName := range groupNames {
groupNames [ i ] = scopedStore . trimNamespacePrefix ( groupName )
}
return groupNames , nil
}
// Usage example: `for groupName, err := range scopedStore.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }`
2026-03-30 20:46:43 +00:00
// Usage example: `for groupName, err := range scopedStore.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
func ( scopedStore * ScopedStore ) GroupsSeq ( groupPrefix ... string ) iter . Seq2 [ string , error ] {
2026-03-30 19:14:09 +00:00
return func ( yield func ( string , error ) bool ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.GroupsSeq" )
2026-04-03 06:31:35 +00:00
if err != nil {
yield ( "" , err )
return
}
2026-03-30 19:14:09 +00:00
namespacePrefix := scopedStore . namespacePrefix ( )
2026-04-03 08:36:40 +00:00
for groupName , err := range backingStore . GroupsSeq ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) ) {
2026-03-30 19:14:09 +00:00
if err != nil {
if ! yield ( "" , err ) {
return
}
continue
}
if ! yield ( core . TrimPrefix ( groupName , namespacePrefix ) , nil ) {
return
}
}
}
}
2026-03-30 16:13:55 +00:00
// Usage example: `renderedTemplate, err := scopedStore.Render("Hello {{ .name }}", "user")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Render ( templateSource , group string ) ( string , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Render" )
2026-04-03 06:31:35 +00:00
if err != nil {
return "" , err
}
2026-04-03 08:36:40 +00:00
return backingStore . Render ( templateSource , scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
// Usage example: `parts, err := scopedStore.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }`
func ( scopedStore * ScopedStore ) GetSplit ( group , key , separator string ) ( iter . Seq [ string ] , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.GetSplit" )
2026-04-03 06:31:35 +00:00
if err != nil {
return nil , err
}
2026-04-03 08:36:40 +00:00
return backingStore . GetSplit ( scopedStore . namespacedGroup ( group ) , key , separator )
2026-03-30 19:14:09 +00:00
}
// Usage example: `fields, err := scopedStore.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }`
func ( scopedStore * ScopedStore ) GetFields ( group , key string ) ( iter . Seq [ string ] , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.GetFields" )
2026-04-03 06:31:35 +00:00
if err != nil {
return nil , err
}
2026-04-03 08:36:40 +00:00
return backingStore . GetFields ( scopedStore . namespacedGroup ( group ) , key )
2026-03-30 19:14:09 +00:00
}
2026-04-03 06:55:37 +00:00
// Usage example: `events := scopedStore.Watch("config")`
func ( scopedStore * ScopedStore ) Watch ( group string ) <- chan Event {
2026-04-03 09:02:04 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Watch" )
2026-04-03 06:55:37 +00:00
if err != nil {
return closedEventChannel ( )
}
2026-04-03 07:00:35 +00:00
if group != "*" {
2026-04-03 08:36:40 +00:00
return backingStore . Watch ( scopedStore . namespacedGroup ( group ) )
2026-04-03 07:00:35 +00:00
}
forwardedEvents := make ( chan Event , watcherEventBufferCapacity )
binding := & scopedWatcherBinding {
2026-04-04 13:16:24 +00:00
store : backingStore ,
2026-04-03 08:36:40 +00:00
underlyingEvents : backingStore . Watch ( "*" ) ,
2026-04-03 07:00:35 +00:00
done : make ( chan struct { } ) ,
stop : make ( chan struct { } ) ,
}
scopedStore . scopedWatchersLock . Lock ( )
if scopedStore . scopedWatchers == nil {
scopedStore . scopedWatchers = make ( map [ uintptr ] * scopedWatcherBinding )
}
scopedStore . scopedWatchers [ channelPointer ( forwardedEvents ) ] = binding
scopedStore . scopedWatchersLock . Unlock ( )
namespacePrefix := scopedStore . namespacePrefix ( )
go func ( ) {
defer close ( forwardedEvents )
defer close ( binding . done )
defer scopedStore . forgetScopedWatcher ( forwardedEvents )
for {
select {
case event , ok := <- binding . underlyingEvents :
if ! ok {
return
}
if ! core . HasPrefix ( event . Group , namespacePrefix ) {
continue
}
select {
case forwardedEvents <- event :
default :
}
case <- binding . stop :
return
2026-04-03 08:36:40 +00:00
case <- backingStore . purgeContext . Done ( ) :
2026-04-03 07:00:35 +00:00
return
}
}
} ( )
return forwardedEvents
2026-04-03 06:55:37 +00:00
}
// Usage example: `scopedStore.Unwatch("config", events)`
func ( scopedStore * ScopedStore ) Unwatch ( group string , events <- chan Event ) {
2026-04-03 09:02:04 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.Unwatch" )
2026-04-03 06:55:37 +00:00
if err != nil {
return
}
2026-04-03 07:00:35 +00:00
if group == "*" {
scopedStore . forgetAndStopScopedWatcher ( events )
return
}
2026-04-03 08:36:40 +00:00
backingStore . Unwatch ( scopedStore . namespacedGroup ( group ) , events )
2026-04-03 06:55:37 +00:00
}
// Usage example: `unregister := scopedStore.OnChange(func(event store.Event) { fmt.Println(event.Group, event.Key) })`
func ( scopedStore * ScopedStore ) OnChange ( callback func ( Event ) ) func ( ) {
2026-04-03 09:02:04 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.OnChange" )
2026-04-03 06:55:37 +00:00
if err != nil {
return func ( ) { }
}
if callback == nil {
return func ( ) { }
}
namespacePrefix := scopedStore . namespacePrefix ( )
2026-04-03 08:36:40 +00:00
return backingStore . OnChange ( func ( event Event ) {
2026-04-03 06:55:37 +00:00
if ! core . HasPrefix ( event . Group , namespacePrefix ) {
return
}
callback ( event )
} )
}
2026-03-30 19:14:09 +00:00
// Usage example: `removedRows, err := scopedStore.PurgeExpired(); if err != nil { return }; fmt.Println(removedRows)`
func ( scopedStore * ScopedStore ) PurgeExpired ( ) ( int64 , error ) {
2026-04-03 08:58:15 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.ScopedStore.PurgeExpired" )
2026-04-03 06:31:35 +00:00
if err != nil {
return 0 , err
}
2026-04-03 08:36:40 +00:00
removedRows , err := backingStore . purgeExpiredMatchingGroupPrefix ( scopedStore . namespacePrefix ( ) )
2026-03-30 20:03:19 +00:00
if err != nil {
return 0 , core . E ( "store.ScopedStore.PurgeExpired" , "delete expired rows" , err )
}
return removedRows , nil
2026-03-30 19:14:09 +00:00
}
2026-04-04 10:03:41 +00:00
// 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
}
2026-04-04 10:36:57 +00:00
if err := scopedTransaction . checkQuota ( "store.ScopedStoreTransaction.SetIn" , group , key ) ; err != nil {
return err
}
2026-04-04 10:03:41 +00:00
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
}
2026-04-04 10:36:57 +00:00
if err := scopedTransaction . checkQuota ( "store.ScopedStoreTransaction.SetWithTTL" , group , key ) ; err != nil {
return err
}
2026-04-04 10:03:41 +00:00
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 )
}
2026-04-04 10:36:57 +00:00
// checkQuota("store.ScopedStoreTransaction.SetIn", "config", "colour") uses
// the transaction's own read state so staged writes inside the same
// transaction count towards the namespace limits.
func ( scopedTransaction * ScopedStoreTransaction ) checkQuota ( operation , group , key string ) error {
if scopedTransaction == nil {
return core . E ( operation , "scoped transaction is nil" , nil )
}
if scopedTransaction . scopedStore == nil {
return core . E ( operation , "scoped store is nil" , nil )
}
storeTransaction , err := scopedTransaction . resolvedTransaction ( operation )
if err != nil {
return err
}
2026-04-04 13:16:24 +00:00
return checkNamespaceQuota (
operation ,
group ,
key ,
scopedTransaction . scopedStore . namespacedGroup ( group ) ,
scopedTransaction . scopedStore . namespacePrefix ( ) ,
scopedTransaction . scopedStore . MaxKeys ,
scopedTransaction . scopedStore . MaxGroups ,
storeTransaction ,
)
2026-04-04 10:36:57 +00:00
}
2026-04-03 07:00:35 +00:00
func ( scopedStore * ScopedStore ) forgetScopedWatcher ( events <- chan Event ) {
if scopedStore == nil || events == nil {
return
}
scopedStore . scopedWatchersLock . Lock ( )
defer scopedStore . scopedWatchersLock . Unlock ( )
if scopedStore . scopedWatchers == nil {
return
}
delete ( scopedStore . scopedWatchers , channelPointer ( events ) )
}
func ( scopedStore * ScopedStore ) forgetAndStopScopedWatcher ( events <- chan Event ) {
if scopedStore == nil || events == nil {
return
}
scopedStore . scopedWatchersLock . Lock ( )
binding := scopedStore . scopedWatchers [ channelPointer ( events ) ]
if binding != nil {
delete ( scopedStore . scopedWatchers , channelPointer ( events ) )
}
scopedStore . scopedWatchersLock . Unlock ( )
if binding == nil {
return
}
binding . stopOnce . Do ( func ( ) {
close ( binding . stop )
} )
2026-04-04 13:16:24 +00:00
if binding . store != nil {
binding . store . Unwatch ( "*" , binding . underlyingEvents )
2026-04-03 07:00:35 +00:00
}
<- binding . done
}
2026-03-30 18:49:17 +00:00
// checkQuota("store.ScopedStore.Set", "config", "colour") returns nil when the
2026-03-30 17:37:50 +00:00
// namespace still has quota available and QuotaExceededError when a new key or
// group would exceed the configured limit. Existing keys are treated as
// upserts and do not consume quota.
2026-03-30 16:27:54 +00:00
func ( scopedStore * ScopedStore ) checkQuota ( operation , group , key string ) error {
2026-04-03 06:31:35 +00:00
if scopedStore == nil {
return core . E ( operation , "scoped store is nil" , nil )
}
2026-04-04 13:16:24 +00:00
return checkNamespaceQuota (
operation ,
group ,
key ,
scopedStore . namespacedGroup ( group ) ,
scopedStore . namespacePrefix ( ) ,
scopedStore . MaxKeys ,
scopedStore . MaxGroups ,
scopedStore . store ,
)
}
type namespaceQuotaReader interface {
Get ( group , key string ) ( string , error )
Count ( group string ) ( int , error )
CountAll ( groupPrefix string ) ( int , error )
Groups ( groupPrefix ... string ) ( [ ] string , error )
}
func checkNamespaceQuota ( operation , group , key , namespacedGroup , namespacePrefix string , maxKeys , maxGroups int , reader namespaceQuotaReader ) error {
if maxKeys == 0 && maxGroups == 0 {
2026-02-20 08:19:11 +00:00
return nil
}
2026-04-04 13:16:24 +00:00
// Upserts never consume quota.
_ , err := reader . Get ( namespacedGroup , key )
2026-02-20 08:19:11 +00:00
if err == nil {
return nil
}
2026-03-30 14:22:49 +00:00
if ! core . Is ( err , NotFoundError ) {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-03-09 08:20:38 +00:00
}
2026-02-20 08:19:11 +00:00
2026-04-04 13:16:24 +00:00
if maxKeys > 0 {
keyCount , err := reader . CountAll ( namespacePrefix )
2026-02-20 08:19:11 +00:00
if err != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-04-04 13:16:24 +00:00
if keyCount >= maxKeys {
return core . E ( operation , core . Sprintf ( "key limit (%d)" , maxKeys ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
2026-04-04 13:16:24 +00:00
if maxGroups > 0 {
existingGroupCount , err := reader . Count ( namespacedGroup )
2026-02-20 08:19:11 +00:00
if err != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-03-30 14:54:34 +00:00
if existingGroupCount == 0 {
2026-04-04 13:16:24 +00:00
groupNames , err := reader . Groups ( namespacePrefix )
2026-04-04 10:17:28 +00:00
if err != nil {
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-04-04 13:16:24 +00:00
if len ( groupNames ) >= maxGroups {
return core . E ( operation , core . Sprintf ( "group limit (%d)" , maxGroups ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
}
return nil
}