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:
parent
a705eb7044
commit
b758fb3e06
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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...))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue