add caching per key
This commit is contained in:
parent
8c07bd7219
commit
17bb55655e
5 changed files with 462 additions and 28 deletions
|
|
@ -7,17 +7,30 @@ import (
|
|||
|
||||
type CachedNode struct {
|
||||
cb func() *Element
|
||||
isByKey bool
|
||||
byKeyCache map[any]*Entry
|
||||
mutex sync.Mutex
|
||||
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 Cached(duration time.Duration, cb GetElementFunc) func() *Element {
|
||||
element := &Element{
|
||||
tag: CachedNodeTag,
|
||||
|
|
@ -32,6 +45,127 @@ func Cached(duration time.Duration, cb GetElementFunc) func() *Element {
|
|||
}
|
||||
}
|
||||
|
||||
func CachedPerKey[K comparable](duration time.Duration, cb GetElementFuncWithKey[K]) func() *Element {
|
||||
element := &Element{
|
||||
tag: CachedNodeTag,
|
||||
meta: &CachedNode{
|
||||
isByKey: true,
|
||||
cb: nil,
|
||||
html: "",
|
||||
expiration: time.Now().Add(duration),
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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: "",
|
||||
expiration: time.Now().Add(duration),
|
||||
},
|
||||
}
|
||||
return func(data T) *Element {
|
||||
key, componentFunc := cb(data)
|
||||
return &Element{
|
||||
tag: CachedNodeByKeyEntry,
|
||||
meta: &ByKeyEntry{
|
||||
key: key,
|
||||
parent: element,
|
||||
cb: componentFunc,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: "",
|
||||
expiration: time.Now().Add(duration),
|
||||
},
|
||||
}
|
||||
return func(data T, data2 T2) *Element {
|
||||
key, componentFunc := cb(data, data2)
|
||||
return &Element{
|
||||
tag: CachedNodeByKeyEntry,
|
||||
meta: &ByKeyEntry{
|
||||
key: key,
|
||||
parent: element,
|
||||
cb: componentFunc,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: "",
|
||||
expiration: time.Now().Add(duration),
|
||||
},
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: "",
|
||||
expiration: time.Now().Add(duration),
|
||||
},
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CachedT[T any](duration time.Duration, cb GetElementFuncT[T]) func(T) *Element {
|
||||
element := &Element{
|
||||
tag: CachedNodeTag,
|
||||
|
|
@ -102,16 +236,57 @@ func (c *CachedNode) ClearCache() {
|
|||
}
|
||||
|
||||
func (c *CachedNode) Render(ctx *RenderContext) {
|
||||
c.mutex.Lock()
|
||||
if c.expiration.Before(time.Now()) {
|
||||
c.html = ""
|
||||
if c.isByKey == true {
|
||||
panic("CachedPerKey should not be rendered directly")
|
||||
} else {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if c.expiration.Before(time.Now()) {
|
||||
c.html = ""
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if parentMeta.byKeyCache == nil {
|
||||
parentMeta.byKeyCache = make(map[any]*Entry)
|
||||
}
|
||||
|
||||
if c.html != "" {
|
||||
ctx.builder.WriteString(c.html)
|
||||
} else {
|
||||
c.html = Render(c.cb())
|
||||
ctx.builder.WriteString(c.html)
|
||||
entry := parentMeta.byKeyCache[key]
|
||||
|
||||
var setAndWrite = func() {
|
||||
html := Render(c.cb())
|
||||
parentMeta.byKeyCache[key] = &Entry{
|
||||
expiration: parentMeta.expiration,
|
||||
html: html,
|
||||
}
|
||||
ctx.builder.WriteString(html)
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
// exists in cache but expired
|
||||
if entry != nil && entry.expiration.Before(time.Now()) {
|
||||
delete(parentMeta.byKeyCache, key)
|
||||
setAndWrite()
|
||||
return
|
||||
}
|
||||
|
||||
// not in cache
|
||||
if entry == nil {
|
||||
setAndWrite()
|
||||
return
|
||||
}
|
||||
|
||||
// exists in cache and not expired
|
||||
ctx.builder.WriteString(entry.html)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,190 @@ func TestCacheMultiple(t *testing.T) {
|
|||
assert.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestCacheByKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
callCount := 0
|
||||
cachedItem := CachedPerKey(time.Hour, func() (any, GetElementFunc) {
|
||||
key := "key"
|
||||
if callCount == 3 {
|
||||
key = "key2"
|
||||
}
|
||||
if callCount == 4 {
|
||||
key = "key"
|
||||
}
|
||||
callCount++
|
||||
return key, func() *Element {
|
||||
renderCount++
|
||||
return Div(Text("hello"))
|
||||
}
|
||||
})
|
||||
|
||||
Render(Div(
|
||||
cachedItem(),
|
||||
cachedItem(),
|
||||
cachedItem(),
|
||||
cachedItem(),
|
||||
cachedItem(),
|
||||
))
|
||||
|
||||
assert.Equal(t, 5, callCount)
|
||||
assert.Equal(t, 2, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyT(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
cachedItem := CachedPerKeyT(time.Hour, func(key string) (any, GetElementFunc) {
|
||||
return key, func() *Element {
|
||||
renderCount++
|
||||
return Div(Text("hello"))
|
||||
}
|
||||
})
|
||||
|
||||
Render(Div(
|
||||
cachedItem("one"),
|
||||
cachedItem("one"),
|
||||
cachedItem("two"),
|
||||
cachedItem("two"),
|
||||
cachedItem("three"),
|
||||
))
|
||||
|
||||
assert.Equal(t, 3, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyT4(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
cachedItem := CachedPerKeyT4(time.Hour, func(arg1 string, arg2 string, arg3 string, arg4 string) (any, GetElementFunc) {
|
||||
return arg1, func() *Element {
|
||||
renderCount++
|
||||
return Div(Text("hello"))
|
||||
}
|
||||
})
|
||||
|
||||
Render(Div(
|
||||
cachedItem("one", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("one", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("two", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("two", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("three", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
||||
))
|
||||
|
||||
assert.Equal(t, 3, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyT3(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
cachedItem := CachedPerKeyT3(time.Hour, func(arg1 string, arg2 string, arg3 string) (any, GetElementFunc) {
|
||||
return arg1, func() *Element {
|
||||
renderCount++
|
||||
return Div(Text("hello"))
|
||||
}
|
||||
})
|
||||
|
||||
Render(Div(
|
||||
cachedItem("one", uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("one", uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("two", uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("two", uuid.NewString(), uuid.NewString()),
|
||||
cachedItem("three", uuid.NewString(), uuid.NewString()),
|
||||
))
|
||||
|
||||
assert.Equal(t, 3, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyT2(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
cachedItem := CachedPerKeyT2(time.Hour, func(arg1 string, arg2 string) (any, GetElementFunc) {
|
||||
return arg1, func() *Element {
|
||||
renderCount++
|
||||
return Div(Text("hello"))
|
||||
}
|
||||
})
|
||||
|
||||
Render(Div(
|
||||
cachedItem("one", uuid.NewString()),
|
||||
cachedItem("one", uuid.NewString()),
|
||||
cachedItem("two", uuid.NewString()),
|
||||
cachedItem("two", uuid.NewString()),
|
||||
cachedItem("three", uuid.NewString()),
|
||||
))
|
||||
|
||||
assert.Equal(t, 3, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
callCount := 0
|
||||
cachedItem := CachedPerKey(time.Hour, func() (any, GetElementFunc) {
|
||||
key := "key"
|
||||
if callCount == 3 {
|
||||
key = "key2"
|
||||
}
|
||||
if callCount == 4 {
|
||||
key = "key"
|
||||
}
|
||||
callCount++
|
||||
return key, func() *Element {
|
||||
renderCount++
|
||||
return Div(Text("hello"))
|
||||
}
|
||||
})
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
Render(Div(
|
||||
cachedItem(),
|
||||
))
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
assert.Equal(t, 5, callCount)
|
||||
assert.Equal(t, 2, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyT1_2(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
cachedItem := CachedPerKeyT(time.Hour, func(key string) (any, GetElementFunc) {
|
||||
return key, func() *Element {
|
||||
renderCount++
|
||||
return Pf(key)
|
||||
}
|
||||
})
|
||||
|
||||
assert.Equal(t, "<p >one</p>", Render(cachedItem("one")))
|
||||
assert.Equal(t, "<p >two</p>", Render(cachedItem("two")))
|
||||
assert.Equal(t, "<p >two</p>", Render(cachedItem("two")))
|
||||
assert.Equal(t, 2, renderCount)
|
||||
}
|
||||
|
||||
func TestCacheByKeyT1Expired(t *testing.T) {
|
||||
t.Parallel()
|
||||
renderCount := 0
|
||||
cachedItem := CachedPerKeyT(time.Millisecond, func(key string) (any, GetElementFunc) {
|
||||
return key, func() *Element {
|
||||
renderCount++
|
||||
return Pf(key)
|
||||
}
|
||||
})
|
||||
|
||||
assert.Equal(t, "<p >one</p>", Render(cachedItem("one")))
|
||||
assert.Equal(t, "<p >two</p>", Render(cachedItem("two")))
|
||||
time.Sleep(time.Millisecond * 2)
|
||||
assert.Equal(t, "<p >two</p>", Render(cachedItem("two")))
|
||||
assert.Equal(t, 3, renderCount)
|
||||
}
|
||||
|
||||
func BenchmarkMailToStatic(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
ctx := RenderContext{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import (
|
|||
type CustomElement = string
|
||||
|
||||
var (
|
||||
CachedNodeTag CustomElement = "htmgo_cache_node"
|
||||
CachedNodeTag CustomElement = "htmgo_cache_node"
|
||||
CachedNodeByKeyEntry CustomElement = "htmgo_cached_node_by_key_entry"
|
||||
)
|
||||
|
||||
/*
|
||||
|
|
@ -57,6 +58,12 @@ func (node *Element) Render(context *RenderContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if node.tag == CachedNodeByKeyEntry {
|
||||
meta := node.meta.(*ByKeyEntry)
|
||||
meta.Render(context)
|
||||
return
|
||||
}
|
||||
|
||||
if node.tag != "" {
|
||||
context.builder.WriteString("<")
|
||||
context.builder.WriteString(node.tag)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
**Caching Components**
|
||||
**Caching Components Globally**
|
||||
|
||||
You may want to cache components to improve performance. This is especially useful for components that are expensive to render
|
||||
or make external requests for data.
|
||||
|
|
@ -17,6 +17,9 @@ h.CachedT3(duration time.Duration, cb GetElementFunc)
|
|||
// Four arguments passed to the component
|
||||
h.CachedT4(duration time.Duration, cb GetElementFunc)
|
||||
```
|
||||
For caching components per user, see [Caching Components Per User](#performance-caching-per-user).
|
||||
|
||||
<br>
|
||||
|
||||
The `duration` parameter is the time the component should be cached for. The `cb` parameter is a function that returns the component.
|
||||
|
||||
|
|
@ -44,25 +47,10 @@ func IndexPage(ctx *h.RequestContext) *h.Page {
|
|||
}
|
||||
```
|
||||
|
||||
**Real Example:**
|
||||
I want to make a navbar that renders how many github stars my repository has. I don't want to make a request to the GitHub API everytime someone visits my page, so I will cache the component for 15 minutes.
|
||||
```go
|
||||
var CachedGithubStars = h.CachedT(time.Minute*15, func(t *h.RequestContext) *h.Element {
|
||||
return GithubStars(t)
|
||||
})
|
||||
|
||||
func GithubStars(ctx *h.RequestContext) *h.Element {
|
||||
stars := http.Get("https://api.github.com/repos/maddalax/htmgo/stargazers")
|
||||
return h.Div(
|
||||
h.Text(stars),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** We are using CachedT because the component takes one argument, the RequestContext.
|
||||
If the component takes more arguments, use CachedT2, CachedT3, etc.
|
||||
|
||||
**Important**:
|
||||
1. The cached value is stored globally in memory, so it is shared across all requests. Do not store request-specific data in a cached component. Only cache components that you are OK with all users seeing the same data.
|
||||
**Important Note When Using CachedT and NOT CachedPerKeyT:**
|
||||
1. When using h.CachedT(T2, T3, etc) and not **CachedPerKey**, The cached value is stored globally in memory, so it is shared across all requests. Do not store request-specific data in a cached component. Only cache components that you are OK with all users seeing the same data.
|
||||
2. The arguments passed into cached component **DO NOT** affect the cache key. You will get the same cached component regardless of the arguments passed in. This is different from what you may be used to from something like React useMemo.
|
||||
3. Ensure the declaration of the cached component is **outside the function** that uses it. This is to prevent the component from being redeclared on each request.
|
||||
80
htmgo-site/md/docs/5_performance/1_caching_per_user.md
Normal file
80
htmgo-site/md/docs/5_performance/1_caching_per_user.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
**Caching Components Per User**
|
||||
|
||||
If you need to cache a component per user, you can use the `CachedPerKey` functions.
|
||||
These functions allow you to cache a component by a specific key. This key can be any string that uniquely identifies the user.
|
||||
|
||||
Note: I'm using the term 'user' to simply mean a unique identifier. This could be a user ID, session ID, or any other unique identifier.
|
||||
|
||||
To cache a component by unique identifier / key in htmgo, we offer:
|
||||
|
||||
```go
|
||||
// No arguments passed to the component, the component can be cached by a specific key
|
||||
h.CachedPerKey(duration time.Duration, cb GetElementFuncWithKey)
|
||||
// One argument passed to the component, the component can be cached by a specific key
|
||||
h.CachedPerKeyT1(duration time.Duration, cb GetElementFuncWithKey)
|
||||
// Two argument passed to the component, the component can be cached by a specific key
|
||||
h.CachedPerKeyT2(duration time.Duration, cb GetElementFuncWithKey)
|
||||
// Three arguments passed to the component, the component can be cached by a specific key
|
||||
h.CachedPerKeyT3(duration time.Duration, cb GetElementFuncWithKey)
|
||||
// Four arguments passed to the component, the component can be cached by a specific key
|
||||
h.CachedPerKeyT4(duration time.Duration, cb GetElementFuncWithKey)
|
||||
|
||||
```
|
||||
|
||||
The `duration` parameter is the time the component should be cached for. The `cb` parameter is a function that returns the component and the key.
|
||||
|
||||
When a request is made for a cached component, the component is rendered and stored in memory. Subsequent requests for the same component with the same key within the cache duration will return the cached component instead of rendering it again.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```go
|
||||
var CachedUserDocuments = h.CachedPerKeyT(time.Minute*15, func(ctx *h.RequestContext) (string, h.GetElementFunc) {
|
||||
userId := getUserIdFromSession(ctx)
|
||||
return userId, func() *h.Element {
|
||||
return UserDocuments(ctx)
|
||||
}
|
||||
})
|
||||
|
||||
func UserDocuments(ctx *h.RequestContext) *h.Element {
|
||||
docService := NewDocumentService(ctx)
|
||||
// Expensive call
|
||||
docs := docService.getDocuments()
|
||||
return h.Div(
|
||||
h.Class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"),
|
||||
h.List(docs, func(doc Document, index int) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("p-4 bg-white border border-gray-200 rounded-md"),
|
||||
h.H3(doc.Title),
|
||||
h.P(doc.Description),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func MyPage(ctx *h.RequestContext) *h.Page {
|
||||
|
||||
// Note this is not a real way to create a context, just an example
|
||||
user1 := &h.RequestContext{
|
||||
Session: "user_1_session",
|
||||
}
|
||||
|
||||
user2 := &h.RequestContext{
|
||||
Session: "user_2_session",
|
||||
}
|
||||
|
||||
// Different users will get different cached components
|
||||
return h.NewPage(
|
||||
CachedUserDocuments(user1),
|
||||
CachedUserDocuments(user2),
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Note:** We are using CachedPerKeyT because the component takes one argument, the RequestContext.
|
||||
If the component takes more arguments, use CachedPerKeyT2, CachedPerKeyT3, etc.
|
||||
|
||||
**Important**
|
||||
1. The cached value is stored globally in memory by key, it is shared across all requests. Ensure if you are storing request-specific data in a cached component, you are using a unique key for each user.
|
||||
2. The arguments passed into cached component **DO NOT** affect the cache key. The only thing that affects the cache key is the key returned by the `GetElementFuncWithKey` function.
|
||||
3. Ensure the declaration of the cached component is **outside the function** that uses it. This is to prevent the component from being redeclared on each request.
|
||||
Loading…
Reference in a new issue