diff --git a/sdk/go/execute-script-sugar.go b/sdk/go/execute-script-sugar.go index d8957256..310b9fa6 100644 --- a/sdk/go/execute-script-sugar.go +++ b/sdk/go/execute-script-sugar.go @@ -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 `
` element which is a direct child of the `
` 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 { diff --git a/sdk/go/execute.go b/sdk/go/execute.go index 0180180a..5b838c67 100644 --- a/sdk/go/execute.go +++ b/sdk/go/execute.go @@ -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) { diff --git a/sdk/go/fragments-sugar.go b/sdk/go/fragments-sugar.go index 27caff7e..effba471 100644 --- a/sdk/go/fragments-sugar.go +++ b/sdk/go/fragments-sugar.go @@ -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) } - 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...)) } diff --git a/sdk/go/fragments.go b/sdk/go/fragments.go index efe50307..41159ba1 100644 --- a/sdk/go/fragments.go +++ b/sdk/go/fragments.go @@ -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, diff --git a/sdk/go/fragments_test.go b/sdk/go/fragments_test.go new file mode 100644 index 00000000..efd4d63c --- /dev/null +++ b/sdk/go/fragments_test.go @@ -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") + } +} diff --git a/sdk/go/signals-sugar.go b/sdk/go/signals-sugar.go index 629916fa..42264cef 100644 --- a/sdk/go/signals-sugar.go +++ b/sdk/go/signals-sugar.go @@ -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) diff --git a/sdk/go/signals.go b/sdk/go/signals.go index 07849963..f05fce87 100644 --- a/sdk/go/signals.go +++ b/sdk/go/signals.go @@ -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 diff --git a/sdk/go/sse-compression.go b/sdk/go/sse-compression.go index b021a5c4..5a8b8a87 100644 --- a/sdk/go/sse-compression.go +++ b/sdk/go/sse-compression.go @@ -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), } diff --git a/sdk/go/sse.go b/sdk/go/sse.go index 2ad6d16e..ad31b4ab 100644 --- a/sdk/go/sse.go +++ b/sdk/go/sse.go @@ -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 } diff --git a/sdk/go/types.go b/sdk/go/types.go index 12a1205b..d3991a73 100644 --- a/sdk/go/types.go +++ b/sdk/go/types.go @@ -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) )