2024-09-28 02:29:53 +00:00
|
|
|
package h
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"time"
|
2025-06-26 18:38:38 +00:00
|
|
|
|
|
|
|
|
"github.com/maddalax/htmgo/framework/h/cache"
|
2024-09-28 02:29:53 +00:00
|
|
|
)
|
|
|
|
|
|
2025-06-26 18:38:38 +00:00
|
|
|
// A single key to represent the cache entry for non-per-key components.
|
|
|
|
|
const _singleCacheKey = "__htmgo_single_cache_key__"
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
type CachedNode struct {
|
2025-06-26 18:38:38 +00:00
|
|
|
cb func() *Element
|
|
|
|
|
isByKey bool
|
|
|
|
|
duration time.Duration
|
|
|
|
|
cache cache.Store[any, string]
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
type Entry struct {
|
|
|
|
|
expiration time.Time
|
|
|
|
|
html string
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
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
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
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)
|
|
|
|
|
|
2025-06-26 18:38:38 +00:00
|
|
|
// CacheOption defines a function that configures a CachedNode.
|
|
|
|
|
type CacheOption func(*CachedNode)
|
|
|
|
|
|
|
|
|
|
// WithStore allows providing a custom cache implementation for a cached component.
|
|
|
|
|
func WithStore(store cache.Store[any, string]) CacheOption {
|
|
|
|
|
return func(c *CachedNode) {
|
|
|
|
|
c.cache = store
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DefaultCacheProvider is a package-level function that creates a default cache instance.
|
|
|
|
|
// Initially, this uses a TTL-based map cache, but could be swapped for an LRU cache later.
|
|
|
|
|
// Advanced users can override this for the entire application.
|
|
|
|
|
var DefaultCacheProvider = func() cache.Store[any, string] {
|
|
|
|
|
return cache.NewTTLStore[any, string]()
|
2024-09-28 17:38:42 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
// Use CachedPerKey to cache elements per unique identifier.
|
|
|
|
|
func Cached(duration time.Duration, cb GetElementFunc, opts ...CacheOption) func() *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
cb: cb,
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
return func() *Element {
|
|
|
|
|
return element
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
func CachedPerKey[K comparable](duration time.Duration, cb GetElementFuncWithKey[K], opts ...CacheOption) func() *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
isByKey: true,
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
func CachedPerKeyT[K comparable, T any](duration time.Duration, cb GetElementFuncTWithKey[K, T], opts ...CacheOption) func(T) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
isByKey: true,
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
return func(data T) *Element {
|
|
|
|
|
key, componentFunc := cb(data)
|
|
|
|
|
return &Element{
|
|
|
|
|
tag: CachedNodeByKeyEntry,
|
|
|
|
|
meta: &ByKeyEntry{
|
|
|
|
|
key: key,
|
|
|
|
|
parent: element,
|
|
|
|
|
cb: componentFunc,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
func CachedPerKeyT2[K comparable, T any, T2 any](duration time.Duration, cb GetElementFuncT2WithKey[K, T, T2], opts ...CacheOption) func(T, T2) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
isByKey: true,
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
return func(data T, data2 T2) *Element {
|
|
|
|
|
key, componentFunc := cb(data, data2)
|
|
|
|
|
return &Element{
|
|
|
|
|
tag: CachedNodeByKeyEntry,
|
|
|
|
|
meta: &ByKeyEntry{
|
|
|
|
|
key: key,
|
|
|
|
|
parent: element,
|
|
|
|
|
cb: componentFunc,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
func CachedPerKeyT3[K comparable, T any, T2 any, T3 any](duration time.Duration, cb GetElementFuncT3WithKey[K, T, T2, T3], opts ...CacheOption) func(T, T2, T3) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
isByKey: true,
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
func CachedPerKeyT4[K comparable, T any, T2 any, T3 any, T4 any](duration time.Duration, cb GetElementFuncT4WithKey[K, T, T2, T3, T4], opts ...CacheOption) func(T, T2, T3, T4) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
isByKey: true,
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
// Use CachedPerKey to cache elements per unique identifier.
|
|
|
|
|
func CachedT[T any](duration time.Duration, cb GetElementFuncT[T], opts ...CacheOption) func(T) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
return func(data T) *Element {
|
2025-06-26 18:38:38 +00:00
|
|
|
node.cb = func() *Element {
|
2024-09-28 02:29:53 +00:00
|
|
|
return cb(data)
|
|
|
|
|
}
|
|
|
|
|
return element
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
// Use CachedPerKey to cache elements per unique identifier.
|
|
|
|
|
func CachedT2[T any, T2 any](duration time.Duration, cb GetElementFuncT2[T, T2], opts ...CacheOption) func(T, T2) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|
2025-06-26 18:38:38 +00:00
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
return func(data T, data2 T2) *Element {
|
2025-06-26 18:38:38 +00:00
|
|
|
node.cb = func() *Element {
|
2024-09-28 02:29:53 +00:00
|
|
|
return cb(data, data2)
|
|
|
|
|
}
|
|
|
|
|
return element
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
// Use CachedPerKey to cache elements per unique identifier.
|
|
|
|
|
func CachedT3[T any, T2 any, T3 any](duration time.Duration, cb GetElementFuncT3[T, T2, T3], opts ...CacheOption) func(T, T2, T3) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|
2025-06-26 18:38:38 +00:00
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
return func(data T, data2 T2, data3 T3) *Element {
|
2025-06-26 18:38:38 +00:00
|
|
|
node.cb = func() *Element {
|
2024-09-28 02:29:53 +00:00
|
|
|
return cb(data, data2, data3)
|
|
|
|
|
}
|
|
|
|
|
return element
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// 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.
|
2025-06-26 18:38:38 +00:00
|
|
|
// Use CachedPerKey to cache elements per unique identifier.
|
|
|
|
|
func CachedT4[T any, T2 any, T3 any, T4 any](duration time.Duration, cb GetElementFuncT4[T, T2, T3, T4], opts ...CacheOption) func(T, T2, T3, T4) *Element {
|
|
|
|
|
node := &CachedNode{
|
|
|
|
|
duration: duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.cache == nil {
|
|
|
|
|
node.cache = DefaultCacheProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
element := &Element{
|
2025-06-26 18:38:38 +00:00
|
|
|
tag: CachedNodeTag,
|
|
|
|
|
meta: node,
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|
2025-06-26 18:38:38 +00:00
|
|
|
|
2024-09-28 02:29:53 +00:00
|
|
|
return func(data T, data2 T2, data3 T3, data4 T4) *Element {
|
2025-06-26 18:38:38 +00:00
|
|
|
node.cb = func() *Element {
|
2024-09-28 02:29:53 +00:00
|
|
|
return cb(data, data2, data3, data4)
|
|
|
|
|
}
|
|
|
|
|
return element
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 02:59:17 +00:00
|
|
|
// ClearCache clears the cached HTML of the element. This is called automatically by the framework.
|
2024-09-28 02:29:53 +00:00
|
|
|
func (c *CachedNode) ClearCache() {
|
2025-06-26 18:38:38 +00:00
|
|
|
c.cache.Purge()
|
2024-09-28 17:38:42 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-26 18:38:38 +00:00
|
|
|
// ClearExpired is deprecated and does nothing. Cache expiration is now handled by the Store implementation.
|
2024-09-28 17:38:42 +00:00
|
|
|
func (c *CachedNode) ClearExpired() {
|
2025-06-26 18:38:38 +00:00
|
|
|
// No-op for backward compatibility
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *CachedNode) Render(ctx *RenderContext) {
|
2024-10-22 13:32:17 +00:00
|
|
|
if c.isByKey {
|
2024-09-28 16:27:07 +00:00
|
|
|
panic("CachedPerKey should not be rendered directly")
|
|
|
|
|
} else {
|
2025-06-26 18:38:38 +00:00
|
|
|
// For simple cached components, we use a single key
|
2025-07-03 14:46:09 +00:00
|
|
|
// Use GetOrCompute for atomic check-and-set
|
|
|
|
|
html := c.cache.GetOrCompute(_singleCacheKey, func() string {
|
|
|
|
|
return Render(c.cb())
|
|
|
|
|
}, c.duration)
|
|
|
|
|
ctx.builder.WriteString(html)
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|
2024-09-28 16:27:07 +00:00
|
|
|
}
|
2024-09-28 02:29:53 +00:00
|
|
|
|
2024-09-28 16:27:07 +00:00
|
|
|
func (c *ByKeyEntry) Render(ctx *RenderContext) {
|
|
|
|
|
key := c.key
|
|
|
|
|
parentMeta := c.parent.meta.(*CachedNode)
|
|
|
|
|
|
2025-07-03 14:46:09 +00:00
|
|
|
// Use GetOrCompute for atomic check-and-set
|
|
|
|
|
html := parentMeta.cache.GetOrCompute(key, func() string {
|
|
|
|
|
return Render(c.cb())
|
|
|
|
|
}, parentMeta.duration)
|
2025-06-26 18:38:38 +00:00
|
|
|
ctx.builder.WriteString(html)
|
2024-09-28 02:29:53 +00:00
|
|
|
}
|