Expand Golang SDK documentation by adding comments to every method. (#838)

* externalize interface compatibility for two optional template engines

Templ and GoStar are currently module dependencies for Golang SDK. They
should not be required for 1.0 release. This step replaces two component
interfaces with copies that ensure compatibility without having to
include various optional engines as dependencies with each Datastar
deployment.

* update comments and documentation for Golang SDK

Added comments to `execute.go` file. I will make similar changes to
other files. I submit those as a way of getting feedback.

The only public API change is the removal of execute script options
struct. It is changed to private by letter case because the options
pattern is used to configure script execution. The struct serves no
purpose and pollutes the public API.

* add hot reload example to Golang SDK

Updated the Golang SDK README.md file to include a list of examples that
will be expanded in the future. A basic example is moved into its own
directory. A hot reload example is added alongside it.

* expand Golang SDK documentation by adding comments to every method

Added a comment to every public method. Removed inaccessible options
structs from public API by letter case change. Exposed one hidden option
to public API. Refactored fragment merge mode parsing and added a
matching validation test.

* reinstate ValidFragmentMergeTypes because the website documentation depends on it
This commit is contained in:
Dmitry Kotik 2025-04-14 12:40:01 -04:00 committed by GitHub
parent a705eb7044
commit b758fb3e06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 328 additions and 72 deletions

View File

@ -9,31 +9,44 @@ import (
"time"
)
// ConsoleLog is a convenience method for [see.ExecuteScript].
// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.log(msg)`.
func (sse *ServerSentEventGenerator) ConsoleLog(msg string, opts ...ExecuteScriptOption) error {
call := fmt.Sprintf("console.log(%q)", msg)
return sse.ExecuteScript(call, opts...)
}
// ConsoleLogf is a convenience method for [see.ExecuteScript].
// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.log(fmt.Sprintf(format, args...))`.
func (sse *ServerSentEventGenerator) ConsoleLogf(format string, args ...any) error {
return sse.ConsoleLog(fmt.Sprintf(format, args...))
}
// ConsoleError is a convenience method for [see.ExecuteScript].
// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.error(msg)`.
func (sse *ServerSentEventGenerator) ConsoleError(err error, opts ...ExecuteScriptOption) error {
call := fmt.Sprintf("console.error(%q)", err.Error())
return sse.ExecuteScript(call, opts...)
}
// Redirectf is a convenience method for [see.ExecuteScript].
// It sends a redirect event to the client formatted using [fmt.Sprintf].
func (sse *ServerSentEventGenerator) Redirectf(format string, args ...any) error {
url := fmt.Sprintf(format, args...)
return sse.Redirect(url)
}
// Redirect is a convenience method for [see.ExecuteScript].
// It sends a redirect event to the client .
func (sse *ServerSentEventGenerator) Redirect(url string, opts ...ExecuteScriptOption) error {
js := fmt.Sprintf("setTimeout(() => window.location.href = %q)", url)
return sse.ExecuteScript(js, opts...)
}
type DispatchCustomEventOptions struct {
// dispatchCustomEventOptions holds the configuration data
// modified by [DispatchCustomEventOption]s
// for dispatching custom events to the client.
type dispatchCustomEventOptions struct {
EventID string
RetryDuration time.Duration
Selector string
@ -42,44 +55,78 @@ type DispatchCustomEventOptions struct {
Composed bool
}
type DispatchCustomEventOption func(*DispatchCustomEventOptions)
// DispatchCustomEventOption configures one custom
// server-sent event.
type DispatchCustomEventOption func(*dispatchCustomEventOptions)
// WithDispatchCustomEventEventID configures an optional event ID for the custom event.
// The client message field [lastEventId] will be set to this value.
// If the next event does not have an event ID, the last used event ID will remain.
//
// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
func WithDispatchCustomEventEventID(id string) DispatchCustomEventOption {
return func(o *DispatchCustomEventOptions) {
return func(o *dispatchCustomEventOptions) {
o.EventID = id
}
}
// WithDispatchCustomEventRetryDuration overrides the [DefaultSseRetryDuration] for one custom event.
func WithDispatchCustomEventRetryDuration(retryDuration time.Duration) DispatchCustomEventOption {
return func(o *DispatchCustomEventOptions) {
return func(o *dispatchCustomEventOptions) {
o.RetryDuration = retryDuration
}
}
// WithDispatchCustomEventSelector replaces the default custom event target `document` with a
// [CSS selector]. If the selector matches multiple HTML elements, the event will be dispatched
// from each one. For example, if the selector is `#my-element`, the event will be dispatched
// from the element with the ID `my-element`. If the selector is `main > section`, the event will be dispatched
// from each `<section>` element which is a direct child of the `<main>` element.
//
// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors
func WithDispatchCustomEventSelector(selector string) DispatchCustomEventOption {
return func(o *DispatchCustomEventOptions) {
return func(o *dispatchCustomEventOptions) {
o.Selector = selector
}
}
// WithDispatchCustomEventBubbles overrides the default custom [event bubbling] `true` value.
// Setting bubbling to `false` is equivalent to calling `event.stopPropagation()` Javascript
// command on the client side for the dispatched event. This prevents the event from triggering
// event handlers of its parent elements.
//
// [event bubbling]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling
func WithDispatchCustomEventBubbles(bubbles bool) DispatchCustomEventOption {
return func(o *DispatchCustomEventOptions) {
return func(o *dispatchCustomEventOptions) {
o.Bubbles = bubbles
}
}
// WithDispatchCustomEventCancelable overrides the default custom [event cancelability] `true` value.
// Setting cancelability to `false` is blocks `event.preventDefault()` Javascript
// command on the client side for the dispatched event.
//
// [event cancelability]: https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable
func WithDispatchCustomEventCancelable(cancelable bool) DispatchCustomEventOption {
return func(o *DispatchCustomEventOptions) {
return func(o *dispatchCustomEventOptions) {
o.Cancelable = cancelable
}
}
// WithDispatchCustomEventComposed overrides the default custom [event composed] `true` value.
// It indicates whether or not the event will propagate across the shadow HTML DOM boundary into
// the document DOM tree. When `false`, the shadow root will be the last node to be offered the event.
//
// [event composed]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
func WithDispatchCustomEventComposed(composed bool) DispatchCustomEventOption {
return func(o *DispatchCustomEventOptions) {
return func(o *dispatchCustomEventOptions) {
o.Composed = composed
}
}
// DispatchCustomEvent is a convenience method for dispatching a custom event by executing
// a client side script via [sse.ExecuteScript] call. The detail struct is marshaled to JSON and
// passed as a parameter to the event.
func (sse *ServerSentEventGenerator) DispatchCustomEvent(eventName string, detail any, opts ...DispatchCustomEventOption) error {
if eventName == "" {
return fmt.Errorf("eventName is required")
@ -91,7 +138,7 @@ func (sse *ServerSentEventGenerator) DispatchCustomEvent(eventName string, detai
}
const defaultSelector = "document"
options := DispatchCustomEventOptions{
options := dispatchCustomEventOptions{
EventID: "",
RetryDuration: DefaultSseRetryDuration,
Selector: defaultSelector,
@ -143,17 +190,25 @@ elements.forEach((element) => {
}
// ReplaceURL replaces the current URL in the browser's history.
func (sse *ServerSentEventGenerator) ReplaceURL(u url.URL, opts ...ExecuteScriptOption) error {
js := fmt.Sprintf(`window.history.replaceState({}, "", %q)`, u.String())
return sse.ExecuteScript(js, opts...)
}
// ReplaceURLQuerystring is a convenience wrapper for [sse.ReplaceURL] that replaces the query
// string of the current URL request with new a new query built from the provided values.
func (sse *ServerSentEventGenerator) ReplaceURLQuerystring(r *http.Request, values url.Values, opts ...ExecuteScriptOption) error {
// TODO: rename this function to ReplaceURLQuery
u := *r.URL
u.RawQuery = values.Encode()
return sse.ReplaceURL(u, opts...)
}
// Prefetch is a convenience wrapper for [sse.ExecuteScript] that prefetches the provided links.
// It follows the Javascript [speculation rules API] prefetch specification.
//
// [speculation rules API]: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
func (sse *ServerSentEventGenerator) Prefetch(urls ...string) error {
wrappedURLs := make([]string, len(urls))
for i, url := range urls {

View File

@ -29,7 +29,7 @@ func WithExecuteScriptEventID(id string) ExecuteScriptOption {
}
}
// WithExecuteScriptRetryDuration overrides the [DefaultRetryDuration] for this script
// WithExecuteScriptRetryDuration overrides the [DefaultSseRetryDuration] for this script
// execution only.
func WithExecuteScriptRetryDuration(retryDuration time.Duration) ExecuteScriptOption {
return func(o *executeScriptOptions) {

View File

@ -8,6 +8,7 @@ import (
"github.com/valyala/bytebufferpool"
)
// ValidFragmentMergeTypes is a list of valid fragment merge modes.
var ValidFragmentMergeTypes = []FragmentMergeMode{
FragmentMergeModeMorph,
FragmentMergeModeInner,
@ -19,63 +20,92 @@ var ValidFragmentMergeTypes = []FragmentMergeMode{
FragmentMergeModeUpsertAttributes,
}
// FragmentMergeTypeFromString converts a string to a [FragmentMergeMode].
func FragmentMergeTypeFromString(s string) (FragmentMergeMode, error) {
for _, t := range ValidFragmentMergeTypes {
if string(t) == s {
return t, nil
}
}
switch s {
case "morph":
return FragmentMergeModeMorph, nil
case "inner":
return FragmentMergeModeInner, nil
case "outer":
return FragmentMergeModeOuter, nil
case "prepend":
return FragmentMergeModePrepend, nil
case "append":
return FragmentMergeModeAppend, nil
case "before":
return FragmentMergeModeBefore, nil
case "after":
return FragmentMergeModeAfter, nil
case "upsertAttributes":
return FragmentMergeModeUpsertAttributes, nil
default:
return "", fmt.Errorf("invalid fragment merge type: %s", s)
}
}
// WithMergeMorph creates a MergeFragmentOption that merges fragments using the morph mode.
func WithMergeMorph() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeMorph)
}
// WithMergeInner creates a MergeFragmentOption that merges fragments using the inner mode.
func WithMergeInner() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeInner)
}
// WithMergeOuter creates a MergeFragmentOption that merges fragments using the outer mode.
func WithMergeOuter() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeOuter)
}
// WithMergePrepend creates a MergeFragmentOption that merges fragments using the prepend mode.
func WithMergePrepend() MergeFragmentOption {
return WithMergeMode(FragmentMergeModePrepend)
}
// WithMergeAppend creates a MergeFragmentOption that merges fragments using the append mode.
func WithMergeAppend() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeAppend)
}
// WithMergeBefore creates a MergeFragmentOption that merges fragments using the before mode.
func WithMergeBefore() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeBefore)
}
// WithMergeAfter creates a MergeFragmentOption that merges fragments using the after mode.
func WithMergeAfter() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeAfter)
}
// WithMergeUpsertAttributes creates a MergeFragmentOption that merges fragments using the upsert attributes mode.
func WithMergeUpsertAttributes() MergeFragmentOption {
return WithMergeMode(FragmentMergeModeUpsertAttributes)
}
// WithSelectorID is a convenience wrapper for [WithSelector] option
// equivalent to calling `WithSelector("#"+id)`.
func WithSelectorID(id string) MergeFragmentOption {
return WithSelector("#" + id)
}
// WithViewTransitions enables the use of view transitions when merging fragments.
func WithViewTransitions() MergeFragmentOption {
return func(o *MergeFragmentOptions) {
return func(o *mergeFragmentOptions) {
o.UseViewTransitions = true
}
}
// WithoutViewTransitions disables the use of view transitions when merging fragments.
func WithoutViewTransitions() MergeFragmentOption {
return func(o *MergeFragmentOptions) {
return func(o *mergeFragmentOptions) {
o.UseViewTransitions = false
}
}
// MergeFragmentf is a convenience wrapper for [MergeFragments] option
// equivalent to calling `MergeFragments(fmt.Sprintf(format, args...))`.
func (sse *ServerSentEventGenerator) MergeFragmentf(format string, args ...any) error {
return sse.MergeFragments(fmt.Sprintf(format, args...))
}
@ -124,22 +154,37 @@ func (sse *ServerSentEventGenerator) MergeFragmentGostar(child GoStarElementRend
return nil
}
// GetSSE is a convenience method for generating Datastar backend [get] action attribute.
//
// [get]: https://data-star.dev/reference/action_plugins#get
func GetSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@get('%s')`, fmt.Sprintf(urlFormat, args...))
}
// PostSSE is a convenience method for generating Datastar backend [post] action attribute.
//
// [post]: https://data-star.dev/reference/action_plugins#post
func PostSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@post('%s')`, fmt.Sprintf(urlFormat, args...))
}
// PutSSE is a convenience method for generating Datastar backend [put] action attribute.
//
// [put]: https://data-star.dev/reference/action_plugins#put
func PutSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@put('%s')`, fmt.Sprintf(urlFormat, args...))
}
// PatchSSE is a convenience method for generating Datastar backend [patch] action attribute.
//
// [patch]: https://data-star.dev/reference/action_plugins#patch
func PatchSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@patch('%s')`, fmt.Sprintf(urlFormat, args...))
}
// DeleteSSE is a convenience method for generating Datastar backend [delete] action attribute.
//
// [delete]: https://data-star.dev/reference/action_plugins#delete
func DeleteSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@delete('%s')`, fmt.Sprintf(urlFormat, args...))
}

View File

@ -7,7 +7,9 @@ import (
"time"
)
type MergeFragmentOptions struct {
// mergeFragmentOptions holds the configuration data for [MergeFragmentOption]s used
// for initialization of [sse.MergeFragments] event.
type mergeFragmentOptions struct {
EventID string
RetryDuration time.Duration
Selector string
@ -15,34 +17,47 @@ type MergeFragmentOptions struct {
UseViewTransitions bool
}
type MergeFragmentOption func(*MergeFragmentOptions)
// MergeFragmentOption configures the [sse.MergeFragments] event initialization.
type MergeFragmentOption func(*mergeFragmentOptions)
// WithSelectorf is a convenience wrapper for [WithSelector] option that formats the selector string
// using the provided format and arguments similar to [fmt.Sprintf].
func WithSelectorf(selectorFormat string, args ...any) MergeFragmentOption {
selector := fmt.Sprintf(selectorFormat, args...)
return WithSelector(selector)
}
// WithSelector specifies the [CSS selector] for HTML elements that a fragment will be merged over or
// merged next to, depending on the merge mode.
//
// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors
func WithSelector(selector string) MergeFragmentOption {
return func(o *MergeFragmentOptions) {
return func(o *mergeFragmentOptions) {
o.Selector = selector
}
}
// WithMergeMode overrides the [DefaultFragmentMergeMode] for the fragment.
// Choose a valid [FragmentMergeMode].
func WithMergeMode(merge FragmentMergeMode) MergeFragmentOption {
return func(o *MergeFragmentOptions) {
return func(o *mergeFragmentOptions) {
o.MergeMode = merge
}
}
// WithUseViewTransitions specifies whether to use [view transitions] when merging fragments.
//
// [view transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API
func WithUseViewTransitions(useViewTransition bool) MergeFragmentOption {
return func(o *MergeFragmentOptions) {
return func(o *mergeFragmentOptions) {
o.UseViewTransitions = useViewTransition
}
}
// MergeFragments sends an HTML fragment to the client to update the DOM tree with.
func (sse *ServerSentEventGenerator) MergeFragments(fragment string, opts ...MergeFragmentOption) error {
options := &MergeFragmentOptions{
EventID: "",
options := &mergeFragmentOptions{
EventID: "", // TODO: Implement EventID option? currently field does nothing.
RetryDuration: DefaultSseRetryDuration,
Selector: "",
MergeMode: FragmentMergeModeMorph,
@ -88,38 +103,54 @@ func (sse *ServerSentEventGenerator) MergeFragments(fragment string, opts ...Mer
return nil
}
type RemoveFragmentsOptions struct {
// mergeFragmentOptions holds the configuration data for [RemoveFragmentsOption]s used
// for initialization of [sse.RemoveFragments] event.
type removeFragmentsOptions struct {
EventID string
RetryDuration time.Duration
UseViewTransitions *bool
}
type RemoveFragmentsOption func(*RemoveFragmentsOptions)
// RemoveFragmentsOption configures the [sse.RemoveFragments] event.
type RemoveFragmentsOption func(*removeFragmentsOptions)
// WithRemoveEventID configures an optional event ID for the fragment removal event.
// The client message field [lastEventId] will be set to this value.
// If the next event does not have an event ID, the last used event ID will remain.
//
// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
func WithRemoveEventID(id string) RemoveFragmentsOption {
return func(o *RemoveFragmentsOptions) {
return func(o *removeFragmentsOptions) {
o.EventID = id
}
}
// WithExecuteScriptRetryDuration overrides the [DefaultSseRetryDuration] for this script
// execution only.
func WithRemoveRetryDuration(d time.Duration) RemoveFragmentsOption {
return func(o *RemoveFragmentsOptions) {
return func(o *removeFragmentsOptions) {
o.RetryDuration = d
}
}
// WithRemoveUseViewTransitions specifies whether to use [view transitions] when merging fragments.
//
// [view transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API
func WithRemoveUseViewTransitions(useViewTransition bool) RemoveFragmentsOption {
return func(o *RemoveFragmentsOptions) {
return func(o *removeFragmentsOptions) {
o.UseViewTransitions = &useViewTransition
}
}
// MergeFragments sends a [CSS selector] to the client to update the DOM tree by removing matching elements.
//
// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors
func (sse *ServerSentEventGenerator) RemoveFragments(selector string, opts ...RemoveFragmentsOption) error {
if selector == "" {
panic("missing " + SelectorDatalineLiteral)
}
options := &RemoveFragmentsOptions{
options := &removeFragmentsOptions{
EventID: "",
RetryDuration: DefaultSseRetryDuration,
UseViewTransitions: nil,

20
sdk/go/fragments_test.go Normal file
View File

@ -0,0 +1,20 @@
package datastar
import "testing"
func TestAllValidFragmentMergeTypes(t *testing.T) {
var err error
for _, validType := range ValidFragmentMergeTypes {
if _, err = FragmentMergeTypeFromString(string(validType)); err != nil {
t.Errorf("Expected %v to be a valid fragment merge type, but it was rejected: %v", validType, err)
}
}
if _, err = FragmentMergeTypeFromString(""); err == nil {
t.Errorf("Expected an empty string to be an invalid fragment merge type, but it was accepted")
}
if _, err = FragmentMergeTypeFromString("fakeType"); err == nil {
t.Errorf("Expected a fake type to be an invalid fragment merge type, but it was accepted")
}
}

View File

@ -5,6 +5,9 @@ import (
"fmt"
)
// MarshalAndMergeSignals is a convenience method for [see.MergeSignals].
// It marshals a given signals struct into JSON and
// emits a [EventTypeMergeSignals] event.
func (sse *ServerSentEventGenerator) MarshalAndMergeSignals(signals any, opts ...MergeSignalsOption) error {
b, err := json.Marshal(signals)
if err != nil {
@ -17,6 +20,8 @@ func (sse *ServerSentEventGenerator) MarshalAndMergeSignals(signals any, opts ..
return nil
}
// MarshalAndMergeSignalsIfMissing is a convenience method for [see.MarshalAndMergeSignals].
// It is equivalent to calling [see.MarshalAndMergeSignals] with [see.WithOnlyIfMissing(true)] option.
func (sse *ServerSentEventGenerator) MarshalAndMergeSignalsIfMissing(signals any, opts ...MergeSignalsOption) error {
if err := sse.MarshalAndMergeSignals(
signals,
@ -27,6 +32,8 @@ func (sse *ServerSentEventGenerator) MarshalAndMergeSignalsIfMissing(signals any
return nil
}
// MergeSignalsIfMissingRaw is a convenience method for [see.MergeSignals].
// It is equivalent to calling [see.MergeSignals] with [see.WithOnlyIfMissing(true)] option.
func (sse *ServerSentEventGenerator) MergeSignalsIfMissingRaw(signalsJSON string) error {
if err := sse.MergeSignals([]byte(signalsJSON), WithOnlyIfMissing(true)); err != nil {
return fmt.Errorf("failed to merge signals if missing: %w", err)

View File

@ -13,37 +13,49 @@ import (
)
var (
// ErrNoPathsProvided is returned when no paths were provided for // for [sse.RemoveSignals] call.
ErrNoPathsProvided = errors.New("no paths provided")
)
type MergeSignalsOptions struct {
// mergeSignalsOptions holds configuration options for merging signals.
type mergeSignalsOptions struct {
EventID string
RetryDuration time.Duration
OnlyIfMissing bool
}
type MergeSignalsOption func(*MergeSignalsOptions)
// MergeSignalsOption configures one [EventTypeMergeSignals] event.
type MergeSignalsOption func(*mergeSignalsOptions)
// WithMergeSignalsEventID configures an optional event ID for the signals merge event.
// The client message field [lastEventId] will be set to this value.
// If the next event does not have an event ID, the last used event ID will remain.
//
// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
func WithMergeSignalsEventID(id string) MergeSignalsOption {
return func(o *MergeSignalsOptions) {
return func(o *mergeSignalsOptions) {
o.EventID = id
}
}
// WithMergeSignalsRetryDuration overrides the [DefaultSseRetryDuration] for signal merging.
func WithMergeSignalsRetryDuration(retryDuration time.Duration) MergeSignalsOption {
return func(o *MergeSignalsOptions) {
return func(o *mergeSignalsOptions) {
o.RetryDuration = retryDuration
}
}
// WithOnlyIfMissing instructs the client to only merge signals if they are missing.
func WithOnlyIfMissing(onlyIfMissing bool) MergeSignalsOption {
return func(o *MergeSignalsOptions) {
return func(o *mergeSignalsOptions) {
o.OnlyIfMissing = onlyIfMissing
}
}
// MergeSignals sends a [EventTypeMergeSignals] to the client.
// Requires a JSON-encoded payload.
func (sse *ServerSentEventGenerator) MergeSignals(signalsContents []byte, opts ...MergeSignalsOption) error {
options := &MergeSignalsOptions{
options := &mergeSignalsOptions{
EventID: "",
RetryDuration: DefaultSseRetryDuration,
OnlyIfMissing: false,
@ -79,6 +91,8 @@ func (sse *ServerSentEventGenerator) MergeSignals(signalsContents []byte, opts .
return nil
}
// RemoveSignals sends a [EventTypeRemoveSignals] event to the client.
// Requires a non-empty list of paths.
func (sse *ServerSentEventGenerator) RemoveSignals(paths ...string) error {
if len(paths) == 0 {
return ErrNoPathsProvided
@ -98,6 +112,12 @@ func (sse *ServerSentEventGenerator) RemoveSignals(paths ...string) error {
return nil
}
// ReadSignals extracts Datastar signals from
// an HTTP request and unmarshals them into the signals target,
// which should be a pointer to a struct.
//
// Expects signals in [URL.Query] for [http.MethodGet] requests.
// Expects JSON-encoded signals in [Request.Body] for other request methods.
func ReadSignals(r *http.Request, signals any) error {
var dsInput []byte

View File

@ -12,37 +12,65 @@ import (
"github.com/CAFxX/httpcompression"
)
// CompressionStrategy indicates the strategy for selecting the compression algorithm.
type CompressionStrategy string
const (
ClientPriority = "client_priority"
ServerPriority = "server_priority"
Forced = "forced"
// ClientPriority indicates that the client's preferred compression algorithm
// should be used if possible.
ClientPriority CompressionStrategy = "client_priority"
// ServerPriority indicates that the server's preferred compression algorithm
// should be used.
ServerPriority CompressionStrategy = "server_priority"
// Forced indicates that the first provided compression
// algorithm must be used regardless of client or server preferences.
Forced CompressionStrategy = "forced"
)
// Compressor pairs a [httpcompression.CompressorProvider]
// with an encoding HTTP content type.
type Compressor struct {
Encoding string
Compressor httpcompression.CompressorProvider
}
type CompressionConfig struct {
// compressionOptions holds all the data for server-sent events
// message compression configuration initiated by [CompressionOption]s.
type compressionOptions struct {
CompressionStrategy CompressionStrategy
ClientEncodings []string
Compressors []Compressor
}
type CompressionOption func(*CompressionConfig)
// CompressionOption configures server-sent events
// message compression.
type CompressionOption func(*compressionOptions)
// GzipOption configures the Gzip compression algorithm.
type GzipOption func(*gzip.Options)
// WithGzipLevel determines the algorithm's compression level.
// Higher values result in smaller output at the cost of higher CPU usage.
//
// Choose one of the following levels:
// - [gzip.NoCompression]
// - [gzip.BestSpeed]
// - [gzip.BestCompression]
// - [gzip.DefaultCompression]
// - [gzip.HuffmanOnly]
func WithGzipLevel(level int) GzipOption {
return func(opts *gzip.Options) {
opts.Level = level
}
}
// WithGzip appends a [Gzip] compressor to the list of compressors.
//
// [Gzip]: https://en.wikipedia.org/wiki/Gzip
func WithGzip(opts ...GzipOption) CompressionOption {
return func(cfg *CompressionConfig) {
return func(cfg *compressionOptions) {
// set default options
options := gzip.Options{
Level: gzip.DefaultCompression,
@ -60,26 +88,40 @@ func WithGzip(opts ...GzipOption) CompressionOption {
}
cfg.Compressors = append(cfg.Compressors, compressor)
}
}
// DeflateOption configures the Deflate compression algorithm.
type DeflateOption func(*zlib.Options)
// WithDeflateLevel determines the algorithm's compression level.
// Higher values result in smaller output at the cost of higher CPU usage.
//
// Choose one of the following levels:
// - [zlib.NoCompression]
// - [zlib.BestSpeed]
// - [zlib.BestCompression]
// - [zlib.DefaultCompression]
// - [zlib.HuffmanOnly]
func WithDeflateLevel(level int) DeflateOption {
return func(opts *zlib.Options) {
opts.Level = level
}
}
// WithDeflateDictionary sets the dictionary used by the algorithm.
// This can improve compression ratio for repeated data.
func WithDeflateDictionary(dict []byte) DeflateOption {
return func(opts *zlib.Options) {
opts.Dictionary = dict
}
}
// WithDeflate appends a [Deflate] compressor to the list of compressors.
//
// [Deflate]: https://en.wikipedia.org/wiki/Deflate
func WithDeflate(opts ...DeflateOption) CompressionOption {
return func(cfg *CompressionConfig) {
return func(cfg *compressionOptions) {
options := zlib.Options{
Level: zlib.DefaultCompression,
}
@ -99,22 +141,33 @@ func WithDeflate(opts ...DeflateOption) CompressionOption {
}
}
type brotliOption func(*brotli.Options)
// BrotliOption configures the Brotli compression algorithm.
type BrotliOption func(*brotli.Options)
func WithBrotliLevel(level int) brotliOption {
// WithBrotliLevel determines the algorithm's compression level.
// Higher values result in smaller output at the cost of higher CPU usage.
// Fastest compression level is 0. Best compression level is 11.
// Defaults to 6.
func WithBrotliLevel(level int) BrotliOption {
return func(opts *brotli.Options) {
opts.Quality = level
}
}
func WithBrotliLGWin(lgwin int) brotliOption {
// WithBrotliLGWin the sliding window size for Brotli compression
// algorithm. Select a value between 10 and 24.
// Defaults to 0, indicating automatic window size selection based on compression quality.
func WithBrotliLGWin(lgwin int) BrotliOption {
return func(opts *brotli.Options) {
opts.LGWin = lgwin
}
}
func WithBrotli(opts ...brotliOption) CompressionOption {
return func(cfg *CompressionConfig) {
// WithBrotli appends a [Brotli] compressor to the list of compressors.
//
// [Brotli]: https://en.wikipedia.org/wiki/Brotli
func WithBrotli(opts ...BrotliOption) CompressionOption {
return func(cfg *compressionOptions) {
options := brotli.Options{
Quality: brotli.DefaultCompression,
}
@ -134,9 +187,11 @@ func WithBrotli(opts ...brotliOption) CompressionOption {
}
}
// WithZstd appends a [Zstd] compressor to the list of compressors.
//
// [Zstd]: https://en.wikipedia.org/wiki/Zstd
func WithZstd(opts ...zstd_opts.EOption) CompressionOption {
return func(cfg *CompressionConfig) {
return func(cfg *compressionOptions) {
zstdCompressor, _ := zstd.New(opts...)
compressor := Compressor{
@ -148,28 +203,37 @@ func WithZstd(opts ...zstd_opts.EOption) CompressionOption {
}
}
// WithClientPriority sets the compression strategy to [ClientPriority].
// The compression algorithm will be selected based on the
// client's preference from the list of included compressors.
func WithClientPriority() CompressionOption {
return func(cfg *CompressionConfig) {
return func(cfg *compressionOptions) {
cfg.CompressionStrategy = ClientPriority
}
}
// WithServerPriority sets the compression strategy to [ServerPriority].
// The compression algorithm will be selected based on the
// server's preference from the list of included compressors.
func WithServerPriority() CompressionOption {
return func(cfg *CompressionConfig) {
return func(cfg *compressionOptions) {
cfg.CompressionStrategy = ServerPriority
}
}
// WithForced sets the compression strategy to [Forced].
// The first compression algorithm will be selected
// from the list of included compressors.
func WithForced() CompressionOption {
return func(cfg *CompressionConfig) {
return func(cfg *compressionOptions) {
cfg.CompressionStrategy = Forced
}
}
// WithCompression adds compression to server-sent event stream.
func WithCompression(opts ...CompressionOption) SSEOption {
return func(sse *ServerSentEventGenerator) {
cfg := &CompressionConfig{
cfg := &compressionOptions{
CompressionStrategy: ClientPriority,
ClientEncodings: parseEncodings(sse.acceptEncoding),
}

View File

@ -13,6 +13,8 @@ import (
"github.com/valyala/bytebufferpool"
)
// ServerSentEventGenerator streams events into
// an [http.ResponseWriter]. Each event is flushed immediately.
type ServerSentEventGenerator struct {
ctx context.Context
mu *sync.Mutex
@ -23,8 +25,13 @@ type ServerSentEventGenerator struct {
acceptEncoding string
}
// SSEOption configures the initialization of an
// HTTP Server-Sent Event stream.
type SSEOption func(*ServerSentEventGenerator)
// NewSSE upgrades an [http.ResponseWriter] to an HTTP Server-Sent Event stream.
// The connection is kept alive until the context is canceled or the response is closed by returning from the handler.
// Run an event loop for persistent streaming.
func NewSSE(w http.ResponseWriter, r *http.Request, opts ...SSEOption) *ServerSentEventGenerator {
rc := http.NewResponseController(w)
@ -43,7 +50,7 @@ func NewSSE(w http.ResponseWriter, r *http.Request, opts ...SSEOption) *ServerSe
acceptEncoding: r.Header.Get("Accept-Encoding"),
}
// Apply options
// apply options
for _, opt := range opts {
opt(sseHandler)
}
@ -65,27 +72,39 @@ func NewSSE(w http.ResponseWriter, r *http.Request, opts ...SSEOption) *ServerSe
return sseHandler
}
// Context returns the context associated with the upgraded connection.
// It is equivalent to calling [request.Context].
func (sse *ServerSentEventGenerator) Context() context.Context {
return sse.ctx
}
type ServerSentEventData struct {
// serverSentEventData holds event configuration data for
// [SSEEventOption]s.
type serverSentEventData struct {
Type EventType
EventID string
Data []string
RetryDuration time.Duration
}
type SSEEventOption func(*ServerSentEventData)
// SSEEventOption modifies one server-sent event.
type SSEEventOption func(*serverSentEventData)
// WithSSEEventId configures an optional event ID for one server-sent event.
// The client message field [lastEventId] will be set to this value.
// If the next event does not have an event ID, the last used event ID will remain.
//
// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
func WithSSEEventId(id string) SSEEventOption {
return func(e *ServerSentEventData) {
return func(e *serverSentEventData) {
e.EventID = id
}
}
// WithSSERetryDuration overrides the [DefaultSseRetryDuration] for
// one server-sent event.
func WithSSERetryDuration(retryDuration time.Duration) SSEEventOption {
return func(e *ServerSentEventData) {
return func(e *serverSentEventData) {
e.RetryDuration = retryDuration
}
}
@ -102,12 +121,14 @@ func writeJustError(w io.Writer, b []byte) (err error) {
return err
}
// Send emits a server-sent event to the client. Method is safe for
// concurrent use.
func (sse *ServerSentEventGenerator) Send(eventType EventType, dataLines []string, opts ...SSEEventOption) error {
sse.mu.Lock()
defer sse.mu.Unlock()
// create the event
evt := ServerSentEventData{
evt := serverSentEventData{
Type: eventType,
Data: dataLines,
RetryDuration: DefaultSseRetryDuration,
@ -187,6 +208,5 @@ func (sse *ServerSentEventGenerator) Send(eventType EventType, dataLines []strin
}
// log.Print(NewLine + buf.String())
return nil
}

View File

@ -1,17 +1,11 @@
package datastar
import (
"errors"
)
const (
NewLine = "\n"
DoubleNewLine = "\n\n"
)
var (
ErrEventTypeError = errors.New("event type is required")
newLineBuf = []byte(NewLine)
doubleNewLineBuf = []byte(DoubleNewLine)
)