295 lines
8.0 KiB
Go
295 lines
8.0 KiB
Go
package datastar
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/CAFxX/httpcompression/contrib/andybalholm/brotli"
|
|
"github.com/CAFxX/httpcompression/contrib/compress/gzip"
|
|
"github.com/CAFxX/httpcompression/contrib/compress/zlib"
|
|
"github.com/CAFxX/httpcompression/contrib/klauspost/zstd"
|
|
zstd_opts "github.com/klauspost/compress/zstd"
|
|
|
|
"github.com/CAFxX/httpcompression"
|
|
)
|
|
|
|
// CompressionStrategy indicates the strategy for selecting the compression algorithm.
|
|
type CompressionStrategy string
|
|
|
|
const (
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 *compressionOptions) {
|
|
// set default options
|
|
options := gzip.Options{
|
|
Level: gzip.DefaultCompression,
|
|
}
|
|
// Apply all provided options.
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
gzipCompressor, _ := gzip.New(options)
|
|
|
|
compressor := Compressor{
|
|
Encoding: gzip.Encoding,
|
|
Compressor: gzipCompressor,
|
|
}
|
|
|
|
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 *compressionOptions) {
|
|
options := zlib.Options{
|
|
Level: zlib.DefaultCompression,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
zlibCompressor, _ := zlib.New(options)
|
|
|
|
compressor := Compressor{
|
|
Encoding: zlib.Encoding,
|
|
Compressor: zlibCompressor,
|
|
}
|
|
|
|
cfg.Compressors = append(cfg.Compressors, compressor)
|
|
}
|
|
}
|
|
|
|
// BrotliOption configures the Brotli compression algorithm.
|
|
type BrotliOption func(*brotli.Options)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
brotliCompressor, _ := brotli.New(options)
|
|
|
|
compressor := Compressor{
|
|
Encoding: brotli.Encoding,
|
|
Compressor: brotliCompressor,
|
|
}
|
|
|
|
cfg.Compressors = append(cfg.Compressors, compressor)
|
|
}
|
|
}
|
|
|
|
// 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 *compressionOptions) {
|
|
zstdCompressor, _ := zstd.New(opts...)
|
|
|
|
compressor := Compressor{
|
|
Encoding: zstd.Encoding,
|
|
Compressor: zstdCompressor,
|
|
}
|
|
|
|
cfg.Compressors = append(cfg.Compressors, compressor)
|
|
}
|
|
}
|
|
|
|
// 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 *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 *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 *compressionOptions) {
|
|
cfg.CompressionStrategy = Forced
|
|
}
|
|
}
|
|
|
|
// WithCompression adds compression to server-sent event stream.
|
|
func WithCompression(opts ...CompressionOption) SSEOption {
|
|
return func(sse *ServerSentEventGenerator) {
|
|
cfg := &compressionOptions{
|
|
CompressionStrategy: ClientPriority,
|
|
ClientEncodings: parseEncodings(sse.acceptEncoding),
|
|
}
|
|
|
|
// apply options
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
// set defaults
|
|
if len(cfg.Compressors) == 0 {
|
|
WithBrotli()(cfg)
|
|
WithZstd()(cfg)
|
|
WithGzip()(cfg)
|
|
WithDeflate()(cfg)
|
|
}
|
|
|
|
switch cfg.CompressionStrategy {
|
|
case ClientPriority:
|
|
for _, clientEnc := range cfg.ClientEncodings {
|
|
for _, comp := range cfg.Compressors {
|
|
if comp.Encoding == clientEnc {
|
|
sse.w = comp.Compressor.Get(sse.w)
|
|
sse.encoding = comp.Encoding
|
|
return
|
|
}
|
|
}
|
|
}
|
|
case ServerPriority:
|
|
for _, comp := range cfg.Compressors {
|
|
for _, clientEnc := range cfg.ClientEncodings {
|
|
if comp.Encoding == clientEnc {
|
|
sse.w = comp.Compressor.Get(sse.w)
|
|
sse.encoding = comp.Encoding
|
|
return
|
|
}
|
|
}
|
|
}
|
|
case Forced:
|
|
if len(cfg.Compressors) > 0 {
|
|
sse.w = cfg.Compressors[0].Compressor.Get(sse.w)
|
|
sse.encoding = cfg.Compressors[0].Encoding
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseEncodings(header string) []string {
|
|
parts := strings.Split(header, ",")
|
|
var tokens []string
|
|
for _, part := range parts {
|
|
token := strings.SplitN(strings.TrimSpace(part), ";", 2)[0]
|
|
if token != "" {
|
|
tokens = append(tokens, token)
|
|
}
|
|
}
|
|
return tokens
|
|
}
|