Merge branch 'develop'

This commit is contained in:
Ben Croker 2025-04-18 11:21:29 -06:00
commit 37fedbd3f1
No known key found for this signature in database
GPG Key ID: 09D799816F1CF332
64 changed files with 1341 additions and 405 deletions

View File

@ -22,8 +22,8 @@ jobs:
- name: Deploy to fly.io
uses: superfly/flyctl-actions/setup-flyctl@master
- run: |
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'task tools'
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'task support'
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task tools'
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task support'
flyctl deploy --local-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

View File

@ -26,5 +26,5 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run CI tests
run: |
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'task tools'
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'task test'
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task tools'
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task test'

View File

@ -53,7 +53,7 @@ Below are details and options available in each of the provided `make` commands:
You can pass in optional CLI arguments to override the default settings Datastar dev uses:
* `TAG=` (default: `1.23.1-alpine`) - allows you to specify the official [golang Docker image](https://hub.docker.com/_/golang) tag that should be used. Using this, you can change the version of Go the container runs, e.g.: `make image-build TAG="1.23-alpine"` will use the latest version of Go 1.23 for Alpine Linux.
* `TAG=` (default is defined in `Dockerfile-dev`) - allows you to specify the official [golang Docker image](https://hub.docker.com/_/golang) tag that should be used. Using this, you can change the version of Go the container runs, e.g.: `make image-build TAG="1.24"` will use the latest patch version of Go 1.24 official Docker image.
### Terminating

View File

@ -1,4 +1,4 @@
FROM docker.io/golang:1.23.3-alpine AS build
FROM docker.io/golang:1.24.2-alpine AS build
RUN apk add --no-cache upx
ENV PORT=8080

View File

@ -1,4 +1,4 @@
ARG TAG=1.23
ARG TAG=1.24
FROM golang:$TAG
@ -37,13 +37,6 @@ RUN apt update && sudo apt upgrade \
npm install -g npm@^10.0.0 \
npm install -g pnpm \
&& \
# Install needed Go tooling \
go install github.com/go-task/task/v3/cmd/task@latest \
&& \
go install github.com/a-h/templ/cmd/templ@latest \
&& \
go install github.com/valyala/quicktemplate/qtc@latest \
&& \
# Install flyctl cli \
curl -L https://fly.io/install.sh | sh \
&& \

View File

@ -1,4 +1,4 @@
TAG?=1.23
TAG?=1.24
CONTAINER?=$(shell basename $(CURDIR))-dev
DEV_PORT?=8080
IMAGE_INFO=$(shell docker image inspect $(CONTAINER):$(TAG))
@ -17,17 +17,17 @@ clean:
docker volume rm go-modules
# Run the development server
dev: --image-check
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'task -w'
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task -w'
# Build the Docker image
image-build:
docker build -f Dockerfile-dev . -t ${IMAGE_NAME} --build-arg TAG=${TAG} --no-cache
${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} -c 'task tools'
${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} -c 'go tool task tools'
# Run the passed in task command
task: --image-check
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'task $(filter-out $@,$(MAKECMDGOALS)) $(MAKEFLAGS)'
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task $(filter-out $@,$(MAKECMDGOALS)) $(MAKEFLAGS)'
# Run the test suite
test: --image-check
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'task test'
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task test'
# Open a shell inside of the container
ssh: --image-check
${DOCKER_RUN} --name ${CONTAINER}-$@ --entrypoint=/bin/sh ${IMAGE_NAME}

View File

@ -28,7 +28,7 @@ Visit the [Datastar Website »](https://data-star.dev/)
Watch the [Videos »](https://www.youtube.com/@data-star)
Join the [Discord Server »](https://discord.com/channels/1296224603642925098/1296224603642925102)
Join the [Discord Server »](https://discord.com/invite/bnRNgZjgPh)
## Getting Started
@ -58,3 +58,5 @@ You can manually add your own plugins to the core:
)
</script>
```
[![Star History Chart](https://api.star-history.com/svg?repos=starfederation/datastar&type=Date)](https://www.star-history.com/#starfederation/datastar&Date)

View File

@ -10,11 +10,18 @@ vars:
tasks:
tools:
platforms: [windows, linux, darwin/arm64, darwin/amd64, openbsd, dragonfly, freebsd, netbsd]
platforms:
[
windows,
linux,
darwin/arm64,
darwin/amd64,
openbsd,
dragonfly,
freebsd,
netbsd,
]
cmds:
- go install github.com/go-task/task/v3/cmd/task@latest
- go install github.com/a-h/templ/cmd/templ@latest
- platforms: [linux/amd64]
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-x64
@ -37,7 +44,6 @@ tasks:
cmd: test -f site/tailwindcli || (echo "#!/bin/sh" > site/tailwindcli && echo "tailwindcss $@" >> site/tailwindcli)
- chmod +x site/tailwindcli
- go install github.com/valyala/quicktemplate/qtc@latest
version:
cmds:
@ -49,7 +55,7 @@ tasks:
generates:
- "**/*.qtpl.go"
cmds:
- qtc
- go tool qtc
build:
deps:
@ -143,7 +149,7 @@ tasks:
sources:
- "**/*.templ"
cmds:
- templ generate .
- go tool templ generate .
kill:
method: none

View File

@ -63,4 +63,6 @@ You can manually add your own plugins to the core:
</script>
```
[![Star History Chart](https://api.star-history.com/svg?repos=starfederation/datastar&type=Date)](https://www.star-history.com/#starfederation/datastar&Date)
{%- endfunc -%}

View File

@ -152,6 +152,7 @@ func writeOutConsts(version string) error {
"examples/ruby/hello-world/hello-world.html": helloWorldExample,
"examples/rust/axum/hello-world/hello-world.html": helloWorldExample,
"examples/rust/rocket/hello-world/hello-world.html": helloWorldExample,
"examples/rust/rama/hello-world/hello-world.html": helloWorldExample,
}
for path, tmplFn := range templates {

View File

@ -1,19 +1,22 @@
module github.com/starfederation/datastar/examples/go/hello-world
go 1.23.3
go 1.24.2
require (
github.com/go-chi/chi/v5 v5.2.0
github.com/starfederation/datastar v1.0.0-beta.2
github.com/go-chi/chi/v5 v5.2.1
github.com/starfederation/datastar v1.0.0-beta.11
)
require (
github.com/a-h/templ v0.3.819 // indirect
github.com/CAFxX/httpcompression v0.0.9 // indirect
github.com/a-h/templ v0.3.857 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/delaneyj/gostar v0.8.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/igrmk/treemap/v2 v2.0.1 // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/text v0.24.0 // indirect
)

View File

@ -1,22 +1,59 @@
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg=
github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/delaneyj/gostar v0.8.0 h1:uT1JR+77P5ePL4BVTXsKNLtwUUtMAu/dNLryjEk95RA=
github.com/delaneyj/gostar v0.8.0/go.mod h1:mlxRWAVbntRR2VWlpXAzt7y9HY+bQtEm/lsyFnGLx/w=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/igrmk/treemap/v2 v2.0.1 h1:Jhy4z3yhATvYZMWCmxsnHO5NnNZBdueSzvxh6353l+0=
github.com/igrmk/treemap/v2 v2.0.1/go.mod h1:PkTPvx+8OHS8/41jnnyVY+oVsfkaOUZGcr+sfonosd4=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/starfederation/datastar v1.0.0-beta.2 h1:r1+PXLSe+wwZhIdGqbwdHEuDthIf5dL+BrP1pNRML3I=
github.com/starfederation/datastar v1.0.0-beta.2/go.mod h1:iUb4gqem8vHiZ7NyAC09GN0hrAKd0LoY3w8ZuQ7b2gQ=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/starfederation/datastar v1.0.0-beta.11 h1:8P5Yt4B40cDWgHHAI7CkWbuxTxSSYqk48pLDfi3JZF0=
github.com/starfederation/datastar v1.0.0-beta.11/go.mod h1:PCPV2lpiI0c86BrE2ndc+5yq8rO2IZWvLqILwk9tVV4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -52,7 +52,7 @@ async def updates_asgi(request):
async def time_updates():
while True:
yield DatastarStreamingHttpResponse.merge_fragments(
[f"""<span id="currentTime">{datetime.now().isoformat()}"""]
f"""<span id="currentTime">{datetime.now().isoformat()}"""
)
await asyncio.sleep(1)
yield DatastarStreamingHttpResponse.merge_signals(
@ -110,7 +110,7 @@ def updates_wsgi(request):
def time_updates():
while True:
yield DatastarStreamingHttpResponse.merge_fragments(
[f"""<span id="currentTime">{datetime.now().isoformat()}"""]
f"""<span id="currentTime">{datetime.now().isoformat()}"""
)
time.sleep(0.5)
yield DatastarStreamingHttpResponse.merge_signals(

View File

@ -54,7 +54,7 @@ async def read_root():
async def time_updates():
while True:
yield DatastarStreamingResponse.merge_fragments(
[f"""<span id="currentTime">{datetime.now().isoformat()}"""]
f"""<span id="currentTime">{datetime.now().isoformat()}"""
)
await asyncio.sleep(1)
yield DatastarStreamingResponse.merge_signals({"currentTime": f"{datetime.now().isoformat()}"})

View File

@ -93,7 +93,7 @@ def GreatTable(pattern=default_pattern):
@app.post
async def table(filter: str):
async def _():
yield DatastarStreamingResponse.merge_fragments([GreatTable(filter)])
yield DatastarStreamingResponse.merge_fragments(GreatTable(filter))
return DatastarStreamingResponse(_())
@ -151,7 +151,7 @@ def index():
async def clock():
while True:
now = datetime.isoformat(datetime.now())
yield DatastarStreamingResponse.merge_fragments([Span(id="currentTime")(now)])
yield DatastarStreamingResponse.merge_fragments(Span(id="currentTime")(now))
await asyncio.sleep(1)
@ -165,7 +165,7 @@ async def hello():
async def _():
# Simulate load time
await asyncio.sleep(1)
yield DatastarStreamingResponse.merge_fragments([HELLO_BUTTON])
yield DatastarStreamingResponse.merge_fragments(HELLO_BUTTON)
return DatastarStreamingResponse(_())
@ -185,7 +185,7 @@ async def reset():
async def _(sse):
await asyncio.sleep(1)
yield sse.merge_fragments([reset_and_hello])
yield sse.merge_fragments(reset_and_hello)
return DatastarFastHTMLResponse(_)

View File

@ -46,7 +46,7 @@ async def index():
async def clock():
while True:
now = datetime.isoformat(datetime.now())
yield DatastarStreamingResponse.merge_fragments([Span(id="currentTime")(now)])
yield DatastarStreamingResponse.merge_fragments(Span(id="currentTime")(now))
await asyncio.sleep(1)
yield DatastarStreamingResponse.merge_signals({"currentTime": f"{now}"})
await asyncio.sleep(1)

View File

@ -47,7 +47,7 @@ async def updates():
async def time_updates():
while True:
yield ServerSentEventGenerator.merge_fragments(
[f"""<span id="currentTime">{datetime.now().isoformat()}"""]
f"""<span id="currentTime">{datetime.now().isoformat()}"""
)
await asyncio.sleep(1)
yield ServerSentEventGenerator.merge_signals({"currentTime": f"{datetime.now().isoformat()}"})

View File

@ -58,13 +58,11 @@ async def add_signal(request):
await response.send(
SSE.merge_fragments(
[
"""
"""
<div class="time signal">
Current time from signal: <span data-text="$currentTime">CURRENT_TIME</span>
</div>
"""
],
""",
selector="#timers",
merge_mode=FragmentMergeMode.FragmentMergeModeAppend,
)
@ -79,13 +77,11 @@ async def add_fragment(request):
await response.send(
SSE.merge_fragments(
[
f"""\
f"""\
<div class="time fragment">
Current time from fragment: {datetime.now().isoformat()}
</div>
"""
],
""",
selector="#timers",
merge_mode=FragmentMergeMode.FragmentMergeModeAppend,
)
@ -101,13 +97,11 @@ async def updates(request):
while True:
await response.send(
SSE.merge_fragments(
[
f"""
f"""
<div class="time fragment" >
Current time from fragment: {datetime.now().isoformat()}
</div>
"""
],
""",
selector=".fragment",
)
)

View File

@ -1,7 +1,8 @@
[package]
edition = "2021"
edition = "2024"
name = "hello_world"
version = "0.1.0"
rust-version = "1.85.0"
[dependencies]
async-stream = "0.3.6"

View File

@ -0,0 +1,2 @@
target
Cargo.lock

View File

@ -0,0 +1,16 @@
[package]
edition = "2024"
name = "hello_world"
version = "0.1.0"
rust-version = "1.85.0"
[dependencies]
async-stream = "0.3.6"
rama = { version = "0.2.0-alpha.11", features = ["http-full"] }
datastar = { path = "../../../../sdk/rust", features = ["rama"] }
futures = "0.3.31"
serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] }
tokio-stream = "0.1.17"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

View File

@ -0,0 +1,35 @@
<!-- This is auto-generated by Datastar. DO NOT EDIT. -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Datastar SDK Demo</title>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
</head>
<body class="bg-white dark:bg-gray-900 text-lg max-w-xl mx-auto my-16">
<div data-signals-delay="400" class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-lg px-6 py-8 ring shadow-xl ring-gray-900/5 space-y-2">
<div class="flex justify-between items-center">
<h1 class="text-gray-900 dark:text-white text-3xl font-semibold">
Datastar SDK Demo
</h1>
<img src="https://data-star.dev/static/images/rocket.png" alt="Rocket" width="64" height="64"/>
</div>
<p class="mt-2">
SSE events will be streamed from the backend to the frontend.
</p>
<div class="space-x-2">
<label for="delay">
Delay in milliseconds
</label>
<input data-bind-delay id="delay" type="number" step="100" min="0" class="w-36 rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-sky-500 focus:outline focus:outline-sky-500 dark:disabled:border-gray-700 dark:disabled:bg-gray-800/20" />
</div>
<button data-on-click="@get(&#39;/hello-world&#39;)" class="rounded-md bg-sky-500 px-5 py-2.5 leading-5 font-semibold text-white hover:bg-sky-700 hover:text-gray-100 cursor-pointer">
Start
</button>
</div>
<div class="my-16 text-8xl font-bold text-transparent" style="background: linear-gradient(to right in oklch, red, orange, yellow, green, blue, blue, violet); background-clip: text">
<div id="message">Hello, world!</div>
</div>
</body>
</html>

View File

@ -0,0 +1,57 @@
use {
async_stream::stream,
core::time::Duration,
datastar::{
Sse,
prelude::{MergeFragments, ReadSignals},
},
rama::{
error::BoxError,
http::{IntoResponse, response::Html, server::HttpServer, service::web::Router},
rt::Executor,
},
serde::Deserialize,
tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt},
};
#[tokio::main]
async fn main() -> Result<(), BoxError> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let app = Router::new()
.get("/", index)
.get("/hello-world", hello_world);
tracing::debug!("listening on 127.0.0.1:3000");
HttpServer::auto(Executor::default())
.listen("127.0.0.1:3000", app)
.await?;
Ok(())
}
async fn index() -> Html<&'static str> {
Html(include_str!("../hello-world.html"))
}
const MESSAGE: &str = "Hello, world!";
#[derive(Deserialize)]
pub struct Signals {
pub delay: u64,
}
async fn hello_world(ReadSignals(signals): ReadSignals<Signals>) -> impl IntoResponse {
Sse(stream! {
for i in 0..MESSAGE.len() {
yield MergeFragments::new(format!("<div id='message'>{}</div>", &MESSAGE[0..i + 1])).into();
tokio::time::sleep(Duration::from_millis(signals.delay)).await;
}
})
}

View File

@ -1,7 +1,8 @@
[package]
edition = "2021"
edition = "2024"
name = "hello_world"
version = "0.1.0"
rust-version = "1.85.0"
[dependencies]
datastar = { path = "../../../../sdk/rust", features = ["rocket"] }

82
go.mod
View File

@ -1,16 +1,15 @@
module github.com/starfederation/datastar
go 1.23.3
go 1.24.2
require (
github.com/CAFxX/httpcompression v0.0.9
github.com/Jeffail/gabs/v2 v2.7.0
github.com/TwiN/go-away v1.6.14
github.com/a-h/templ v0.3.819
github.com/TwiN/go-away v1.6.15
github.com/a-h/templ v0.3.857
github.com/alecthomas/chroma v0.10.0
github.com/benbjohnson/hashfs v0.2.2
github.com/blevesearch/bleve/v2 v2.4.4
github.com/delaneyj/gostar v0.8.0
github.com/delaneyj/toolbelt v0.3.16
github.com/drhodes/golorem v0.0.0-20220328165741-da82e5b29246
github.com/dustin/go-humanize v1.0.1
@ -27,10 +26,10 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huantt/plaintext-extractor v1.1.0
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.17.11
github.com/klauspost/compress v1.18.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/nats-io/nats-server/v2 v2.10.24
github.com/nats-io/nats.go v1.38.0
github.com/nats-io/nats-server/v2 v2.11.1
github.com/nats-io/nats.go v1.41.1
github.com/samber/lo v1.47.0
github.com/segmentio/encoding v0.4.1
github.com/stretchr/testify v1.10.0
@ -42,7 +41,14 @@ require (
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Ladicle/tabwriter v1.0.0 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/RoaringBitmap/roaring v1.9.3 // indirect
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/alecthomas/chroma/v2 v2.15.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bits-and-blooms/bitset v1.12.0 // indirect
github.com/blevesearch/bleve_index_api v1.1.12 // indirect
@ -63,53 +69,91 @@ require (
github.com/blevesearch/zapx/v15 v15.3.16 // indirect
github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/chainguard-dev/git-urls v1.0.2 // indirect
github.com/chewxy/math32 v1.11.1 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.14.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-task/task/v3 v3.42.1 // indirect
github.com/go-task/template v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/igrmk/treemap/v2 v2.0.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-zglob v0.0.6 // indirect
github.com/minio/highwayhash v1.0.3 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
github.com/nats-io/jwt/v2 v2.7.3 // indirect
github.com/nats-io/nkeys v0.4.9 // indirect
github.com/nats-io/nkeys v0.4.10 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/radovskyb/watcher v1.0.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rzajac/clock v0.2.0 // indirect
github.com/rzajac/zflake v0.8.0 // indirect
github.com/sajari/fuzzy v1.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/ysmood/fetchup v0.2.4 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/protobuf v1.36.2 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.61.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.1 // indirect
modernc.org/sqlite v1.34.4 // indirect
mvdan.cc/sh/v3 v3.11.0 // indirect
zombiezen.com/go/sqlite v1.4.0 // indirect
)
tool (
github.com/a-h/templ/cmd/templ
github.com/go-task/task/v3/cmd/task
github.com/valyala/quicktemplate/qtc
)

196
go.sum
View File

@ -1,18 +1,43 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg=
github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/TwiN/go-away v1.6.14 h1:gjFP+6/A36gmj0NpYX0Sz9hrdU0KtHwtNWYnsJgV4fo=
github.com/TwiN/go-away v1.6.14/go.mod h1:d+Gv3XuqjIeFqXYuAIzlyNoDzr1vNsP5B/hRY3u/VLs=
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
github.com/TwiN/go-away v1.6.15 h1:pm1UvsteYNnOWl4bcbhAXQnebR5UcqfcuCi3kl6LJhE=
github.com/TwiN/go-away v1.6.15/go.mod h1:cgCIChHZZU7u9QjVuGAf3X95MPoMPuceCwnvj8+JDB0=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg=
github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/benbjohnson/hashfs v0.2.2 h1:vFZtksphM5LcnMRFctj49jCUkCc7wp3NP6INyfjkse4=
github.com/benbjohnson/hashfs v0.2.2/go.mod h1:7OMXaMVo1YkfiIPxKrl7OXkUTUgWjmsAKyR+E6xDIRM=
github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
@ -55,15 +80,24 @@ github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b h1:ju9Az5Y
github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b/go.mod h1:BlrYNpOu4BvVRslmIG+rLtKhmjIaRhIbG8sb9scGTwI=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8=
github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/delaneyj/gostar v0.8.0 h1:uT1JR+77P5ePL4BVTXsKNLtwUUtMAu/dNLryjEk95RA=
github.com/delaneyj/gostar v0.8.0/go.mod h1:mlxRWAVbntRR2VWlpXAzt7y9HY+bQtEm/lsyFnGLx/w=
github.com/delaneyj/toolbelt v0.3.16 h1:SSBIGQ7a46pSuj91YU4MZYOIqpb/2KNTImaczXWUDUA=
github.com/delaneyj/toolbelt v0.3.16/go.mod h1:2boweQCsRLt3IfxvP/rIJKnS+7PW7SIfMGZGipcvs3U=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
@ -71,37 +105,71 @@ github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbj
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/drhodes/golorem v0.0.0-20220328165741-da82e5b29246 h1:m0+1paUpmLlBpUxldAEvJZVCrNQpt2iyecCw4TdHdOc=
github.com/drhodes/golorem v0.0.0-20220328165741-da82e5b29246/go.mod h1:NsKVpF4h4j13Vm6Cx7Kf0V03aJKjfaStvm5rvK4+FyQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/evanw/esbuild v0.24.2 h1:PQExybVBrjHjN6/JJiShRGIXh1hWVm6NepVnhZhrt0A=
github.com/evanw/esbuild v0.24.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-faker/faker/v4 v4.5.0 h1:ARzAY2XoOL9tOUK+KSecUQzyXQsUaZHefjyF8x6YFHc=
github.com/go-faker/faker/v4 v4.5.0/go.mod h1:p3oq1GRjG2PZ7yqeFFfQI20Xm61DoBDlCA8RiSyZ48M=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-sanitize/sanitize v1.1.0 h1:wq9tl5+VfkyCacCZIVQf6ksegRpfWl3N2vAyyYD0F1I=
github.com/go-sanitize/sanitize v1.1.0/go.mod h1:r+anm3xp/Y1+pTNvPSgHMznwb0VVZgszoMQs3naOf0A=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/task/v3 v3.42.1 h1:HOaFbZGLOrAy2V/dLsX2rGJZVG2Qx6268KUIAIXdNE4=
github.com/go-task/task/v3 v3.42.1/go.mod h1:q9a3NGSYIQL6GdI920pypRSj0rcfKZ0gV/3sHtMxzds=
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g=
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -116,23 +184,27 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/huantt/plaintext-extractor v1.1.0 h1:dZkJN0fGZf1o8x9UdR6hHqkZnqIwX94YlGJ/lSXUZ5c=
github.com/huantt/plaintext-extractor v1.1.0/go.mod h1:zIIbG/hZnsnLgzDbZ2T8fOrA4SLGWCoHWWYZo0Anx9c=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/igrmk/treemap/v2 v2.0.1 h1:Jhy4z3yhATvYZMWCmxsnHO5NnNZBdueSzvxh6353l+0=
github.com/igrmk/treemap/v2 v2.0.1/go.mod h1:PkTPvx+8OHS8/41jnnyVY+oVsfkaOUZGcr+sfonosd4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede h1:YrgBGwxMRK0Vq0WSCWFaZUnTsrA/PZE/xs1QZh+/edg=
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -141,48 +213,76 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE=
github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4=
github.com/nats-io/nats-server/v2 v2.10.24 h1:KcqqQAD0ZZcG4yLxtvSFJY7CYKVYlnlWoAiVZ6i/IY4=
github.com/nats-io/nats-server/v2 v2.10.24/go.mod h1:olvKt8E5ZlnjyqBGbAXtxvSQKsPodISK5Eo/euIta4s=
github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA=
github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw=
github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=
github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=
github.com/nats-io/nats-server/v2 v2.11.1 h1:LwdauqMqMNhTxTN3+WFTX6wGDOKntHljgZ+7gL5HCnk=
github.com/nats-io/nats-server/v2 v2.11.1/go.mod h1:leXySghbdtXSUmWem8K9McnJ6xbJOb0t9+NQ5HTRZjI=
github.com/nats-io/nats.go v1.41.1 h1:lCc/i5x7nqXbspxtmXaV4hRguMPHqE/kYltG9knrCdU=
github.com/nats-io/nats.go v1.41.1/go.mod h1:mzHiutcAdZrg6WLfYVKXGseqqow2fWmwlTEUOHsI4jY=
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rzajac/clock v0.2.0 h1:mxiL5/iTu7+pciqYGMxqUNTR+T2nxVvIdEUn3wfF4rU=
github.com/rzajac/clock v0.2.0/go.mod h1:7ybePrkaEnyNk5tBHJZYZbeBU+2werzUVXn+mKT6iyw=
github.com/rzajac/zflake v0.8.0 h1:EYNCn2jh16JAGuKw+NJmTz0unAH81elaSrefd3KWriU=
github.com/rzajac/zflake v0.8.0/go.mod h1:uSQN20u/2bvKMkRLrqnKRqUk6tb2Ixac09WMljsSFhc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w=
github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.2-0.20201103103935-92707c0b2d50/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -199,6 +299,8 @@ github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdq
github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM=
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/ysmood/fetchup v0.2.4 h1:2kfWr/UrdiHg4KYRrxL2Jcrqx4DZYD+OtWu7WPBZl5o=
@ -224,11 +326,12 @@ go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
@ -243,28 +346,33 @@ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -272,8 +380,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -282,8 +390,11 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@ -291,10 +402,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -304,13 +415,16 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -338,5 +452,7 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=

View File

@ -58,3 +58,5 @@ You can manually add your own plugins to the core:
)
</script>
```
[![Star History Chart](https://api.star-history.com/svg?repos=starfederation/datastar&type=Date)](https://www.star-history.com/#starfederation/datastar&Date)

View File

@ -1,6 +1,5 @@
{:lint-as {fr.jeremyschoffen.datastar.utils/defroutes clojure.core/def
starfederation.datastar.clojure.utils/transient-> clojure.core/->
starfederation.datastar.clojure.utils/def-clone clojure.core/def}
starfederation.datastar.clojure.utils/transient-> clojure.core/->}
:hooks
{:analyze-call
{test.utils/with-server hooks.test-hooks/with-server}}

View File

@ -1,5 +1,32 @@
# Release notes for the Clojure SDK
## 2025-04-07
### Added
- The vars holding keywords (like `on-open`) in the adapter namespaces were not
properly recognized by Clj-kondo. This generated `unresolved var` warnings. A
specific Clj-kondo config has been added to fix these warnings.
### Fixed
- The HTTP version detection that determines whether to add the
`Connection keep-alive` HTTP header has been changed. The header is now
properly added for versions older than `HTTP/1.1`.
### Changed
- Bumped the ring version from `1.13.0` to `1.14.1`. This encourages users
to use Jetty 12 when using ring jetty adapter.
## 2025-03-31
### Changed
- Removed the use of the deprecated `:on-open` and `:on-close` keywords. The
`->sse-response` functions of both adapters will not use them anymore. The
corresponding docstrings are updated.
## 2025-03-11
### Deprecated

View File

@ -20,7 +20,6 @@
(def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile)
;; TODO: remove deprecated get-on-open/close next release
(defn ->sse-response
"Make a Ring like response that will start a SSE stream.
@ -30,9 +29,6 @@
Note that the SSE connection stays opened util you close it.
General options:
- `:on-open`: deprecated in favor of [[on-open]]
- `:on-close`: deprecated in favor of [[on-close]]
- `:status`: status for the HTTP response, defaults to 200.
- `:headers`: ring headers map to add to the response.
- [[on-open]]: mandatory callback called when the generator is ready to send.
@ -53,9 +49,9 @@
namespace if you want to write your own profiles.
"
[ring-request opts]
{:pre [(ac/get-on-open opts)]}
(let [on-open-cb (ac/get-on-open opts)
on-close-cb (ac/get-on-close opts)
{:pre [(ac/on-open opts)]}
(let [on-open-cb (ac/on-open opts)
on-close-cb (ac/on-close opts)
future-send! (promise)
future-gen (promise)]
(hk-server/as-channel ring-request

View File

@ -1,2 +1,2 @@
{:paths ["src/main"]
:deps {org.ring-clojure/ring-core-protocols {:mvn/version "1.13.0"}}}
:deps {org.ring-clojure/ring-core-protocols {:mvn/version "1.14.1"}}}

View File

@ -41,9 +41,6 @@
- [[write-profile]]: write profile for the connection
defaults to [[basic-profile]]
- `:on-open`: deprecated in favor of [[on-open]]
- `:on-close`: deprecated in favor of [[on-close]]
When it comes to write profiles, the SDK provides:
- [[basic-profile]]
- [[buffered-writer-profile]]
@ -54,7 +51,7 @@
namespace if you want to write your own profiles.
"
[ring-request {:keys [status] :as opts}]
{:pre [(ac/get-on-open opts)]}
{:pre [(ac/on-open opts)]}
(let [sse-gen (impl/->sse-gen)]
{:status (or status 200)
:headers (ac/headers ring-request opts)

View File

@ -23,7 +23,6 @@
(write! writer event-type data-lines event-opts)
(ac/flush writer)))))
;; TODO: Remove the get-on-open/close next release
;; Note that the send! field has 2 usages:
;; - it stores the sending function
@ -49,7 +48,7 @@
(set! send! (->send output-stream opts))
(set! on-exception (or (ac/on-exception opts)
ac/default-on-exception))
(when-let [cb (ac/get-on-close opts)]
(when-let [cb (ac/on-close opts)]
(set! on-close cb)))
;; flush the HTTP headers
@ -68,7 +67,7 @@
(if-let [e @!error]
(throw e) ;; if error throw, the lock is already released
;; if all is ok call on-open, it can safely throw...
(when-let [on-open (-> response ::opts (ac/get-on-open))]
(when-let [on-open (-> response ::opts ac/on-open)]
(on-open this)))))))
p/SSEGenerator

View File

@ -1,6 +1,7 @@
{:paths ["sdk/src/main"]
{:paths []
:deps {io.github.paintparty/fireworks {:mvn/version "0.10.4"}
:deps {sdk/sdk {:local/root "./sdk"}
io.github.paintparty/fireworks {:mvn/version "0.10.4"}
com.taoensso/telemere {:mvn/version "1.0.0-RC3"}
com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.18.0"}}
@ -42,7 +43,7 @@
:http-kit {:extra-deps {sdk/adapter-http-kit {:local/root "./adapter-http-kit"}}}
:ring-jetty {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
ring/ring-jetty-adapter {:mvn/version "1.13.0"}}}
ring/ring-jetty-adapter {:mvn/version "1.14.1"}}}
:ring-rj9a {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
info.sunng/ring-jetty9-adapter {:mvn/version "0.36.1"}}}

View File

@ -1,2 +1,2 @@
{:paths ["src/main"]}
{:paths ["src/main" "resources"]}

View File

@ -0,0 +1,3 @@
{:lint-as
{starfederation.datastar.clojure.utils/def-clone clojure.core/def}}

View File

@ -134,15 +134,17 @@
(-> (transient {})
(u/merge-transient! sse/base-SSE-headers)
(cond->
(sse/http1? ring-request) (assoc! "Connection" "keep-alive",)
encoding (assoc! "Content-Encoding" encoding))
(sse/add-keep-alive? ring-request) (assoc! "Connection" "keep-alive",)
encoding (assoc! "Content-Encoding" encoding))
(u/merge-transient! (:headers opts))
persistent!)))
(comment
(headers {:protocol "HTTP/2"} {})
(headers {:protocol "HTTP/2"} {write-profile {content-encoding "br"}}))
(headers {:protocol "HTTP/1.0"} {})
(headers {:protocol "HTTP/1.1"} {})
(headers {:protocol "HTTP/2"} {})
(headers {:protocol "HTTP/2"} {write-profile {content-encoding "br"}}))
;; -----------------------------------------------------------------------------
;; Utilities for wrapping an OutputStream
@ -411,12 +413,3 @@
(throw (ex-info "Error sending SSE event." ctx e))))
;; TODO: remove next version
(defn get-on-open [opts]
(or (on-open opts) (:on-open opts)))
(defn get-on-close [opts]
(or (on-close opts) (:on-close opts)))

View File

@ -15,11 +15,16 @@
"Content-Type" "text/event-stream"})
(defn http1? [ring-request]
(defn add-keep-alive? [ring-request]
(let [protocol (:protocol ring-request)]
(or
(nil? protocol)
(= "HTTP/1.1" protocol))))
(or (nil? protocol)
(neg? (compare protocol "HTTP/1.1")))))
(comment
(add-keep-alive? {:protocol "HTTP/0.9"})
(add-keep-alive? {:protocol "HTTP/1.0"})
(add-keep-alive? {:protocol "HTTP/1.1"})
(add-keep-alive? {:protocol "HTTP/2"}))
(defn headers
@ -41,7 +46,7 @@
(-> (transient {})
(u/merge-transient! base-SSE-headers)
(cond->
(http1? ring-request) (assoc! "Connection" "keep-alive",))
(add-keep-alive? ring-request) (assoc! "Connection" "keep-alive",))
(u/merge-transient! (:headers opts))
persistent!))

View File

@ -55,6 +55,3 @@
(u/clear-terminal!)
(u/reboot-hk-server! #'handler))

View File

@ -11,9 +11,7 @@
[starfederation.datastar.clojure.adapter.common :as ac]
[starfederation.datastar.clojure.api :as d*]
[starfederation.datastar.clojure.api.sse :as sse]
[test.utils :as u])
(:import
java.util.concurrent.CountDownLatch))
[test.utils :as u]))
(def ^:dynamic *ctx* nil)
@ -93,7 +91,8 @@
(println "Killing agents")
(shutdown-agents)))))
(install-shutdown-hooks!)
(defonce _ (install-shutdown-hooks!))
;; -----------------------------------------------------------------------------
;; Generic counters tests
;; -----------------------------------------------------------------------------
@ -143,13 +142,12 @@
(defn- ->persistent-sse-handler
"Make a ring handler that puts a sse gen into an atom for use later.
Counts down an latch to allow the tests to continue."
[->sse-response !conn ^CountDownLatch latch]
[->sse-response !conn]
(fn handler
([req]
(->sse-response req
{ac/on-open (fn [sse-gen]
(reset! !conn sse-gen)
(.countDown latch))}))
(deliver !conn sse-gen))}))
([req respond _raise]
(respond (handler req)))))
@ -159,11 +157,9 @@
We put together an atom to store a sse generator, a countdown latch and a
ring handler hooked to them."
[->sse-response]
(let [!conn (atom nil)
latch (CountDownLatch. 1)
handler (->persistent-sse-handler ->sse-response !conn latch)]
(let [!conn (promise)
handler (->persistent-sse-handler ->sse-response !conn)]
{:!conn !conn
:latch latch
:handler handler}))
@ -177,8 +173,7 @@
{:keys [!conn latch handler]} (setup-persistent-see-state ->sse-response)]
(u/with-server server handler (dissoc server-opts :get-port)
(binding [*ctx* {:port (get-port server)
:!conn !conn
:latch latch}]
:!conn !conn}]
(f))))))
@ -188,13 +183,14 @@
(defn run-persistent-sse-test! []
(let [{:keys [port latch !conn]} *ctx*
response (http/request {:url (u/url port "")})]
(.await ^CountDownLatch latch)
(let [sse-gen @!conn]
(persistent-see-send-events! sse-gen)
(d*/close-sse! sse-gen)
(deref response 10 :error))))
(let [{:keys [port !conn]} *ctx*
response (http/request {:url (u/url port "")})
sse-gen (deref !conn 100 nil)]
(when-not sse-gen
(throw (ex-info "The handler did not deliver the persistent sse-gen." {})))
(persistent-see-send-events! sse-gen)
(d*/close-sse! sse-gen)
(deref response 10 :error)))
@ -202,12 +198,18 @@
(lt/expect (= (:status response) 200)))
(def SSE-headers-1 (update-keys (sse/headers {}) (comp keyword string/lower-case)))
#_{:clj-kondo/ignore true}
(def SSE-headers-2+ (update-keys (sse/headers {:protocol "2"}) (comp keyword string/lower-case)))
(defn ->headers [req]
(-> req
sse/headers
(update-keys (comp keyword string/lower-case))))
(def SSE-headers-1-dot-0 (->headers {}))
(def SSE-headers-1-dot-1 (->headers {:protocol "HTTP/1.1"}))
(def SSE-headers-2+ (->headers {:protocol "HTTP/2"}))
(defn p-sse-http1-headers-ok? [response]
(lt/expect (mc/match? SSE-headers-1 (:headers response))))
(lt/expect (mc/match? SSE-headers-1-dot-1 (:headers response))))
(def expected-p-sse-res-body

View File

@ -9,7 +9,7 @@
(:import
[java.io Closeable ByteArrayOutputStream OutputStreamWriter]))
;; Mock Http-kit channel
(defrecord Channel [^ByteArrayOutputStream baos
!ch-open?
!on-close]
@ -64,7 +64,7 @@
(hk-server/on-close c
(fn [status]
(send!)
(when-let [callback (:on-close opts)]
(when-let [callback (ac/on-close opts)]
(callback c status))))
(impl/->sse-gen c send!)))

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>StarFederation.Datastar</AssemblyName>
<Version>1.0.0-beta.4</Version>
<Version>1.0.0-beta.5</Version>
<RootNamespace>StarFederation.Datastar</RootNamespace>
<Nullable>disabled</Nullable>

View File

@ -72,7 +72,7 @@ module ServerSentEvent =
then $"id: {sse.Id |> ValueOption.get}"
if (sse.Retry <> Consts.DefaultSseRetryDuration)
then $"retry: {sse.Retry.Milliseconds}"
then $"retry: {sse.Retry.TotalMilliseconds}"
yield! sse.DataLines |> Array.map (fun dataLine -> $"data: {dataLine}")

View File

@ -1,89 +1,18 @@
# Go SDK for Datastar
[![Go
Reference](https://pkg.go.dev/badge/github.com/starfederation/datastar.svg)](https://pkg.go.dev/github.com/starfederation/datastar)
[![Go Reference](https://pkg.go.dev/badge/github.com/starfederation/datastar.svg)](https://pkg.go.dev/github.com/starfederation/datastar)
Implements the [SDK spec](../README.md) and exposes an abstract
ServerSentEventGenerator struct that can be used to implement runtime specific
classes.
ServerSentEventGenerator struct that can be used to implement runtime specific classes.
Usage is straightforward:
## Installation
```go
package main
```sh
go get -u github.com/starfederation/datastar/sdk/go
```
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log/slog"
"net/http"
"os"
"time"
## Examples
datastar "github.com/starfederation/datastar/sdk/go"
)
const port = 9001
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
cdn := "https://cdn.jsdelivr.net/gh/starfederation/datastar@develop/bundles/datastar.js"
style := "display:flex;flex-direction:column;background-color:oklch(25.3267% 0.015896
252.417568);height:100vh;justify-content:center;align-items:center;font-family:ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';"
page := []byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<script type="module" defer src="%s"></script>
</head>
<body style="%s">
<span id="feed" data-on-load="%s"></span>
</body>
</html>
`, cdn, style, datastar.GetSSE("/stream")))
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
})
mux.HandleFunc("GET /stream", func(w http.ResponseWriter, r *http.Request) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
sse := datastar.NewSSE(w, r)
for {
select {
case <-r.Context().Done(): logger.Debug("Client connection closed") return case <-ticker.C: bytes :=make([]byte, 3) _,
err :=rand.Read(bytes) if err !=nil { logger.Error("Error generating random bytes: ", slog.String(" error",
err.Error())) return } hexString :=hex.EncodeToString(bytes) frag :=fmt.Sprintf(`<span id="feed"
style="color:#%s;border:1px solid #%s;border-radius:0.25rem;padding:1rem;">%s</span>`, hexString, hexString,
hexString)
sse.MergeFragments(frag)
}
}
})
logger.Info(fmt.Sprintf("Server starting at 0.0.0.0:%d", port))
if err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", port), mux); err != nil {
logger.Error("Error starting server:", slog.String("error", err.Error()))
}
}
```
## Examples
The [Datastar website](https://data-star.dev) acts as a [set of
examples](https://github.com/starfederation/datastar/tree/develop/site) for how to use the SDK.
- [Basic Usage](https://github.com/starfederation/datastar/tree/develop/sdk/go/examples/basic)
- [Hot Reload](https://github.com/starfederation/datastar/tree/develop/sdk/go/examples/hotreload)
- The [Datastar website](https://data-star.dev) also acts as a [set of examples](https://github.com/starfederation/datastar/tree/develop/site) for how to use the SDK.

View File

@ -0,0 +1,73 @@
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log/slog"
"net/http"
"time"
datastar "github.com/starfederation/datastar/sdk/go"
)
const (
cdn = "https://cdn.jsdelivr.net/gh/starfederation/datastar@develop/bundles/datastar.js"
port = 9001
)
func main() {
mux := http.NewServeMux()
style := "display:flex;flex-direction:column;background-color:oklch(25.3267% 0.015896 252.417568);height:100vh;justify-content:center;align-items:center;font-family:ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';"
page := []byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<script type="module" defer src="%s"></script>
</head>
<body style="%s">
<span id="feed" data-on-load="%s"></span>
</body>
</html>
`, cdn, style, datastar.GetSSE("/stream")))
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
})
mux.HandleFunc("GET /stream", func(w http.ResponseWriter, r *http.Request) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
sse := datastar.NewSSE(w, r)
for {
select {
case <-r.Context().Done():
slog.Debug("Client connection closed")
return
case <-ticker.C:
bytes := make([]byte, 3)
if _, err := rand.Read(bytes); err != nil {
slog.Error("Error generating random bytes: ", slog.String("error", err.Error()))
return
}
hexString := hex.EncodeToString(bytes)
frag := fmt.Sprintf(`<span id="feed" style="color:#%s;border:1px solid #%s;border-radius:0.25rem;padding:1rem;">%s</span>`, hexString, hexString, hexString)
sse.MergeFragments(frag)
}
}
})
slog.Info(fmt.Sprintf("Server starting at 0.0.0.0:%d", port))
if err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", port), mux); err != nil {
slog.Error("Error starting server:", slog.String("error", err.Error()))
}
}

View File

@ -0,0 +1,91 @@
package main
import (
"fmt"
"log/slog"
"net/http"
"sync"
"time"
datastar "github.com/starfederation/datastar/sdk/go"
)
const (
serverAddress = "localhost:9001"
cdn = "https://cdn.jsdelivr.net/gh/starfederation/datastar@develop/bundles/datastar.js"
)
var hotReloadOnlyOnce sync.Once
func HotReloadHandler(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
hotReloadOnlyOnce.Do(func() {
// Refresh the client page as soon as connection
// is established. This will occur only once
// after the server starts.
sse.ExecuteScript(
"window.location.reload()",
datastar.WithExecuteScriptRetryDuration(time.Second),
)
})
// Freeze the event stream until the connection
// is lost for any reason. This will force the client
// to attempt to reconnect after the server reboots.
<-r.Context().Done()
}
func PageWithHotReload(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<script type="module" defer src="%s"></script>
</head>
<!-- next line mounts the refresh script -->
<body data-on-load="@get('/hotreload', {retryInterval: 100})">
<main>
<p>
This page will automatically reload on any filesystem change. Update this paragraph, save changes, and
switch back to the open browser tab to observe
the update.
</p>
</main>
</body>
</html>
`, cdn)))
}
func main() {
// Hot reload requires a file system watcher and
// a refresh script. Reflex is one of the best tools
// for running a command on each file change.
//
// $ go install github.com/cespare/reflex@latest
// $ reflex --start-service -- sh -c 'go run .'
//
// The refresh script is a Datastar handler
// that emits a page refresh event only once
// for each server start.
//
// When the the file watcher forces the server to restart,
// Datastar client will lose the network connection to the
// server and attempt to reconnect. Once the connection is
// established, the client will receive the refresh event.
http.HandleFunc("/hotreload", HotReloadHandler)
http.HandleFunc("/", PageWithHotReload)
slog.Info(fmt.Sprintf(
"Open your browser to: http://%s/",
serverAddress,
))
http.ListenAndServe(serverAddress, nil)
// Tip: read the reflex documentation to see advanced usage
// options like responding to specific file changes by filter.
//
// $ reflex --help
}

View File

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

View File

@ -7,33 +7,48 @@ import (
"time"
)
type ExecuteScriptOptions struct {
// executeScriptOptions hold script options that will be translated to [SSEEventOptions].
type executeScriptOptions struct {
EventID string
RetryDuration time.Duration
Attributes []string
AutoRemove *bool
}
type ExecuteScriptOption func(*ExecuteScriptOptions)
// ExecuteScriptOption configures script execution event that will be sent to the client.
type ExecuteScriptOption func(*executeScriptOptions)
// WithExecuteScriptEventID configures an optional event ID for the script execution 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 WithExecuteScriptEventID(id string) ExecuteScriptOption {
return func(o *ExecuteScriptOptions) {
return func(o *executeScriptOptions) {
o.EventID = id
}
}
// WithExecuteScriptRetryDuration overrides the [DefaultSseRetryDuration] for this script
// execution only.
func WithExecuteScriptRetryDuration(retryDuration time.Duration) ExecuteScriptOption {
return func(o *ExecuteScriptOptions) {
return func(o *executeScriptOptions) {
o.RetryDuration = retryDuration
}
}
// WithExecuteScriptAttributes overrides the default script attribute
// value `type module`, which renders as `<script type="module">` in client's browser.
// Each attribute should include a pair of plain words representing the attribute name and value
// without any formatting.
func WithExecuteScriptAttributes(attributes ...string) ExecuteScriptOption {
return func(o *ExecuteScriptOptions) {
return func(o *executeScriptOptions) {
o.Attributes = attributes
}
}
// WithExecuteScriptAttributeKVs is an alternative option for [WithExecuteScriptAttributes].
// Even parameters are keys, odd parameters are their values.
func WithExecuteScriptAttributeKVs(kvs ...string) ExecuteScriptOption {
if len(kvs)%2 != 0 {
panic("WithExecuteScriptAttributeKVs requires an even number of arguments")
@ -46,14 +61,16 @@ func WithExecuteScriptAttributeKVs(kvs ...string) ExecuteScriptOption {
return WithExecuteScriptAttributes(attributes...)
}
// WithExecuteScriptAutoRemove requires the client to eliminate the script element after its execution.
func WithExecuteScriptAutoRemove(autoremove bool) ExecuteScriptOption {
return func(o *ExecuteScriptOptions) {
return func(o *executeScriptOptions) {
o.AutoRemove = &autoremove
}
}
// ExecuteScript runs a script in the client browser. Seperate commands with semicolons.
func (sse *ServerSentEventGenerator) ExecuteScript(scriptContents string, opts ...ExecuteScriptOption) error {
options := &ExecuteScriptOptions{
options := &executeScriptOptions{
RetryDuration: DefaultSseRetryDuration,
Attributes: []string{"type module"},
}

View File

@ -1,13 +1,14 @@
package datastar
import (
"context"
"fmt"
"io"
"github.com/a-h/templ"
"github.com/delaneyj/gostar/elements"
"github.com/valyala/bytebufferpool"
)
// ValidFragmentMergeTypes is a list of valid fragment merge modes.
var ValidFragmentMergeTypes = []FragmentMergeMode{
FragmentMergeModeMorph,
FragmentMergeModeInner,
@ -19,68 +20,107 @@ 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...))
}
func (sse *ServerSentEventGenerator) MergeFragmentTempl(c templ.Component, opts ...MergeFragmentOption) error {
// TemplComponent satisfies the component rendering interface for HTML template engine [Templ].
// This separate type ensures compatibility with [Templ] without imposing a dependency requirement
// on those who prefer to use a different template engine.
//
// [Templ]: https://templ.guide/
type TemplComponent interface {
Render(ctx context.Context, w io.Writer) error
}
// MergeFragmentTempl is a convenience adaptor of [sse.MergeFragments] for [TemplComponent].
func (sse *ServerSentEventGenerator) MergeFragmentTempl(c TemplComponent, opts ...MergeFragmentOption) error {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
if err := c.Render(sse.Context(), buf); err != nil {
@ -92,7 +132,17 @@ func (sse *ServerSentEventGenerator) MergeFragmentTempl(c templ.Component, opts
return nil
}
func (sse *ServerSentEventGenerator) MergeFragmentGostar(child elements.ElementRenderer, opts ...MergeFragmentOption) error {
// GoStarElementRenderer satisfies the component rendering interface for HTML template engine [GoStar].
// This separate type ensures compatibility with [GoStar] without imposing a dependency requirement
// on those who prefer to use a different template engine.
//
// [GoStar]: https://github.com/delaneyj/gostar
type GoStarElementRenderer interface {
Render(w io.Writer) error
}
// MergeFragmentGostar is a convenience adaptor of [sse.MergeFragments] for [GoStarElementRenderer].
func (sse *ServerSentEventGenerator) MergeFragmentGostar(child GoStarElementRenderer, opts ...MergeFragmentOption) error {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
if err := child.Render(buf); err != nil {
@ -104,22 +154,37 @@ func (sse *ServerSentEventGenerator) MergeFragmentGostar(child elements.ElementR
return nil
}
// GetSSE is a convenience method for generating Datastar backend [get] action attribute.
//
// [get]: https://data-star.dev/reference/action_plugins#get
func GetSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@get('%s')`, fmt.Sprintf(urlFormat, args...))
}
// PostSSE is a convenience method for generating Datastar backend [post] action attribute.
//
// [post]: https://data-star.dev/reference/action_plugins#post
func PostSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@post('%s')`, fmt.Sprintf(urlFormat, args...))
}
// PutSSE is a convenience method for generating Datastar backend [put] action attribute.
//
// [put]: https://data-star.dev/reference/action_plugins#put
func PutSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@put('%s')`, fmt.Sprintf(urlFormat, args...))
}
// PatchSSE is a convenience method for generating Datastar backend [patch] action attribute.
//
// [patch]: https://data-star.dev/reference/action_plugins#patch
func PatchSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@patch('%s')`, fmt.Sprintf(urlFormat, args...))
}
// DeleteSSE is a convenience method for generating Datastar backend [delete] action attribute.
//
// [delete]: https://data-star.dev/reference/action_plugins#delete
func DeleteSSE(urlFormat string, args ...any) string {
return fmt.Sprintf(`@delete('%s')`, fmt.Sprintf(urlFormat, args...))
}

View File

@ -7,7 +7,9 @@ import (
"time"
)
type MergeFragmentOptions struct {
// mergeFragmentOptions holds the configuration data for [MergeFragmentOption]s used
// for initialization of [sse.MergeFragments] event.
type mergeFragmentOptions struct {
EventID string
RetryDuration time.Duration
Selector string
@ -15,37 +17,61 @@ type MergeFragmentOptions struct {
UseViewTransitions bool
}
type MergeFragmentOption func(*MergeFragmentOptions)
// MergeFragmentOption configures the [sse.MergeFragments] event initialization.
type MergeFragmentOption func(*mergeFragmentOptions)
// WithMergeFragmentsEventID configures an optional event ID for the fragments 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 WithMergeFragmentsEventID(id string) MergeFragmentOption {
return func(o *mergeFragmentOptions) {
o.EventID = id
}
}
// 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: "",
RetryDuration: DefaultSseRetryDuration,
Selector: "",
MergeMode: FragmentMergeModeMorph,
}
for _, opt := range opts {
opt(options)
@ -88,38 +114,54 @@ func (sse *ServerSentEventGenerator) MergeFragments(fragment string, opts ...Mer
return nil
}
type RemoveFragmentsOptions struct {
// mergeFragmentOptions holds the configuration data for [RemoveFragmentsOption]s used
// for initialization of [sse.RemoveFragments] event.
type removeFragmentsOptions struct {
EventID string
RetryDuration time.Duration
UseViewTransitions *bool
}
type RemoveFragmentsOption func(*RemoveFragmentsOptions)
// RemoveFragmentsOption configures the [sse.RemoveFragments] event.
type RemoveFragmentsOption func(*removeFragmentsOptions)
// WithRemoveEventID configures an optional event ID for the fragment removal event.
// The client message field [lastEventId] will be set to this value.
// If the next event does not have an event ID, the last used event ID will remain.
//
// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
func WithRemoveEventID(id string) RemoveFragmentsOption {
return func(o *RemoveFragmentsOptions) {
return func(o *removeFragmentsOptions) {
o.EventID = id
}
}
// WithExecuteScriptRetryDuration overrides the [DefaultSseRetryDuration] for this script
// execution only.
func WithRemoveRetryDuration(d time.Duration) RemoveFragmentsOption {
return func(o *RemoveFragmentsOptions) {
return func(o *removeFragmentsOptions) {
o.RetryDuration = d
}
}
// WithRemoveUseViewTransitions specifies whether to use [view transitions] when merging fragments.
//
// [view transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API
func WithRemoveUseViewTransitions(useViewTransition bool) RemoveFragmentsOption {
return func(o *RemoveFragmentsOptions) {
return func(o *removeFragmentsOptions) {
o.UseViewTransitions = &useViewTransition
}
}
// MergeFragments sends a [CSS selector] to the client to update the DOM tree by removing matching elements.
//
// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors
func (sse *ServerSentEventGenerator) RemoveFragments(selector string, opts ...RemoveFragmentsOption) error {
if selector == "" {
panic("missing " + SelectorDatalineLiteral)
}
options := &RemoveFragmentsOptions{
options := &removeFragmentsOptions{
EventID: "",
RetryDuration: DefaultSseRetryDuration,
UseViewTransitions: nil,

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
# Datastar-py
# datastar-py
The `datastar_py` package provides backend helpers for the [Datastar](https://data-star.dev) JS library.
The `datastar-py` package provides backend helpers for the [Datastar](https://data-star.dev) JS library.
Datastar requires all backend responses to use SSE. This allows the backend to
send any number of responses, from zero to inifinity.
`Datastar-py` helps with the formatting of these responses, while also
`datastar-py` helps with the formatting of these responses, while also
providing helper functions for the different supported responses.
To use `datastar-py`, import the SSE generator in your app and then use
@ -32,7 +32,7 @@ async def updates():
return response
```
There are also a number of custom responses/helpers for various frameworks. Current ly the following frameworks are supported:
There are also a number of custom responses/helpers for various frameworks. Currently the following frameworks are supported:
* [Sanic](https://sanic.dev/en/)
* [Django](https://www.djangoproject.com/)

View File

@ -37,7 +37,7 @@ build-backend = "hatchling.build"
dev = [
"django>=4.2.17",
"fastapi[standard]>=0.115.4",
# "fasthtml>=0.12.0",
"python-fasthtml>=0.12.0; python_version > '3.10'",
"flask[async]>=3.0.3",
"quart>=0.19.9",
"sanic>=24.6.0",

View File

@ -1,15 +1,2 @@
from typing import override
from fastcore.xml import to_xml
from .sse import SSE_HEADERS, ServerSentEventGenerator
from .starlette import DatastarStreamingResponse as _DatastarStreamingResponse
class DatastarStreamingResponse(_DatastarStreamingResponse):
@classmethod
@override
def merge_fragments(cls, fragments, *args, **kwargs):
xml_fragments = [f if isinstance(f, str) else to_xml(f) for f in fragments]
# From here, business as usual
return super().merge_fragments(xml_fragments, *args, **kwargs)
from .sse import ServerSentEventGenerator
from .starlette import DatastarStreamingResponse

View File

@ -1,6 +1,6 @@
import json
from itertools import chain
from typing import Optional
from typing import Optional, Protocol, Union, runtime_checkable
import datastar_py.consts as consts
@ -11,6 +11,17 @@ SSE_HEADERS = {
}
@runtime_checkable
class _HtmlProvider(Protocol):
"""A type that produces text ready to be placed in an HTML document.
This is a convention used by html producing/consuming libraries. This lets
e.g. fasthtml fasttags, or htpy elements, be passed straight in to
merge_fragments."""
def __html__(self) -> str: ...
class ServerSentEventGenerator:
__slots__ = ()
@ -38,13 +49,15 @@ class ServerSentEventGenerator:
@classmethod
def merge_fragments(
cls,
fragments: list[str],
fragments: Union[str, _HtmlProvider],
selector: Optional[str] = None,
merge_mode: Optional[consts.FragmentMergeMode] = None,
use_view_transition: bool = consts.DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS,
event_id: Optional[int] = None,
retry_duration: int = consts.DEFAULT_SSE_RETRY_DURATION,
):
if isinstance(fragments, _HtmlProvider):
fragments = fragments.__html__()
data_lines = []
if merge_mode:
data_lines.append(f"data: {consts.MERGE_MODE_DATALINE_LITERAL} {merge_mode}")
@ -57,8 +70,7 @@ class ServerSentEventGenerator:
data_lines.extend(
f"data: {consts.FRAGMENTS_DATALINE_LITERAL} {x}"
for fragment in fragments
for x in fragment.splitlines()
for x in fragments.splitlines()
)
return ServerSentEventGenerator._send(
@ -162,3 +174,7 @@ class ServerSentEventGenerator:
event_id,
retry_duration,
)
@classmethod
def redirect(cls, location: str):
return cls.execute_script(f"setTimeout(() => window.location = '{location}')")

View File

@ -1,43 +1,51 @@
[package]
authors = ["Johnathan Stevers <jmstevers@gmail.com>"]
authors = [
"Johnathan Stevers <jmstevers@gmail.com>",
"Glen Henri J. De Cauwsemaecker <glen@plabayo.tech>",
]
categories = ["web-programming"]
description = "Datastar is the Rust implementation of the [Datastar](https://data-star.dev) SDK."
edition = "2021"
edition = "2024"
homepage = "https://data-star.dev"
keywords = ["datastar", "web", "backend"]
license = "MIT OR Apache-2.0"
name = "datastar"
readme = "README.md"
repository = "https://github.com/starfederation/datastar-rs"
version = "0.1.0"
version = "0.1.1"
rust-version = "1.85.0"
[dev-dependencies]
async-stream = { version = "0.3.6", default-features = false }
serde = { version = "1.0.217", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.138", default-features = false, features = [
"std",
] }
serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = { version = "1", default-features = false, features = ["std"] }
tokio = { version = "1.43.0", features = ["full"] }
axum = { version = "0.8.1" }
rocket = { version = "0.5.1", features = ["json"] }
rama = { version = "0.2.0-alpha.10", features = ["http-full"] }
[dependencies]
matchit = "0.8.4"
axum = { version = "0.8.1", default-features = false, optional = true, features = [
"query",
"tokio",
] }
futures-util = { version = "0.3.31", default-features = false }
http-body = { version = "1.0.1", default-features = false, optional = true }
pin-project-lite = { version = "0.2.16", default-features = false, optional = true }
futures-util = { version = "0.3", default-features = false }
http-body = { version = "1.0", default-features = false, optional = true }
pin-project-lite = { version = "0.2", default-features = false, optional = true }
rocket = { version = "0.5.1", default-features = false, optional = true }
serde = { version = "1.0.217", default-features = false, optional = true, features = [
rama = { version = "0.2.0-alpha.11", default-features = false, optional = true, features = [
"http",
] }
serde = { version = "1", default-features = false, optional = true, features = [
"derive",
] }
serde_json = { version = "1.0.138", default-features = false, optional = true, features = [
serde_json = { version = "1", default-features = false, optional = true, features = [
"std",
] }
sync_wrapper = { version = "1.0.2", default-features = false, optional = true }
sync_wrapper = { version = "1", default-features = false, optional = true }
bytes = { version = "1", default-features = false, optional = true }
[features]
@ -51,6 +59,14 @@ axum = [
]
http2 = []
rocket = ["dep:rocket"]
rama = [
"dep:rama",
"dep:serde",
"dep:serde_json",
"dep:pin-project-lite",
"dep:bytes",
"dep:sync_wrapper",
]
[profile.dev]
opt-level = 1

View File

@ -1,6 +1,6 @@
# Datastar Rust SDK
An implementation of the Datastar SDK in Rust with framework integration for Axum and Rocket.
An implementation of the Datastar SDK in Rust with framework integration for Axum, Rocket and Rama.
# Usage

View File

@ -5,6 +5,8 @@
#[cfg(feature = "axum")]
pub mod axum;
#[cfg(feature = "rama")]
pub mod rama;
#[cfg(feature = "rocket")]
pub mod rocket;
@ -23,6 +25,8 @@ mod consts;
pub mod prelude {
#[cfg(feature = "axum")]
pub use crate::axum::ReadSignals;
#[cfg(all(feature = "rama", not(feature = "axum")))]
pub use crate::rama::ReadSignals;
pub use crate::{
consts::FragmentMergeMode, execute_script::ExecuteScript, merge_fragments::MergeFragments,
merge_signals::MergeSignals, remove_fragments::RemoveFragments,

185
sdk/rust/src/rama.rs Normal file
View File

@ -0,0 +1,185 @@
//! Rama integration for Datastar.
//!
//! Learn more about rama at
//! <https://github.com/plabayo/rama>.
use {
crate::{Sse, TrySse, prelude::DatastarEvent},
bytes::Bytes,
futures_util::{Stream, StreamExt},
pin_project_lite::pin_project,
rama::http::{
Body, BodyExtractExt, IntoResponse, Method, Request, Response, StatusCode,
dep::http_body::{Body as HttpBody, Frame},
header,
service::web::extract::{FromRequest, Query},
},
serde::{Deserialize, de::DeserializeOwned},
std::{
convert::Infallible,
pin::Pin,
task::{Context, Poll},
},
sync_wrapper::SyncWrapper,
};
pin_project! {
struct SseBody<S> {
#[pin]
stream: SyncWrapper<S>,
}
}
impl<S> IntoResponse for Sse<S>
where
S: Stream<Item = DatastarEvent> + Send + 'static,
{
fn into_response(self) -> Response {
(
[
(header::CONTENT_TYPE, "text/event-stream"),
(header::CACHE_CONTROL, "no-cache"),
(header::CONNECTION, "keep-alive"),
],
Body::new(SseBody {
stream: SyncWrapper::new(self.0.map(Ok::<_, Infallible>)),
}),
)
.into_response()
}
}
impl<S, E> IntoResponse for TrySse<S>
where
S: Stream<Item = Result<DatastarEvent, E>> + Send + 'static,
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
fn into_response(self) -> Response {
(
[
(header::CONTENT_TYPE, "text/event-stream"),
(header::CACHE_CONTROL, "no-cache"),
(header::CONNECTION, "keep-alive"),
],
Body::new(SseBody {
stream: SyncWrapper::new(self.0),
}),
)
.into_response()
}
}
impl<S, E> HttpBody for SseBody<S>
where
S: Stream<Item = Result<DatastarEvent, E>>,
{
type Data = Bytes;
type Error = E;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let this = self.project();
match this.stream.get_pin_mut().poll_next(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Ok(event))) => {
Poll::Ready(Some(Ok(Frame::data(event.to_string().into()))))
}
}
}
}
#[derive(Deserialize)]
struct DatastarParam {
datastar: serde_json::Value,
}
/// [`ReadSignals`] is a request extractor that reads datastar signals from the request.
///
/// # Examples
///
/// ```
/// use datastar::rama::ReadSignals;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Signals {
/// foo: String,
/// bar: i32,
/// }
///
/// async fn handler(ReadSignals(signals): ReadSignals<Signals>) {
/// println!("foo: {}", signals.foo);
/// println!("bar: {}", signals.bar);
/// }
///
/// ```
#[derive(Debug)]
pub struct ReadSignals<T: DeserializeOwned>(pub T);
impl<T> FromRequest for ReadSignals<T>
where
T: DeserializeOwned + Send + Sync + 'static,
{
type Rejection = Response;
async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
let json = match *req.method() {
Method::GET => {
let query =
Query::<DatastarParam>::parse_query_str(req.uri().query().unwrap_or(""))
.map_err(IntoResponse::into_response)?;
let signals = query.0.datastar.as_str().ok_or_else(|| {
(StatusCode::BAD_REQUEST, "Failed to parse JSON").into_response()
})?;
serde_json::from_str(signals)
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()).into_response())?
}
_ => req
.into_body()
.try_into_json()
.await
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()).into_response())?,
};
Ok(Self(json))
}
}
#[cfg(test)]
mod tests {
use {
super::Sse,
crate::{
prelude::ReadSignals,
testing::{self, Signals},
},
rama::{
error::BoxError,
http::{IntoResponse, server::HttpServer, service::web::Router},
rt::Executor,
},
};
async fn test(ReadSignals(signals): ReadSignals<Signals>) -> impl IntoResponse {
Sse(testing::test(signals.events))
}
#[tokio::test]
async fn sdk_test() -> Result<(), BoxError> {
HttpServer::auto(Executor::default())
.listen(
"127.0.0.1:3000",
Router::new().get("/test", test).post("/test", test),
)
.await?;
Ok(())
}
}