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 08:57:06 +00:00
// Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a"); if scopedStore == nil { return }; if err := scopedStore.Set("colour", "blue"); err != nil { return }; if err := scopedStore.SetIn("config", "language", "en-GB"); err != nil { return }`
2026-02-20 08:19:11 +00:00
type ScopedStore struct {
2026-04-03 06:35:23 +00:00
backingStore * Store
namespace string
MaxKeys int
MaxGroups int
2026-04-03 07:00:35 +00:00
scopedWatchersLock sync . Mutex
scopedWatchers map [ uintptr ] * scopedWatcherBinding
}
type scopedWatcherBinding struct {
2026-04-03 08:36:40 +00:00
backingStore * 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-03 06:35:23 +00:00
if scopedStore . backingStore == nil {
2026-04-03 06:31:35 +00:00
return nil , core . E ( operation , "underlying store is nil" , nil )
}
2026-04-03 06:35:23 +00:00
if err := scopedStore . backingStore . ensureReady ( operation ) ; err != nil {
2026-04-03 06:31:35 +00:00
return nil , err
}
2026-04-03 06:35:23 +00:00
return scopedStore . backingStore , 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-03 06:35:23 +00:00
scopedStore := & ScopedStore { backingStore : storeInstance , namespace : namespace }
2026-04-03 04:44:45 +00:00
return scopedStore
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `scopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}); if err != nil { return }`
2026-03-30 15:06:20 +00:00
func NewScopedWithQuota ( storeInstance * Store , namespace string , quota QuotaConfig ) ( * ScopedStore , error ) {
2026-04-03 04:44:45 +00:00
scopedStore := NewScoped ( storeInstance , namespace )
if scopedStore == nil {
if storeInstance == nil {
return nil , core . E ( "store.NewScopedWithQuota" , "store instance is nil" , nil )
}
return nil , core . E ( "store.NewScopedWithQuota" , core . Sprintf ( "namespace %q is invalid; use names like %q or %q" , namespace , "tenant-a" , "tenant-42" ) , nil )
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:03:19 +00:00
if quota . MaxKeys < 0 || quota . MaxGroups < 0 {
return nil , core . E (
"store.NewScopedWithQuota" ,
core . Sprintf ( "quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d" , quota . MaxKeys , quota . MaxGroups ) ,
nil ,
)
}
2026-03-30 20:46:43 +00:00
scopedStore . MaxKeys = quota . MaxKeys
scopedStore . MaxGroups = quota . MaxGroups
2026-03-30 14:54:34 +00:00
return scopedStore , nil
2026-02-20 08:19:11 +00:00
}
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-03 08:36:40 +00:00
backingStore , err := scopedStore . resolvedStore ( "store.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-03 08:36:40 +00:00
backingStore : backingStore ,
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-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-03 08:36:40 +00:00
if binding . backingStore != nil {
binding . backingStore . 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-03-30 20:46:43 +00:00
if scopedStore . MaxKeys == 0 && scopedStore . MaxGroups == 0 {
2026-02-20 08:19:11 +00:00
return nil
}
2026-03-30 15:02:28 +00:00
namespacedGroup := scopedStore . namespacedGroup ( group )
2026-03-30 19:14:09 +00:00
namespacePrefix := scopedStore . namespacePrefix ( )
2026-02-20 08:19:11 +00:00
// Check if this is an upsert (key already exists) — upserts never exceed quota.
2026-04-03 06:35:23 +00:00
_ , err := scopedStore . backingStore . Get ( namespacedGroup , key )
2026-02-20 08:19:11 +00:00
if err == nil {
// Key exists — this is an upsert, no quota check needed.
return nil
}
2026-03-30 14:22:49 +00:00
if ! core . Is ( err , NotFoundError ) {
2026-03-09 08:20:38 +00:00
// A database error occurred, not just a "not found" result.
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
// Check MaxKeys quota.
2026-03-30 20:46:43 +00:00
if scopedStore . MaxKeys > 0 {
2026-04-03 06:35:23 +00:00
keyCount , err := scopedStore . backingStore . 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-03-30 20:46:43 +00:00
if keyCount >= scopedStore . MaxKeys {
return core . E ( operation , core . Sprintf ( "key limit (%d)" , scopedStore . MaxKeys ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
// Check MaxGroups quota — only if this would create a new group.
2026-03-30 20:46:43 +00:00
if scopedStore . MaxGroups > 0 {
2026-04-03 06:35:23 +00:00
existingGroupCount , err := scopedStore . backingStore . 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-02-20 08:19:11 +00:00
// This group is new — check if adding it would exceed the group limit.
2026-03-30 14:54:34 +00:00
knownGroupCount := 0
2026-04-03 06:35:23 +00:00
for _ , iterationErr := range scopedStore . backingStore . GroupsSeq ( namespacePrefix ) {
2026-03-30 14:54:34 +00:00
if iterationErr != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , iterationErr )
2026-02-23 05:21:39 +00:00
}
2026-03-30 14:54:34 +00:00
knownGroupCount ++
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
if knownGroupCount >= scopedStore . MaxGroups {
return core . E ( operation , core . Sprintf ( "group limit (%d)" , scopedStore . MaxGroups ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
}
return nil
}