From 303b75444d99c4829e3f59f1045b20b1f1e2f7ad Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 06:55:37 +0000 Subject: [PATCH] feat(scope): add scoped event delegation Co-Authored-By: Virgil --- scope.go | 37 +++++++++++++++++++++++++++++++++++++ scope_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/scope.go b/scope.go index 0ab34bc..5ff39e7 100644 --- a/scope.go +++ b/scope.go @@ -279,6 +279,43 @@ func (scopedStore *ScopedStore) GetFields(group, key string) (iter.Seq[string], return storeInstance.GetFields(scopedStore.namespacedGroup(group), key) } +// Usage example: `events := scopedStore.Watch("config")` +func (scopedStore *ScopedStore) Watch(group string) <-chan Event { + storeInstance, err := scopedStore.storeInstance("store.Watch") + if err != nil { + return closedEventChannel() + } + return storeInstance.Watch(scopedStore.namespacedGroup(group)) +} + +// Usage example: `scopedStore.Unwatch("config", events)` +func (scopedStore *ScopedStore) Unwatch(group string, events <-chan Event) { + storeInstance, err := scopedStore.storeInstance("store.Unwatch") + if err != nil { + return + } + storeInstance.Unwatch(scopedStore.namespacedGroup(group), events) +} + +// Usage example: `unregister := scopedStore.OnChange(func(event store.Event) { fmt.Println(event.Group, event.Key) })` +func (scopedStore *ScopedStore) OnChange(callback func(Event)) func() { + storeInstance, err := scopedStore.storeInstance("store.OnChange") + if err != nil { + return func() {} + } + if callback == nil { + return func() {} + } + + namespacePrefix := scopedStore.namespacePrefix() + return storeInstance.OnChange(func(event Event) { + if !core.HasPrefix(event.Group, namespacePrefix) { + return + } + callback(event) + }) +} + // Usage example: `removedRows, err := scopedStore.PurgeExpired(); if err != nil { return }; fmt.Println(removedRows)` func (scopedStore *ScopedStore) PurgeExpired() (int64, error) { storeInstance, err := scopedStore.storeInstance("store.PurgeExpired") diff --git a/scope_test.go b/scope_test.go index 1d5b76b..0c5f3e8 100644 --- a/scope_test.go +++ b/scope_test.go @@ -471,6 +471,41 @@ func TestScope_ScopedStore_Good_PurgeExpired_NamespaceLocal(t *testing.T) { assert.Equal(t, 1, rawEntryCount(t, storeInstance, "tenant-b:session")) } +func TestScope_ScopedStore_Good_WatchAndUnwatch(t *testing.T) { + storeInstance, _ := New(":memory:") + defer storeInstance.Close() + + scopedStore := mustScoped(t, storeInstance, "tenant-a") + events := scopedStore.Watch("config") + scopedStore.Unwatch("config", events) + + _, open := <-events + assert.False(t, open, "channel should be closed after Unwatch") + + require.NoError(t, scopedStore.SetIn("config", "theme", "dark")) +} + +func TestScope_ScopedStore_Good_OnChange(t *testing.T) { + storeInstance, _ := New(":memory:") + defer storeInstance.Close() + + scopedStore := mustScoped(t, storeInstance, "tenant-a") + + var seen []Event + unregister := scopedStore.OnChange(func(event Event) { + seen = append(seen, event) + }) + defer unregister() + + require.NoError(t, scopedStore.SetIn("config", "theme", "dark")) + require.NoError(t, storeInstance.Set("other", "key", "value")) + + require.Len(t, seen, 1) + assert.Equal(t, "tenant-a:config", seen[0].Group) + assert.Equal(t, "theme", seen[0].Key) + assert.Equal(t, "dark", seen[0].Value) +} + // --------------------------------------------------------------------------- // Quota enforcement — MaxKeys // --------------------------------------------------------------------------- -- 2.45.3