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 }