* Refactor caching system to use pluggable stores The commit modernizes the caching implementation by introducing a pluggable store interface that allows different cache backends. Key changes: - Add Store interface for custom cache implementations - Create default TTL-based store for backwards compatibility - Add example LRU store for memory-bounded caching - Support cache store configuration via options pattern - Make cache cleanup logic implementation-specific - Add comprehensive tests and documentation The main goals were to: 1. Prevent unbounded memory growth through pluggable stores 2. Enable distributed caching support 3. Maintain backwards compatibility 4. Improve testability and maintainability Signed-off-by: franchb <hello@franchb.com> * Add custom cache stores docs and navigation Signed-off-by: franchb <hello@franchb.com> * Use GetOrCompute for atomic cache access The commit introduces an atomic GetOrCompute method to the cache interface and refactors all cache implementations to use it. This prevents race conditions and duplicate computations when multiple goroutines request the same uncached key simultaneously. The changes eliminate a time-of-check to time-of-use race condition in the original caching implementation, where separate Get/Set operations could lead to duplicate renders under high concurrency. With GetOrCompute, the entire check-compute-store operation happens atomically while holding the lock, ensuring only one goroutine computes a value for any given key. The API change is backwards compatible as the framework handles the GetOrCompute logic internally. Existing applications will automatically benefit from the * rename to WithCacheStore --------- Signed-off-by: franchb <hello@franchb.com> Co-authored-by: maddalax <jm@madev.me>
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
package pushing_data
|
|
|
|
import (
|
|
"github.com/maddalax/htmgo/framework/h"
|
|
. "htmgo-site/pages/docs"
|
|
"htmgo-site/ui"
|
|
)
|
|
|
|
func ServerSentEvents(ctx *h.RequestContext) *h.Page {
|
|
return DocPage(
|
|
ctx,
|
|
h.Div(
|
|
h.Class("flex flex-col gap-3"),
|
|
Title("Server Sent Events (SSE)"),
|
|
Text(`
|
|
htmgo supports server-sent events (SSE) out of the box.
|
|
This allows you to push data from the server to the client in real-time.
|
|
`),
|
|
h.P(
|
|
h.Text("Example of this can be found in the "),
|
|
Link("examples/chat", "/examples/chat"),
|
|
h.Text(" project."),
|
|
),
|
|
SubTitle("How it works"),
|
|
Text(`1. The client sends a request to the server to establish a connection.
|
|
2. The server holds the connection open and sends data (in our case, most likely elements) to the client whenever there is new data to send.
|
|
3. The htmgo SSE extension uses hx-swap-oob to swap out the elements that the server sends.
|
|
`),
|
|
HelpText("Note: SSE is unidirectional (the server can only send data to the client). For the client to send data to the server, normal xhr behavior should be used (form submission, triggers, etc)."),
|
|
Text(`<b>Usage:</b>`),
|
|
Text("Add the SSE connection attribute and the path to the handler that will handle the connection."),
|
|
ui.GoCodeSnippet(SseConnectAttribute),
|
|
Text("The following <b>Event Handlers</b> can be used to react to SSE connections."),
|
|
ui.GoCodeSnippet(SseEventHandlers),
|
|
Text("Example: Adding an event listener handle SSE errors."),
|
|
ui.GoCodeSnippet(SseErrorHandlingExample),
|
|
Text("Example: Clearing the input field after sending a message."),
|
|
ui.GoCodeSnippet(SseClearInputExample),
|
|
NextStep(
|
|
"mt-4",
|
|
PrevBlock("Custom Cache Stores", DocPath("/performance/pluggable-caches")),
|
|
NextBlock("HTMX extensions", DocPath("/htmx-extensions/overview")),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
const SseConnectAttribute = `
|
|
h.Attribute("sse-connect", fmt.Sprintf("/chat/%s", roomId))
|
|
`
|
|
|
|
const SseEventHandlers = `
|
|
h.HxOnSseOpen
|
|
h.HxBeforeSseMessage
|
|
h.HxAfterSseMessage
|
|
h.HxOnSseError
|
|
h.HxOnSseClose
|
|
h.HxOnSseConnecting
|
|
`
|
|
|
|
const SseErrorHandlingExample = `
|
|
h.HxOnSseError(
|
|
js.EvalJs(fmt.Sprintf("
|
|
const reason = e.detail.event.data
|
|
if(['invalid room', 'no session', 'invalid user'].includes(reason)) {
|
|
window.location.href = '/?roomId=%s';
|
|
} else if(e.detail.event.code === 1011) {
|
|
window.location.reload()
|
|
} else if (e.detail.event.code === 1008 || e.detail.event.code === 1006) {
|
|
window.location.href = '/?roomId=%s';
|
|
} else {
|
|
console.error('Connection closed:', e.detail.event)
|
|
}
|
|
", roomId, roomId)),
|
|
),
|
|
`
|
|
|
|
const SseClearInputExample = `
|
|
func MessageInput() *h.Element {
|
|
return h.Input("text",
|
|
h.Id("message-input"),
|
|
h.Required(),
|
|
h.HxAfterSseMessage(
|
|
js.SetValue(""),
|
|
),
|
|
)
|
|
}`
|