github.com/mitranim/gg@v0.1.17/mem.go (about) 1 package gg 2 3 import ( 4 "sync" 5 "time" 6 ) 7 8 /* 9 Tool for deduplicating and caching expensive work. All methods are safe for 10 concurrent use. The first type parameter is used to determine expiration 11 duration, and should typically be a zero-sized stateless type, such as 12 `DurSecond`, `DurMinute`, `DurHour` provided by this package. The given type 13 `Tar` must implement `Initer` on its pointer type: `(*Tar).Init`. The init 14 method is used to populate data whenever it's missing or expired. See methods 15 `Mem.Get` and `Mem.Peek`. A zero value of `Mem` is ready for use. 16 Contains a synchronization primitive and must not be copied. 17 18 Usage example: 19 20 type Model struct { Id string } 21 22 type DatModels []Model 23 24 // Pretend that this is expensive. 25 func (self *DatModels) Init() { *self = DatModels{{10}, {20}, {30}} } 26 27 type Dat struct { 28 Models gg.Mem[gg.DurHour, DatModels, *DatModels] 29 // ... other caches 30 } 31 32 var dat Dat 33 34 func init() { fmt.Println(dat.Models.Get()) } 35 */ 36 type Mem[Dur Durationer, Tar any, Ptr IniterPtr[Tar]] struct { 37 timed Timed[Tar] 38 lock sync.RWMutex 39 } 40 41 /* 42 Returns the inner value after ensuring it's initialized and not expired. If the 43 data is missing or expired, it's initialized by calling `(*Tar).Init`. Otherwise 44 the data is returned as-is. 45 46 This method avoids redundant concurrent work. When called concurrently by 47 multiple goroutines, only 1 goroutine performs work, while the others simply 48 wait for it. 49 50 Expiration is determined by consulting the `Dur` type provided to `Mem` as its 51 first type parameter, calling `Dur.Duration` on a zero value. 52 53 Method `(*Tar).Init` is always called on a new pointer to a zero value, for 54 multiple reasons. If `(*Tar).Init` appends data to the target instead of 55 replacing it, this avoids accumulating redundant data and leaking memory. 56 Additionally, this avoids the possibility of concurrent modification 57 (between `Mem` and its callers) that could lead to observing an inconsistent 58 state of the data. 59 60 Compare `Mem.Peek` which does not perform initialization. 61 */ 62 func (self *Mem[_, Tar, _]) Get() Tar { return self.Timed().Get() } 63 64 /* 65 Same as `Mem.Get` but returns `Timed` which contains both the inner value and 66 the timestamp at which it was generated or set. 67 */ 68 func (self *Mem[_, Tar, Ptr]) Timed() Timed[Tar] { 69 dur := self.Duration() 70 val := self.PeekTimed() 71 if !val.IsExpired(dur) { 72 return val 73 } 74 75 defer Lock(&self.lock).Unlock() 76 timed := &self.timed 77 78 if timed.IsExpired(dur) { 79 // See comment on `Mem.Get` why we avoid calling this on `&self.timed.Val`. 80 var tar Tar 81 Ptr(&tar).Init() 82 timed.Set(tar) 83 } 84 return *timed 85 } 86 87 /* 88 Similar to `Mem.Get` but returns the inner value as-is, without performing 89 initialization. 90 */ 91 func (self *Mem[_, Tar, _]) Peek() Tar { return self.PeekTimed().Get() } 92 93 /* 94 Similar to `Mem.Timed` but returns inner `Timed` as-is, without performing 95 initialization. The result contains the current state of the data, and the 96 timestamp at which the value was last set. If the data has not been initialized 97 or set, the timestamp is zero. 98 */ 99 func (self *Mem[_, Tar, _]) PeekTimed() Timed[Tar] { 100 defer Lock(self.lock.RLocker()).Unlock() 101 return self.timed 102 } 103 104 // Clears the inner value and timestamp. 105 func (self *Mem[_, _, _]) Clear() { 106 defer Lock(&self.lock).Unlock() 107 self.timed.Clear() 108 } 109 110 /* 111 Implement `Durationer`. Shortcut for `Zero[Dur]().Duration()` using the first 112 type parameter provided to this type. For internal use. 113 */ 114 func (*Mem[Dur, _, _]) Duration() time.Duration { return Zero[Dur]().Duration() } 115 116 /* 117 Implement `json.Marshaler` by proxying to `Timed.MarshalJSON` on the inner 118 instance of `.Timed`. Like other methods, this is safe for concurrent use. 119 */ 120 func (self *Mem[_, _, _]) MarshalJSON() ([]byte, error) { 121 defer Lock(&self.lock).Unlock() 122 return self.timed.MarshalJSON() 123 } 124 125 /* 126 Implement `json.Unmarshaler` by proxying to `Timed.UnmarshalJSON` on the inner 127 instance of `.Timed`. Like other methods, this is safe for concurrent use. 128 */ 129 func (self *Mem[_, _, _]) UnmarshalJSON(src []byte) error { 130 defer Lock(&self.lock).Unlock() 131 return self.timed.UnmarshalJSON(src) 132 } 133 134 /* 135 Implements `Durationer` by returning `time.Second`. This type is zero-sized, and 136 can be embedded in other types, like a mixin, at no additional cost. 137 */ 138 type DurSecond struct{} 139 140 // Implement `Durationer` by returning `time.Second`. 141 func (DurSecond) Duration() time.Duration { return time.Second } 142 143 /* 144 Implements `Durationer` by returning `time.Minute`. This type is zero-sized, and 145 can be embedded in other types, like a mixin, at no additional cost. 146 */ 147 type DurMinute struct{} 148 149 // Implement `Durationer` by returning `time.Minute`. 150 func (DurMinute) Duration() time.Duration { return time.Minute } 151 152 /* 153 Implements `Durationer` by returning `time.Hour`. This type is zero-sized, and 154 can be embedded in other types, like a mixin, at no additional cost. 155 */ 156 type DurHour struct{} 157 158 // Implement `Durationer` by returning `time.Hour`. 159 func (DurHour) Duration() time.Duration { return time.Hour }