Skip to content

Commit d370d3b

Browse files
authored
Add item specific callbacks to OnChangeMap (#442)
1 parent 3fd811a commit d370d3b

2 files changed

Lines changed: 131 additions & 34 deletions

File tree

core/generics/onchangemap/onchangemap.go

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/iotaledger/hive.go/core/generics/constraints"
88
"github.com/iotaledger/hive.go/core/generics/lo"
9+
"github.com/iotaledger/hive.go/core/generics/options"
910
)
1011

1112
// Item represents an item in the OnChangeMap.
@@ -14,53 +15,105 @@ type Item[K comparable, C constraints.ComparableStringer[K]] interface {
1415
Clone() Item[K, C]
1516
}
1617

17-
// OnChangeMap is a map that executes a callback if the map or an item is modified,
18+
// OnChangeMap is a map that executes callbacks if the map or an item is modified,
1819
// in case callbackEnabled is true.
1920
type OnChangeMap[K comparable, C constraints.ComparableStringer[K], I Item[K, C]] struct {
2021
mutex sync.RWMutex
2122

22-
m map[K]I
23-
callback func([]I) error
24-
callbackEnabled bool
23+
m map[K]I
24+
callbacksEnabled bool
25+
26+
changedCallback func([]I) error
27+
itemAddedCallback func(I) error
28+
itemModifiedCallback func(I) error
29+
itemDeletedCallback func(I) error
2530
}
2631

27-
// NewOnChangeMap creates a new OnChangeMap.
28-
func NewOnChangeMap[K comparable, C constraints.ComparableStringer[K], I Item[K, C]](callback func([]I) error) *OnChangeMap[K, C, I] {
29-
return &OnChangeMap[K, C, I]{
30-
m: make(map[K]I),
31-
callback: callback,
32-
callbackEnabled: false,
32+
// WithChangedCallback is triggered when something in the OnChangeMap is changed (added/modified/deleted).
33+
func WithChangedCallback[K comparable, C constraints.ComparableStringer[K], I Item[K, C]](changedCallback func([]I) error) options.Option[OnChangeMap[K, C, I]] {
34+
return func(r *OnChangeMap[K, C, I]) {
35+
r.changedCallback = changedCallback
36+
}
37+
}
38+
39+
// WithItemAddedCallback is triggered when a new item is added.
40+
func WithItemAddedCallback[K comparable, C constraints.ComparableStringer[K], I Item[K, C]](itemAddedCallback func(I) error) options.Option[OnChangeMap[K, C, I]] {
41+
return func(r *OnChangeMap[K, C, I]) {
42+
r.itemAddedCallback = itemAddedCallback
43+
}
44+
}
45+
46+
// WithItemModifiedCallback is triggered when an item is modified.
47+
func WithItemModifiedCallback[K comparable, C constraints.ComparableStringer[K], I Item[K, C]](itemModifiedCallback func(I) error) options.Option[OnChangeMap[K, C, I]] {
48+
return func(r *OnChangeMap[K, C, I]) {
49+
r.itemModifiedCallback = itemModifiedCallback
50+
}
51+
}
52+
53+
// WithItemDeletedCallback is triggered when an item is deleted.
54+
func WithItemDeletedCallback[K comparable, C constraints.ComparableStringer[K], I Item[K, C]](itemDeletedCallback func(I) error) options.Option[OnChangeMap[K, C, I]] {
55+
return func(r *OnChangeMap[K, C, I]) {
56+
r.itemDeletedCallback = itemDeletedCallback
3357
}
3458
}
3559

36-
// CallbackEnabled sets whether executing the callback on change is active or not.
37-
func (r *OnChangeMap[K, C, I]) CallbackEnabled(enabled bool) {
38-
r.callbackEnabled = enabled
60+
// NewOnChangeMap creates a new OnChangeMap.
61+
func NewOnChangeMap[K comparable, C constraints.ComparableStringer[K], I Item[K, C]](opts ...options.Option[OnChangeMap[K, C, I]]) *OnChangeMap[K, C, I] {
62+
return options.Apply(&OnChangeMap[K, C, I]{
63+
m: make(map[K]I),
64+
callbacksEnabled: false,
65+
changedCallback: nil,
66+
itemAddedCallback: nil,
67+
itemModifiedCallback: nil,
68+
itemDeletedCallback: nil,
69+
}, opts)
70+
}
71+
72+
// CallbacksEnabled sets whether executing the callbacks on change is active or not.
73+
func (r *OnChangeMap[K, C, I]) CallbacksEnabled(enabled bool) {
74+
r.callbacksEnabled = enabled
3975
}
4076

41-
// executeCallbackWithoutLocking calls the callback if callbackEnabled is true.
42-
func (r *OnChangeMap[K, C, I]) executeCallbackWithoutLocking() error {
43-
if !r.callbackEnabled {
77+
// executeChangedCallback calls the changedCallback if callbackEnabled is true.
78+
func (r *OnChangeMap[K, C, I]) executeChangedCallback() error {
79+
if !r.callbacksEnabled {
4480
return nil
4581
}
4682

47-
if r.callback == nil {
83+
if r.changedCallback != nil {
84+
if err := r.changedCallback(lo.Values(r.m)); err != nil {
85+
return fmt.Errorf("failed to execute callback in OnChangeMap: %w", err)
86+
}
87+
}
88+
89+
return nil
90+
}
91+
92+
// executeItemCallback calls the given callback if callbackEnabled is true.
93+
func (r *OnChangeMap[K, C, I]) executeItemCallback(callback func(I) error, item I) error {
94+
if !r.callbacksEnabled {
4895
return nil
4996
}
5097

51-
if err := r.callback(lo.Values(r.m)); err != nil {
52-
return fmt.Errorf("failed to execute callback in OnChangeMap: %w", err)
98+
if err := r.executeChangedCallback(); err != nil {
99+
return err
100+
}
101+
102+
if callback != nil {
103+
if err := callback(item); err != nil {
104+
return fmt.Errorf("failed to execute item callback in OnChangeMap: %w", err)
105+
}
53106
}
54107

55108
return nil
56109
}
57110

58-
// ExecuteCallback calls the callback if callbackEnabled is true.
59-
func (r *OnChangeMap[K, C, I]) ExecuteCallback() error {
111+
// ExecuteChangedCallback calls the changedCallback if callbackEnabled is true.
112+
func (r *OnChangeMap[K, C, I]) ExecuteChangedCallback() error {
60113
r.mutex.RLock()
61114
defer r.mutex.RUnlock()
62115

63-
return r.executeCallbackWithoutLocking()
116+
return r.executeChangedCallback()
64117
}
65118

66119
// All returns a copy of all items.
@@ -100,7 +153,7 @@ func (r *OnChangeMap[K, C, I]) Add(item I) error {
100153

101154
r.m[item.ID().Key()] = item
102155

103-
return r.executeCallbackWithoutLocking()
156+
return r.executeItemCallback(r.itemAddedCallback, item)
104157
}
105158

106159
// Modify modifies an item in the map and returns a copy.
@@ -118,19 +171,20 @@ func (r *OnChangeMap[K, C, I]) Modify(id C, callback func(item I) bool) (I, erro
118171
return item.Clone().(I), nil
119172
}
120173

121-
return item.Clone().(I), r.executeCallbackWithoutLocking()
174+
return item.Clone().(I), r.executeItemCallback(r.itemModifiedCallback, item)
122175
}
123176

124177
// Delete removes an item from the map.
125178
func (r *OnChangeMap[K, C, I]) Delete(id C) error {
126179
r.mutex.Lock()
127180
defer r.mutex.Unlock()
128181

129-
if _, exists := r.m[id.Key()]; !exists {
182+
item, exists := r.m[id.Key()]
183+
if !exists {
130184
return fmt.Errorf("unable to remove item: \"%s\" does not exist in map", id)
131185
}
132186

133187
delete(r.m, id.Key())
134188

135-
return r.executeCallbackWithoutLocking()
189+
return r.executeItemCallback(r.itemDeletedCallback, item)
136190
}

core/generics/onchangemap/onchangemap_test.go

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,39 @@ func (i *testItem) Clone() Item[int, comparableInteger] {
4242

4343
func TestOnChangeMap(t *testing.T) {
4444
var storedItems []*testItem
45-
46-
onChangeMap := NewOnChangeMap[int, comparableInteger](func(items []*testItem) error {
47-
storedItems = items
48-
return nil
49-
})
45+
var itemAdded *testItem
46+
var itemModified *testItem
47+
var itemDeleted *testItem
48+
49+
onChangeMap := NewOnChangeMap(
50+
WithChangedCallback[int, comparableInteger](func(items []*testItem) error {
51+
storedItems = items
52+
return nil
53+
}),
54+
WithItemAddedCallback[int, comparableInteger](func(item *testItem) error {
55+
itemAdded = item
56+
return nil
57+
}),
58+
WithItemModifiedCallback[int, comparableInteger](func(item *testItem) error {
59+
itemModified = item
60+
return nil
61+
}),
62+
WithItemDeletedCallback[int, comparableInteger](func(item *testItem) error {
63+
itemDeleted = item
64+
return nil
65+
}),
66+
)
5067

5168
require.NotNil(t, onChangeMap)
5269

5370
item1 := newTestItem(1, "one")
5471
item2 := newTestItem(2, "two")
5572
item3 := newTestItem(3, "three")
5673

74+
require.Nil(t, itemAdded)
75+
require.Nil(t, itemModified)
76+
require.Nil(t, itemDeleted)
77+
5778
// add and get an item
5879
err := onChangeMap.Add(item1)
5980
require.NoError(t, err)
@@ -74,28 +95,39 @@ func TestOnChangeMap(t *testing.T) {
7495

7596
// check store on change
7697
require.Equal(t, len(storedItems), 0)
77-
err = onChangeMap.ExecuteCallback()
98+
err = onChangeMap.ExecuteChangedCallback()
7899
require.NoError(t, err)
79100
require.Equal(t, len(storedItems), 0)
80101

81102
// enable store on changed
82-
onChangeMap.CallbackEnabled(true)
83-
err = onChangeMap.ExecuteCallback()
103+
onChangeMap.CallbacksEnabled(true)
104+
err = onChangeMap.ExecuteChangedCallback()
84105
require.NoError(t, err)
85106
require.Equal(t, len(storedItems), 1)
86107

87108
// add duplicate
88109
err = onChangeMap.Add(item1)
89110
require.Error(t, err)
111+
require.Nil(t, itemAdded)
112+
require.Nil(t, itemModified)
113+
require.Nil(t, itemDeleted)
114+
itemAdded = nil
90115

91116
// add second item
92117
err = onChangeMap.Add(item2)
93118
require.NoError(t, err)
94119
require.Equal(t, len(storedItems), 2)
120+
require.NotNil(t, itemAdded)
121+
require.Nil(t, itemModified)
122+
require.Nil(t, itemDeleted)
123+
itemAdded = nil
95124

96125
// modify non existing item
97126
_, err = onChangeMap.Modify(3, nil)
98127
require.Error(t, err)
128+
require.Nil(t, itemAdded)
129+
require.Nil(t, itemModified)
130+
require.Nil(t, itemDeleted)
99131

100132
// modify existing item
101133
item2Copy, err := onChangeMap.Modify(2, func(item *testItem) bool {
@@ -105,6 +137,10 @@ func TestOnChangeMap(t *testing.T) {
105137
require.NoError(t, err)
106138
require.Equal(t, len(storedItems), 2)
107139
require.Equal(t, item2Copy.value, item3.value)
140+
require.Nil(t, itemAdded)
141+
require.NotNil(t, itemModified)
142+
require.Nil(t, itemDeleted)
143+
itemModified = nil
108144

109145
// get modified item
110146
item2Copy, err = onChangeMap.Get(2)
@@ -115,8 +151,15 @@ func TestOnChangeMap(t *testing.T) {
115151
err = onChangeMap.Delete(2)
116152
require.NoError(t, err)
117153
require.Equal(t, len(storedItems), 1)
154+
require.Nil(t, itemAdded)
155+
require.Nil(t, itemModified)
156+
require.NotNil(t, itemDeleted)
157+
itemDeleted = nil
118158

119159
// delete non-existing item
120160
err = onChangeMap.Delete(2)
121161
require.Error(t, err)
162+
require.Nil(t, itemAdded)
163+
require.Nil(t, itemModified)
164+
require.Nil(t, itemDeleted)
122165
}

0 commit comments

Comments
 (0)