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.
This commit is contained in:
parent
d97ce36508
commit
a4b00fd025
|
@ -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,74 +8,92 @@ import (
|
|||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
var ValidFragmentMergeTypes = []FragmentMergeMode{
|
||||
FragmentMergeModeMorph,
|
||||
FragmentMergeModeInner,
|
||||
FragmentMergeModeOuter,
|
||||
FragmentMergeModePrepend,
|
||||
FragmentMergeModeAppend,
|
||||
FragmentMergeModeBefore,
|
||||
FragmentMergeModeAfter,
|
||||
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)
|
||||
}
|
||||
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 +142,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,37 +17,50 @@ 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: "",
|
||||
RetryDuration: DefaultSseRetryDuration,
|
||||
Selector: "",
|
||||
MergeMode: FragmentMergeModeMorph,
|
||||
options := &mergeFragmentOptions{
|
||||
EventID: "", // TODO: Implement EventID option? currently field does nothing.
|
||||
RetryDuration: DefaultSseRetryDuration,
|
||||
Selector: "",
|
||||
MergeMode: FragmentMergeModeMorph,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
|
@ -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,31 @@
|
|||
package datastar
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAllValidFragmentMergeTypes(t *testing.T) {
|
||||
validFragmentMergeTypes := [...]FragmentMergeMode{
|
||||
FragmentMergeModeMorph,
|
||||
FragmentMergeModeInner,
|
||||
FragmentMergeModeOuter,
|
||||
FragmentMergeModePrepend,
|
||||
FragmentMergeModeAppend,
|
||||
FragmentMergeModeBefore,
|
||||
FragmentMergeModeAfter,
|
||||
FragmentMergeModeUpsertAttributes,
|
||||
}
|
||||
|
||||
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