htmgo/framework/h/cache.go
2024-10-25 21:59:17 -05:00

403 lines
11 KiB
Go

package h
import (
"flag"
"log/slog"
"sync"
"time"
)
type CachedNode struct {
cb func() *Element
isByKey bool
byKeyCache map[any]*Entry
byKeyExpiration map[any]time.Time
mutex sync.Mutex
duration time.Duration
expiration time.Time
html string
}
type Entry struct {
expiration time.Time
html string
}
type GetElementFunc func() *Element
type GetElementFuncT[T any] func(T) *Element
type GetElementFuncT2[T any, T2 any] func(T, T2) *Element
type GetElementFuncT3[T any, T2 any, T3 any] func(T, T2, T3) *Element
type GetElementFuncT4[T any, T2 any, T3 any, T4 any] func(T, T2, T3, T4) *Element
type GetElementFuncWithKey[K comparable] func() (K, GetElementFunc)
type GetElementFuncTWithKey[K comparable, T any] func(T) (K, GetElementFunc)
type GetElementFuncT2WithKey[K comparable, T any, T2 any] func(T, T2) (K, GetElementFunc)
type GetElementFuncT3WithKey[K comparable, T any, T2 any, T3 any] func(T, T2, T3) (K, GetElementFunc)
type GetElementFuncT4WithKey[K comparable, T any, T2 any, T3 any, T4 any] func(T, T2, T3, T4) (K, GetElementFunc)
func startExpiredCacheCleaner(node *CachedNode) {
isTests := flag.Lookup("test.v") != nil
go func() {
for {
if isTests {
time.Sleep(time.Second)
} else {
time.Sleep(time.Minute)
}
node.ClearExpired()
}
}()
}
// Cached caches the given element for the given duration. The element is only rendered once, and then cached for the given duration.
// Please note this element is globally cached, and not per unique identifier / user.
// Use CachedPerKey to cache elements per unqiue identifier.
func Cached(duration time.Duration, cb GetElementFunc) func() *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
cb: cb,
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func() *Element {
return element
}
}
// CachedPerKey caches the given element for the given duration. The element is only rendered once per key, and then cached for the given duration.
// The element is cached by the unique identifier that is returned by the callback function.
func CachedPerKey[K comparable](duration time.Duration, cb GetElementFuncWithKey[K]) func() *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
isByKey: true,
cb: nil,
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func() *Element {
key, componentFunc := cb()
return &Element{
tag: CachedNodeByKeyEntry,
meta: &ByKeyEntry{
key: key,
parent: element,
cb: componentFunc,
},
}
}
}
type ByKeyEntry struct {
key any
cb func() *Element
parent *Element
}
// CachedPerKeyT caches the given element for the given duration. The element is only rendered once per key, and then cached for the given duration.
// The element is cached by the unique identifier that is returned by the callback function.
func CachedPerKeyT[K comparable, T any](duration time.Duration, cb GetElementFuncTWithKey[K, T]) func(T) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
isByKey: true,
cb: nil,
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T) *Element {
key, componentFunc := cb(data)
return &Element{
tag: CachedNodeByKeyEntry,
meta: &ByKeyEntry{
key: key,
parent: element,
cb: componentFunc,
},
}
}
}
// CachedPerKeyT2 caches the given element for the given duration. The element is only rendered once per key, and then cached for the given duration.
// The element is cached by the unique identifier that is returned by the callback function.
func CachedPerKeyT2[K comparable, T any, T2 any](duration time.Duration, cb GetElementFuncT2WithKey[K, T, T2]) func(T, T2) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
isByKey: true,
cb: nil,
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2) *Element {
key, componentFunc := cb(data, data2)
return &Element{
tag: CachedNodeByKeyEntry,
meta: &ByKeyEntry{
key: key,
parent: element,
cb: componentFunc,
},
}
}
}
// CachedPerKeyT3 caches the given element for the given duration. The element is only rendered once per key, and then cached for the given duration.
// The element is cached by the unique identifier that is returned by the callback function.
func CachedPerKeyT3[K comparable, T any, T2 any, T3 any](duration time.Duration, cb GetElementFuncT3WithKey[K, T, T2, T3]) func(T, T2, T3) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
isByKey: true,
cb: nil,
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3) *Element {
key, componentFunc := cb(data, data2, data3)
return &Element{
tag: CachedNodeByKeyEntry,
meta: &ByKeyEntry{
key: key,
parent: element,
cb: componentFunc,
},
}
}
}
// CachedPerKeyT4 caches the given element for the given duration. The element is only rendered once per key, and then cached for the given duration.
// The element is cached by the unique identifier that is returned by the callback function.
func CachedPerKeyT4[K comparable, T any, T2 any, T3 any, T4 any](duration time.Duration, cb GetElementFuncT4WithKey[K, T, T2, T3, T4]) func(T, T2, T3, T4) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
isByKey: true,
cb: nil,
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3, data4 T4) *Element {
key, componentFunc := cb(data, data2, data3, data4)
return &Element{
tag: CachedNodeByKeyEntry,
meta: &ByKeyEntry{
key: key,
parent: element,
cb: componentFunc,
},
}
}
}
// CachedT caches the given element for the given duration. The element is only rendered once, and then cached for the given duration.
// Please note this element is globally cached, and not per unique identifier / user.
// Use CachedPerKey to cache elements per unqiue identifier.
func CachedT[T any](duration time.Duration, cb GetElementFuncT[T]) func(T) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
html: "",
duration: duration,
mutex: sync.Mutex{},
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T) *Element {
element.meta.(*CachedNode).cb = func() *Element {
return cb(data)
}
return element
}
}
// CachedT2 caches the given element for the given duration. The element is only rendered once, and then cached for the given duration.
// Please note this element is globally cached, and not per unique identifier / user.
// Use CachedPerKey to cache elements per unqiue identifier.
func CachedT2[T any, T2 any](duration time.Duration, cb GetElementFuncT2[T, T2]) func(T, T2) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2) *Element {
element.meta.(*CachedNode).cb = func() *Element {
return cb(data, data2)
}
return element
}
}
// CachedT3 caches the given element for the given duration. The element is only rendered once, and then cached for the given duration.
// Please note this element is globally cached, and not per unique identifier / user.
// Use CachedPerKey to cache elements per unqiue identifier.
func CachedT3[T any, T2 any, T3 any](duration time.Duration, cb GetElementFuncT3[T, T2, T3]) func(T, T2, T3) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3) *Element {
element.meta.(*CachedNode).cb = func() *Element {
return cb(data, data2, data3)
}
return element
}
}
// CachedT4 caches the given element for the given duration. The element is only rendered once, and then cached for the given duration.
// Please note this element is globally cached, and not per unique identifier / user.
// Use CachedPerKey to cache elements per unqiue identifier.
func CachedT4[T any, T2 any, T3 any, T4 any](duration time.Duration, cb GetElementFuncT4[T, T2, T3, T4]) func(T, T2, T3, T4) *Element {
element := &Element{
tag: CachedNodeTag,
meta: &CachedNode{
html: "",
duration: duration,
},
}
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3, data4 T4) *Element {
element.meta.(*CachedNode).cb = func() *Element {
return cb(data, data2, data3, data4)
}
return element
}
}
// ClearCache clears the cached HTML of the element. This is called automatically by the framework.
func (c *CachedNode) ClearCache() {
c.html = ""
if c.byKeyCache != nil {
for key := range c.byKeyCache {
delete(c.byKeyCache, key)
}
}
if c.byKeyExpiration != nil {
for key := range c.byKeyExpiration {
delete(c.byKeyExpiration, key)
}
}
}
// ClearExpired clears all expired cached HTML of the element. This is called automatically by the framework.
func (c *CachedNode) ClearExpired() {
c.mutex.Lock()
defer c.mutex.Unlock()
deletedCount := 0
if c.isByKey == true {
if c.byKeyCache != nil && c.byKeyExpiration != nil {
for key := range c.byKeyCache {
expir, ok := c.byKeyExpiration[key]
if ok && expir.Before(time.Now()) {
delete(c.byKeyCache, key)
delete(c.byKeyExpiration, key)
deletedCount++
}
}
}
} else {
now := time.Now()
expiration := c.expiration
if c.html != "" && expiration.Before(now) {
c.html = ""
deletedCount++
}
}
if deletedCount > 0 {
slog.Debug("Deleted expired cache entries", slog.Int("count", deletedCount))
}
}
func (c *CachedNode) Render(ctx *RenderContext) {
if c.isByKey == true {
panic("CachedPerKey should not be rendered directly")
} else {
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
expiration := c.expiration
if expiration.IsZero() || expiration.Before(now) {
c.html = ""
c.expiration = now.Add(c.duration)
}
if c.html != "" {
ctx.builder.WriteString(c.html)
} else {
c.html = Render(c.cb())
ctx.builder.WriteString(c.html)
}
}
}
func (c *ByKeyEntry) Render(ctx *RenderContext) {
key := c.key
parentMeta := c.parent.meta.(*CachedNode)
parentMeta.mutex.Lock()
defer parentMeta.mutex.Unlock()
if parentMeta.byKeyCache == nil {
parentMeta.byKeyCache = make(map[any]*Entry)
}
if parentMeta.byKeyExpiration == nil {
parentMeta.byKeyExpiration = make(map[any]time.Time)
}
var setAndWrite = func() {
html := Render(c.cb())
parentMeta.byKeyCache[key] = &Entry{
expiration: parentMeta.expiration,
html: html,
}
ctx.builder.WriteString(html)
}
expEntry, ok := parentMeta.byKeyExpiration[key]
if !ok {
parentMeta.byKeyExpiration[key] = time.Now().Add(parentMeta.duration)
} else {
// key is expired
if expEntry.Before(time.Now()) {
parentMeta.byKeyExpiration[key] = time.Now().Add(parentMeta.duration)
setAndWrite()
return
}
}
entry := parentMeta.byKeyCache[key]
// not in cache
if entry == nil {
setAndWrite()
return
}
// exists in cache and not expired
ctx.builder.WriteString(entry.html)
}