diff --git a/DOCKER.md b/DOCKER.md deleted file mode 100644 index 08d97c782..000000000 --- a/DOCKER.md +++ /dev/null @@ -1,60 +0,0 @@ -# Building Datastar using Docker - -Datastar comes with a development environment for developing Datastar via a Docker container. - -It takes care of the setup listed in the for you in a Dockerized environment, which allows you to get it up and running quickly & easily. - -The only requirement is that you have [Docker](https://www.docker.com/products/docker-desktop) installed (you do not need `golang`, `tmpl`, `npm`, `pnpm` or any other project dependencies installed locally). - -## Why Docker? - -Developers who have adopted [Docker](https://www.docker.com/) for a containerized approach to development are used to not having to install a matching development infrastructure each time they approach a project. - -This allows you to "shrink-wrap" the devops needed to run a project in a container, which will run anywhere, on any machine, without having to do any meticulous setup. - -It also allows you to easily swap between basic things like Go versions, without affecting your local computer. - -## Quick Start - -In terminal, `cd` to the `datastar/` directory, and then type: - -``` -make dev -``` - -The first time you run this command, it may take a bit of time to build the Docker image, and download all of the appropriate packages, and cache them locally. - -Then just navigate to `http://localhost:8080` in your browser, and Datastar site from `site` will be up and running. - -You can freely make changes to the `library` Datastar codebase, and the changes will be rebuilt and reload automatically. - -You can also make changes to the `site` Datastar website backend, and the backend will be rebuilt and reload automatically. - -## Using Datastar Docker Dev - -Datastar Docker Dev uses the venerable `make` command to automate setup and access to the Docker containers used. - -It uses `make` because it's available pre-installed on any development machine. - -See the [Using Make & Makefiles to Automate your Frontend Workflow](https://nystudio107.com/blog/using-make-makefiles-to-automate-your-frontend-workflow) article for more on `make`. - -The make tool is available for just about every platform you can imagine, and is installed with the [XCode CLI Tools](https://www.embarcadero.com/starthere/xe5/mobdevsetup/ios/en/installing_the_commandline_tools.html) on the Mac, and [WSL2](https://docs.microsoft.com/en-us/windows/wsl/install-win10) on Windows. Probably you have these installed already if you’re doing development. - -Below are details and options available in each of the provided `make` commands: - -* `make dev` - starts up the Go website server for the backend with hot reloading as you make changes -* `make test` - run the project tests inside the Docker container -* `make task xxx` - runs the specified task from the `Taskfile.yml` inside of the Docker container -* `make ssh` - gives you a shell inside of the Docker container -* `make image-build` - rebuilds the Docker image from scratch (you will probably never need this) -* `make clean` - removes all build artifacts, including the Docker image & volumes, to start from scratch - -### CLI Arguments - -You can pass in optional CLI arguments to override the default settings Datastar dev uses: - -* `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 - -To terminate the `datastar-dev` Docker container, enter `Control-C` in the terminal. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bde47417a..000000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM docker.io/golang:1.24.2-alpine AS build - -RUN apk add --no-cache upx -ENV PORT=8080 - -WORKDIR /src -COPY . . -RUN go mod download -COPY site ./site -RUN --mount=type=cache,target=/root/.cache/go-build \ - go build -ldflags="-s" -o /out/site site/cmd/site/main.go -RUN upx -9 -k /out/site - -FROM alpine -RUN chmod a=rwx,u+t /tmp -COPY --from=build /out/site / -ENTRYPOINT ["/site"] \ No newline at end of file diff --git a/Dockerfile-dev b/Dockerfile-dev deleted file mode 100644 index 37e8ea053..000000000 --- a/Dockerfile-dev +++ /dev/null @@ -1,48 +0,0 @@ -ARG TAG=1.24 - -FROM golang:$TAG - -WORKDIR /app - -# Install packages -RUN apt update && sudo apt upgrade \ - && \ - set -eux; \ - # Packages to install - apt install -y \ - git \ - jq \ - rsync \ - nodejs \ - npm \ - # Needed for headless chrome/tests - libglib2.0-dev \ - libnss3-dev \ - libdbus-1-dev \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libcups2 \ - libxcomposite-dev \ - libxdamage1 \ - libxrandr2 \ - libgbm-dev \ - libxkbcommon-x11-0 \ - libpangocairo-1.0-0 \ - libasound2 \ - && \ - # Clean out directories that don't need to be part of the image - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && \ - # Install node packages \ - npm install -g npm@^10.0.0 \ - npm install -g pnpm \ - && \ - # Install flyctl cli \ - curl -L https://fly.io/install.sh | sh \ - && \ - ln -s /root/.fly/bin/flyctl /usr/local/bin/fly \ - && \ - # Make this a safe .git directory - git config --global --add safe.directory /app - -ENTRYPOINT ["/bin/sh"] \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 8563984f5..000000000 --- a/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -TAG?=1.24 -CONTAINER?=$(shell basename $(CURDIR))-dev -DEV_PORT?=8080 -IMAGE_INFO=$(shell docker image inspect $(CONTAINER):$(TAG)) -IMAGE_NAME=${CONTAINER}:${TAG} -DOCKER_RUN=docker container run --rm -it -v "${CURDIR}":/app -v go-modules:/go/pkg/mod -ARCH=$(shell uname -m) - -.PHONY: build clean dev image-build task test ssh - -# Perform a dist build -build: image-check - ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} build -# Clean up all build artifacts to start from scratch -clean: - docker image rm ${IMAGE_NAME} - 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 '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 '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 '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 'go tool task test' -# Open a shell inside of the container -ssh: --image-check - ${DOCKER_RUN} --name ${CONTAINER}-$@ --entrypoint=/bin/sh ${IMAGE_NAME} -# Ensure the image has been created ---image-check: -ifeq ($(IMAGE_INFO), []) ---image-check: image-build -endif -%: - @: -# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line diff --git a/Taskfile.yml b/Taskfile.yml index cb3d846f9..e33f297b0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -9,67 +9,10 @@ vars: sh: cat VERSION tasks: - tools: - platforms: - [ - windows, - linux, - darwin/arm64, - darwin/amd64, - openbsd, - dragonfly, - freebsd, - netbsd, - ] - cmds: - - 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 - - - platforms: [linux/arm64] - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-arm64 - - - platforms: [darwin/arm64] - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-arm64 - - - platforms: [darwin/amd64] - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-x64 - - - platforms: [windows] - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-windows-x64.exe - - - platforms: [openbsd, dragonfly, freebsd, netbsd] - cmd: pnpm add tailwindcss @tailwindcss/container-queries @tailwindcss/typography daisyui - - - platforms: [openbsd, dragonfly, freebsd, netbsd] - cmd: test -f site/tailwindcli || (echo "#!/bin/sh" > site/tailwindcli && echo "tailwindcss $@" >> site/tailwindcli) - - - chmod +x site/tailwindcli - version: cmds: - echo {{.VERSION}} - qtc: - sources: - - "**/*.qtpl" - generates: - - "**/*.qtpl.go" - cmds: - - go tool qtc - - build: - deps: - - qtc - sources: - - VERSION - - "**/*.qtpl" - - "**/package.json" - - "**/*.ts" - generates: - - "bundles/**/*" - cmds: - - go run build/cmd/build/main.go - library: dir: library requires: @@ -84,115 +27,37 @@ tasks: - pnpm i - pnpm build - libpub: - dir: library - requires: - vars: [VERSION] - deps: - - build - - library - cmds: - - git push origin - - git tag v{{.VERSION}} - - git push origin --tags - - npm publish --access public - - curl https://purge.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js - - git push origin --tags - - GOPROXY=proxy.golang.org go list -m github.com/starfederation/datastar@v{{.VERSION}} - - task: deploy - - sdktspub: - dir: sdk/typescript - requires: - vars: [VERSION] - cmds: - - git push origin - - git tag sdktsv{{.VERSION}} - - git push origin --tags - - deno run -A build.ts {{.VERSION}} - - cd npm && npm publish --access public - - curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/web/serverSentEventGenerator.js - - curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/abstractServerSentEventGenerator.js - - curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/node/serverSentEventGenerator.js - - git push origin --tags - - idiomorph: - dir: library/src/vendored - cmds: - - wget -O idiomorph.esm.js https://raw.githubusercontent.com/bigskysoftware/idiomorph/refs/heads/main/dist/idiomorph.esm.js - - npx -p typescript tsc *.js --declaration --allowJs --emitDeclarationOnly --outDir . - css: - dir: site - sources: - - "**/*.templ" - - "**/*.md" - - "**/*.go" - generates: - - "static/css/site.css" - - cmds: - - ./tailwindcli build -i src/css/site.css -o static/css/site.css - - templ: + test-sdk: + desc: Run SDK tests + dir: sdk/tests env: - TEMPL_EXPERIMENT: rawgo - # deps: - # - library - generates: - - "**/*_templ.go" - sources: - - "**/*.templ" + TEST_SERVER_URL: '{{.SERVER | default "http://localhost:7331"}}' cmds: - - go tool templ generate . + - go mod tidy + - go test -v ./... - kill: - method: none + install-sdk-tests: + desc: Install the datastar-sdk-tests command + dir: sdk/tests cmds: - - fuser -k 8080/tcp > /dev/null 2>&1 || true + - go install ./cmd/datastar-sdk-tests - support: + build-sdk-tests-docker: + desc: Build Docker image for datastar-sdk-tests + dir: sdk/tests sources: - - "**/*.templ" - "**/*.go" - - "**/*.md" - - site/static/**/* + - "go.mod" + - "go.sum" + - "Dockerfile" generates: - - ./datastar-website - deps: - - kill - - build - - templ - - css - - site: - method: none - desc: build and run site - deps: - - support - cmds: - - go mod tidy - - go build -o ./datastar-website "site/cmd/site/main.go" - - deploy: - method: none - deps: - - support - cmds: - - fly deploy --local-only - - test: - deps: - - support - cmds: - - go test -v github.com/starfederation/datastar/site/smoketests -run '^TestUnit' -count=1 -timeout=1m - - test-all: - deps: - - support + - ".docker-build-timestamp" cmds: - - go test -v github.com/starfederation/datastar/site/smoketests -count=1 -timeout=1m + - docker build -t datastar-sdk-tests:latest . + - touch .docker-build-timestamp - default: + run-sdk-tests-docker: + desc: Run datastar-sdk-tests in Docker container + deps: [build-sdk-tests-docker] cmds: - - task: site - - ./datastar-website + - docker run --rm --network host datastar-sdk-tests:latest {{.CLI_ARGS}} diff --git a/build/.gitignore b/build/.gitignore deleted file mode 100644 index bf2a1ec52..000000000 --- a/build/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*_templ.go -twcli \ No newline at end of file diff --git a/build/cmd/build/main.go b/build/cmd/build/main.go deleted file mode 100644 index ed79cdec3..000000000 --- a/build/cmd/build/main.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "log" - "time" - - build "github.com/starfederation/datastar/build" -) - -func main() { - start := time.Now() - log.Print("Datastar built in TS compiler!") - defer func() { - log.Printf("Datastar built in %s", time.Since(start)) - }() - - if err := build.Build(); err != nil { - log.Fatal(err) - } - -} diff --git a/build/consts.go b/build/consts.go deleted file mode 100644 index b6541d620..000000000 --- a/build/consts.go +++ /dev/null @@ -1,225 +0,0 @@ -package build - -import ( - "time" - - "github.com/delaneyj/toolbelt" -) - -type EnumValueDefinition struct { - Name toolbelt.CasedString - Description string - Value string -} - -type EnumDefinition struct { - Name toolbelt.CasedString - Description string - Values []*EnumValueDefinition - DefaultIndex int - Default *EnumValueDefinition -} - -type DefaultDuration struct { - Name toolbelt.CasedString - Description string - Duration time.Duration -} - -type DefaultBool struct { - Name toolbelt.CasedString - Description string - Value bool -} - -type DefaultString struct { - Name toolbelt.CasedString - Description string - Value string -} - -type Language struct { - Handle string - Name string - Icon string - SdkUrl string -} - -type ConstTemplateData struct { - DoNotEdit string - SDKLanguages []Language - Version string - VersionClientByteSize int - VersionClientByteSizeBrotli int - DatastarKey string - DatalineLiterals []toolbelt.CasedString - DefaultBools []*DefaultBool - DefaultDurations []*DefaultDuration - DefaultStrings []*DefaultString - Enums []*EnumDefinition -} - -var Consts = &ConstTemplateData{ - DoNotEdit: "This is auto-generated by Datastar. DO NOT EDIT.", - SDKLanguages: []Language{ - { - Handle: "clojure", - Name: "Clojure", - Icon: "vscode-icons:file-type-clojure", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/clojure", - }, - { - Handle: "csharp", - Name: "C#", - Icon: "vscode-icons:file-type-csharp2", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/dotnet", - }, - { - Handle: "go", - Name: "Go", - Icon: "vscode-icons:file-type-go-gopher", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/go", - }, - { - Handle: "haskell", - Name: "Haskell", - Icon: "vscode-icons:file-type-haskell", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/haskell", - }, - { - Handle: "java", - Name: "Java", - Icon: "vscode-icons:file-type-java", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/java", - }, - { - Handle: "php", - Name: "PHP", - Icon: "vscode-icons:file-type-php", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/php", - }, - { - Handle: "python", - Name: "Python", - Icon: "vscode-icons:file-type-python", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/python", - }, - { - Handle: "rust", - Name: "Rust", - Icon: "vscode-icons:file-type-rust", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/rust", - }, - { - Handle: "ruby", - Name: "Ruby", - Icon: "vscode-icons:file-type-ruby", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/ruby", - }, - { - Handle: "typescript", - Name: "TypeScript", - Icon: "vscode-icons:file-type-typescript-official", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/typescript", - }, - { - Handle: "zig", - Name: "Zig", - Icon: "vscode-icons:file-type-zig", - SdkUrl: "https://github.com/starfederation/datastar/tree/main/sdk/zig", - }, - }, - DatastarKey: "datastar", - DefaultBools: []*DefaultBool{ - { - Name: toolbelt.ToCasedString("elementsUseViewTransitions"), - Description: "Should elements be patched using the ViewTransition API?", - Value: false, - }, - { - Name: toolbelt.ToCasedString("patchSignalsOnlyIfMissing"), - Description: "Should a given set of signals patch if they are missing?", - Value: false, - }, - }, - DefaultDurations: []*DefaultDuration{ - { - Name: toolbelt.ToCasedString("sseRetryDuration"), - Description: "The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.", - Duration: 1 * time.Second, - }, - }, - DefaultStrings: []*DefaultString{}, - DatalineLiterals: []toolbelt.CasedString{ - // Shared - toolbelt.ToCasedString("selector"), - - // PatchElements - toolbelt.ToCasedString("mode"), - toolbelt.ToCasedString("elements"), - toolbelt.ToCasedString("useViewTransition"), - - // PatchSignals - toolbelt.ToCasedString("signals"), - toolbelt.ToCasedString("onlyIfMissing"), - }, - Enums: []*EnumDefinition{ - { - Name: toolbelt.ToCasedString("ElementPatchMode"), - Description: "The mode in which an element is patched into the DOM.", - DefaultIndex: 0, - Values: []*EnumValueDefinition{ - { - Value: "outer", - Description: "Morphs the element into the existing element.", - }, - { - Value: "inner", - Description: "Replaces the inner HTML of the existing element.", - }, - { - Value: "remove", - Description: "Removes the existing element.", - }, - { - Value: "replace", - Description: "Replaces the existing element with the new element.", - }, - { - Value: "prepend", - Description: "Prepends the element inside to the existing element.", - }, - { - Value: "append", - Description: "Appends the element inside the existing element.", - }, - { - Value: "before", - Description: "Inserts the element before the existing element.", - }, - { - Value: "after", - Description: "Inserts the element after the existing element.", - }, - }, - }, - - { - Name: toolbelt.ToCasedString("EventType"), - Description: "The type protocol on top of SSE which allows for core pushed based communication between the server and the client.", - DefaultIndex: -1, - Values: []*EnumValueDefinition{ - { - Name: toolbelt.ToCasedString("PatchElements"), - Description: "An event for patching HTML elements into the DOM.", - Value: "datastar-patch-elements", - }, - { - Name: toolbelt.ToCasedString("PatchSignals"), - Description: "An event for patching signals.", - Value: "datastar-patch-signals", - }, - }, - }, - }, -} diff --git a/build/consts_clojure.qtpl b/build/consts_clojure.qtpl deleted file mode 100644 index 40e2f724e..000000000 --- a/build/consts_clojure.qtpl +++ /dev/null @@ -1,77 +0,0 @@ -{%- func clojureConsts(data *ConstTemplateData) -%} -;; {%s data.DoNotEdit %} -(ns starfederation.datastar.clojure.consts - (:require - [clojure.string :as string])) - - -(def datastar-key "{%s data.DatastarKey %}") -(def version "{%s data.Version %}") - - -;; ----------------------------------------------------------------------------- -;; Default durations -;; ----------------------------------------------------------------------------- -{%- for _, d := range data.DefaultDurations -%} -(def default-{%s d.Name.Kebab %} - "{%s= d.Description %}" - {%d durationToMs(d.Duration) %}) - -{%- endfor -%} - -;; ----------------------------------------------------------------------------- -;; Default values -;; ----------------------------------------------------------------------------- -{%- for _, s := range data.DefaultStrings -%} -(def default-{%s s.Name.Kebab %} - "{%s= s.Description %}" - (-> "{%s s.Value %}" - (string/split #" ") - (update 0 keyword) - (->> (apply array-map)))) - -{%- endfor -%} - -;; ----------------------------------------------------------------------------- -;; Dataline literals -;; ----------------------------------------------------------------------------- -{%- for _, literal := range data.DatalineLiterals -%} -(def {%s literal.Kebab %}-dataline-literal "{%s literal.Camel %} ") -{%- endfor -%} - - -;; ----------------------------------------------------------------------------- -;; Default booleans -;; ----------------------------------------------------------------------------- -{%- for _, b := range data.DefaultBools -%} -(def default-{%s b.Name.Kebab %} - "{%s= b.Description %}" - {%v b.Value %}) - -{%- endfor -%} - - -;; ----------------------------------------------------------------------------- -;; Enums -;; ----------------------------------------------------------------------------- -{%- for _, enum := range data.Enums -%} -;; {%s enum.Name.Pascal %} - -{%- for _, entry := range enum.Values -%} -(def {%s enum.Name.Kebab %}-{%s entry.Name.Kebab %} - "{%s= entry.Description %}" - "{%s entry.Value %}") - -{%- endfor -%} - -{%- if enum.Default != nil -%} -(def default-{%s enum.Name.Kebab %} - "Default value for {%s enum.Name.Pascal %}. - {%s= enum.Default.Description %}" - {%s enum.Name.Kebab %}-{%s enum.Default.Name.Kebab %}) -{%- endif -%} - - -{%- endfor -%} - -{%- endfunc -%} diff --git a/build/consts_csharp.qtpl b/build/consts_csharp.qtpl deleted file mode 100644 index b47c60378..000000000 --- a/build/consts_csharp.qtpl +++ /dev/null @@ -1,51 +0,0 @@ -{%- func csharpConsts(data *ConstTemplateData) -%} -// {%s data.DoNotEdit %} - -namespace StarFederation.Datastar; - -using System; - -{%- for _, enum := range data.Enums -%} -public enum {%s enum.Name.Pascal %} -{ -{% for _, entry := range enum.Values %} /// {%s entry.Description %} - {%s entry.Name.Pascal %}, -{% endfor %} -} -{% endfor %} -public static class Consts -{ - public const string DatastarKey = "{%s data.DatastarKey %}"; - public const string Version = "{%s data.Version %}"; - - {%- for _, d := range data.DefaultDurations -%} - /// Default: TimeSpan.FromMilliseconds {%d durationToMs(d.Duration) %} - public static readonly TimeSpan DefaultSseRetryDuration = TimeSpan.FromMilliseconds({%d durationToMs(d.Duration) %}); - {%- endfor %} - - {%- for _, enum := range data.Enums -%}{%- if enum.DefaultIndex > -1 -%} - /// Default: {%s enum.Default.Value %} - {%s enum.Default.Description %} - public const {%s enum.Name.Pascal %} Default{%s enum.Name.Pascal %} = {%s enum.Name.Pascal %}.{%s enum.Default.Name.Pascal %};{% endif %}{% endfor %} - - {%- for _, b := range data.DefaultBools -%} - public const bool Default{%s b.Name.Pascal %} = {%v b.Value %}; - {%- endfor -%} - - {%- for _, s := range data.DefaultStrings -%} - public const string Default{%s s.Name.Pascal %} = "{%s s.Value %}"; - {%- endfor -%} - - {%- for _, literal := range data.DatalineLiterals -%} - public const string DatastarDataline{%s literal.Pascal %} = "{%s literal.Camel %}"; - {%- endfor -%} - - {%- for _, enum := range data.Enums -%} - public static string EnumToString( {%s enum.Name.Pascal %} enumValue ) => enumValue switch { - {%- for _, entry := range enum.Values -%} - {%s enum.Name.Pascal %}.{%s entry.Name.Pascal %} => "{%s entry.Value %}", - {%- endfor -%} - _ => throw new NotImplementedException($"{%s enum.Name.Pascal %}.{enumValue}") - }; - {%- endfor -%} -} -{%- endfunc -%} diff --git a/build/consts_datastar_client.qtpl b/build/consts_datastar_client.qtpl deleted file mode 100644 index 0abc22fef..000000000 --- a/build/consts_datastar_client.qtpl +++ /dev/null @@ -1,110 +0,0 @@ -{% import "github.com/delaneyj/toolbelt" %} - -{%- func datastarClientConsts(data *ConstTemplateData) -%} -// {%s data.DoNotEdit %} -const lol = /πŸ–•JS_DSπŸš€/.source -export const DSP = lol.slice(0, 5) -export const DSS = lol.slice(4) - -export const DATASTAR = "{%s data.DatastarKey %}"; -export const DATASTAR_REQUEST = "{%s toolbelt.Pascal( data.DatastarKey) %}-Request"; - -// #region Defaults - -// #region Default durations - -{%- for _, d := range data.DefaultDurations -%} -// {%s= d.Description %} -export const Default{%s d.Name.Pascal %}Ms = {%d durationToMs(d.Duration) %}; -{%- endfor -%} - -// #endregion - - -// #region Default strings - -{%- for _, s := range data.DefaultStrings -%} -// {%s= s.Description %} -export const Default{%s s.Name.Pascal %} = "{%s s.Value %}"; -{%- endfor -%} - -// #endregion - - -// #region Default booleans - -{%- for _, b := range data.DefaultBools -%} -// {%s= b.Description %} -export const Default{%s b.Name.Pascal %} = {%v b.Value %}; - -{%- endfor -%} -// #endregion - - -// #region Enums - -{%- for _, enum := range data.Enums -%} -// {%s= enum.Description %} -{%- for _, entry := range enum.Values -%} -// {%s= entry.Description %} -export const {%s enum.Name.Pascal %}{%s entry.Name.Pascal %} = "{%s entry.Value %}" -{%- endfor -%} -{%- if enum.Default != nil -%} - -// Default value for {%s enum.Name.Pascal %} -export const Default{%s enum.Name.Pascal %} = {%s enum.Name.Pascal %}{%s enum.Default.Name.Pascal %}; - -{%- endif -%} -{%- endfor -%} -// #endregion - -// #endregion -{%- endfunc -%} - -{%- func datastarClientPackageJSON(data *ConstTemplateData) -%} -{ - "name": "@starfederation/datastar", - "author": "Delaney Gillilan", - "description": "The hypermedia framework.", - "version": "{%s data.Version %}", - "license": "MIT", - "private": false, - "homepage": "https://data-star.dev", - "repository": { - "type": "git", - "url": "git+https://github.com/starfederation/datastar.git" - }, - "bugs": { - "url": "https://github.com/starfederation/datastar/issues" - }, - "files": [ - "dist" - ], - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./plugins": { - "import": "./dist/plugins/index.js", - "types": "./dist/plugins/index.d.ts" - }, - "./types": { - "import": "./dist/engine/types.js", - "types": "./dist/engine/types.d.ts" - }, - "./bundles/*": { - "import": "./dist/bundles/*.js", - "types": "./dist/bundles/*.d.ts" - } - }, - "scripts": { - "build": "tsc" - }, - "devDependencies": { - "typescript": "^5.6.3" - } -} -{%- endfunc -%} \ No newline at end of file diff --git a/build/consts_fsharp.qtpl b/build/consts_fsharp.qtpl deleted file mode 100644 index 18442d568..000000000 --- a/build/consts_fsharp.qtpl +++ /dev/null @@ -1,48 +0,0 @@ -{%- func fsharpConsts(data *ConstTemplateData) -%} -// {%s data.DoNotEdit %} - -namespace StarFederation.Datastar.FSharp - -open System - -{%- for _, enum := range data.Enums -%} -type {%s enum.Name.Pascal %} = -{% for _, entry := range enum.Values %}/// {%s entry.Description %} -| {%s entry.Name.Pascal %} -{% endfor %} -{% endfor %} -module Consts = - let [] DatastarKey = "{%s data.DatastarKey %}" - let [] Version = "{%s data.Version %}" - - {%- for _, d := range data.DefaultDurations -%} - /// Default: TimeSpan.FromMilliseconds {%d durationToMs(d.Duration) %} - let Default{%s d.Name.Pascal %} = TimeSpan.FromMilliseconds {%d durationToMs(d.Duration) %} - {%- endfor %} - - {%- for _, enum := range data.Enums -%}{%- if enum.DefaultIndex > -1 -%} - /// Default: {%s enum.Default.Value %} - {%s enum.Default.Description %} - let Default{%s enum.Name.Pascal %} = {%s enum.Default.Name.Pascal %}{% endif %}{% endfor %} - - {%- for _, b := range data.DefaultBools -%} - let [] Default{%s b.Name.Pascal %} = {%v b.Value %} - {%- endfor -%} - - {%- for _, s := range data.DefaultStrings -%} - let [] Default{%s s.Name.Pascal %} = "{%s s.Value %}" - {%- endfor -%} - - {%- for _, literal := range data.DatalineLiterals -%} - let [] DatastarDataline{%s literal.Pascal %} = "{%s literal.Camel %}" - {%- endfor -%} - -{%- for _, enum := range data.Enums -%} - module {%s enum.Name.Pascal %} = - let inline toString this = - match this with - {%- for _, entry := range enum.Values -%} - | {%s enum.Name.Pascal %}.{%s entry.Name.Pascal %} -> "{%s entry.Value %}" - {%- endfor -%} - -{%- endfor -%} -{%- endfunc -%} diff --git a/build/consts_go.qtpl b/build/consts_go.qtpl deleted file mode 100644 index 6ee4d45c8..000000000 --- a/build/consts_go.qtpl +++ /dev/null @@ -1,73 +0,0 @@ -{%- func goConsts(data *ConstTemplateData) -%} -// {%s data.DoNotEdit %} - -package datastar - -import "time" - -const ( - DatastarKey = "{%s data.DatastarKey %}" - Version = "{%s data.Version %}" - VersionClientByteSize = {%d data.VersionClientByteSize %} - VersionClientByteSizeBrotli = {%d data.VersionClientByteSizeBrotli %} - - //region Default durations - - {%- for _, d := range data.DefaultDurations -%} - // {%s= d.Description %} - Default{%s d.Name.Pascal %} = {%d durationToMs(d.Duration) %} * time.Millisecond - {%- endfor -%} - - //endregion Default durations - - //region Default strings - - {%- for _, s := range data.DefaultStrings -%} - // {%s= s.Description %} - Default{%s s.Name.Pascal %} = "{%s s.Value %}" - {%- endfor -%} - - //endregion Default strings - - //region Dataline literals - {%- for _, literal := range data.DatalineLiterals -%} - {%s literal.Pascal %}DatalineLiteral = "{%s literal.Camel %} " - {%- endfor -%} - //endregion Dataline literals -) - -var ( - //region Default booleans - - {%- for _, b := range data.DefaultBools -%} - // {%s= b.Description %} - Default{%s b.Name.Pascal %} = {%v b.Value %} - - {%- endfor -%} - //endregion Default booleans -) - -//region Enums - -{%- for _, enum := range data.Enums -%} -//region {%s= enum.Description %} -type {%s enum.Name.Pascal %} string - -const ( -{%- if enum.Default != nil -%} - // Default value for {%s enum.Name.Pascal %} - // {%s= enum.Default.Description %} - Default{%s enum.Name.Pascal %} = {%s enum.Name.Pascal %}{%s enum.Default.Name.Pascal %} - -{%- endif -%} -{%- for _, entry := range enum.Values -%} - // {%s= entry.Description %} - {%s enum.Name.Pascal %}{%s entry.Name.Pascal %} {%s enum.Name.Pascal %} = "{%s entry.Value %}" - -{%- endfor -%} -) -//endregion {%s enum.Name.Pascal %} - -{%- endfor -%} -//endregion Enums -{%- endfunc -%} diff --git a/build/consts_haskell.qtpl b/build/consts_haskell.qtpl deleted file mode 100644 index 372ee6aeb..000000000 --- a/build/consts_haskell.qtpl +++ /dev/null @@ -1,72 +0,0 @@ -{% import "strings" %} -{% import "strconv" %} -{% import "fmt" %} -{% func PadRight(s string, maxLen int) %}{% stripspace %} - {%s s %}{%s strings.Repeat(" ", maxLen-len(s)) %} -{% endstripspace %}{% endfunc %} -{%- func haskellConsts(data *ConstTemplateData) -%} --- {%s data.DoNotEdit %} -module ServerSentEventGenerator.Constants where - -import Prelude -import ServerSentEventGenerator.Class (StringLike) - --- taken from consts.ts --- why? --- functions must start with a lower case letter --- I could have used a type class, but it would have required --- a function call in front of each data constructor, so I --- decided to just use the prefix "c" - -cDatastarKey :: StringLike a => a -cDatastarKey = "{%s data.DatastarKey %}" -cVersion :: StringLike a => a -cVersion = "{%s data.Version %}" -{%- for _, d := range data.DefaultDurations -%} -{%s PadRight(fmt.Sprintf("cDefault%s", d.Name.Pascal), 40) %} :: Int -{%s PadRight(fmt.Sprintf("cDefault%s", d.Name.Pascal), 40) %} = {%d durationToMs(d.Duration) %} -- milliseconds -{%- endfor -%} -{%- for _, s := range data.DefaultStrings -%} -{%s PadRight(fmt.Sprintf("c%s", s.Name.Pascal), 40) %} :: StringLike a => a -{%s PadRight(fmt.Sprintf("c%s", s.Name.Pascal), 40) %} = "{%s s.Value %}" -{%- endfor -%} -{%- for _, literal := range data.DatalineLiterals -%} -{%s PadRight(fmt.Sprintf("c%s", literal.Pascal), 40) %} :: StringLike a => a -{%s PadRight(fmt.Sprintf("c%s", literal.Pascal), 40) %} = "{%s literal.Camel %}" -{%- endfor -%} -{%- for _, b := range data.DefaultBools -%} -{% code - boolStr := strconv.FormatBool(b.Value) - capitalizedBool := strings.ToUpper(boolStr[:1]) + boolStr[1:] --%} -{%s PadRight(fmt.Sprintf("cDefault%s", b.Name.Pascal), 40) %} :: Bool -{%s PadRight(fmt.Sprintf("cDefault%s", b.Name.Pascal), 40) %} = {%s capitalizedBool %} -{%- endfor -%} -{%- for _, enum := range data.Enums -%} -{%- for _, entry := range enum.Values -%} -{%s PadRight(fmt.Sprintf("c%s", entry.Name.Pascal), 40) %} :: StringLike a => a -{%s PadRight(fmt.Sprintf("c%s", entry.Name.Pascal), 40) %} = "{%v entry.Value %}" -{%- endfor -%} -{%- endfor -%} --- Added by Henry -cData :: StringLike a => a -cData = "data" -cEvent :: StringLike a => a -cEvent = "event" -cEventId :: StringLike a => a -cEventId = "id" -cRetryDuration :: StringLike a => a -cRetryDuration = "retry" -cSColon :: StringLike a => a -cSColon = ": " -cSpace :: StringLike a => a -cSpace = " " -cDefaultSelector :: StringLike a => a -cDefaultSelector = "" -cDefaultEventId :: StringLike a => a -cDefaultEventId = "" -cDefaultOnlyIfMissing :: Bool -cDefaultOnlyIfMissing = False -cDefaultAttributes :: StringLike a => a -cDefaultAttributes = "" -{%- endfunc -%} diff --git a/build/consts_hello_world.qtpl b/build/consts_hello_world.qtpl deleted file mode 100644 index 3338552b1..000000000 --- a/build/consts_hello_world.qtpl +++ /dev/null @@ -1,53 +0,0 @@ -{%- func helloWorldExample(data *ConstTemplateData) -%} - - {%s= helloWorldExampleSource(data, "@get('/hello-world')") %} - -{%- endfunc -%} - - -{%- func helloWorldExamplePHP(data *ConstTemplateData) -%} - - {%s= helloWorldExampleSource(data, "@get('/hello-world.php')") %} - -{%- endfunc -%} - - -{%- func helloWorldExampleSource(data *ConstTemplateData, action string) -%} - - - - - - - Datastar SDK Demo - - - - -
-
-

- Datastar SDK Demo -

- Rocket -
-

- SSE events will be streamed from the backend to the frontend. -

-
- - -
- -
-
-
Hello, world!
-
- - - -{%- endfunc -%} diff --git a/build/consts_java.qtpl b/build/consts_java.qtpl deleted file mode 100644 index f6203b9bd..000000000 --- a/build/consts_java.qtpl +++ /dev/null @@ -1,104 +0,0 @@ -{%- func javaConsts(data *ConstTemplateData) -%} -package starfederation.datastar; - -import starfederation.datastar.enums.ElementPatchMode; - -/** - * {%s data.DoNotEdit %} - */ -public final class Consts { - public static final String DATASTAR_KEY = "{%s data.DatastarKey %}"; - public static final String VERSION = "{%s data.Version %}"; - {%- for _, d := range data.DefaultDurations %} - // {%s= d.Description %} - public static final int DEFAULT_{%s d.Name.ScreamingSnake %} = {%d durationToMs(d.Duration) %}; - {%- endfor -%} - {%- for _, b := range data.DefaultBools %} - // {%s= b.Description %} - public static final boolean DEFAULT_{%s b.Name.ScreamingSnake %} = {%v b.Value %}; - {%- endfor -%} - {%- for _, s := range data.DefaultStrings %} - // {%s= s.Description %} - public static final String DEFAULT_{%s s.Name.ScreamingSnake %} = "{%s s.Value %}"; - {%- endfor -%} - {%- for _, enum := range data.Enums -%} - {%- if enum.Default != nil %} - // {%s= enum.Description %} - public static final ElementPatchMode DEFAULT_{%s enum.Name.ScreamingSnake %} = ElementPatchMode.{%s enum.Default.Name.Pascal %}; - {%- endif -%} - {%- endfor -%} - - // Dataline literals. - {%- for _, literal := range data.DatalineLiterals -%} - public static final String {%s literal.ScreamingSnake %}_DATALINE_LITERAL = "{%s literal.Camel %} "; - {%- endfor -%} -} -{%- endfunc -%} - -{%- func javaEventType(data *ConstTemplateData) -%} -package starfederation.datastar.enums; - -/** - * {%s data.DoNotEdit %} - */ -public enum EventType { -{%- for _, enum := range data.Enums -%} - {%- if enum.Name.Pascal == "EventType" -%} - {%- for i, entry := range enum.Values %}{% if i < len(enum.Values) - 1 %} - // {%s entry.Description %} - {%s entry.Name.Pascal %}("{%s entry.Value %}"), -{%- else -%} - - // {%s entry.Description %} - {%s entry.Name.Pascal %}("{%s entry.Value %}"); - {%- endif -%} -{%- endfor -%} - - private final String value; - - EventType(String value) { - this.value = value; - } - - @Override - public String toString() { - return value; - } - {%- endif -%} -{%- endfor -%} -} -{%- endfunc -%} - -{%- func javaElementPatchMode(data *ConstTemplateData) -%} -package starfederation.datastar.enums; - -/** - * {%s data.DoNotEdit %} - */ -public enum ElementPatchMode { -{%- for _, enum := range data.Enums -%} - {%- if enum.Name.Pascal == "ElementPatchMode" -%} - {%- for i, entry := range enum.Values %}{% if i < len(enum.Values) - 1 %} - // {%s entry.Description %} - {%s entry.Name.Pascal %}("{%s entry.Value %}"), -{%- else -%} - - // {%s entry.Description %} - {%s entry.Name.Pascal %}("{%s entry.Value %}"); - {%- endif -%} -{%- endfor -%} - - private final String value; - - ElementPatchMode(String value) { - this.value = value; - } - - @Override - public String toString() { - return value; - } - {%- endif -%} -{%- endfor -%} -} -{%- endfunc -%} diff --git a/build/consts_php.qtpl b/build/consts_php.qtpl deleted file mode 100644 index 06d547f18..000000000 --- a/build/consts_php.qtpl +++ /dev/null @@ -1,81 +0,0 @@ -{%- func phpConsts(data *ConstTemplateData) -%} - &str { - match self { -{%- for _, entry := range enum.Values -%} - Self::{%s entry.Name.Pascal %} => "{%s entry.Value %}", -{%- endfor -%} - } - } -} -{%- endfor -%} -// #endregion - -// #endregion -{%- endfunc -%} diff --git a/build/consts_typescript.qtpl b/build/consts_typescript.qtpl deleted file mode 100644 index 66889973b..000000000 --- a/build/consts_typescript.qtpl +++ /dev/null @@ -1,69 +0,0 @@ -{% import "github.com/delaneyj/toolbelt" %} - -{%- func typescriptConsts(data *ConstTemplateData) -%} -// {%s data.DoNotEdit %} - -export const DATASTAR = "{%s data.DatastarKey %}" as const; -export const DATASTAR_REQUEST = "{%s toolbelt.Pascal( data.DatastarKey) %}-Request"; -export const VERSION = "{%s data.Version %}"; - -// #region Defaults - -// #region Default durations - -{%- for _, d := range data.DefaultDurations -%} -// {%s= d.Description %} -export const Default{%s d.Name.Pascal %}Ms = {%d durationToMs(d.Duration) %}; -{%- endfor -%} - -// #endregion - - -// #region Default strings - -{%- for _, s := range data.DefaultStrings -%} -// {%s= s.Description %} -export const Default{%s s.Name.Pascal %} = "{%s s.Value %}"; -{%- endfor -%} - -// #endregion - - -// #region Default booleans - -{%- for _, b := range data.DefaultBools -%} -// {%s= b.Description %} -export const Default{%s b.Name.Pascal %} = {%v b.Value %}; - -{%- endfor -%} -// #endregion - -// #region Datalines - -{%- for _, literal := range data.DatalineLiterals -%} -export const DatastarDataline{%s literal.Pascal %} = "{%s literal.Camel %}" -{%- endfor -%} -// #endregion - - -// #region Enums - -{%- for _, enum := range data.Enums -%} -// {%s= enum.Description %} -export const {%s enum.Name.Pascal %}s = [ -{%- for _, entry := range enum.Values -%} -// {%s= entry.Description %} - "{%s entry.Value %}", -{%- endfor -%} -] as const; -{%- if enum.Default != nil -%} - -// Default value for {%s enum.Name.Pascal %} -export const Default{%s enum.Name.Pascal %} = "{%s enum.Default.Value %}"; - -{%- endif -%} -{%- endfor -%} -// #endregion - -// #endregion -{%- endfunc -%} diff --git a/build/consts_zig.qtpl b/build/consts_zig.qtpl deleted file mode 100644 index 5dace0341..000000000 --- a/build/consts_zig.qtpl +++ /dev/null @@ -1,83 +0,0 @@ -{%- func zigConsts(data *ConstTemplateData) -%} -// {%s data.DoNotEdit %} - -const std = @import("std"); - -pub const datastar_key = "{%s data.DatastarKey %}"; -pub const version = "{%s data.Version %}"; - -// #region Defaults - -// #region Default durations - -{%- for _, d := range data.DefaultDurations -%} -/// {%s= d.Description %} -pub const default_{%s d.Name.Snake %} = {%d durationToMs(d.Duration) %}; -{%- endfor -%} - -// #endregion - -// #region Default strings - -{%- for _, s := range data.DefaultStrings -%} -/// {%s= s.Description %} -pub const default_{%s s.Name.Snake %} = "{%s s.Value %}"; -{%- endfor -%} - -// #endregion - -// #region Datalines - -{%- for _, literal := range data.DatalineLiterals -%} -pub const {%s literal.Snake %}_dataline_literal = "{%s literal.Camel %}"; -{%- endfor -%} - -// #endregion - -// #region Default booleans - -{%- for _, b := range data.DefaultBools -%} -/// {%s= b.Description %} -pub const default_{%s b.Name.Snake %} = {%v b.Value %}; -{%- endfor -%} - -// #endregion - -// #region Enums - -{%- for _, enum := range data.Enums -%} -/// {%s= enum.Description %} -pub const {%s enum.Name.Pascal %} = enum { -{%- for _, entry := range enum.Values -%} - /// {%s= entry.Description %} - {%s entry.Name.Snake %}, -{%- endfor -%} - - pub fn format( - self: @This(), - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = fmt; - _ = options; - - try writer.writeAll( - switch (self) { -{%- for _, entry := range enum.Values -%} - .{%s entry.Name.Snake %} => "{%s entry.Value %}", -{%- endfor -%} - }, - ); - } -}; - -{%- if enum.Default != nil -%} -pub const default_{%s enum.Name.Snake %} = {%s enum.Name.Pascal %}.{%s enum.Default.Name.Snake %}; -{%- endif -%} - -{%- endfor -%} -// #endregion - -// #endregion -{%- endfunc -%} \ No newline at end of file diff --git a/build/run.go b/build/run.go deleted file mode 100644 index 2166fb14c..000000000 --- a/build/run.go +++ /dev/null @@ -1,151 +0,0 @@ -package build - -import ( - "bytes" - "errors" - "fmt" - "log" - "os" - "os/exec" - "slices" - "strings" - "time" - - "github.com/andybalholm/brotli" - "github.com/delaneyj/toolbelt" - "github.com/valyala/bytebufferpool" -) - -func Build() error { - version, err := extractVersion() - if err != nil { - return fmt.Errorf("error extracting version: %w", err) - } - - if err := errors.Join( - writeOutConsts(version), - ); err != nil { - return fmt.Errorf("error creating bundles: %w", err) - } - - return nil -} - -func extractVersion() (string, error) { - versionFilepath := "VERSION" - versionBytes, err := os.ReadFile(versionFilepath) - if err != nil { - return "", fmt.Errorf("error reading package.json: %w", err) - } - - version := strings.TrimSpace(string(versionBytes)) - - return version, nil -} - -func writeOutConsts(version string) error { - Consts.Version = version - - build, err := os.ReadFile("bundles/datastar.js") - if err != nil { - return fmt.Errorf("error reading datastar.js: %w", err) - } - Consts.VersionClientByteSize = len(build) - - compressed, err := compressWithBrotli(build) - if err != nil { - return fmt.Errorf("error compressing with brotli: %w", err) - } - Consts.VersionClientByteSizeBrotli = len(compressed) - - log.Printf("Datastar client bundle size: %d bytes (Brotli: %d bytes)", Consts.VersionClientByteSize, Consts.VersionClientByteSizeBrotli) - - var zeroCased toolbelt.CasedString - // Make sure all enums are set up. - for _, enum := range Consts.Enums { - for _, value := range enum.Values { - if value.Name == zeroCased { - value.Name = toolbelt.ToCasedString(value.Value) - } - } - if enum.DefaultIndex >= 0 { - enum.Default = enum.Values[enum.DefaultIndex] - } - } - - slices.SortFunc(Consts.SDKLanguages, func(a, b Language) int { - return strings.Compare(a.Name, b.Name) - }) - - templates := map[string]func(data *ConstTemplateData) string{ - "sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj": clojureConsts, - "sdk/go/datastar/consts.go": goConsts, - "sdk/dotnet/fsharp/src/Consts.fs": fsharpConsts, - "sdk/dotnet/csharp/src/Consts.cs": csharpConsts, - "sdk/php/src/Consts.php": phpConsts, - "sdk/php/src/enums/EventType.php": phpEventType, - "sdk/php/src/enums/ElementPatchMode.php": phpElementPatchMode, - "sdk/java/core/src/main/java/starfederation/datastar/Consts.java": javaConsts, - "sdk/java/core/src/main/java/starfederation/datastar/enums/EventType.java": javaEventType, - "sdk/java/core/src/main/java/starfederation/datastar/enums/ElementPatchMode.java": javaElementPatchMode, - "sdk/python/src/datastar_py/consts.py": pythonConsts, - "sdk/typescript/src/consts.ts": typescriptConsts, - "sdk/ruby/lib/datastar/consts.rb": rubyConsts, - "sdk/rust/src/consts.rs": rustConsts, - "sdk/zig/src/consts.zig": zigConsts, - "examples/clojure/hello-world/resources/public/hello-world.html": helloWorldExample, - "examples/dotnet/csharp/HelloWorld/wwwroot/hello-world.html": helloWorldExample, - "examples/dotnet/fsharp/HelloWorld/wwwroot/hello-world.html": helloWorldExample, - "examples/go/hello-world/hello-world.html": helloWorldExample, - "examples/php/hello-world/public/hello-world.html": helloWorldExamplePHP, - "examples/zig/httpz/hello-world/src/hello-world.html": helloWorldExample, - "examples/zig/tokamak/hello-world/hello-world.html": helloWorldExample, - "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, - } - - for path, tmplFn := range templates { - log.Printf("Writing %s...", path) - contents := strings.TrimSpace(tmplFn(Consts)) - if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { - return fmt.Errorf("error writing version file: %w", err) - } - } - - return nil -} - -func durationToMs(d time.Duration) int { - return int(d.Milliseconds()) -} - -func compressWithBrotli(data []byte) ([]byte, error) { - // Check if brotli CLI is available - if _, err := exec.LookPath("brotli"); err == nil { - // Use CLI version of brotli - cmd := exec.Command("brotli", "-c", "-q", "11", "-") - cmd.Stdin = bytes.NewReader(data) - var out bytes.Buffer - cmd.Stdout = &out - if err := cmd.Run(); err == nil { - log.Print("Using brotli CLI for compression") - return out.Bytes(), nil - } - } - - // Fallback to Go library - log.Print("Using Go brotli library for compression") - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - w := brotli.NewWriterV2(buf, brotli.BestCompression) - if _, err := w.Write(data); err != nil { - return nil, err - } - w.Close() - - result := make([]byte, buf.Len()) - copy(result, buf.Bytes()) - return result, nil -} diff --git a/go.mod b/go.mod index aaa0dc9ad..8d7330134 100644 --- a/go.mod +++ b/go.mod @@ -2,81 +2,22 @@ module github.com/starfederation/datastar 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.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/toolbelt v0.3.16 - github.com/drhodes/golorem v0.0.0-20220328165741-da82e5b29246 - github.com/dustin/go-humanize v1.0.1 - github.com/evanw/esbuild v0.24.2 - github.com/go-chi/chi/v5 v5.2.0 - github.com/go-faker/faker/v4 v4.5.0 - github.com/go-rod/rod v0.116.2 - github.com/go-sanitize/sanitize v1.1.0 - github.com/goccy/go-json v0.10.4 - github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 - github.com/gorilla/csrf v1.7.2 - github.com/gorilla/sessions v1.4.0 - github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc - 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.18.0 - github.com/lithammer/fuzzysearch v1.1.8 - 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 - github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/quicktemplate v1.8.0 - github.com/wcharczuk/go-chart/v2 v2.1.2 - github.com/ysmood/gson v0.7.3 - github.com/zeebo/xxh3 v1.0.2 -) - 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/a-h/templ v0.3.857 // 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 - github.com/blevesearch/geo v0.1.20 // indirect - github.com/blevesearch/go-faiss v1.0.24 // indirect - github.com/blevesearch/go-porterstemmer v1.0.3 // indirect - github.com/blevesearch/gtreap v0.1.1 // indirect - github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect - github.com/blevesearch/segment v0.9.1 // indirect - github.com/blevesearch/snowballstem v0.9.0 // indirect - github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.0.10 // indirect - github.com/blevesearch/zapx/v11 v11.3.10 // indirect - github.com/blevesearch/zapx/v12 v12.3.10 // indirect - github.com/blevesearch/zapx/v13 v13.3.10 // indirect - github.com/blevesearch/zapx/v14 v14.3.10 // indirect - 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 @@ -89,67 +30,37 @@ require ( 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/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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // 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.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/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/valyala/quicktemplate v1.8.0 // 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 + github.com/zeebo/xxh3 v1.0.2 // 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/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/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 ( diff --git a/go.sum b/go.sum index 99a3049ca..a105bcec4 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ 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= @@ -13,79 +9,26 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo 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.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= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/blevesearch/bleve/v2 v2.4.4 h1:RwwLGjUm54SwyyykbrZs4vc1qjzYic4ZnAnY9TwNl60= -github.com/blevesearch/bleve/v2 v2.4.4/go.mod h1:fa2Eo6DP7JR+dMFpQe+WiZXINKSunh7WBtlDGbolKXk= -github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY= -github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= -github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= -github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.24 h1:K79IvKjoKHdi7FdiXEsAhxpMuns0x4fM0BO93bW5jLI= -github.com/blevesearch/go-faiss v1.0.24/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= -github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= -github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= -github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= -github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= -github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= -github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= -github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= -github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= -github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= -github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= -github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= -github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= -github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= -github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= -github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= -github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= -github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= -github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8= -github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= -github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= -github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= -github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE= -github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= -github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b h1:ju9Az5YgrzCeK3M1QwvZIpxYhChkXp7/L0RhDYsxXoE= -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/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= @@ -98,37 +41,22 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/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= -github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= -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= @@ -139,71 +67,26 @@ github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi 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/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.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= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= -github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= -github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= -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/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.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.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= @@ -211,8 +94,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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= @@ -220,30 +101,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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.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/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= @@ -253,22 +116,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI 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.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= @@ -277,146 +128,55 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQ 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= -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= -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/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdqCysW5k= 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= -github.com/ysmood/fetchup v0.2.4/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A= -github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= -github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= -github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= -github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= -github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= -github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= -github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= -github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= -github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= -github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= -github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -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.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= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.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.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= -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.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= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -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= -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.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= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= -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= @@ -425,34 +185,7 @@ 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= -modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= -modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE= -modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= -modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo= -modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A= -modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= -modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= -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= diff --git a/sdk/ADR.md b/sdk/ADR.md new file mode 100644 index 000000000..83835da89 --- /dev/null +++ b/sdk/ADR.md @@ -0,0 +1,427 @@ +# Architecture Decision Record: Datastar SDK + +## Summary + +Datastar SDK provides unified tooling for building Hypermedia On Whatever you Like (HOWL) based UIs across multiple languages. While Datastar supports various plugins, the default bundle focuses on a robust Server-Sent Event (SSE) approach, addressing the lack of good SSE tooling in most languages and backends. + +## Decision + +Provide a language-agnostic SDK with these principles: + +1. **Minimal Core**: Keep the SDK as minimal as possible +2. **Sugar Extensions**: Allow per-language/framework extended features in SDK "sugar" versions + +### Naming Rationale + +**Why "Patch" instead of "Merge":** +The prefix "Patch" was chosen to better reflect the non-idempotent nature of these operations. Unlike PUT requests that replace entire resources, PATCH requests apply partial modifications. This aligns with our SDKs behavior where operations modify specific parts of the DOM or signal state rather than replacing them entirely. + +**Why "Elements" instead of "Fragments":** +We use "Elements" because it accurately describes what the SDK handles - complete HTML elements, not arbitrary DOM nodes like text nodes or document fragments. This naming matches the actual intent and constraints of the system, making the API clearer and more predictable for developers. + +## Details + +### Core Mechanics + +The core mechanics of Datastar’s SSE support is + +1. **Server β†’ Browser**: Data is sent as SSE events +2. **Browser β†’ Server**: Data arrives as JSON under the `datastar` namespace + +# SDK Specification + +> [!WARNING] +> All naming conventions use Go as the reference implementation. Adapt to language-specific conventions while maintaining consistency. + +## ServerSentEventGenerator + +**Required**: A `ServerSentEventGenerator` namespace/class/struct (implementation may vary by language). + +--- + +### Construction / Initialization + +**Requirements:** + +| Requirement | Description | +|-------------|-------------| +| **Constructor** | ***Must*** accept HTTP Request and Response objects | +| **Response Headers** | ***Must*** set:
β€’ `Cache-Control: no-cache`
β€’ `Content-Type: text/event-stream`
β€’ `Connection: keep-alive` (HTTP/1.1 only - [see spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)) | +| **Immediate Flush** | ***Should*** flush response immediately to prevent timeouts | +| **Thread Safety** | ***Should*** ensure ordered delivery (e.g., mutex in Go) | + +--- + +### `ServerSentEventGenerator.send` + +``` +ServerSentEventGenerator.send( + eventType: EventType, + dataLines: string[], + options?: { + eventId?: string, + retryDuration?: durationInMilliseconds + } +) +``` + +A unified sending function ***should*** be used internally (private/protected). + +#### Parameters + +##### EventType + +String enum of supported events: + +| Event | Description | +|-------|-------------| +| `datastar-patch-elements` | Patches HTML elements into the DOM | +| `datastar-patch-signals` | Patches signals into the signal store | + +##### Options + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `eventId` | string | - | Unique event identifier for replay functionality ([SSE spec](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id)) | +| `retryDuration` | ms | `1000` | Reconnection delay after connection loss ([SSE spec](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry)) | + +#### Implementation Requirements + +***Must*** write to response buffer in this exact order: + +1. `event: EVENT_TYPE\n` +2. `id: EVENT_ID\n` (if `eventId` provided) +3. `retry: RETRY_DURATION\n` (***unless*** default of `1000`) +4. `data: DATA\n` (for each of the `dataLines`) +5. `\n` (end of event) +6. ***Should*** flush immediately (note: compression middleware may interfere) + +**Error Handling**: ***Must*** return/throw errors per language conventions. + +--- + +### `ServerSentEventGenerator.PatchElements` + +```go +ServerSentEventGenerator.PatchElements( + elements: string, + options?: { + selector?: string, + mode?: ElementPatchMode, + useViewTransition?: boolean, + eventId?: string, + retryDuration?: durationInMilliseconds + } +) +``` + +#### Example Output + +
+ Minimal Example + + ``` + event: datastar-patch-elements + data: elements
1
+ + ``` +
+ +
+ Full Example (all options) + + ``` + event: datastar-patch-elements + id: 123 + retry: 2000 + data: mode inner + data: selector #feed + data: useViewTransition true + data: elements
+ data: elements 1 + data: elements
+ + ``` +
+ +
+ Patch elements based on their ID + + ``` + event: datastar-patch-elements + data: elements
New content.
+ data: elements
Other new content.
+ ``` +
+ +
+ Insert a new element based on a selector + + ``` + event: datastar-patch-elements + data: mode append + data: selector #mycontainer + data: elements
New content
+ ``` +
+ +
+ Remove elements based on a selector + + ``` + event: datastar-patch-elements + data: mode remove + data: selector #feed, #otherid + ``` +
+ +
+ Remove elements without a selector + + ``` + event: datastar-patch-elements + data: mode remove + data: elements
+ ``` +
+ +`PatchElements` sends HTML elements to the browser for DOM manipulation. + +> [!TIP] +> - To remove elements, use the `remove` patch mode +> - To execute JavaScript, send a ` + ``` + + +
+ Full Example (all options) + + ``` + event: datastar-patch-elements + id: 123 + retry: 2000 + data: mode append + data: selector body + data: elements + ``` +
+ +### Parameters + +- **script**: One or more lines of JavaScript. + +### Options + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `autoRemove` | boolean | `true` | Removes the script tag after executing | +| `attributes` | []string | - | Attributes to add to the script tag | + +### Implementation + +***Must*** call `ServerSentEventGenerator.send` with event type `datastar-patch-elements`, sending a `script` tag containing the JavaScript to execute. If `autoRemove` is `true`, `data-effect="el.remove()"` must be added to the `script` tag. If `attributes` exist, they must be added to the `script` tag. + +**Data format** (only include non-defaults): +- `selector body\n` +- `mode append\n` +- `elements SCRIPT_TAG\n` + +--- + +## `ReadSignals` + +`ReadSignals` parses incoming signal data from the browser into backend objects. + +```go +ReadSignals(request *http.Request, signals any) error +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `request` | HTTP Request | Language-specific request object | +| `signals` | any | Target object/struct for unmarshaling | + +### Implementation + +The function ***must*** parse the incoming HTTP request based on the method: + +| Method | Data Location | Format | Description | +|--------|---------------|--------|-------------| +| `GET` | Query parameter `datastar` | URL-encoded JSON | Extract from query string | +| Others | Request body | JSON | Parse request body directly | + +**Error Handling**: ***Must*** return error for invalid JSON. diff --git a/sdk/README.md b/sdk/README.md index 83835da89..4d24dcfd9 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -1,427 +1,110 @@ -# Architecture Decision Record: Datastar SDK +# Datastar SDK Directory -## Summary +This directory contains the SDK test suite and related tools for Datastar SDK development. -Datastar SDK provides unified tooling for building Hypermedia On Whatever you Like (HOWL) based UIs across multiple languages. While Datastar supports various plugins, the default bundle focuses on a robust Server-Sent Event (SSE) approach, addressing the lack of good SSE tooling in most languages and backends. +## Structure -## Decision +- `tests/` - Comprehensive test suite for validating SDK implementations +- `datastar-sdk-config.json` - SDK configuration with constants and defaults +- `datastar-sdk-config.schema.json-v1.json` - JSON schema for configuration validation +- `ADR.md` - Architecture Decision Record for SDK specifications -Provide a language-agnostic SDK with these principles: +## SDK Test Suite -1. **Minimal Core**: Keep the SDK as minimal as possible -2. **Sugar Extensions**: Allow per-language/framework extended features in SDK "sugar" versions +The `tests/` directory contains a Go-based test suite that validates Datastar SDK implementations across different languages. -### Naming Rationale +### Installation -**Why "Patch" instead of "Merge":** -The prefix "Patch" was chosen to better reflect the non-idempotent nature of these operations. Unlike PUT requests that replace entire resources, PATCH requests apply partial modifications. This aligns with our SDKs behavior where operations modify specific parts of the DOM or signal state rather than replacing them entirely. - -**Why "Elements" instead of "Fragments":** -We use "Elements" because it accurately describes what the SDK handles - complete HTML elements, not arbitrary DOM nodes like text nodes or document fragments. This naming matches the actual intent and constraints of the system, making the API clearer and more predictable for developers. - -## Details - -### Core Mechanics - -The core mechanics of Datastar’s SSE support is - -1. **Server β†’ Browser**: Data is sent as SSE events -2. **Browser β†’ Server**: Data arrives as JSON under the `datastar` namespace - -# SDK Specification - -> [!WARNING] -> All naming conventions use Go as the reference implementation. Adapt to language-specific conventions while maintaining consistency. - -## ServerSentEventGenerator - -**Required**: A `ServerSentEventGenerator` namespace/class/struct (implementation may vary by language). - ---- - -### Construction / Initialization - -**Requirements:** - -| Requirement | Description | -|-------------|-------------| -| **Constructor** | ***Must*** accept HTTP Request and Response objects | -| **Response Headers** | ***Must*** set:
β€’ `Cache-Control: no-cache`
β€’ `Content-Type: text/event-stream`
β€’ `Connection: keep-alive` (HTTP/1.1 only - [see spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)) | -| **Immediate Flush** | ***Should*** flush response immediately to prevent timeouts | -| **Thread Safety** | ***Should*** ensure ordered delivery (e.g., mutex in Go) | - ---- - -### `ServerSentEventGenerator.send` - -``` -ServerSentEventGenerator.send( - eventType: EventType, - dataLines: string[], - options?: { - eventId?: string, - retryDuration?: durationInMilliseconds - } -) -``` - -A unified sending function ***should*** be used internally (private/protected). - -#### Parameters - -##### EventType - -String enum of supported events: - -| Event | Description | -|-------|-------------| -| `datastar-patch-elements` | Patches HTML elements into the DOM | -| `datastar-patch-signals` | Patches signals into the signal store | - -##### Options - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `eventId` | string | - | Unique event identifier for replay functionality ([SSE spec](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id)) | -| `retryDuration` | ms | `1000` | Reconnection delay after connection loss ([SSE spec](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry)) | - -#### Implementation Requirements - -***Must*** write to response buffer in this exact order: - -1. `event: EVENT_TYPE\n` -2. `id: EVENT_ID\n` (if `eventId` provided) -3. `retry: RETRY_DURATION\n` (***unless*** default of `1000`) -4. `data: DATA\n` (for each of the `dataLines`) -5. `\n` (end of event) -6. ***Should*** flush immediately (note: compression middleware may interfere) - -**Error Handling**: ***Must*** return/throw errors per language conventions. - ---- - -### `ServerSentEventGenerator.PatchElements` - -```go -ServerSentEventGenerator.PatchElements( - elements: string, - options?: { - selector?: string, - mode?: ElementPatchMode, - useViewTransition?: boolean, - eventId?: string, - retryDuration?: durationInMilliseconds - } -) +```bash +go get github.com/starfederation/datastar/sdk/tests ``` -#### Example Output - -
- Minimal Example - - ``` - event: datastar-patch-elements - data: elements
1
- - ``` -
- -
- Full Example (all options) - - ``` - event: datastar-patch-elements - id: 123 - retry: 2000 - data: mode inner - data: selector #feed - data: useViewTransition true - data: elements
- data: elements 1 - data: elements
- - ``` -
- -
- Patch elements based on their ID - - ``` - event: datastar-patch-elements - data: elements
New content.
- data: elements
Other new content.
- ``` -
+### Usage -
- Insert a new element based on a selector +#### Running Tests - ``` - event: datastar-patch-elements - data: mode append - data: selector #mycontainer - data: elements
New content
- ``` -
+```bash +# Run all tests against default server (localhost:7331) +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -
- Remove elements based on a selector +# Run with custom server +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -server http://localhost:8080 - ``` - event: datastar-patch-elements - data: mode remove - data: selector #feed, #otherid - ``` -
+# Run only GET tests +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -type get -
- Remove elements without a selector +# Run only POST tests +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -type post - ``` - event: datastar-patch-elements - data: mode remove - data: elements
- ``` -
- -`PatchElements` sends HTML elements to the browser for DOM manipulation. - -> [!TIP] -> - To remove elements, use the `remove` patch mode -> - To execute JavaScript, send a ` - ``` - +### Test Structure -
- Full Example (all options) +- `tests/golden/get/` - Test cases for GET endpoints +- `tests/golden/post/` - Test cases for POST endpoints +- Each test case contains: + - `input.json` - Request payload + - `output.txt` - Expected SSE response - ``` - event: datastar-patch-elements - id: 123 - retry: 2000 - data: mode append - data: selector body - data: elements - ``` -
+## SDK Development -### Parameters +### Configuration -- **script**: One or more lines of JavaScript. +The SDK configuration system provides: -### Options +- **Constants**: Event types, element patch modes, and other enums +- **Defaults**: Default values for booleans and durations +- **Literals**: List of known dataline literals +- **JSON Schema**: Validation for the configuration format -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `autoRemove` | boolean | `true` | Removes the script tag after executing | -| `attributes` | []string | - | Attributes to add to the script tag | +### Creating a New SDK -### Implementation +1. Read the [Architecture Decision Record](./ADR.md) for SDK specifications +2. Use [`datastar-sdk-config.json`](./datastar-sdk-config.json) as your source of constants +3. Implement the required `ServerSentEventGenerator` interface +4. Validate your implementation using the test suite -***Must*** call `ServerSentEventGenerator.send` with event type `datastar-patch-elements`, sending a `script` tag containing the JavaScript to execute. If `autoRemove` is `true`, `data-effect="el.remove()"` must be added to the `script` tag. If `attributes` exist, they must be added to the `script` tag. +### Testing Your SDK -**Data format** (only include non-defaults): -- `selector body\n` -- `mode append\n` -- `elements SCRIPT_TAG\n` +To validate your SDK implementation: ---- +1. Start your test server on port 7331 (or specify a different port) +2. Implement the `/test` endpoint that: + - For GET: reads the `datastar` query parameter + - For POST: reads the JSON body + - Returns appropriate SSE responses based on the `events` array +3. Run `go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest` to validate your implementation -## `ReadSignals` +## Features -`ReadSignals` parses incoming signal data from the browser into backend objects. - -```go -ReadSignals(request *http.Request, signals any) error -``` +- **HTML Normalization**: Test suite automatically handles HTML attribute ordering +- **Embedded Test Data**: All test cases are embedded in the binary for portability +- **Comprehensive Coverage**: Tests cover all SSE event types and edge cases +- **Clear Error Messages**: Detailed output helps identify implementation issues -### Parameters +## Official SDKs -| Parameter | Type | Description | -|-----------|------|-------------| -| `request` | HTTP Request | Language-specific request object | -| `signals` | any | Target object/struct for unmarshaling | +| Language | Repository | +|----------|------------| +| Go | [starfederation/datastar-go](https://github.com/starfederation/datastar-go) | +| PHP | [starfederation/datastar-php](https://github.com/starfederation/datastar-php) | -### Implementation +## Community SDKs -The function ***must*** parse the incoming HTTP request based on the method: +We welcome community contributions! If you've created a Datastar SDK for a language not listed above, please open a PR to add it here. -| Method | Data Location | Format | Description | -|--------|---------------|--------|-------------| -| `GET` | Query parameter `datastar` | URL-encoded JSON | Extract from query string | -| Others | Request body | JSON | Parse request body directly | +## Support -**Error Handling**: ***Must*** return error for invalid JSON. +For SDK-specific issues, please open an issue in the respective SDK repository. For general Datastar questions or cross-SDK concerns, use the main [Datastar repository](https://github.com/starfederation/datastar). \ No newline at end of file diff --git a/sdk/clojure/.clj-kondo/config.edn b/sdk/clojure/.clj-kondo/config.edn deleted file mode 100644 index 1ce7f0a2b..000000000 --- a/sdk/clojure/.clj-kondo/config.edn +++ /dev/null @@ -1,6 +0,0 @@ -{:lint-as {fr.jeremyschoffen.datastar.utils/defroutes clojure.core/def - starfederation.datastar.clojure.utils/transient-> clojure.core/->} - :hooks - {:analyze-call - {test.utils/with-server hooks.test-hooks/with-server}} - :linters {:redundant-ignore {:exclude [:clojure-lsp/unused-public-var]}}} diff --git a/sdk/clojure/.clj-kondo/hooks/test_hooks.clj b/sdk/clojure/.clj-kondo/hooks/test_hooks.clj deleted file mode 100644 index 871699247..000000000 --- a/sdk/clojure/.clj-kondo/hooks/test_hooks.clj +++ /dev/null @@ -1,17 +0,0 @@ -(ns hooks.test-hooks - (:require - [clj-kondo.hooks-api :as api])) - - -(defn with-server [{:keys [node] :as exp}] - (let [[s-name handler opts & body] (-> node :children rest) - underscore (api/token-node '_) - new-children (list* - (api/token-node 'let) - (api/vector-node - [s-name handler - underscore opts]) - body) - new-node (assoc node :children new-children)] - (assoc exp :node new-node))) - diff --git a/sdk/clojure/.gitignore b/sdk/clojure/.gitignore deleted file mode 100644 index 4824193fd..000000000 --- a/sdk/clojure/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -.cpcache -.nrepl-port -.lsp -.clj-kondo/** -!.clj-kondo/config.edn -!.clj-kondo/hooks** -**/target -test-resources/test.config.edn -/.lsp-root -/.nfnl.fnl -/.nvim.fnl -/.nvim.lua diff --git a/sdk/clojure/CHANGELOG.md b/sdk/clojure/CHANGELOG.md deleted file mode 100644 index 9e82851a3..000000000 --- a/sdk/clojure/CHANGELOG.md +++ /dev/null @@ -1,176 +0,0 @@ -# Release notes for the Clojure SDK - -## 2025-06-22 - -### Changed - -- The public API has seen it's main functions renamed following the new SDK ADR. - Several functions have been renamed or removed: - - | Old | new | - | ------------------- | --------------------- | - | `merge-fragment!` | `patch-elements!` | - | `merge-fragments!` | `patch-elements-seq!` | - | `remove-fragments!` | `remove-element!` | - | `merge-signals!` | `patch-signals!` | - | `remove-signals` | removed | - -- All the examples and snippets have been updated following the ADR changes. - -### Fixed - -- A superflous newline character was send when marking the end of a SSE event -- The clj-kondo config file for the SDK has been moved in a - `clj-kondo.exports/starfederation.datastar.clojure/sdk` directory. This change - allows for other projects to use - `starfederation.datastar.clojure/XXXX/config.edn` for their clj-kondo config. - -### Added - -- There is a new http-kit API that allows a more natural ring response model when - using SSE. With the current API the status and headers for a response are - sent directly while `->sse-response` is running, the `on-open` callback runs - just after. For instance that any middleware that would add headers after the - execution of the `->sse-response` function won't work, the initial response - being already sent. - The new `starfederation.datastar.clojure.adapter.http-kit2` - API changes this behavior. In this new api the initial response is not sent - during `->sse-response`. Instead a middleware takes care of sending it and - only then calls the `on-open` callback. If this middleware is the last to run - on the return any addition to the response map will be taken into account. -- A new library providing Brotli write profile has been added. - -## 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 - -- the use of `:on-open` and `:on-close` keywords for the `->sse-response` - function is deprecated and will be removed in a future release. - See `->sse-response`'s docstring. - -## 2025-03-11 - -### Added - -- Added a "write profile" mechanism that allows user to use compression of - SSE streams and let them control the IO buffering behavior. -- Added new malli schemas to go with the "write profiles". -- Added the ability to pass an on-error callback to SSE generators, it allows - for changing or augmenting the behavior on exceptions. -- `starfederation.datastar.clojure.api/lock-sse!`. This is a macro allowing - its body to be protected by a SSE generator's lock. -- Added a Flowstorm setup to help debugging when working on the SDK. - -### Fixed - -- Fixed a typo in the cache-control HTTP header. -- Fixed some malli schemas -- There were problems with the handling of concurrent uses of the ring adapter. - This adapter uses a re-entrant lock to prevent some bad behaviors and the - previous implementation was too simplistic. The lock management has been - redone and there are now some basic tests for the hairiest part of the lock's - handling. - -### Changed - -- The handling of errors and the management of concurrency for the adapters has - been redesigned. The ring implementation particularly needed some work. - Now when a `IOException` is thrown sending an event, adapters will close - themselves. Other exceptions are rethrown. When trying to close an already - closed SSE generator, the `close-sse!` function just returns `false`. - Previously the ring adapter was potentially throwing in that case. - Also the `on-close` callback can be called at most once. - Both adapters behave similarly when it comes to errors. -- A slight change to the `starfederation.datastar.clojure.api.sse` namespace - makes it a generic SSE event formatter. - - `starfederation.datastar.clojure.sse/write-event!` is now Datastar agnostic - - `starfederation.datastar.clojure.sse/headers` is now a generic function - to make HTTP headers containing the SSE specific ones. -- The SSE headers added by the SDK no longer override the user provided ones. -- bumped http-kit version: `2.9.0-alpha2` -> `2.9.0-alpha4` - -### Docs - -- Several docstrings, and documentation files have been corrected/added. -- Added documentation giving the rationale for the "write profile" concept - and giving some usage examples. -- Added the load_more code snippet to the main site's code snippets - -## 2025-02-15 - -### Added - -- `starfederation.datastar.clojure.adapter.test/->sse-response`. This is a mock - for a SSE ring response that records the SSE events sent with it. -- Example snippets for the main site, ie, polling and redirection. These - examples are runnable from the development examples. -- Development example of the usage of the redirect sugar. - -### Fixed - -- Fixed the main readme example (wrong arity of `:on-close` callback using http-kit) -- The jetty adapter now returns a harmless value when sending an event. It used - to return the write buffer which shouldn't be used directly. -- The `starfederation.datastar.clojure.api/redirect!` helper function uses a js - timeout for redirection - -## 2025-02-03 - -### Changed - -- The ring adapter for the SDK is now a generic ring adapter. This adapter - depends solely on the ring core protocols, the dependency to the ring - jetty adapter has been removed. - -> [!important] -> This change comes with these breaking changes: -> -> - The `adapter-jetty` directory has been renamed `adapter-ring`, this mean a -> change in your `deps.edn`. Check the readme. -> - The `starfederation.datastar.clojure.adapter.ring-jetty` has been moved to -> `starfederation.datastar.clojure.adapter.ring` - -### Added - -- Added a function to the main API that test for the presence of Datastar - headers in a ring request. -- The `->sse-response` functions from the adapters now have the option to - modify the HTTP response status code. It is useful when wanting to return - 204 responses for instance. -- rj9a will be supported as soon as there is a fix for the connection lifetime - issue in asynchronous ring. - -### Fixed - -- SSE retry duration data line is now properly elided when its value passed - as an option is equal the default retry duration value from the generated - constants. -- Fixed miss-management of web-drivers in tests, the drivers are now killed - when the JVM shuts down diff --git a/sdk/clojure/README.md b/sdk/clojure/README.md deleted file mode 100644 index 5a4d6105d..000000000 --- a/sdk/clojure/README.md +++ /dev/null @@ -1,190 +0,0 @@ -# Datastar Clojure SDK - -We provide several libraries for working with [Datastar](https://data-star.dev/): - -- A generic SDK to generate and send SSE events formatted for Datastar. -- A SSE generator abstraction defined by the - `starfederation.datastar.clojure.protocols/SSEGenerator` protocol as well as - several libraries implementing it to work with specific ring adapters. -- A library containing [malli schemas](https://github.com/metosin/malli) - covering the generic API and our adapter implementations. - -There currently are adapter implementations for: - -- [ring](https://github.com/ring-clojure/ring) -- [http-kit](https://github.com/http-kit/http-kit) - -If you want to roll your own adapter implementation, see -[implementing-adapters](/sdk/clojure/doc/implementing-adapters.md). - -## Installation - -For now the libraries are distributed as git dependencies. You need to add a -dependency for each library you use. - -> [!important] -> This project is new and there isn't a release process yet other than using git shas. -> Replace `LATEST_SHA` in the git coordinates below by the actual latest commit sha of this repository. - -To your `deps.edn` file you can add the following coordinates: - -- SDK - -```clojure -{datastar/sdk {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST_SHA" - :deps/root "sdk/clojure/sdk"}} -``` - -- ring implementation - -```clojure -{datastar/ring {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST_SHA" - :deps/root "sdk/clojure/adapter-ring"} - - ring-compliant/adapter "Coordinate for the ring compliant adapter you want to use."} -``` - -By ring compliant adapter we mean adapters that are implementing the -`ring.core.protocols/StreamableResponseBody` protocol to deal with response bodies. - -- http-kit implementation - -```clojure -{datastar/http-kit {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST_SHA" - :deps/root "sdk/clojure/adapter-http-kit"}} -``` - -- Malli schemas: - -```clojure -{datastar/malli-schemas {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST_SHA" - :deps/root "sdk/clojure/malli-schemas"}} -``` - -## Usage - -### Basic Concepts - -By convention SDK adapters provide a single `->sse-response` function. This -function returns a valid ring response tailored to work with the ring -adapter it is made for. This function will receive an implementation of -`SSEGenerator` protocol also tailored to the ring adapter used. - -You then use the Datastar SDK functions with the SSE generator. - -### Short example - -Start by requiring the main API and an adapter. With Http-kit for instance: - -```clojure -(require '[starfederation.datastar.clojure.api :as d*]) - '[starfederation.datastar.clojure.adapter.http-kit :as hk-gen]) - -``` - -Using the adapter you create ring responses in your handlers: - -```clojure -(defn sse-handler [request] - (hk-gen/->sse-response request - {hk-gen/on-open - (fn [sse-gen] - (d*/patch-elements! sse-gen "
test
") - (d*/close-sse! sse-gen))})) - -``` - -In the callback we use the SSE generator `sse-gen` with the Datastar SDK functions. - -Depending on the adapter you use, you can keep the SSE generator open by storing -it somewhere and use it later: - -```clojure -(def !connections (atom #{})) - - -(defn sse-handler [request] - (hk-gen/->sse-response request - {hk-gen/on-open - (fn [sse-gen] - (swap! !connections conj sse-gen)) - - hk-gen/on-close - (fn [sse-gen status] - (swap! !connections disj sse-gen))})) - - -(defn broadcast-elements! [elements] - (doseq [c @!connections] - (d*/patch-elements! c elements))) - -``` - -Check the docstrings in the `starfederation.datastar.clojure.api` namespace for -more details. - -### Advanced features - -This SDK is essentially a tool to manage SSE connections with helpers to format -events the way the Datastar framework expects them on the front end. - -It provides advanced functionality for managing several aspects of SSE. - -You can find more information in several places: - -- the docstings for the `->sse-response` function you are using. -- the [SSE design notes document](/sdk/clojure/doc/SSE-design-notes.md) details - what considerations are taken into account in the SDK. -- the [write profiles document](/sdk/clojure/doc/Write-profiles.md) details the - tools the SDK provides to control the buffering behaviors of a SSE stream and - how to use compression. -- the [adapter implementation guide](/sdk/clojure/doc/implementing-adapters.md) - lists the conventions by which SDK adapters are implemented if the need to - implement your own ever arises. - -## Adapter behaviors: - -Ring adapters are not made equals. This leads to our SSE generators not having -the exact same behaviors in some cases. - -### SSE connection lifetime in ring when trying to store the sse-gen somewhere - -#### With ring sync - -| Adapter | | -| -------- | ---------------------------------------------------------------------- | -| ring | same as the thread creating the sse response | -| http-kit | alive until the client or the server explicitely closes the connection | - -You may keep the connection open in ring sync mode by somehow blocking the thread -handling the request. This is a valid strategy when using java's virtual threads. - -#### With ring async - -| Adapter | | -| ------------ | ---------------------------------------------------------------------- | -| all adapters | alive until the client or the server explicitely closes the connection | - -### Detecting a closed connection - -| Adapter | | -| -------- | ----------------------------------------------------------------------------------------------- | -| ring | Sending events on a closed connection will fail at some point and the sse-gen will close itself | -| http-kit | Http-kit detects closed connections by itself and closes the sse-gen | - -At this moment, when using the ring adapter and jetty, our SSE generators need -to send 2 small events or 1 big event to detect a closed connection. -There must be some buffering happening independent of our implementation. - -## TODO: - -- Streamlined release process (cutting releases and publish jar to a maven repo) -- Review the etaoin tests, there are some race conditions - -## License - -[![License](https://img.shields.io/github/license/starfederation/datastar)](https://github.com/starfederation/datastar/blob/main/LICENSE) diff --git a/sdk/clojure/adapter-http-kit/README.md b/sdk/clojure/adapter-http-kit/README.md deleted file mode 100644 index 790e8f069..000000000 --- a/sdk/clojure/adapter-http-kit/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Datastar http-kit adapter - -## Installation - -For now the SDK and adapters are distributed as git dependencies using a `deps.edn` file. - -```clojure -{datastar/sdk {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/sdk"} - - datastar/http-kit {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/adapter-http-kit"}} -``` - -> [!important] -> Replace `LATEST_SHA` in the git coordinates below by the actual latest commit sha of the repository. diff --git a/sdk/clojure/adapter-http-kit/build.clj b/sdk/clojure/adapter-http-kit/build.clj deleted file mode 100644 index 70c10ea44..000000000 --- a/sdk/clojure/adapter-http-kit/build.clj +++ /dev/null @@ -1,45 +0,0 @@ -(ns build - (:require [clojure.tools.build.api :as b] - [clojure.edn :as edn])) - -(def project (-> (edn/read-string (slurp "deps.edn")) - :aliases :neil :project)) -(def lib (:name project)) -(def version (:version project)) -(assert lib ":name must be set in deps.edn under the :neil alias") -(assert version ":version must be set in deps.edn under the :neil alias") - -(def class-dir "target/classes") -(def basis (b/create-basis {:project "deps.edn"})) -(def jar-file (format "target/%s-%s.jar" (name lib) version)) - -(defn clean [_] - (b/delete {:path "target"})) - -(defn jar [_] - (b/write-pom {:class-dir class-dir - :lib lib - :version version - :basis basis - :src-dirs ["src"]}) - (b/copy-dir {:src-dirs ["src" "resources"] - :target-dir class-dir}) - (b/jar {:class-dir class-dir - :jar-file jar-file})) - -(defn install [_] - (jar {}) - (b/install {:basis basis - :lib lib - :version version - :jar-file jar-file - :class-dir class-dir})) - -(defn deploy [opts] - (jar opts) - ((requiring-resolve 'deps-deploy.deps-deploy/deploy) - (merge {:installer :remote - :artifact jar-file - :pom-file (b/pom-path {:lib lib :class-dir class-dir})} - opts)) - opts) diff --git a/sdk/clojure/adapter-http-kit/deps.edn b/sdk/clojure/adapter-http-kit/deps.edn deleted file mode 100644 index 22f3d9d09..000000000 --- a/sdk/clojure/adapter-http-kit/deps.edn +++ /dev/null @@ -1,9 +0,0 @@ -;; NOTE: Track the next release of http-kit to switch to maven dep -{:paths ["src/main"] - :deps {http-kit/http-kit {:mvn/version "2.9.0-alpha4"}} - :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" - :git/sha "e405aac"} - slipset/deps-deploy {:mvn/version "0.2.2"}} - :ns-default build} - :neil {:project {:name dev.data-star/http-kit - :version "1.0.0-beta.11"}}}} diff --git a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit.clj b/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit.clj deleted file mode 100644 index a6f9e912a..000000000 --- a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit.clj +++ /dev/null @@ -1,76 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.http-kit - (:require - [org.httpkit.server :as hk-server] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.http-kit.impl :as impl] - [starfederation.datastar.clojure.utils :refer [def-clone]])) - - -(def-clone on-open ac/on-open) -(def-clone on-close ac/on-close) -(def-clone on-exception ac/on-exception) -(def-clone default-on-exception ac/default-on-exception) - - -(def-clone write-profile ac/write-profile) - -(def-clone basic-profile impl/basic-profile) -(def-clone buffered-writer-profile ac/buffered-writer-profile) -(def-clone gzip-profile ac/gzip-profile) -(def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile) - - -(defn ->sse-response - "Make a Ring like response that will start a SSE stream. - - The status code and the the SSE specific headers are sent automatically - before [[on-open]] is called. - - Note that the SSE connection stays opened util you close it. - - General options: - - `: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. - - [[on-close]]: callback called when the underlying Http-kit AsyncChannel is - closed. It receives a second argument, the `:status-code` value we get from - the closing AsyncChannel. - - [[on-exception]]: callback called when sending a SSE event throws. - - [[write-profile]]: write profile for the connection. - Defaults to [[basic-profile]] - - SDK provided write profiles: - - [[basic-profile]] - - [[buffered-writer-profile]] - - [[gzip-profile]] - - [[gzip-buffered-writer-profile]] - - You can also take a look at the `starfederation.datastar.clojure.adapter.common` - namespace if you want to write your own profiles. - " - [ring-request 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 - {:on-open - (fn [ch] - (impl/send-base-sse-response! ch ring-request opts) - (let [send! (impl/->send! ch opts) - sse-gen (impl/->sse-gen ch send! opts)] - (deliver future-gen sse-gen) - (deliver future-send! send!) - (on-open-cb sse-gen))) - - :on-close - (fn [_ status] - (let [closing-res - (ac/close-sse! - #(when-let [send! (deref future-send! 0 nil)] (send!)) - #(when on-close-cb - (on-close-cb (deref future-gen 0 nil) status)))] - (if (instance? Exception closing-res) - (throw closing-res) - closing-res)))}))) diff --git a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit/impl.clj b/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit/impl.clj deleted file mode 100644 index ab0267f4d..000000000 --- a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit/impl.clj +++ /dev/null @@ -1,106 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.http-kit.impl - (:require - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.protocols :as p] - [starfederation.datastar.clojure.utils :as u] - [org.httpkit.server :as hk-server]) - (:import - [java.util.concurrent.locks ReentrantLock] - [java.io ByteArrayOutputStream Closeable])) - - -(def basic-profile - "Basic write profile using temporary [[StringBuilder]]s, no output stream and - no compression." - {ac/write! (ac/->build-event-str)}) - - -;; ----------------------------------------------------------------------------- -;; Sending the headers -;; ----------------------------------------------------------------------------- -(defn send-base-sse-response! - "Send the response headers, this should be done as soon as the conneciton is - open." - [ch req {:keys [status] :as opts}] - (hk-server/send! ch - {:status (or status 200) - :headers (ac/headers req opts)} - false)) - -;; ----------------------------------------------------------------------------- -;; Sending events machinery -;; ----------------------------------------------------------------------------- -(defn ->send-simple [ch write-profile] - (let [write! (ac/write! write-profile)] - (fn - ([]) - ([event-type data-lines opts] - (let [event (write! event-type data-lines opts)] - (hk-server/send! ch event false)))))) - - -(defn flush-baos! [^ByteArrayOutputStream baos ch] - (let [msg (.toByteArray baos)] - (.reset baos) - (hk-server/send! ch msg false))) - - -(defn ->send-with-output-stream [ch write-profile] - (let [^ByteArrayOutputStream baos (ByteArrayOutputStream.) - {wrap-os ac/wrap-output-stream - write! ac/write!} write-profile - writer (wrap-os baos)] - (fn - ([] - ;; Close the writer first to finish the gzip process - (.close ^Closeable writer) - ;; Flush towards SSE out - (flush-baos! baos ch)) - ([event-type data-lines opts] - (write! writer event-type data-lines opts) - (ac/flush writer) - (flush-baos! baos ch))))) - - -(defn ->send! [ch opts] - (let [write-profile (or (ac/write-profile opts) - basic-profile)] - (if (ac/wrap-output-stream write-profile) - (->send-with-output-stream ch write-profile) - (->send-simple ch write-profile)))) - -;; ----------------------------------------------------------------------------- -;; SSE gen -;; ----------------------------------------------------------------------------- -(deftype SSEGenerator [ch lock send! on-exception] - p/SSEGenerator - (send-event! [this event-type data-lines opts] - (u/lock! lock - (try - (send! event-type data-lines opts) - (catch Exception e - (when (on-exception this e {:sse-gen this - :event-type event-type - :data-lines data-lines - :opts opts}) - (p/close-sse! this)) - false)))) - - (get-lock [_] lock) - - (close-sse! [_] - (hk-server/close ch)) - - (sse-gen? [_] true) - - Closeable - (close [this] - (p/close-sse! this))) - - -(defn ->sse-gen - ([ch send!] - (->sse-gen ch send! {})) - ([ch send! opts] - (SSEGenerator. ch (ReentrantLock.) send! (or (ac/on-exception opts) - ac/default-on-exception)))) diff --git a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj b/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj deleted file mode 100644 index 1ac84e75a..000000000 --- a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj +++ /dev/null @@ -1,151 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.http-kit2 - (:require - [org.httpkit.server :as hk-server] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.http-kit.impl :as impl] - [starfederation.datastar.clojure.utils :refer [def-clone]])) - - -(def-clone on-open ac/on-open) -(def-clone on-close ac/on-close) -(def-clone on-exception ac/on-exception) -(def-clone default-on-exception ac/default-on-exception) - - -(def-clone write-profile ac/write-profile) - -(def-clone basic-profile impl/basic-profile) -(def-clone buffered-writer-profile ac/buffered-writer-profile) -(def-clone gzip-profile ac/gzip-profile) -(def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile) - - -(defn- as-channel - " - Replacement for [[hk-server/as-channel]] that doesn't deal with websockets - and doen't call `on-open` itself. - - `on-open` is meant to be called by either a middleware or an interceptor on the return. - " - [ring-req {:keys [on-close on-open init]}] - - (when-let [ch (:async-channel ring-req)] - - (when-let [f init] (f ch)) - (when-let [f on-close] (org.httpkit.server/on-close ch (partial f ch))) - - {:body ch ::on-open on-open})) - - -(defn ->sse-response - "Make a Ring like response that will start a SSE stream. - - The status code and the the SSE specific headers are not sent automatically. - You need to use either [[start-responding-middleware]] or - [[start-responding-interceptor]]. - - Note that the SSE connection stays opened util you close it. - - General options: - - `: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. - - [[on-close]]: callback called when the underlying Http-kit AsyncChannel is - closed. It receives a second argument, the `:status-code` value we get from - the closing AsyncChannel. - - [[on-exception]]: callback called when sending a SSE event throws. - - [[write-profile]]: write profile for the connection. - Defaults to [[basic-profile]] - - SDK provided write profiles: - - [[basic-profile]] - - [[buffered-writer-profile]] - - [[gzip-profile]] - - [[gzip-buffered-writer-profile]] - - You can also take a look at the `starfederation.datastar.clojure.adapter.common` - namespace if you want to write your own profiles. - " - [ring-request {:keys [status] :as 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)] - (assoc - (as-channel ring-request - {:on-open - (fn [ch] - (let [send! (impl/->send! ch opts) - sse-gen (impl/->sse-gen ch send! opts)] - (deliver future-gen sse-gen) - (deliver future-send! send!) - (on-open-cb sse-gen))) - - :on-close - (fn [_ status] - (let [closing-res - (ac/close-sse! - #(when-let [send! (deref future-send! 0 nil)] (send!)) - #(when on-close-cb - (on-close-cb (deref future-gen 0 nil) status)))] - (if (instance? Exception closing-res) - (throw closing-res) - closing-res)))}) - :status (or status 200) - :headers (ac/headers ring-request opts) - ::datastar-sse-response true))) - - -(defn start-responding! - "Function that takes a ring response map and sends HTTP status & headers using - the [[AsyncChannel]] that should be in the body if the - `::datastar-sse-response` key is present." - [response] - (if (::datastar-sse-response response) - (let [{on-open ::on-open - ch :body} response - response (dissoc response :body ::on-open ::datastar-sse-response)] - (hk-server/send! ch response false) - (on-open ch)) - response)) - - - -(defn wrap-start-responding - "Middleware necessary to use in conjunction with [[->sse-response]]. - - It will check if the response is a datastar-sse-response - (created with [[->sse-response]]). In this case it will send the initial - response containing headers and status code, then call `on-open`." - [handler] - (fn - ([req] - (let [response (handler req)] - (start-responding! response) - response)) - ([req respond _raise] - (let [response (handler req)] - (start-responding! response) - (respond response))))) - - -(def start-responding-middleware - "Reitit middleware map for [[wrap-start-responding]]." - {:name ::start-responding - :wrap wrap-start-responding}) - - -(def start-responding-interceptor - " - Interceptor necessary to use in conjunction with [[->sse-response]]. - - In the `:leave` fn, it will check if the response is a datastar-sse-response - (created with [[->sse-response]]). In this case it will send the initial - response containing headers and status code, then call `on-open`." - {:name ::start-responding - :leave (fn [ctx] - (let [response (:response ctx)] - (start-responding! response) - ctx))}) - diff --git a/sdk/clojure/adapter-ring/README.md b/sdk/clojure/adapter-ring/README.md deleted file mode 100644 index 054c403ae..000000000 --- a/sdk/clojure/adapter-ring/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Datastar ring adapter - -Datastar SDK adapter for [ring](https://github.com/ring-clojure/ring). It is currently -tested with -[ring-jetty-adapter](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter) - -This SDK adapter is based on the `ring.core.protocols/StreamableResponseBody` protocol. -Any ring adapter using this protocol should work with this library. - -## Installation - -For now the SDK and adapters are distributed as git dependencies using a `deps.edn` file. - -```clojure -{datastar/sdk {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/sdk"} - - datastar/ring {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/adapter-ring"}} -``` - -> [!important] -> -> - Replace `LATEST_SHA` in the git coordinates below by the actual latest commit sha of the repository. -> - You also need to add a dependency to an actual ring compliant adapter. diff --git a/sdk/clojure/adapter-ring/build.clj b/sdk/clojure/adapter-ring/build.clj deleted file mode 100644 index 70c10ea44..000000000 --- a/sdk/clojure/adapter-ring/build.clj +++ /dev/null @@ -1,45 +0,0 @@ -(ns build - (:require [clojure.tools.build.api :as b] - [clojure.edn :as edn])) - -(def project (-> (edn/read-string (slurp "deps.edn")) - :aliases :neil :project)) -(def lib (:name project)) -(def version (:version project)) -(assert lib ":name must be set in deps.edn under the :neil alias") -(assert version ":version must be set in deps.edn under the :neil alias") - -(def class-dir "target/classes") -(def basis (b/create-basis {:project "deps.edn"})) -(def jar-file (format "target/%s-%s.jar" (name lib) version)) - -(defn clean [_] - (b/delete {:path "target"})) - -(defn jar [_] - (b/write-pom {:class-dir class-dir - :lib lib - :version version - :basis basis - :src-dirs ["src"]}) - (b/copy-dir {:src-dirs ["src" "resources"] - :target-dir class-dir}) - (b/jar {:class-dir class-dir - :jar-file jar-file})) - -(defn install [_] - (jar {}) - (b/install {:basis basis - :lib lib - :version version - :jar-file jar-file - :class-dir class-dir})) - -(defn deploy [opts] - (jar opts) - ((requiring-resolve 'deps-deploy.deps-deploy/deploy) - (merge {:installer :remote - :artifact jar-file - :pom-file (b/pom-path {:lib lib :class-dir class-dir})} - opts)) - opts) diff --git a/sdk/clojure/adapter-ring/deps.edn b/sdk/clojure/adapter-ring/deps.edn deleted file mode 100644 index 25b7704e6..000000000 --- a/sdk/clojure/adapter-ring/deps.edn +++ /dev/null @@ -1,8 +0,0 @@ -{:paths ["src/main"] - :deps {org.ring-clojure/ring-core-protocols {:mvn/version "1.14.1"}} - :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" - :git/sha "e405aac"} - slipset/deps-deploy {:mvn/version "0.2.2"}} - :ns-default build} - :neil {:project {:name dev.data-star/ring - :version "1.0.0-beta.11"}}}} diff --git a/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring.clj b/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring.clj deleted file mode 100644 index f58de0b46..000000000 --- a/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring.clj +++ /dev/null @@ -1,60 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.ring - (:require - [starfederation.datastar.clojure.adapter.ring.impl :as impl] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.utils :refer [def-clone]])) - - -(def-clone on-open ac/on-open) -(def-clone on-close ac/on-close) -(def-clone on-exception ac/on-exception) -(def-clone default-on-exception ac/default-on-exception) - - -(def-clone write-profile ac/write-profile) - -(def-clone basic-profile ac/basic-profile) -(def-clone buffered-writer-profile ac/buffered-writer-profile) -(def-clone gzip-profile ac/gzip-profile) -(def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile) - - -(defn ->sse-response - "Returns a ring response that will start a SSE stream. - - The status code will be either 200 or the user provided one. - Specific SSE headers are set automatically, the user provided ones will be - merged. The response body is a sse generator implementing - `ring.core.protocols/StreamableResponseBody`. - - In sync mode, the connection is closed automatically when the handler is - done running. You need to explicitely close it in rinc async. - - Opts: - - `:status`: status for the HTTP response, defaults to 200 - - `:headers`: Ring headers map to add to the response - - [[on-open]]: Mandatory callback (fn [sse-gen] ...) called when the generator - is ready to send. - - [[on-close]]: callback (fn [sse-gen] ...) called right after the generator - has closed it's connection. - - [[on-exception]]: callback called when sending a SSE event throws - - [[write-profile]]: write profile for the connection - defaults to [[basic-profile]] - - When it comes to write profiles, the SDK provides: - - [[basic-profile]] - - [[buffered-writer-profile]] - - [[gzip-profile]] - - [[gzip-buffered-writer-profile]] - - You can also take a look at the `starfederation.datastar.clojure.adapter.common` - namespace if you want to write your own profiles. - " - [ring-request {:keys [status] :as opts}] - {:pre [(ac/on-open opts)]} - (let [sse-gen (impl/->sse-gen)] - {:status (or status 200) - :headers (ac/headers ring-request opts) - :body sse-gen - ::impl/opts opts})) - diff --git a/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj b/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj deleted file mode 100644 index 504843330..000000000 --- a/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj +++ /dev/null @@ -1,118 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.ring.impl - (:require - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.protocols :as p] - [starfederation.datastar.clojure.utils :as u] - [ring.core.protocols :as rp]) - (:import - [java.io Closeable OutputStream] - java.util.concurrent.locks.ReentrantLock)) - - -(def default-write-profile ac/basic-profile) - - -(defn ->send [os opts] - (let [{wrap ac/wrap-output-stream - write! ac/write!} (ac/write-profile opts default-write-profile) - writer (wrap os)] - (fn - ([] - (.close ^Closeable writer)) - ([event-type data-lines event-opts] - (write! writer event-type data-lines event-opts) - (ac/flush writer))))) - - -;; Note that the send! field has 2 usages: -;; - it stores the sending function -;; - it acts as a `is-open?` flag -;; Also the on-close not being nil means the callback hasn't been called yet. -(deftype SSEGenerator [^:unsynchronized-mutable send! - ^ReentrantLock lock - ^:unsynchronized-mutable on-close - ^:unsynchronized-mutable on-exception] - rp/StreamableResponseBody - (write-body-to-stream [this response output-stream] - (.lock lock) - - ;; already initialized, unlock and throw, we are out - (when send! - (.unlock lock) - (throw (ex-info "Reused SSE-gen as several ring responses body. Don't do this." {}))) - - (let [!error (volatile! nil)] - (try - ;; initializing the internal state - (let [opts (::opts response)] - (set! send! (->send output-stream opts)) - (set! on-exception (or (ac/on-exception opts) - ac/default-on-exception)) - (when-let [cb (ac/on-close opts)] - (set! on-close cb))) - - ;; flush the HTTP headers - (.flush ^OutputStream output-stream) - true ;; dummy return - ;; We catch everything here, if not a Throwable may pass through - ;; !error won't catch it, on-open would be called - (catch Throwable t - (vreset! !error t)) - (finally - ;; Any exception should have been caught, - ;; the setup the internal state is done, - ;; the HTTP headers are sent - ;; we can now release the lock now - (.unlock lock) - (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/on-open)] - (on-open this))))))) - - p/SSEGenerator - (send-event! [this event-type data-lines opts] - (u/lock! lock - (if send! ;; still open? - (try - (send! event-type data-lines opts) - true ;; successful send - (catch Exception e - (when (on-exception this e {:sse-gen this - :event-type event-type - :data-lines data-lines - :opts opts}) - (set! send! nil) - (p/close-sse! this)) - false)) ;; the event wasn't sent - false))) ; closed return false - - (get-lock [_] lock) - - (close-sse! [this] - (u/lock! lock - ;; If either send! or on-close are here we try to close them - (if (or send! on-close) - (let [res (ac/close-sse! #(when send! (send!)) - #(when on-close (on-close this)))] - ;; We make sure to clean them up after closing - (set! send! nil) - (set! on-close nil) - (if (instance? Exception res) - (throw res) - true)) - false))) - - (sse-gen? [_] true) - - Closeable - (close [this] - (p/close-sse! this))) - - -(defn ->sse-gen [] - (SSEGenerator. nil - (ReentrantLock.) - nil - nil)) - diff --git a/sdk/clojure/bb.edn b/sdk/clojure/bb.edn deleted file mode 100644 index 9f2e960b7..000000000 --- a/sdk/clojure/bb.edn +++ /dev/null @@ -1,76 +0,0 @@ -{:paths ["src/main" "src/bb"] - :tasks - {:requires ([tasks :as t]) - :init (do - (def sdk-lib-dirs ["sdk" "adapter-ring" "adapter-http-kit"])) - - -prep (t/prep-libs) - - dev {:task (t/dev :http-kit :ring-jetty :malli-schemas)} - - dev:rj9a {:task (t/dev :http-kit :ring-rj9a :malli-schemas)} - - dev:empty (t/dev :malli-schemas) - - test:all (t/lazytest [:http-kit - :ring-jetty - :malli-schemas - :sdk-brotli] - [:test.paths/core-sdk - :test.paths/malli-schemas - :test.paths/brotli - :test.paths/adapter-ring - :test.paths/adapter-http-kit - :test.paths/adapter-ring-jetty]) - - test:all-w (t/lazytest [:http-kit - :ring-jetty - :malli-schemas - :sdk-brotli] - [:test.paths/core-sdk - :test.paths/malli-schemas - :test.paths/brotli - :test.paths/adapter-ring - :test.paths/adapter-ring-jetty - :test.paths/adapter-http-kit] - "--watch" - "--delay 1000") - - test:rj9a (t/lazytest [:http-kit - :ring-rj9a] - [:test.paths/core-sdk - :test.paths/adapter-ring - :test.paths/adapter-rj9a]) - - jar:sdk {:doc "Build jar for the common sdk" - :task (clojure {:dir "sdk"} "-T:build jar")} - - jar:adapter-ring {:doc "Build jar for the adapter-ring" - :task (clojure {:dir "adapter-ring"} "-T:build jar")} - - jar:adapter-http-kit {:doc "Build jar for the adapter-http-kit" - :task (clojure {:dir "adapter-http-kit"} "-T:build jar")} - - jar:all {:doc "Build the jar for all the libs" - :depends [clean jar:sdk jar:adapter-ring jar:adapter-http-kit]} - - clean {:doc "Clean build artifacts" - :task (doseq [dir sdk-lib-dirs] - (clojure {:dir dir} "-T:build clean"))} - - set-version {:doc "Set the version in all libs" - :task (doseq [dir sdk-lib-dirs] - (shell {:dir dir} (str "neil version set " (first *command-line-args*) " --no-tag")))} - - bump-version {:doc "Bump the version component in all libs. First argument must be one of: major, minor, patch" - :task (doseq [dir sdk-lib-dirs] - (let [component (first *command-line-args*)] - (when-not (contains? #{"major" "minor" "patch"} component) - (println (str "ERROR: First argument must be one of: major, minor, patch. Got: " (or (first *command-line-args*) "nil"))) - (System/exit 1)) - (shell {:dir dir} (str "neil version " component " --no-tag"))))} - - publish:all {:doc "Publish the clojure sdk libs to clojars" - :depends [jar:all] - :task (doseq [dir sdk-lib-dirs] - (clojure {:dir dir} "-T:build deploy"))}}} diff --git a/sdk/clojure/deps.edn b/sdk/clojure/deps.edn deleted file mode 100644 index a72ee3870..000000000 --- a/sdk/clojure/deps.edn +++ /dev/null @@ -1,67 +0,0 @@ -{:paths [] - - :deps {sdk/sdk {:local/root "./sdk"} - io.github.tonsky/clojure-plus {:mvn/version "1.6.1"} - io.github.paintparty/fireworks {:mvn/version "0.10.4"} - mvxcvi/puget {:mvn/version "1.3.4"} - com.taoensso/telemere {:mvn/version "1.0.0-RC3"}} - - :aliases - {:repl {:extra-paths ["src/dev"] - :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} - nrepl/nrepl {:mvn/version "1.3.0"} - cider/cider-nrepl {:mvn/version "0.50.2"} - io.github.tonsky/clj-reload {:mvn/version "0.7.1"} - dom-top/dom-top {:mvn/version "1.0.9"}}} - - :debug {:classpath-overrides {org.clojure/clojure nil} - :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.0-4"} - com.github.flow-storm/flow-storm-dbg {:mvn/version "4.2.0"} - com.github.flow-storm/flow-storm-web-plugin {:mvn/version "1.0.0-beta"}} - :jvm-opts ["-Dclojure.storm.instrumentEnable=true" - "-Dflowstorm.plugins.namespaces=flow-storm.plugins.web.all" - "-Dclojure.storm.instrumentOnlyPrefixes=test.,reitit.,examples.,starfederation.,ring.adapter.jetty,org.httpkit.server"]} - - - - :test {:extra-paths ["test-resources/" - :test.paths/core-sdk - :test.paths/malli-schemas - :test.paths/brotli - :test.paths/adapter-common - :test.paths/adapter-http-kit - :test.paths/adapter-ring - :test.paths/adapter-ring-jetty - :test.paths/adapter-rj9a] - - - :extra-deps {io.github.noahtheduke/lazytest {:mvn/version "1.5.0"} - metosin/reitit {:mvn/version "0.7.2"} - etaoin/etaoin {:mvn/version "1.1.42"} - com.cnuernber/charred {:mvn/version "1.034"} - dev.onionpancakes/chassis {:mvn/version "1.0.365"}}} - - - :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.14.1"}}} - - :ring-rj9a {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"} - info.sunng/ring-jetty9-adapter {:mvn/version "0.36.1"}}} - - :malli-schemas {:extra-deps {sdk/malli {:local/root "./malli-schemas"}}} - - :sdk-brotli {:extra-deps {sdk/brotli {:local/root "./sdk-brotli"} - com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"}}} - - - :test.paths/core-sdk ["src/test/core-sdk"] - :test.paths/brotli ["src/test/brotli"] - :test.paths/malli-schemas ["src/test/malli-schemas"] - :test.paths/adapter-common ["src/test/adapter-common"] - :test.paths/adapter-ring ["src/test/adapter-ring"] - :test.paths/adapter-ring-jetty ["src/test/adapter-ring-jetty"] - :test.paths/adapter-rj9a ["src/test/adapter-rj9a"] - :test.paths/adapter-http-kit ["src/test/adapter-http-kit"]}} - diff --git a/sdk/clojure/doc/SSE-design-notes.md b/sdk/clojure/doc/SSE-design-notes.md deleted file mode 100644 index bffc8a802..000000000 --- a/sdk/clojure/doc/SSE-design-notes.md +++ /dev/null @@ -1,117 +0,0 @@ -# SSE, Buffering, Design considerations - -There is some design work to do when using SSE, particularly around buffering. - -When using a [ring](https://github.com/ring-clojure/ring) compliant adapter our -SSE connection is a `java.io.OutputStream`. There are several considerations -when dealing with those: - -| you may want to | solution | -| -------------------------- | ----------------------------------------------- | -| write bytes directly | just use the `OutputStream` | -| write bytes with buffering | use a `java.io.BufferedOutputStream` | -| compress the stream | use a `java.util.zip.GZIPOuputStream` | -| write text | use a `java.io.OutputStreamWriter` | -| buffer the text writes | that's where it becomes interesting for the SDK | - -## Exploring buffering - -### Why buffering - -- Concatenating arrays without a buffer is really inefficient -- 1 write operation on an `OutputStream` result in 1 IO syscall - (at least that is the mental model). -- With buffering we don't have a IO syscall until we explicitly flush or - until the buffer is full and flushes by itself. - -With limiting allocations when concatenating data, buffering is a strategy to -reduce the number of IO syscalls or at least be smart about when the call is -made. - -### SSE considerations - -In the case of SSE we want to send events as they are ready, and not have them -sit in a buffer. - -However when creating the event's text we assemble it from parts (specific SSE -lines, data lines...). We could send events line by line but this would result -in 1 IO call per line. - -So we need some kind of buffer to assemble an event before flushing it whole. - -Here are some solutions for buffering the writes: - -1. persistent buffer: use a `java.io.BufferedWriter` and keep it around -2. temporary buffer: use temporary buffer (likely a `StringBuilder`) that is - discarded after assembling and sending 1 event -3. buffer pooling: use some sort of buffer pooling - -| solution | impact on memory | -| -------- | ------------------------------------------------------- | -| 1 | long lived SSE -> long lived buffer -> consuming memory | -| 2 | short lived buffer, consuming memory when sending only | -| 3 | long lived, fix / controlable supply of buffers | - -| solution | impact on GC / allocations | -| -------- | -------------------------------------------- | -| 1 | 1 allocation and done | -| 2 | churning through buffers for each event sent | -| 3 | controlled allocations | - -| solution | notes | -| -------- | ------------------------------------------------------------------------------- | -| 1 | we can control the size of the buffer | -| 2 | the jvm gc should be able _recycle_ short lived objects | -| 3 | no direct support in the jvm, gc _recycling_ maybe be better, needs to be tuned | - -> [!note] -> An `OutputStream` compression wrapper comes with an internal buffer and a -> context window that will both allocate and retain memory. - -> [!important] -> A `ByteArrayOutputStream` is also another buffer, it doesn't shrink in size -> when reset is called (see [javadoc]()) - -## Datastar SDK - -### Considerations - -There is too much ground to cover for a truly generic API. Some ring adapters -are partially compliant with the ring spec and provide us with other mechanisms -than an `OutputStream` to materialize the SSE connection. Buffer pooling isn't -really part of the SDK. Going that route would mean adding a dependency to the -SDK and I haven't found a ready made solution anyway. - -### Current SDK implementation - -#### Common SSE machinery - -##### `starfederation.datastar.clojure.api.sse` - -This namespace provides 2 generic functions: - -- `headers`: generate HTTP headers with the SSE specific ones given a ring - request. -- `write-event!` provides a way to assemble an SSE event's string using a - `java.util.appendable`. - -These functions provide a basis for implementing SSE and are orthogonal to -Datastar's specific SSE events. - -##### `starfederation.datastar.clojure.adapter.common` - -This namespace provides helpers we use to build the SSE machinery for ring -adapters. It mainly provides a mechanism called "write profiles" to allow -a user to configure the way the SSE connections should behave with regards -to Buffering and compression. - -See the [write profiles doc](./Write-profiles.md). - -## Beyond the SDK - -If the write profile system doesn't provide enough control there is still -the possibility to implement adapters using the -`starfederation.datastar.clojure.protocols/SSEGenerator` and control everything. - -The hope is that there is enough material between the documentation and the -source code to make this relatively easy. diff --git a/sdk/clojure/doc/Write-profiles.md b/sdk/clojure/doc/Write-profiles.md deleted file mode 100644 index 51b8415b5..000000000 --- a/sdk/clojure/doc/Write-profiles.md +++ /dev/null @@ -1,113 +0,0 @@ -# Write profiles - -To manage several aspects of a SSE connection -(see the [SSE design notes](./SSE-design-notes.md)) the SDK provides a `write profile` -mechanism. It lets you control: - -- the buffering behavior of the SSE connection -- whether you want to add compression to the SSE stream - -## Example - -An example may be the quickest way to get started. - -Let's say we want to have a ring SSE handler using gzip compression with a -temporary write buffer strategy. We can create a write profile to do this. - -```clojure -(require - '[starfederation.datastar.clojure.adapter.common :as ac]) - -(def my-write-profile - ;; We specify a function that will wrap the output stream - ;; used for the SSE connection - {ac/wrap-output-stream (fn [os] (-> os ac/->gzip-os ac/->os-writer)) - - ;; We specify which writing function to use on the output stream - ;; Since we just use an OutputStreamWriter in the wrap function above - ;; we go for the temp buffer writing function helper - ac/write! (ac/->write-with-temp-buffer!) - - ;; We also provide a content encoding header for the HTTP response - ;; this way it is automatically added - ac/content-encoding ac/gzip-content-encoding}) - -``` - -When using the `->sse-response` function we can do: - -```clojure -(require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open]]) - -(defn handler [req] - (->sse-response req - {ac/write-profile my-write-profile ;; note the use of the write profile here - on-open - (fn [sse] - (d*/with-open-sse sse - (d*/patch-elements! sse "some big element")))})) -``` - -This response will have the right `Content-Encoding` header and will compress -SSE event with gzip. - -If we want to control the buffer sizes used by our output stream we can -write another profile: - -```clojure - -(def my-specific-write-profile - {ac/wrap-output-stream - (fn [os] (-> os - (ac/->gzip-os 1024) ;; setting the gzip os buffer size - ac/->os-writer)) - - ac/write! (ac/->write-with-temp-buffer! 16384);; initial size of the StringBuilder - ac/content-encoding ac/gzip-content-encoding}) - -``` - -This also allows for using other compression algorithms as long as they work -like java's `java.util.zip.GZIPOutputStream`. We could also implement a buffer -pooling of some kind by providing a custom `ac/write!` function. - -## SDK provided write profiles - -The SDK tries to provide sensible defaults. There are several write profiles -provided: - -| profile | compression | buffering strategy | write! helper | -| ---------------------------- | ----------- | ------------------------ | --------------------------- | -| basic-profile | no | temporary StringBuilder | `->write-with-temp-buffer!` | -| buffered-writer-profile | no | permanent BufferedWriter | `write-to-buffered-writer!` | -| gzip-profile | gzip | temporary StringBuilder | `->write-with-temp-buffer!` | -| gzip-buffered-writer-profile | gzip | permanent BufferedWriter | `write-to-buffered-writer!` | - -If you don't specify a profile in the `->sse-response` function call, the basic -profile is used by default. - -With `->write-with-temp-buffer!`, the underlying `StringBuilder` default size -is modeled after java's `BufferedWriter`, that is 8192 bytes. - -The rest of the buffer sizes are java's defaults. - -Note that we have specific helper used for the `ac/write!` value, depending on -the buffering strategy. - -Each adapter specific namespace aliases the write profile option key and -the profiles provided by the SDK. - -## Adapter specific behavior - -Http-kit doesn't use an `OutputStream` as its IO primitive. This means that as -soon as you want to wrap an `OutputStream` the Http-kit adapter will create and -hold onto a `ByteArrayOutputStream` for your `ac/wrap-output-stream` function -to wrap. It will take the bytes from this `OutputStream` to send. - -This has an impact on allocated memory and so this behavior is used for the -buffered-writer-profile, the gzip-profile and the gzip-buffered-writer-profile. - -The basic-profile just concatenates SEE events text in a String builder and sends -the text. It doesn't allocate any `ByteArrayOutputStream`. diff --git a/sdk/clojure/doc/implementing-adapters.md b/sdk/clojure/doc/implementing-adapters.md deleted file mode 100644 index cd336a554..000000000 --- a/sdk/clojure/doc/implementing-adapters.md +++ /dev/null @@ -1,129 +0,0 @@ -# Implementing adapters - -If you are using a ring adapter not supported by this library or if you want to -roll your own there are helpers to facilitate making one. At minimum you need to -implement 1 protocol. If you wanna be more in line with the provided adapters -there are more conventions to follow. - -Also, for the library as a whole we try to stay close to the -[SDK's design document](/sdk/README.md) used for all SDKs. - -## Implementing the `SSEGenerator` protocol - -An SSE generator is made by implementing the -`starfederation.datastar.clojure.protocols/SSEGenerator` protocol. - -There are 4 functions to implement: - -- `(send-event! [this event-type data-lines opts] )` - This function must contain the logic to actually send a SSE event. -- `(get-lock [this])` - This function mus return the lock used by the sse-gen. It enables the - `starfederation.datastar.clojure/lock-sse!` macro. -- `(close-sse! [this] "Close connection.")` - This function must close the connection use by the `SSEGenerator`. -- `(sse-gen? [this])` - This function should return true. It allows us not to use - `clojure.core/satisfies` when testing for a generator in schemas for instance. - -### Implementing `get-lock` - -As specified in the ADR for SDKs, the sending of events is protected by a lock. -You need to use a `java.util.concurrent.locks.ReentrantLock` for this. -This function should return the lock the SSE generator is using. - -### Implementing `send-event!` - -To help implement this function you should use the -`starfederation.datastar.clojure.api.sse/write-event!` function. - -It take 4 arguments: - -- `buffer`: A `java.lang.Appendable` -- `event-type`: a string representing a Datastar event type -- `data-lines`: a seq of data lines constituting the 'body' of the event -- `opts`: a map of SSE Options. - -You actually don't need to care about anything other than the `buffer` with this function, -the generic SDK will provide the value for the other arguments. - -For instance implementing a test generator that return the event's text instead -of sending it looks like: - -```clojure -(deftype ReturnMsgGen [] - p/SSEGenerator - (send-event! [_ event-type data-lines opts] - (-> (StringBuilder.) - (sse/write-event! event-type data-lines opts) ; just pass the parameters down - str)) ; we return the event string instead of sending it - - (close-sse! [_])) - - -(defn ->sse-gen [& _] - (->ReturnMsgGen)) - -``` - -As per the design doc that all Datastar SDKs follow, we use a lock in this -function to protect from several threads concurrently writing any underlying -buffer before flushing. - -See `starfederation.datastar.clojure.utils/lock!`, it is a helper macro similar -to the clojure's `locking` but for Reentrant locks. - -> [!note] -> The lock is not needed in this example, since the buffer is created for each call. -> However it is necessary when the buffer is shared. - -### Implementing `close-sse!` - -Just close whatever constitutes a connection for your particular adapter. -This function's body should be protected using the SSE generator's lock. - -### Implementing `sse-gen?` - -This function should return true. - -## Conventions followed in the provided adapters - -The provided adapters follow some conventions beyond the `SSEGenerator` protocol. -You can take a look at how they are implemented and replicate the API. - -### The `->sse-response` function - -Provided adapters have a single `->sse-response` function for an API. - -This function takes 2 arguments: - -- the ring request -- a map whose keys are: - - `:status`: the HTTP status for the response - - `:headers`: a map of `str -> str`, HTTP headers to add to the response. - - `:d*.sse/on-open`: a mandatory callback that must be called when the SSE connection is opened. - It has 1 argument, the SSE Generator. - - `:d*.sse/on-close`: A callback called when the SSE connection is closed. - Each adapter may have a different parameters list for this callback, depending on what - is relevant. Still the first parameter should be the SSE generator. - - `:d*.sse/on-exception`: A callback called when an exception is thrown sending an event. - It takes 3 arguments: the sse-gen, the exception and a context map - - `:d*.sse/write-profile`: a map that allows the configuration of the SSE connection. - - other options you want to add to your generator - -It has 2 responsibilities: - -- This function creates the SSE generator, gives the callbacks to it. -- It must create a valid ring response with the correct HTTP SSE headers and - merge the headers provided with the `:headers` option. - See `starfederation.datastar.clojure.api.sse/headers`. - -### `SSEGenerator` additional logic - -The implementation must call the `on-open` callback when the underlying connection is opened. - -### The `close-sse!` function - -Beyond closing the connection object used by your adapter, This function must call -the `on-close` callback provided when using the `->sse-response` -function. Note that the callback should be protected by using the SSE generator's lock. diff --git a/sdk/clojure/doc/maintainers-guide.md b/sdk/clojure/doc/maintainers-guide.md deleted file mode 100644 index 4d79598e7..000000000 --- a/sdk/clojure/doc/maintainers-guide.md +++ /dev/null @@ -1,88 +0,0 @@ -# Maintainers Guide - -## Directory structure - -In the whole Datastar project: - -- `examples/clojure` -- `site/static/code_snippets/*.clojuresnippet` -- `site/static/going_deeper/*.clojuresnippet` -- `site/static/how_tos/*.clojuresnippet` - -In the SDK code proper `sdk/clojure`: - -- `sdk`: the source folder for the main SDK -- `adapter-*`: source folders for adapter specific code -- `malli-schemas`: self explanatory... -- `src/bb`: tasks used run a repl, tests... -- `src/dev`: dev utils, examples -- `src/test`: centralized tests for all the libraries -- `test-resources`: self explanatory - -## bb tasks - -### Release tasks - -- `bb bump-version patch/minor/major`: to bump a version component across all libs -- `bb set-version x.x.x`: to set the version component across all libs -- `bb jar:all`: Build jars artifacts for all of the libs -- `bb jar:`: Build jars artifacts for one of the libs -- `bb clean`: Clean all build artifacts -- `bb publish:all`: Publish the artifacts to clojars.org - -### Development tasks `bb run dev` - -- `bb run dev`: start a repl with the dev nss, the test nss the malli schemas, - ring-jetty and Http-kit on the classpath -- `bb run dev:rj9a`: same as basic dev task expect for ring-jetty being replaced - with rj9a. - -> [!note] -> You can add additional deps aliases when calling these tasks: -> `bb run dev :debug` will add a Flowstorm setup - -### Test tasks `bb run test` - -- `bb run test:all`: run all test for the SDK, the Http-kit adapter and the - ring adapter using ring-jetty. -- `bb run test:rj9a`: run all test for the SDK and the ring adapter using rj9a. - -## Release - -- The library artifacts are published to Clojars (http://clojars.org) under the `dev.data-star` namespace. -- The Clojars account is managed by Ben Croker, the DNS verification is managed by Delaney. -- The Clojars deploy token is also managed by Ben and added to this repo as a GH Actions Secret - - Secret name: `CLOJARS_USERNAME` - Value: _the clojars account username_ - - Secret name: `CLOJARS_PASSWORD` - Value: _the clojars deploy token_ -- The libraries' versions are bumped in lockstep so that there is no confusion over which version of the common lib should be used with an adapter lib. - -The Github Actions [CI workflow for clojure](../../.github/workflows/clojure-sdk.yml) will always run the tests and produce jar artifacts. - -Triggering a deployment to clojars is a manual process. A Datastar core contributor must trigger the Clojure SDK workflow with the `publish` input boolean set to `true. - -**Release process:** - -1. Use `bb set-version` or `bb bump-version` to update the library versions in lockstep -2. Commit those changes and push to GitHub -3. A core contributor must trigger the workflow manually setting `publish` to `true` - -## Test - -### Running tests - -- for the unit and smoke tests see the bb tasks. -- for the generic bash SDK tests - 1. go to `sdk/clojure/sdk-tests/` - 2. run `clojure -M -m starfederation.datastar.clojure.sdk-test.main` - 3. go to `sdk/test/` - 4. run `./test-all.sh localhost:8080` - -### webdriver config - -Tests resources contains a test.config.edn file. It contains a map whose keys -are: - -- `:drivers`: [etaoin](https://github.com/clj-commons/etaoin) webdriver types to run -- `:webdriver-opts`: a map of webdriver type to webriver specific options diff --git a/sdk/clojure/forms.md b/sdk/clojure/forms.md deleted file mode 100644 index 769392f63..000000000 --- a/sdk/clojure/forms.md +++ /dev/null @@ -1,72 +0,0 @@ -# Divergence of behavior from beta11 to RC12 when it comes to forms - -## The setup -To test the forms behavior I made little page that lets me send the content of a -text input to the server via a click on a button. - -They are 3 parameter used in this test: -- the form method (GET or POST) -- whether the button has a type attribute of button or no such attribute -- whether the `contentType` option is used in the Datastar action - -This give me a HTML page like this: - -```html - - - - - - - -
-
- -
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- - -
- -
- - -``` -The idea is that when I click one button I observe if I correctly get the value -from the input and where I get the value from. - -> [!note] -> Note that the case where I don't set the type attribute on the button while -> not using the `contentType` option (regardless of the HTTP Method) is not a -> valid use of D* with forms. - - -| method | btn type="button" | contentType set | beta11 | RC12 | -| ------ | ----------------- | --------------- | ------------------------------------------- | ------------------------ | -| GET | no | yes | btn works as submit, expected HTML behavior | idem | -| GET | no | no | values via signals in the query str | idem | -| GET | yes | yes | values the query str (HTML behavior) | no value in query string | -| GET | yes | no | values the query str (HTML behavior) | no value in query string | -| POST | no | yes | btn works as submit, expected HTML behavior | idem | -| POST | no | no | values via signals in the request's body | idem | -| POST | yes | yes | values via the request body (HTML behavior) | idem | -| POST | yes | no | values via the request body (HTML behavior) | idem | - - - diff --git a/sdk/clojure/malli-schemas/README.md b/sdk/clojure/malli-schemas/README.md deleted file mode 100644 index e5ad84fa8..000000000 --- a/sdk/clojure/malli-schemas/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Malli schemas for the SDK - -## Installation - -For now the SDK and adapters are distributed as git dependencies using a `deps.edn` file. - -```clojure -{datastar/malli-schemas {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/malli-schemas"}} -``` - -> [!important] -> Replace `LATEST_SHA` in the git coordinates below by the actual latest commit sha of the repository. - -## Usage - -Require the namespaces for which you want schema and/or instrumentation. Then -use malli's instrumentation facilities. - -Notable schema namespaces: - -- `starfederation.datastar.clojure.api-schemas` for the general d\* API -- `starfederation.datastar.clojure.api.*-schemas` for more specific code underlying the main API -- `starfederation.datastar.clojure.adapter.common-schemas` for the common adapter machinery (write profiles) -- `starfederation.datastar.clojure.adapter.http-kit-schemas` for the http-kit adapter -- `starfederation.datastar.clojure.adapter.ring-schemas` for the ring adapter diff --git a/sdk/clojure/malli-schemas/deps.edn b/sdk/clojure/malli-schemas/deps.edn deleted file mode 100644 index 029431a8e..000000000 --- a/sdk/clojure/malli-schemas/deps.edn +++ /dev/null @@ -1,2 +0,0 @@ -{:paths ["src/main"] - :deps {metosin/malli {:mvn/version "0.17.0"}}} diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/common_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/common_schemas.clj deleted file mode 100644 index ddc0ba2c8..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/common_schemas.clj +++ /dev/null @@ -1,116 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.common-schemas - (:require - [malli.core :as m] - [malli.util :as mu] - [starfederation.datastar.clojure.adapter.common :as ac]) - (:import - [java.io BufferedWriter OutputStream OutputStreamWriter Writer] - java.nio.charset.Charset - java.util.zip.GZIPOutputStream)) - - -(defn output-stream? [o] - (instance? OutputStream o)) - -(def output-stream-schema - [:fn {:error/message "should be a java.io.OutputStream"} - output-stream?]) - - -(defn gzip-output-stream? [o] - (instance? GZIPOutputStream o)) - -(def gzip-output-stream-schema - [:fn {:error/message "should be a java.util.zip.GZIPOutputStream"} - gzip-output-stream?]) - - -(m/=> starfederation.datastar.clojure.adapter.common/->gzip-os - [:function - [:-> output-stream-schema gzip-output-stream-schema] - [:-> output-stream-schema :int gzip-output-stream-schema]]) - - -(defn output-stream-writer? [o] - (instance? OutputStreamWriter o)) - -(def output-stream-writer-schema - [:fn {:error/message "should be a java.io.OutputStreamWriter"} - output-stream-writer?]) - - -(defn charset? [c] - (instance? Charset c)) - -(def charset-schema - [:fn {:error/message "should be a java.nio.charset.Charset"} - charset?]) - - -(m/=> starfederation.datastar.clojure.adapter.common/->os-writer - [:function - [:-> output-stream-schema output-stream-writer-schema] - [:-> output-stream-schema charset-schema output-stream-writer-schema]]) - - -(defn buffered-writer? [o] - (instance? BufferedWriter o)) - -(def buffered-writer-schema - [:fn {:error/message "should be a java.io.BufferedWriter"} - buffered-writer?]) - - -(m/=> starfederation.datastar.clojure.adapter.common/->buffered-writer - [:function - [:-> output-stream-writer-schema buffered-writer-schema] - [:-> output-stream-writer-schema :int buffered-writer-schema]]) - - -(defn writer? [x] - (instance? Writer x)) - -(def writer-schema - [:fn {:error/message "should be a java.io.Writer"} - writer?]) - -(def wrap-output-stream-schema - [:-> output-stream-schema writer-schema]) - -(def write-profile-schema - (mu/optional-keys - [:map - [ac/wrap-output-stream wrap-output-stream-schema] - [ac/write! fn?] - [ac/content-encoding :string]] - - [ac/content-encoding])) - -(def SSE-write-profile-opts - (mu/optional-keys - [:map - [ac/write-profile write-profile-schema]])) - - -(def ->sse-response-http-options-schema - (mu/optional-keys - [:map - [:status number?] - [:headers [:map-of :string [:or :string [:seqable :string]]]]] - [:status :headers])) - - -(def ->sse-response-callbacks-options-schema - (mu/optional-keys - [:map - [ac/on-open fn?] - [ac/on-close fn?] - [ac/on-exception fn?]] - [ac/on-close ac/on-exception])) - - -(def ->sse-response-options-schema - (-> ->sse-response-http-options-schema - (mu/merge ->sse-response-callbacks-options-schema) - (mu/merge SSE-write-profile-opts))) - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj deleted file mode 100644 index 3ff6ad993..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj +++ /dev/null @@ -1,19 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.http-kit2-schemas - (:require - [malli.core :as m] - [malli.util :as mu] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.http-kit2] - [starfederation.datastar.clojure.adapter.common-schemas :as cs])) - - -(def options-schema - (mu/update-in cs/->sse-response-options-schema - [ac/write-profile] - (fn [x] - (mu/optional-keys x [ac/wrap-output-stream])))) - - -(m/=> starfederation.datastar.clojure.adapter.http-kit2/->sse-response - [:-> :map options-schema :any]) - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit_schemas.clj deleted file mode 100644 index 46b8ee153..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit_schemas.clj +++ /dev/null @@ -1,22 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.http-kit-schemas - (:require - [malli.core :as m] - [malli.util :as mu] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.http-kit] - [starfederation.datastar.clojure.adapter.common-schemas :as cs])) - - -(def options-schema - (mu/update-in cs/->sse-response-options-schema - [ac/write-profile] - (fn [x] - (mu/optional-keys x [ac/wrap-output-stream])))) - - -(m/=> starfederation.datastar.clojure.adapter.http-kit/->sse-response - [:-> :map options-schema :any]) - - - - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/ring_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/ring_schemas.clj deleted file mode 100644 index e326b7df8..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/ring_schemas.clj +++ /dev/null @@ -1,11 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.ring-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.adapter.ring] - [starfederation.datastar.clojure.adapter.common-schemas :as cs])) - - -(m/=> starfederation.datastar.clojure.adapter.ring/->sse-response - [:-> :map cs/->sse-response-options-schema :any]) - - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj deleted file mode 100644 index fa4287808..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj +++ /dev/null @@ -1,101 +0,0 @@ -(ns starfederation.datastar.clojure.api.common-schemas - (:require - [malli.core :as m] - [malli.util :as mu] - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.protocols :as p])) - -(def sse-gen-schema [:fn {:error/message "argument should be a SSEGenerator"} - p/sse-gen?]) - - -(def event-type-schema - [:enum - consts/event-type-patch-elements - consts/event-type-patch-signals]) - -(def data-lines-schema [:seqable :string]) - -(def sse-options-schema - (mu/optional-keys - [:map - [common/id :string] - [common/retry-duration number?]])) - - -(comment - (m/validate sse-options-schema {common/id "1"}) - (m/validate sse-options-schema {common/id 1})) - -;; ----------------------------------------------------------------------------- -(def elements-schema :string) -(def elements-seq-schema [:seqable :string]) - - -(def patch-modes-schema - [:enum - consts/element-patch-mode-outer - consts/element-patch-mode-inner - consts/element-patch-mode-replace - consts/element-patch-mode-prepend - consts/element-patch-mode-append - consts/element-patch-mode-before - consts/element-patch-mode-after - consts/element-patch-mode-remove]) - -(comment - (m/validate patch-modes-schema consts/element-patch-mode-after) - (m/validate patch-modes-schema "toto")) - - -(def patch-element-options-schemas - (mu/merge - sse-options-schema - (mu/optional-keys - [:map - [common/selector :string] - [common/patch-mode patch-modes-schema] - [common/use-view-transition :boolean]]))) - -;; ----------------------------------------------------------------------------- -(def selector-schema :string) - -(def remove-element-options-schemas patch-element-options-schemas) - - -;; ----------------------------------------------------------------------------- -(def signals-schema :string) - -(def patch-signals-options-schemas - (mu/merge - sse-options-schema - (mu/optional-keys - [:map - [common/only-if-missing :boolean]]))) - - -;; ----------------------------------------------------------------------------- -(def signal-paths-schema [:seqable :string]) - -;; ----------------------------------------------------------------------------- -(def script-content-schema :string) - -(def execute-script-options-schemas - (mu/merge - sse-options-schema - (mu/optional-keys - [:map - [common/auto-remove :boolean] - [common/attributes [:map-of [:or :string :keyword] :any]]]))) - - -(comment - (m/validate execute-script-options-schemas {common/auto-remove true}) - (m/validate execute-script-options-schemas {common/auto-remove "1"}) - (m/validate execute-script-options-schemas {common/attributes {:t1 1}}) - (m/validate execute-script-options-schemas {common/attributes {"t1" 1}}) - (m/validate execute-script-options-schemas {common/attributes {1 1}}) - (m/validate execute-script-options-schemas {common/attributes :t1})) - - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj deleted file mode 100644 index f24253fde..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj +++ /dev/null @@ -1,14 +0,0 @@ -(ns starfederation.datastar.clojure.api.elements-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.api.common-schemas :as cs] - [starfederation.datastar.clojure.api.elements])) - - -(m/=> starfederation.datastar.clojure.api.elements/->patch-elements - [:-> cs/elements-schema cs/patch-element-options-schemas cs/data-lines-schema]) - - -(m/=> starfederation.datastar.clojure.api.elements/->patch-elements-seq - [:-> cs/elements-seq-schema cs/patch-element-options-schemas cs/data-lines-schema]) - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj deleted file mode 100644 index baabbec6a..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj +++ /dev/null @@ -1,9 +0,0 @@ -(ns starfederation.datastar.clojure.api.scripts-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.api.common-schemas :as cs] - [starfederation.datastar.clojure.api.scripts])) - -(m/=> starfederation.datastar.clojure.api.scripts/->script-tag - [:-> cs/script-content-schema cs/execute-script-options-schemas :string]) - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj deleted file mode 100644 index 44635b4dc..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj +++ /dev/null @@ -1,10 +0,0 @@ -(ns starfederation.datastar.clojure.api.signals-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.api.common-schemas :as cs] - [starfederation.datastar.clojure.api.signals])) - - -(m/=> starfederation.datastar.clojure.api.signals/->patch-signals - [:-> cs/signals-schema cs/patch-signals-options-schemas cs/data-lines-schema]) - diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/sse_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/sse_schemas.clj deleted file mode 100644 index 290137129..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/sse_schemas.clj +++ /dev/null @@ -1,11 +0,0 @@ -(ns starfederation.datastar.clojure.api.sse-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.api.common-schemas :as cs] - [starfederation.datastar.clojure.api.sse])) - - -(m/=> starfederation.datastar.clojure.api.sse/send-event! - [:function - [:-> cs/sse-gen-schema cs/event-type-schema cs/data-lines-schema :any] - [:-> cs/sse-gen-schema cs/event-type-schema cs/data-lines-schema cs/sse-options-schema :any]]) diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj deleted file mode 100644 index 0c4bb7d92..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj +++ /dev/null @@ -1,86 +0,0 @@ -(ns starfederation.datastar.clojure.api-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.api] - [starfederation.datastar.clojure.api.common-schemas :as cs])) - - -(m/=> starfederation.datastar.clojure.api/close-sse! - [:-> cs/sse-gen-schema :any]) - - -(m/=> starfederation.datastar.clojure.api/patch-elements! - [:function - [:-> cs/sse-gen-schema cs/elements-schema :any] - [:-> cs/sse-gen-schema cs/elements-schema cs/patch-element-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/patch-elements-seq! - [:function - [:-> cs/sse-gen-schema cs/elements-seq-schema :any] - [:-> cs/sse-gen-schema cs/elements-seq-schema cs/patch-element-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/remove-element! - [:function - [:-> cs/sse-gen-schema cs/selector-schema :any] - [:-> cs/sse-gen-schema cs/selector-schema cs/remove-element-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/patch-signals! - [:function - [:-> cs/sse-gen-schema cs/signals-schema :any] - [:-> cs/sse-gen-schema cs/signals-schema cs/patch-signals-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/execute-script! - [:function - [:-> cs/sse-gen-schema cs/script-content-schema :any] - [:-> cs/sse-gen-schema cs/script-content-schema cs/execute-script-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/sse-get - [:function - [:-> :string :string] - [:-> :string :string :string]]) - -(m/=> starfederation.datastar.clojure.api/sse-post - [:function - [:-> :string :string] - [:-> :string :string :string]]) - - -(m/=> starfederation.datastar.clojure.api/sse-put - [:function - [:-> :string :string] - [:-> :string :string :string]]) - -(m/=> starfederation.datastar.clojure.api/sse-patch - [:function - [:-> :string :string] - [:-> :string :string :string]]) - -(m/=> starfederation.datastar.clojure.api/sse-delete - [:function - [:-> :string :string] - [:-> :string :string :string]]) - - -(m/=> starfederation.datastar.clojure.api/console-log! - [:function - [:-> cs/sse-gen-schema :string :any] - [:-> cs/sse-gen-schema :string cs/execute-script-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/console-error! - [:function - [:-> cs/sse-gen-schema :string :any] - [:-> cs/sse-gen-schema :string cs/execute-script-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/redirect! - [:function - [:-> cs/sse-gen-schema :string :any] - [:-> cs/sse-gen-schema :string cs/execute-script-options-schemas :any]]) - - diff --git a/sdk/clojure/sdk-brotli/README.md b/sdk/clojure/sdk-brotli/README.md deleted file mode 100644 index c7e2f72fd..000000000 --- a/sdk/clojure/sdk-brotli/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Datastar Brotli write profile - -This library contains some utilities to work with Brotli. - -Credits to [Anders]'(https://andersmurphy.com/) and his work on [Hyperlith](https://github.com/andersmurphy/hyperlith) -from which this library takes it's code. - -## Installation - -```clojure -{datastar/brotli {:git/url "https://github.com/starfederation/datastar/" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/sdk-brotli"} - -``` - -> [!important] -> -> - Replace `LATEST_SHA` in the git coordinates below by the actual latest -> commit sha of the repository. -> - You need to add a dependency for your specific plateform see the [Brotli4j page](https://github.com/hyperxpro/Brotli4j) -> For instance on linux `com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"}}}` - -## Supported ring adapters - -At this moment only Http-kit is supported. - -## Usage - -This library provides brotil write profiles you can use like this: - -```clojure -(require - '[starfederation.datastar.clojure.api :as d*]) - '[starfederation.datastar.clojure.adapter.Http-kit :as hk-gen]) - '[starfederation.datastar.clojure.brotli :as brotli])) - -(defn handler [req] - (hk-gen/->sse-response - {hk-gen/write-profile (brotli/->brotli-profile) - hk-gen/on-open - (fn [sse] - (d*/with-open-sse sse - (do ...)}) -``` - -See docstrings in the `starfederation.datastar.clojure.brotli` namespace for -more information. diff --git a/sdk/clojure/sdk-brotli/deps.edn b/sdk/clojure/sdk-brotli/deps.edn deleted file mode 100644 index dc1514b2b..000000000 --- a/sdk/clojure/sdk-brotli/deps.edn +++ /dev/null @@ -1,3 +0,0 @@ -{:paths ["src/main"] - :deps {com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.18.0"} - io.netty/netty-buffer {:mvn/version "4.1.119.Final"}}} diff --git a/sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj b/sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj deleted file mode 100644 index 91249a8e1..000000000 --- a/sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj +++ /dev/null @@ -1,136 +0,0 @@ -(ns starfederation.datastar.clojure.brotli - "Tools to work with Brotli. - - The main api is - - [[compress]] - - [[decompress]] - - [[->brotli-profile]] - - [[->brotli-buffered-writer-profile]] - " - (:require - [clojure.math :as m] - [starfederation.datastar.clojure.adapter.common :as ac]) - (:import - com.aayushatharva.brotli4j.Brotli4jLoader - [com.aayushatharva.brotli4j.encoder - Encoder - Encoder$Parameters - Encoder$Mode - BrotliOutputStream] - [com.aayushatharva.brotli4j.decoder Decoder] - [java.io OutputStream])) - - -;; Code taken from https://github.com/andersmurphy/hyperlith -;; Thanks Anders! -;; ----------------------------------------------------------------------------- -;; Setup & helpers -;; ----------------------------------------------------------------------------- -(defonce ensure-br - (Brotli4jLoader/ensureAvailability)) - - -(defn window-size->kb [window-size] - (/ (- (m/pow 2 window-size) 16) 1000)) - - -(defn encoder-params - "Options used when creating a brotli encoder. - - Arg keys: - - `:quality`: Brotli quality defaults to 5 - - `:window-size`: Brotli window size defaults to 24 - " - [{:keys [quality window-size]}] - (doto (Encoder$Parameters/new) - (.setMode Encoder$Mode/TEXT) - ;; LZ77 window size (0, 10-24) (default: 24) - ;; window size is (pow(2, NUM) - 16) - (.setWindow (or window-size 24)) - (.setQuality (or quality 5)))) - - -;; ----------------------------------------------------------------------------- -;; 1 shot compression -;; ----------------------------------------------------------------------------- -(defn compress - " - Compress `data` (either a byte array or a string) using Brotli. - - Opts keys from [[encoder-params]]: - - `:quality`: Brotli quality - - `:window-size`: Brotli window size - " - - [data & {:as opts}] - (-> (if (string? data) - (String/.getBytes data) - ^byte/1 data) - (Encoder/compress (encoder-params opts)))) - - -(defn decompress - "Decompress Brotli compressed data, returns a string." - [data] - (let [decompressed (Decoder/decompress data)] - (String/new (.getDecompressedData decompressed)))) - - -(comment - (decompress (compress "hello"))) - - -;; ----------------------------------------------------------------------------- -;; Write profiles -;; ----------------------------------------------------------------------------- -(defn ->brotli-os - "Wrap `out-stream` with Brotli compression. - - Opts from [[encoder-params]]: - - `:quality`: Brotli quality - - `:window-size`: Brotli window size - " - [^OutputStream out-stream & {:as opts}] - (BrotliOutputStream/new out-stream (encoder-params opts))) - - -(def brotli-content-encoding "br") - - -(defn ->brotli-profile - "Make a write profile using Brotli compression and a temporary buffer - strategy. - - Opts from [[encoder-params]]: - - `:quality`: Brotli quality - - `:window-size`: Brotli window size - " - [& {:as opts}] - {ac/wrap-output-stream - (fn [^OutputStream os] - (-> os - (->brotli-os opts) - ac/->os-writer)) - ac/write! (ac/->write-with-temp-buffer!) - ac/content-encoding brotli-content-encoding}) - - -(defn ->brotli-buffered-writer-profile - "Make a write profile using Brotli compression and a permanent buffer - strategy. - - Opts from [[encoder-params]]: - - `:quality`: Brotli quality - - `:window-size`: Brotli window size - " - [& {:as opts}] - {ac/wrap-output-stream - (fn [^OutputStream os] - (-> os - (->brotli-os opts) - ac/->os-writer - ac/->buffered-writer)) - ac/write! ac/write-to-buffered-writer! - ac/content-encoding brotli-content-encoding}) - - diff --git a/sdk/clojure/sdk-tests/README.md b/sdk/clojure/sdk-tests/README.md deleted file mode 100644 index a77e473cb..000000000 --- a/sdk/clojure/sdk-tests/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# SDK tests - -This is where the code for the [generic tests](/sdk/test) lives. - -## Running the test app - -- repl: - -``` -clojure -M:repl -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]" -``` - -- main: - -``` -clojure -M -m starfederation.datastar.clojure.sdk-test.main -``` diff --git a/sdk/clojure/sdk-tests/deps.edn b/sdk/clojure/sdk-tests/deps.edn deleted file mode 100644 index 1b5a44a43..000000000 --- a/sdk/clojure/sdk-tests/deps.edn +++ /dev/null @@ -1,17 +0,0 @@ -{:paths ["src/main"] - - :deps {datastar/sdk {:local/root "../sdk"} - datastar/ring {:local/root "../adapter-ring"} - ring/ring-jetty-adapter {:mvn/version "1.13.0"} - metosin/reitit {:mvn/version "0.7.2"} - com.cnuernber/charred {:mvn/version "1.034"} - dev.onionpancakes/chassis {:mvn/version "1.0.365"}} - - - :aliases - {:repl {:extra-paths ["src/dev"] - :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} - nrepl/nrepl {:mvn/version "1.3.0"} - cider/cider-nrepl {:mvn/version "0.50.2"} - io.github.tonsky/clj-reload {:mvn/version "0.7.1"}}}}} - diff --git a/sdk/clojure/sdk-tests/src/dev/user.clj b/sdk/clojure/sdk-tests/src/dev/user.clj deleted file mode 100644 index 6ddb9a8ae..000000000 --- a/sdk/clojure/sdk-tests/src/dev/user.clj +++ /dev/null @@ -1,26 +0,0 @@ -(ns user - (:require - [clj-reload.core :as reload])) - - -(alter-var-root #'*warn-on-reflection* (constantly true)) - -;(rcf/enable!) - - -(reload/init - {:no-reload ['user]}) - - -(defn reload! [] - (reload/reload)) - - - - -(defn clear-terminal! [] - (binding [*out* (java.io.PrintWriter. System/out)] - (print "\033c") - (flush))) - - diff --git a/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj b/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj deleted file mode 100644 index f3aedc269..000000000 --- a/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj +++ /dev/null @@ -1,148 +0,0 @@ -(ns starfederation.datastar.clojure.sdk-test.core - (:require - [charred.api :as charred] - [clojure.set :as set] - [reitit.ring.middleware.parameters :as rrm-params] - [reitit.ring.middleware.multipart :as rrm-multi-params] - [reitit.ring :as rr] - [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open]] - [starfederation.datastar.clojure.api :as d*])) - -;; ----------------------------------------------------------------------------- -;; JSON / Datastar signals utils -;; ----------------------------------------------------------------------------- -(def ^:private bufSize 1024) -(def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) - -(defn get-signals [req] - (-> req d*/get-signals read-json)) - - -;; ----------------------------------------------------------------------------- -;; Parsing / reformatting received events -;; ----------------------------------------------------------------------------- -(def events-json-key "events") - -(defn get-events [signals] - (get signals events-json-key)) - - -(def event-type-json-key "type") - - -(defn get-event-type [event] - (get event event-type-json-key)) - - - -(def str->datastar-opt - ;; SSE - {"eventId" d*/id - "retryDuration" d*/retry-duration - - ;; patch elements - "selector" d*/selector - "patchMode" d*/patch-mode - "useViewTransition" d*/use-view-transition - - ;; patch signals - "onlyIfMissing" d*/only-if-missing - - ;; execute script - "autoRemove" d*/auto-remove - "attributes" d*/attributes}) - -(def options (set (keys str->datastar-opt))) - -;; ----------------------------------------------------------------------------- -;; Testing code: We want to send back the event that we received as datastar -;; signal values in the HTTP request -;; ----------------------------------------------------------------------------- -(defn patch-elements! [sse event] - (let [elements (get event "elements") - opts (-> event - (select-keys options) - (set/rename-keys str->datastar-opt))] - (d*/patch-elements! sse elements opts))) - - -(defn remove-element! [sse event] - (let [selector (get event "selector") - opts (-> event - (select-keys options) - (set/rename-keys str->datastar-opt))] - (d*/remove-element! sse selector opts))) - - -(defn patch-signals! [sse event] - (let [signals (-> (get event "signals") - (->> (into (sorted-map))) ;; for the purpose of the test, keys need to be ordered - (charred/write-json-str)) - opts (-> event - (select-keys options) - (set/rename-keys str->datastar-opt))] - (d*/patch-signals! sse signals opts))) - - -(defn remove-signals! [sse event] - (let [paths (get event "paths") - opts (-> event - (select-keys options) - (set/rename-keys str->datastar-opt))] - (d*/patch-signals! sse paths opts))) - - -(defn execute-script! [sse event] - (let [script (get event "script") - opts (-> event - (select-keys options) - (set/rename-keys str->datastar-opt) - (update d*/attributes update-keys keyword))] - (d*/execute-script! sse script opts))) - - - -(def dispatch - {"patchElements" patch-elements! - "removeElements" remove-element! - "patchSignals" patch-signals! - "removeSignals" remove-signals! - "executeScript" execute-script!}) - - -(defn send-event-back! [sse event] - (let [type (get-event-type event)] - ((dispatch type) sse event))) - - -(defn send-events-back! [sse req] - (let [signals (get-signals req)] - (doseq [event (get-events signals)] - (send-event-back! sse event)))) - - -;; ----------------------------------------------------------------------------- -;; Setting up the endpoint for the shell tests -;; ----------------------------------------------------------------------------- -(defn test-handler [req] - (->sse-response req - {on-open - (fn [sse] - (d*/with-open-sse sse - (send-events-back! sse req)))})) - - -(def routes - [["/test" {:handler test-handler - :parameters {:multipart true} - :middleware [rrm-multi-params/multipart-middleware]}]]) - - -(def router (rr/router routes)) - - -(def handler - (rr/ring-handler router - (rr/create-default-handler) - {:middleware [rrm-params/parameters-middleware]})) - diff --git a/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/main.clj b/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/main.clj deleted file mode 100644 index fdadbb430..000000000 --- a/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/main.clj +++ /dev/null @@ -1,44 +0,0 @@ -(ns starfederation.datastar.clojure.sdk-test.main - (:require - [ring.adapter.jetty :as jetty] - [starfederation.datastar.clojure.sdk-test.core :as c]) - (:import - org.eclipse.jetty.server.Server)) - - - -(defonce !jetty-server (atom nil)) - - -(def default-server-opts {:port 8080 - :join? false}) - -(defn start! [handler & {:as opts}] - (let [opts (merge default-server-opts opts)] - (println "Starting server port" (:port opts)) - (jetty/run-jetty handler opts))) - - -(defn stop! [server] - (println "Stopping server") - (println server) - (.stop ^Server server)) - - -(defn reboot-jetty-server! [handler & {:as opts}] - (swap! !jetty-server - (fn [server] - (when server - (stop! server)) - (start! handler opts)))) - - -(comment - (reboot-jetty-server! #'c/handler)) - -(defn -main [& _] - (let [server (start! c/handler)] - (.addShutdownHook (Runtime/getRuntime) - (Thread. (fn [] - (stop! server) - (shutdown-agents)))))) diff --git a/sdk/clojure/sdk/README.md b/sdk/clojure/sdk/README.md deleted file mode 100644 index 387576574..000000000 --- a/sdk/clojure/sdk/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Generic Clojure SDK for Datastar - -This is where the code for the Generic SDK lives. - -## Installation - -For now the SDK and adapters are distributed as git dependencies using a `deps.edn` file. -If you roll your own adapter you only need: - -```clojure -{datastar/sdk {:git/url "https://github.com/starfederation/datastar/tree/develop" - :git/sha "LATEST SHA" - :deps/root "sdk/clojure/sdk"}} -``` - -> [!important] -> This project is new and there isn't a release process yet other than using git shas. -> Replace `LATEST_SHA` in the git coordinates below by the actual latest commit sha of the repository. diff --git a/sdk/clojure/sdk/build.clj b/sdk/clojure/sdk/build.clj deleted file mode 100644 index 70c10ea44..000000000 --- a/sdk/clojure/sdk/build.clj +++ /dev/null @@ -1,45 +0,0 @@ -(ns build - (:require [clojure.tools.build.api :as b] - [clojure.edn :as edn])) - -(def project (-> (edn/read-string (slurp "deps.edn")) - :aliases :neil :project)) -(def lib (:name project)) -(def version (:version project)) -(assert lib ":name must be set in deps.edn under the :neil alias") -(assert version ":version must be set in deps.edn under the :neil alias") - -(def class-dir "target/classes") -(def basis (b/create-basis {:project "deps.edn"})) -(def jar-file (format "target/%s-%s.jar" (name lib) version)) - -(defn clean [_] - (b/delete {:path "target"})) - -(defn jar [_] - (b/write-pom {:class-dir class-dir - :lib lib - :version version - :basis basis - :src-dirs ["src"]}) - (b/copy-dir {:src-dirs ["src" "resources"] - :target-dir class-dir}) - (b/jar {:class-dir class-dir - :jar-file jar-file})) - -(defn install [_] - (jar {}) - (b/install {:basis basis - :lib lib - :version version - :jar-file jar-file - :class-dir class-dir})) - -(defn deploy [opts] - (jar opts) - ((requiring-resolve 'deps-deploy.deps-deploy/deploy) - (merge {:installer :remote - :artifact jar-file - :pom-file (b/pom-path {:lib lib :class-dir class-dir})} - opts)) - opts) diff --git a/sdk/clojure/sdk/deps.edn b/sdk/clojure/sdk/deps.edn deleted file mode 100644 index e9c416f26..000000000 --- a/sdk/clojure/sdk/deps.edn +++ /dev/null @@ -1,7 +0,0 @@ -{:paths ["src/main" "resources"] - :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" - :git/sha "e405aac"} - slipset/deps-deploy {:mvn/version "0.2.2"}} - :ns-default build} - :neil {:project {:name dev.data-star/sdk - :version "1.0.0-beta.11"}}}} diff --git a/sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar.clojure/sdk/config.edn b/sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar.clojure/sdk/config.edn deleted file mode 100644 index 022e964ca..000000000 --- a/sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar.clojure/sdk/config.edn +++ /dev/null @@ -1,3 +0,0 @@ -{:lint-as - {starfederation.datastar.clojure.utils/def-clone clojure.core/def}} - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/common.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/common.clj deleted file mode 100644 index 5dfb01ba0..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/common.clj +++ /dev/null @@ -1,415 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.common - " - Namespace containing the shared code for each adapter. - - It contains helpers for working with [[OutputStreams]] and assembling SSE - sending function using several strategies and managing exceptions in the - adapters. - - The main concept here is what we call \"write profiles\". A write profile is - a map of 3 keys: - - [[wrap-output-stream]] - - [[write!]] - - [[content-encoding]] - - With this 3 keys we can control buffering aspects of our SSE connction and - and compression functionality. - - Here is an example profile which uses gzip and temporary buffers to - concatenate SSE event text: - - ```clojure - (def gzip-profile - {wrap-output-stream (fn[^OutputStream os] (-> os ->gzip-os ->os-writer)) - content-encoding gzip-content-encoding - write! (->write-with-temp-buffer!)}) - ``` - - The `wrap-output-stream` function will use a [[GZIPOutputStream]] and an - [[OutputStreamWriter]] constructed with the [[->gzip-os]] and [[->os-writer]] - helpers. - - To go with this we use a `write!` function constructed with - [[->write-with-temp-buffer!]]. - - If we wanted specific buffer sizes we could do: - - ```clojure - (def gzip-profile - {wrap-output-stream (fn[^OutputStream os] - (-> os - (->gzip-os 1024) - ->os-writer)) - content-encoding gzip-content-encoding - write! (->write-with-temp-buffer! 4096)}) - ``` - - The output stream wrapping helpers are: - - [[->gzip-os]] - - [[->os-writer]] - - [[->buffered-writer]] - - The write function helper to go with them are: - - [[->write-with-temp-buffer!]] - - [[write-to-buffered-writer!]] - - See the rest of the docstrings for more details. -" - (:refer-clojure :exclude [flush]) - (:require - [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.utils :as u]) - (:import - [java.io - BufferedWriter Flushable IOException - OutputStream OutputStreamWriter Writer] - [java.nio.charset Charset StandardCharsets] - java.util.zip.GZIPOutputStream)) - - -;; ----------------------------------------------------------------------------- -;; Advanced SSE options -;; ----------------------------------------------------------------------------- -(def write-profile - "SSE option key: - - This option allows a user to control the way SSE events are assembled and - sent. It take a write profile which is a map whose keys are: - - [[wrap-output-stream]] - - [[write!]] - - [[content-encoding]] (optional if you don't use compression) - " - :d*.sse/write-profile) - - -(def wrap-output-stream - "SSE write profile key: - - A function that wraps an OutputStream like: - `(fn [os] (clojure.java.io/writer os))` - - This function will be used to wrap the [[OutputStream]] used for a SSE - connection. - - It allows you to add compression and buffering to suit you needs. - - The SDK provides utilities to implement such a wrapping function, see: - - [[->gzip-os]] - - [[->os-writer]] - - [[->buffered-writer]] - " - :d*.sse.write-profile/wrap-output-stream) - - -(def write! - "SSE write profile option: - - A function that writes to a [[java.lang.Appendable]]. It should go in tandem - with the way you wrap the [[OutputStream]]. - - The SDK provide pre-made write function, see: - - [[->write-with-temp-buffer!]] - - [[write-to-buffered-writer!]] - " - :d*.sse.write-profile/write!) - - -(def content-encoding - "SSE write profile option: - - A string value for the Content-Encoding HTTP header. When using gzip - compression the value should be [[gzip-content-encoding]]. - " - :d*.sse.write-profile/content-encoding) - - -;; ----------------------------------------------------------------------------- -;; HTTP headers helper -;; ----------------------------------------------------------------------------- -(defn headers - "Same as [[sse/headers]] with the added responsibility to add the - Content-Encoding header based on a write profile." - [ring-request & {:as opts}] - (let [encoding (-> opts write-profile content-encoding)] - (-> (transient {}) - (u/merge-transient! sse/base-SSE-headers) - (cond-> - (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/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 -;; ----------------------------------------------------------------------------- -(defn ->gzip-os - "Make a GZIPOutputStream from an OutputStream `os` and an optional - `buffer-size`. The syncFlush always set to true. - - Default buffer-size is the GZIPOutputStream's own. - " - ([^OutputStream os] - (GZIPOutputStream. os true)) - ([^OutputStream os buffer-size] - (GZIPOutputStream. os buffer-size true))) - - -(defn ->os-writer - "Make an OutputStreamWriter from an OutputStream `os` and an optional - [[Charset]] `charset`. - - Defaut charset is [[StandardCharsets/UTF_8]] - " - ([^OutputStream os] - (OutputStreamWriter. os StandardCharsets/UTF_8)) - ([^OutputStream os ^Charset charset] - (OutputStreamWriter. os charset))) - - -(defn ->buffered-writer - "Make an BufferedWriter from an OutputStreamWriter `osw` and an optional - `buffer-size`. - - Default buffer-size is the BufferedWriter's own. - " - ([osw] - (BufferedWriter. osw)) - ([osw buffer-size] - (BufferedWriter. osw buffer-size))) - - -;; ----------------------------------------------------------------------------- -;; Writing utilities -;; ----------------------------------------------------------------------------- -(def default-write-buffer-size 8192) - - -(defn ->build-event-str - "Make a function that will assemble a SSE event using a StringBuiler and - return the concatenated string." - ([] - (->build-event-str default-write-buffer-size)) - ([buffer-size] - (fn build-event-str [event-type data-lines opts] - (-> (StringBuilder. (int buffer-size)) - (sse/write-event! event-type data-lines opts) - str)))) - - -(defn ->write-with-temp-buffer! - "Make a function that will assemble a SSE event using a [[StringBuilder]] and - then will write the resulting string to an [[OutputStreamWriter]]. " - ([] - (->write-with-temp-buffer! default-write-buffer-size)) - ([buffer-size] - (let [build-event (->build-event-str buffer-size)] - (fn write-with-temp-buffer! [^Writer writer event-type data-lines opts] - (let [event-str (build-event event-type data-lines opts)] - (.append writer ^String event-str)))))) - - -(defn write-to-buffered-writer! - "Write a SSE event to a stream using a [[BufferedWriter]]." - [^BufferedWriter writer event-type data-lines opts] - (sse/write-event! writer event-type data-lines opts)) - - -(defn flush - "Flush a `java.io.Flushable`." - [^Flushable f] - (.flush f)) - -;; ----------------------------------------------------------------------------- -;; SDK provided write profiles -;; ----------------------------------------------------------------------------- -(def gzip-content-encoding "gzip") - -(def basic-profile - "Basic write profile using temporary [[StringBuilder]]s and no compression." - {wrap-output-stream (fn[os] (-> os ->os-writer)) - write! (->write-with-temp-buffer!)}) - -(def buffered-writer-profile - "Write profile using a permanent [[BufferedWriter]] and no compression." - {wrap-output-stream (fn[os] (-> os ->os-writer ->buffered-writer)) - write! (->write-with-temp-buffer!)}) - -(def gzip-profile - "Write profile using temporary [[StringBuilder]]s and gzip compression." - {wrap-output-stream (fn[os] (-> os ->gzip-os ->os-writer)) - content-encoding gzip-content-encoding - write! (->write-with-temp-buffer!)}) - -(def gzip-buffered-writer-profile - "Write profile using a permanent [[BufferedWriter]] and gzip compression." - {wrap-output-stream (fn[os] (-> os ->gzip-os ->os-writer ->buffered-writer)) - content-encoding gzip-content-encoding - write! write-to-buffered-writer!}) - - -;; ----------------------------------------------------------------------------- -;; Exception handling / Closing sse helpers -;; ----------------------------------------------------------------------------- -(defn try-closing - "Run a thunk `f` that closes some resources. Catches exceptions and returns - them wrapped in a [[ex-info]] with the message `error-msg`." - [f error-msg] - (try - (f) - (catch Throwable t - (ex-info error-msg {} t)))) - - -(def closing-exceptions - "Exceptions caught while closing a sse generator using [[close-sse!]] - are grouped under this key in the ex-info's data that is returned." - :d*.sse/closing-exceptions) - - -(defn- group-exceptions [closing-results] - (group-by (fn [v] (cond - (instance? Exception v) :exceptions - (instance? Throwable v) :throwables - :else :irrelevant)) - closing-results)) - -(comment - (group-exceptions [(Error.) (ex-info "" {})]) - (group-exceptions [:a (ex-info "" {})]) - (group-exceptions [:a (Error.)])) - - -(defn close-sse! - "Closing a sse-gen is a 2 steps process. - 1. close IO resources calling the `close-io!` thunk. - 2. call the sse-gen's `on-close` callback by calling the `on-close!` thunk. - - Both thunks are called using [[try-closing]] to capture exceptions. - - This function rethrows the first `java.lang.Throwable` encountered. Otherwise - it returns either `true` when all thunks are exceptions free or an - exception created with [[ex-info]] that contains the thunks exceptions in it's - data under the key [[closing-exceptions]]." - [close-io! on-close!] - (let [results [(try-closing close-io! "Error closing the output stream.") - (try-closing on-close! "Error calling the on close callback.")] - - {:keys [throwables exceptions]} (group-exceptions results) - throwable (some identity throwables)] - (cond - throwable (throw throwable) - (seq exceptions) (ex-info "Error closing the sse-gen." - {closing-exceptions exceptions}) - :else true))) - - -(defn get-closing-exceptions - "Extract the exceptions in the `ex-data` of a ex-info thrown during the - closing of a sse generator." - [e] - (-> e ex-data closing-exceptions)) - - -(comment - (-> (ex-info "msg" {closing-exceptions [:e1 :e2]}) - get-closing-exceptions)) - - -(def on-open - "SSE option key: - - Mandatory callback `(fn [sse-gen] ...)` called when the SSE connection is - open. - - It is called synchronously as soon as the connection is ready. This means - that you can block the webserver's thread from here. - - Args: - - `sse-gen`: the SSE generator, ready to send events - - Return value: - The return value isn't used. - - Exception behavior: - Exception thrown here aren't caught. - " - :d*.sse/on-open) - - -(def on-close - "SSE option key: - - Callback `(fn [sse-gen & args] ...)` called just after the SSE connection is - closed. More specifically this callback is called when: - - [[starfederation.datastar.clojure.api/close-sse!]] is called explicitely - - the sse-gen detects that the connection is closed by the client. This - detection depends on the ring adapter being used. Http-kit AsyncChannel - detects a broken connection by itself and calls on-close. With the ring - generic adapter, the default behavior is to close the connection when a - [[java.io.IOException]] is thrown sending an event. - - When this callback is called it is already not possible to send events - anymore. It is called synchronously in the same thread that initiates the - closing process. - - Args: - - `sse-gen`: the closed SSE generator - - Depending on the adapter used other arguments may be passed, see doc for - the adapter you are using. - - Return value: - The return value isn't used. - - Exception behavior: - While closing the `sse-gen` several exceptions may be caught: - - one while closing the underlying output stream - - one executing this callback - - These exceptions are rethrown wrapped in an `ex-info`. To get at these see - [[get-closing-exceptions]]." - :d*.sse/on-close) - - -(def on-exception - "SSE option key: - - Callback `(fn [sse-gen e ctx] ...)` that will be called when an exception is - thrown sending an event. - - Args: - - `sse-gen`: the SSEGenrator - - `e`: the exception - - `ctx`: context information about the exception, a map whose keys are: - - `:sse-gen`: the sse generator throwing - - `:event-type`: type of the event that failed - - `:data-lines`: data lines for this event - - `:opts`: options used when sending - - Returned value: - A truthy value means the sse-gen must close itself. - - Exception behavior: - Exception thrown here aren't caught. - - Defaults to [[default-on-exception]] which will close the `sse-gen` on - IOException or rethrow otherwise." - :d*.sse/on-exception) - - -(defn default-on-exception - "Default [[on-exception]] callback, it returns `true` on [[IOException]] which - closes, the generator. It rethrows the exception wrapped with `ex-infor` - otherwise." - [_sse e ctx] - (if (instance? IOException e) - true - (throw (ex-info "Error sending SSE event." ctx e)))) - - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj deleted file mode 100644 index 78392bfeb..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj +++ /dev/null @@ -1,69 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.test - (:require - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.protocols :as p] - [starfederation.datastar.clojure.utils :as u]) - (:import - [java.util.concurrent.locks ReentrantLock] - [java.io Closeable])) - - - -(deftype ReturnMsgGen [] - p/SSEGenerator - (send-event! [_ event-type data-lines opts] - (-> (StringBuilder.) - (sse/write-event! event-type data-lines opts) - str)) - - (get-lock [_]) - - (close-sse! [_]) - (sse-gen? [_] true)) - - - -(defn ->sse-gen [& _] - (->ReturnMsgGen)) - - - - -(deftype RecordMsgGen [lock !rec !open?] - p/SSEGenerator - (send-event! [_ event-type data-lines opts] - (u/lock! lock - (vswap! !rec conj (-> (StringBuilder.) - (sse/write-event! event-type data-lines opts) - str)))) - - (get-lock [_] lock) - - (close-sse! [_] - (u/lock! lock - (vreset! !open? false))) - - (sse-gen? [_] true) - - Closeable - (close [this] - (p/close-sse! this))) - - -(defn ->sse-response - "Fake a sse-response, the events sent with sse-gen during the - `on-open` callback are recorded in a vector stored in an atom returned as the - body of the response." - [req {on-open ac/on-open - :keys [status headers]}] - (let [ - !rec (volatile! []) - sse-gen (->RecordMsgGen (ReentrantLock.) - !rec - (volatile! true))] - (on-open sse-gen) - {:status (or status 200) - :headers (merge headers (sse/headers req)) - :body !rec})) - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj deleted file mode 100644 index 6c7427a91..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj +++ /dev/null @@ -1,460 +0,0 @@ -(ns starfederation.datastar.clojure.api - " -Public api for the Datastar SDK. - -The main api consists several functions that operate on SSE generators, see: -- [[patch-elements!]] -- [[patch-elements-seq!]] -- [[remove-element!]] -- [[patch-signals!]] -- [[execute-script!]] - -These function take options map whose keys are: -- [[id]] -- [[retry-duration]] -- [[selector]] -- [[patch-mode]] -- [[use-view-transition]] -- [[only-if-missing]] -- [[auto-remove]] -- [[attributes]] - -To help manage SSE generators's underlying connection there is: -- [[close-sse!]] -- [[with-open-sse]] - -Some common utilities for HTTP are also provided: -- [[sse-get]] -- [[sse-post]] -- [[sse-put]] -- [[sse-patch]] -- [[sse-delete]] - -Some scripts are provided: -- [[console-log!]] -- [[console-error!]] -- [[redirect!]]" - (:require - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.elements :as elements] - [starfederation.datastar.clojure.api.signals :as signals] - [starfederation.datastar.clojure.api.scripts :as scripts] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.protocols :as p] - [starfederation.datastar.clojure.utils :as u])) - -;; ----------------------------------------------------------------------------- -;; SSE generator management -;; ----------------------------------------------------------------------------- -(defmacro lock-sse! - "Hold onto the lock of a `sse-gen` while executing `body`. This allows for - preventing concurent sending of sse events. Sse generators use - [[java.util.concurrent.locks.ReentrantLock]] under the hood. - - Ex: - ```clojure - (lock-sse! my-sse-gen - (patch-elements! sse frags) - (patch-signals! sse signals)) - ``` - " - [sse-gen & body] - `(u/lock! (p/get-lock ~sse-gen) ~@body)) - - -(comment - (macroexpand-1 - (macroexpand-1 - '(lock-sse! my-sse-gen - (patch-elements! sse frags) - (patch-signals! sse signals))))) - - -(defn close-sse! - "Close the connection of a sse generator. - - Return value: - - true if `sse-gen` closed - - false if it was already closed" - [sse-gen] - (p/close-sse! sse-gen)) - - -(defmacro with-open-sse - "Macro functioning similarly to [[clojure.core/with-open]]. It evalutes the - `body` inside a try expression and closes the `sse-gen` at the end using - [[close-see!]] in a finally clause. - - Ex: - ``` - (with-open-sse sse-gen - (d*/patch-elements! sse-gen frag1) - (d*/patch-signals! sse-gen signals)) - ``` - " - [sse-gen & body] - `(try - ~@body - (finally - (close-sse! ~sse-gen)))) - -(comment - (macroexpand-1 - '(with-open-sse toto - (do-stuff) - (do-stuff)))) - - -;; ----------------------------------------------------------------------------- -;; Option names -;; ----------------------------------------------------------------------------- -(def id - "SSE option use in all event functions, string: - - Each event may include an eventId. This can be used by - the backend to replay events. This is part of the SSE spec and is used to - tell the browser how to handle the event. For more details see - https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id" - common/id) - -(def retry-duration - "SSE option used in all event functions, number: - - Each event may include a retryDuration value. If one is - not provided the SDK must default to 1000 milliseconds. This is part of the - SSE spec and is used to tell the browser how long to wait before reconnecting - if the connection is lost. For more details see - https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry" - common/retry-duration) - -;; patch element opts -(def selector - "[[patch-elements!]] & [[patch-elements-seq!]] option, string: - - The CSS selector to use to insert the elements. If not - provided or empty, Datastar will default to using the id attribute of the - element." - common/selector) - -(def patch-mode - "[[patch-elements!]] & [[patch-elements-seq!]] option, string: - - The mode to use when merging elements into the DOM. - If not provided the Datastar client side will default to morph. - - The set of valid values is: - - [[pm-outer]] default - - [[pm-inner]] - - [[pm-remove]] - - [[pm-prepend]] - - [[pm-append]] - - [[pm-before]] - - [[pm-after]] - - [[pm-replace]] - " - common/patch-mode) - -(def use-view-transition - "[[patch-elements!]] / [[remove-element!] option, boolean: - - Whether to use view transitions, if not provided the - Datastar client side will default to false." - common/use-view-transition) - -;;Signals opts -(def only-if-missing - "[[patch-signals!]] option, boolean: - - Whether to patch the signal only if it does not already - exist. If not provided, the Datastar client side will default to false, which - will cause the data to be patchd into the signals." - common/only-if-missing) - -;; Script opts -(def auto-remove - "[[execute-script!]] option, boolean: - - Whether to remove the script after execution, if not - provided the Datastar client side will default to true." - common/auto-remove) - -(def attributes - "[[execute-script!]] option, map: - - A map of attributes to add to the script element." - common/attributes) - - -;; ----------------------------------------------------------------------------- -;; Data-star base api -;; ----------------------------------------------------------------------------- -(def pm-outer - "patch mode: replaces the outer HTML of the existing element." - consts/element-patch-mode-outer) - -(def pm-inner - "patch mode: replaces the inner HTML of the existing element." - consts/element-patch-mode-inner) - -(def pm-remove - "patch mode: remove the existing element from the dom." - consts/element-patch-mode-remove) - -(def pm-prepend - "patch mode: prepends the element to the existing element." - consts/element-patch-mode-prepend) - -(def pm-append - "patch mode: appends the element to the existing element." - consts/element-patch-mode-append) - -(def pm-before - "patch mode: inserts the element before the existing element." - consts/element-patch-mode-before) - -(def pm-after - "patch mode: inserts the element after the existing element." - consts/element-patch-mode-after) - -(def pm-replace - "patch mode: Do not morph, simply replace the whole element and reset any - related state." - consts/element-patch-mode-replace) - - -(defn patch-elements! - "Send HTML elements to the browser to be patchd into the DOM. - - Args: - - `sse-gen`: the sse generator to send from - - `elements`: A string of HTML elements. - - `opts`: An options map - - Options keys: - - [[id]] - - [[retry-duration]] - - [[selector]] - - [[patch-mode]] - - [[use-view-transition]] - - Return value: - - `false` if the connection is closed - - `true` otherwise - " - ([sse-gen elements] - (patch-elements! sse-gen elements {})) - ([sse-gen elements opts] - (elements/patch-elements! sse-gen elements opts))) - - -(defn patch-elements-seq! - "Same as [[patch-elements!]] except that it takes a seq of elements." - ([sse-gen elements] - (patch-elements-seq! sse-gen elements {})) - ([sse-gen elements opts] - (elements/patch-elements-seq! sse-gen elements opts))) - - - -(defn remove-element! - "Remove element(s) from the dom. It is a convenience function using - [[patch-elements!]] with the [[patch-mode]] options set to [[pm-remove]] - and a [[selector]] set to `selector`. - - Args: - - `sse-gen`: the sse generator to send from - - `selector`: string, CSS selector that represents the elements to be - removed from the DOM. - - `opts`: options map - - Options keys: - - [[id]] - - [[retry-duration]] - - [[use-view-transition]] - - Return value: - - `false` if the connection is closed - - `true` otherwise - " - ([sse-gen selector] - (remove-element! sse-gen selector {})) - ([sse-gen selector opts] - (elements/remove-element! sse-gen selector opts))) - - -(defn patch-signals! - " - Send signals to the browser using - [RFC 7386 JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) - semantics. - - Args: - - `sse-gen`: the sse generator to send from - - `signals-content`: a JavaScript object or JSON string that will be sent to - the browser to update signals. The data must evaluate to a - valid JavaScript Object. `null` values for keys in this JSON object mean - that the signal at these keys are to be removed. - - `opts`: An options map - - Options keys: - - [[id]] - - [[retry-duration]] - - [[only-if-missing]] - - Return value: - - `false` if the connection is closed - - `true` otherwise - " - ([sse-gen signals-content] - (patch-signals! sse-gen signals-content {})) - ([sse-gen signals-content opts] - (signals/patch-signals! sse-gen signals-content opts))) - - -(defn get-signals - "Returns the signals json string from a ring request map. - - The Datastar signals are read from: - - the `:query-params` key of of the ring request map for HTTP get requests - - the body of the request for other HTTP methods - - (Bring your own json parsing)" - [ring-request] - (signals/get-signals ring-request)) - - -(defn execute-script! - " - Construct a HTML script tag using `script-text` as its content. Then sends it - to the brower using [[patch-elements!]] with [[patch-mode]] set to - [[pm-append]] and [[selector]] set to `\"body\"`. - - The default behavior is to auto remove the script after it has run. - - Args: - - `sse-gen`: the sse generator to send from - - `script-text`: string that represents the JavaScript to be executed - by the browser. - - `opts`: An options map - - Options keys: - - [[id]] - - [[retry-duration]] - - [[auto-remove]] defaults to true - - [[attributes]] - - Return value: - - `false` if the connection is closed - - `true` otherwise - " - ([sse-gen script-text] - (scripts/execute-script! sse-gen script-text {})) - ([sse-gen script-text opts] - (scripts/execute-script! sse-gen script-text opts))) - - - -;; ----------------------------------------------------------------------------- -;; SSE helpers -;; ----------------------------------------------------------------------------- -(defn- sse - ([method url] - (str "@" method "('" url "')")) - ([method url opts-string] - (str "@" method "('" url "', " opts-string ")"))) - - -(defn sse-get - "Helper making a @get(...) action." - ([url] - (sse "get" url)) - ([url opts-string] - (sse "get" url opts-string))) - - -(defn sse-post - "Helper making a @post(...) action." - ([url] - (sse "post" url)) - ([url opts-string] - (sse "post" url opts-string))) - - -(defn sse-put - "Helper making a @put(...) action." - ([url] - (sse "put" url)) - ([url opts-string] - (sse "put" url opts-string))) - - -(defn sse-patch - "Helper making a @patch(...) action." - ([url] - (sse "patch" url)) - ([url opts-string] - (sse "patch" url opts-string))) - - -(defn sse-delete - "Helper making a @delete(...) action." - ([url] - (sse "delete" url)) - ([url opts-string] - (sse "delete" url opts-string))) - - -(comment - (sse-get "/a/b") - := "@get('/a/b')" - - (sse-put "/a/b" "{includeLocal: true}") - := "@put('/a/b', {includeLocal: true})") - - -;; ----------------------------------------------------------------------------- -;; Scripts common -;; ----------------------------------------------------------------------------- -(defn console-log! - "Log msg in the browser console. - - Same behavior as [[execute-script!]]. - " - ([sse-gen msg] - (console-log! sse-gen msg {})) - ([sse-gen msg opts] - (execute-script! sse-gen (str "console.log(\"" msg "\")") opts))) - - -(defn console-error! - "Log error msg in the browser console. - - Same behavior as [[execute-script!]]. - " - ([sse-gen msg] - (console-error! sse-gen msg {})) - ([sse-gen msg opts] - (execute-script! sse-gen (str "console.error(\"" msg "\")") opts))) - - -(defn redirect! - "Redirect a page using a script. - - Same behavior as [[execute-script!]]. - " - ([sse-gen url] - (redirect! sse-gen url {})) - ([sse-gen url opts] - (execute-script! sse-gen - (str "setTimeout(() => window.location.href =\"" url "\")") - opts))) - - -;; ----------------------------------------------------------------------------- -;; Misc -;; ----------------------------------------------------------------------------- -(defn datastar-request? - "Test for the presence of the datastar header in a ring request." - [request] - (= "true" (get-in request [:headers "datastar-request"]))) - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj deleted file mode 100644 index ad8c4b879..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj +++ /dev/null @@ -1,61 +0,0 @@ -(ns starfederation.datastar.clojure.api.common) - -;; ----------------------------------------------------------------------------- -;; Option names -;; ----------------------------------------------------------------------------- - -;; SSE Options -(def id :d*.sse/id) -(def retry-duration :d*.sse/retry-duration) - -;; Merge fragment opts -(def selector :d*.elements/selector) -(def patch-mode :d*.elements/patch-mode) -(def use-view-transition :d*.elements/use-view-transition) - -;;Signals opts -(def only-if-missing :d*.signals/only-if-missing) - - -;; Script opts -(def auto-remove :d*.scripts/auto-remove) -(def attributes :d*.scripts/attributes) - - - -;; ----------------------------------------------------------------------------- -;; Data lines construction helpers -;; ----------------------------------------------------------------------------- -(defn add-opt-line! - "Add an option `v` line to the transient `data-lines!` vector. - - Args: - - `data-lines`: a transient vector of data-lines that will be written in a sse - event - - `prefix`: The Datastar specific preffix for that line - - `v`: the value for that line - " - [data-lines! prefix v] - (conj! data-lines! (str prefix v))) - - -(defn add-data-lines! - "Add several data-lines to the `data-lines!` transient vector." - [data-lines! prefix lines-seq] - (reduce - (fn [acc part] - (conj! acc (str prefix part))) - data-lines! - lines-seq)) - - -(defn add-boolean-option? - "Utility used to test whether an boolean option should result in a sse event - data-line. Returns true if `val` a boolean and isn't the `default-val`." - [default-val val] - (and - (boolean? val) - (not= val default-val))) - - - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj deleted file mode 100644 index 1774ac996..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj +++ /dev/null @@ -1,150 +0,0 @@ -(ns starfederation.datastar.clojure.api.elements - (:require - [clojure.string :as string] - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.utils :as u])) - - -;; ----------------------------------------------------------------------------- -;; Patch Elements options handling -;; ----------------------------------------------------------------------------- -(def ^:private valid-selector? u/not-empty-string?) - -(defn- add-epm? [fmm] - (and fmm (not= fmm consts/default-element-patch-mode))) - -(defn- add-view-transition? [v] - (common/add-boolean-option? consts/default-elements-use-view-transitions v)) - - -(defn conj-patch-element-opts! - "Conj the optional data-lines to the transient `data-lines` vector. - vector." - [data-lines! opts] - (let [sel (common/selector opts) - patch-mode (common/patch-mode opts) - use-vt (common/use-view-transition opts)] - - (cond-> data-lines! - (and sel (valid-selector? sel)) - (common/add-opt-line! consts/selector-dataline-literal sel) - - (and patch-mode (add-epm? patch-mode)) - (common/add-opt-line! consts/mode-dataline-literal patch-mode) - - (and use-vt (add-view-transition? use-vt)) - (common/add-opt-line! consts/use-view-transition-dataline-literal use-vt)))) - - - -;; ----------------------------------------------------------------------------- -;; Patch Element -;; ----------------------------------------------------------------------------- -(defn conj-patch-elements! - "Adds a the data-lines when patching a string of elements." - [data-lines! element] - (cond-> data-lines! - (u/not-empty-string? element) - (common/add-data-lines! consts/elements-dataline-literal - (string/split-lines element)))) - - -(defn ->patch-elements - "Make the data-lines for a patch-element operation." - [element opts] - (u/transient-> [] - (conj-patch-element-opts! opts) - (conj-patch-elements! element))) - - -(defn patch-elements! [sse-gen element opts] - (try - (sse/send-event! sse-gen - consts/event-type-patch-elements - (->patch-elements element opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send element." - {:element element} - e))))) - -(comment - (= (->patch-elements "
hello
" {}) - ["elements
hello
"]) - - (= (->patch-elements "
hello
\n
world!!!
" - {common/selector "#toto" - common/patch-mode consts/element-patch-mode-after - common/use-view-transition true}) - ["selector #toto" - "mode after" - "useViewTransition true" - "elements
hello
" - "elements
world!!!
"])) - - -;; ----------------------------------------------------------------------------- -;; Patch Elements -;; ----------------------------------------------------------------------------- -(defn conj-patch-elements-seq - "Adds a the data-lines when patching a seq of strings elements." - [data-lines! elements-seq] - (cond-> data-lines! - (seq elements-seq) - (common/add-data-lines! consts/elements-dataline-literal - (eduction - (comp (mapcat string/split-lines) - (remove string/blank?)) - elements-seq)))) - - -(defn ->patch-elements-seq - "Make the data-lines for a patch-elements operation." - [elements-seq opts] - (u/transient-> [] - (conj-patch-element-opts! opts) - (conj-patch-elements-seq elements-seq))) - - -(defn patch-elements-seq! [sse-gen elements opts] - (try - (sse/send-event! sse-gen - consts/event-type-patch-elements - (->patch-elements-seq elements opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send fragment." - {:elements elements} - e))))) - - -(comment - (= (->patch-elements-seq ["
hello
" " " "
\nworld\n
"] {}) - ["elements
hello
" - "elements
" - "elements world" - "elements
"]) - - (= (->patch-elements-seq ["
hello
\n
world!!!
" "
world!!!
"] - {common/selector "#toto" - common/patch-mode consts/element-patch-mode-after - common/use-view-transition true}) - ["selector #toto" - "mode after" - "useViewTransition true" - "elements
hello
" - "elements
world!!!
" - "elements
world!!!
"])) - - -;; ----------------------------------------------------------------------------- -;; Remove Element -;; ----------------------------------------------------------------------------- -(defn remove-element! [sse-gen selector opts] - (patch-elements! sse-gen "" (assoc opts - common/selector selector - common/patch-mode consts/element-patch-mode-remove))) - - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj deleted file mode 100644 index 93cb65caa..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj +++ /dev/null @@ -1,72 +0,0 @@ -(ns starfederation.datastar.clojure.api.scripts - (:require - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.elements :as elements] - [starfederation.datastar.clojure.consts :as consts])) - - -(defn ->script-tag [script opts] - (let [auto-remove (common/auto-remove opts) - attrs (common/attributes opts) - script-tag-builder (StringBuilder.)] - - ;; Opening - (.append script-tag-builder "") - - ;; Content of the script - (.append script-tag-builder script) - - ;; Closing - (.append script-tag-builder "") - - ;; Returning the built tag - (str script-tag-builder))) - - -(def patch-opts - {common/selector "body" - common/patch-mode consts/element-patch-mode-append}) - - -(defn execute-script! [sse-gen script-text opts] - (elements/patch-elements! sse-gen - (->script-tag script-text opts) - (merge opts patch-opts))) - -(comment - (= (->script-tag "console.log('hello')" {}) - "") - - (= (->script-tag "console.log('hello')" - {common/auto-remove false}) - "") - - - (= (->script-tag "console.log('hello')" - {common/auto-remove false - common/attributes {:type "module"}}) - "") - - - (= (->script-tag "console.log('hello');\nconsole.log('world!!!')" - {common/auto-remove :true - common/attributes {:type "module" :data-something 1}}) - "")) - - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj deleted file mode 100644 index d7eea6597..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj +++ /dev/null @@ -1,60 +0,0 @@ -(ns starfederation.datastar.clojure.api.signals - (:require - [clojure.string :as string] - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.utils :as u])) - - -;; ----------------------------------------------------------------------------- -;; Merge signal -;; ----------------------------------------------------------------------------- -(defn add-only-if-missing? [v] - (common/add-boolean-option? consts/default-patch-signals-only-if-missing v)) - -(defn ->patch-signals [signals opts] - (let [oim (common/only-if-missing opts)] - (u/transient-> [] - (cond-> - (and oim (add-only-if-missing? oim)) - (common/add-opt-line! consts/only-if-missing-dataline-literal oim) - - (u/not-empty-string? signals) - (common/add-data-lines! consts/signals-dataline-literal - (string/split-lines signals)))))) - - - -(comment - (= (->patch-signals "{'some': \n 'json'}" {}) - ["signals {'some': " - "signals 'json'}"])) - -(defn patch-signals! [sse-gen signals-content opts] - (try - (sse/send-event! sse-gen - consts/event-type-patch-signals - (->patch-signals signals-content opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send merge signals" - {:signals signals-content} - e))))) - - -;; ----------------------------------------------------------------------------- -;; Read signals -;; ----------------------------------------------------------------------------- -(defn get-signals - "Returns the signals json string. You need to use some middleware - that adds the :query-params key to the request for this function - to work properly. - - (Bring your own json parsing)" - [request] - (if (= :get (:request-method request)) - (get-in request [:query-params consts/datastar-key]) - (:body request))) - - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj deleted file mode 100644 index 2e94fee04..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj +++ /dev/null @@ -1,164 +0,0 @@ -(ns starfederation.datastar.clojure.api.sse - (:require - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.protocols :as p] - [starfederation.datastar.clojure.utils :as u]) - (:import - java.lang.Appendable)) - -;; ----------------------------------------------------------------------------- -;; HTTP headers management -;; ----------------------------------------------------------------------------- -(def base-SSE-headers - {"Cache-Control" "no-cache" - "Content-Type" "text/event-stream"}) - - -(defn add-keep-alive? [ring-request] - (let [protocol (:protocol ring-request)] - (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 - "Returns headers for a SSE response. It adds specific SSE headers based on the - HTTP protocol version found in the `ring-request`and the gzip content type - if necessary. - - Options: - - `:headers`: custom headers for the response - - The SSE headers this function provides can be overriden by the optional ones. - Be carreful with the following headers: - - - \"Cache-Control\" - - \"Content-Type\" - - \"Connection\" - " - [ring-request & {:as opts}] - (-> (transient {}) - (u/merge-transient! base-SSE-headers) - (cond-> - (add-keep-alive? ring-request) (assoc! "Connection" "keep-alive",)) - (u/merge-transient! (:headers opts)) - persistent!)) - - -;; ----------------------------------------------------------------------------- -;; Assembling SSE event text -;; ----------------------------------------------------------------------------- - -;; ----------------------------------------------------------------------------- -;; SSE prefixes and constants -(def ^:private event-line-prefix "event: ") -(def ^:private id-line-prefix "id: ") -(def ^:private retry-line-prefix "retry: ") -(def ^:private data-line-prefix "data: ") - -(def ^:private new-line "\n") -(def ^:private end-event new-line) - - -;; ----------------------------------------------------------------------------- -;; Appending to a buffer -(defn- append! [^Appendable a v] - (.append a (str v))) - - -(defn- append-line! [buffer prefix line error-msg error-key] - (try - (doto buffer - (append! prefix) - (append! line) - (append! new-line)) - (catch Exception e - (throw (ex-info error-msg - {error-key line} e))))) - -;; ----------------------------------------------------------------------------- -;; Appending event type -(defn- append-event-type! [buffer event-type] - (append-line! buffer event-line-prefix event-type - "Failed to write event type." :event-type)) - -;; ----------------------------------------------------------------------------- -;; Appending event opts -(defn- append-opts! [buffer {event-id common/id retry-duration common/retry-duration}] - (when event-id - (append-line! buffer id-line-prefix event-id - "Failed to write event id" common/id)) - - (when retry-duration - (append-line! buffer retry-line-prefix retry-duration - "Failed to write retry" common/retry-duration))) - -;; ----------------------------------------------------------------------------- -;; Appending event data -(defn- append-data-lines! [buffer data-lines] - (doseq [l data-lines] - (append-line! buffer data-line-prefix l "Failed to write data." :data-line))) - -;; ----------------------------------------------------------------------------- -;; Append end event -(defn- append-end-event! [buffer] - (try - (append! buffer end-event) - (catch Exception e - (throw (ex-info "Failed to write new lines." - {} - e))))) - -;; ----------------------------------------------------------------------------- -;; Public api -(defn write-event! - "Appends and event to an java.lang.Appendable buffer." - [appendable event-type data-lines opts] - (doto appendable - (append-event-type! event-type) - (append-opts! opts) - (append-data-lines! data-lines) - (append-end-event!))) - - -(def ^:private keep-event-id? u/not-empty-string?) - -(defn- keep-retry-duration? [d] - (and d - (> d 0) - (not= d consts/default-sse-retry-duration))) - - -(defn- rework-options - "Standardize opts values and decide whether to keep them or not: - - if the id is an empty string it is thrown away - - if the retry duration is 0 or datastars default it is thrown away - - This function asserts that the id must be a string and the retry duration a - number." - [opts] - (let [id (common/id opts "") - retry-duration (common/retry-duration opts consts/default-sse-retry-duration)] - (u/assert (string? id)) - (u/assert (number? retry-duration)) - {common/id (and (keep-event-id? id) id) - - common/retry-duration (and (keep-retry-duration? retry-duration) - retry-duration)})) - -(defn send-event! - "Wrapper around the [p/send-event!] function. - It provides multiple arities and defaults options." - ([sse-gen event-type data-lines] - (p/send-event! sse-gen event-type data-lines {})) - ([sse-gen event-type data-lines opts] - (p/send-event! sse-gen - event-type - data-lines - (rework-options opts)))) diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj deleted file mode 100644 index 2147dedab..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj +++ /dev/null @@ -1,97 +0,0 @@ -;; This is auto-generated by Datastar. DO NOT EDIT. -(ns starfederation.datastar.clojure.consts) - - -(def datastar-key "datastar") -(def version "1.0.0-RC.1") - - -;; ----------------------------------------------------------------------------- -;; Default durations -;; ----------------------------------------------------------------------------- -(def default-sse-retry-duration - "The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE." - 1000) - - -;; ----------------------------------------------------------------------------- -;; Default values -;; ----------------------------------------------------------------------------- - -;; ----------------------------------------------------------------------------- -;; Dataline literals -;; ----------------------------------------------------------------------------- -(def selector-dataline-literal "selector ") -(def mode-dataline-literal "mode ") -(def elements-dataline-literal "elements ") -(def use-view-transition-dataline-literal "useViewTransition ") -(def signals-dataline-literal "signals ") -(def only-if-missing-dataline-literal "onlyIfMissing ") - - -;; ----------------------------------------------------------------------------- -;; Default booleans -;; ----------------------------------------------------------------------------- -(def default-elements-use-view-transitions - "Should elements be patched using the ViewTransition API?" - false) - -(def default-patch-signals-only-if-missing - "Should a given set of signals patch if they are missing?" - false) - - - -;; ----------------------------------------------------------------------------- -;; Enums -;; ----------------------------------------------------------------------------- -;; ElementPatchMode - -(def element-patch-mode-outer - "Morphs the element into the existing element." - "outer") - -(def element-patch-mode-inner - "Replaces the inner HTML of the existing element." - "inner") - -(def element-patch-mode-remove - "Removes the existing element." - "remove") - -(def element-patch-mode-replace - "Replaces the existing element with the new element." - "replace") - -(def element-patch-mode-prepend - "Prepends the element inside to the existing element." - "prepend") - -(def element-patch-mode-append - "Appends the element inside the existing element." - "append") - -(def element-patch-mode-before - "Inserts the element before the existing element." - "before") - -(def element-patch-mode-after - "Inserts the element after the existing element." - "after") - - -(def default-element-patch-mode - "Default value for ElementPatchMode. - Morphs the element into the existing element." - element-patch-mode-outer) - - -;; EventType - -(def event-type-patch-elements - "An event for patching HTML elements into the DOM." - "datastar-patch-elements") - -(def event-type-patch-signals - "An event for patching signals." - "datastar-patch-signals") diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj deleted file mode 100644 index 2ff01bd53..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj +++ /dev/null @@ -1,40 +0,0 @@ -(ns starfederation.datastar.clojure.protocols) - - -(defprotocol SSEGenerator - (send-event! [this event-type data-lines opts] "Send sse event.") - (get-lock [this] "Access to the lock used in the generator.") - (close-sse! [this] "Close connection.") - (sse-gen? [this] "Test wheter a value is a SSEGenerator.")) - - -(defn throw-not-implemented [type method] - (throw (ex-info (str "Type " type " is not a SSEGenerator.") {:type type :method method}))) - - -(extend-protocol SSEGenerator - nil - (sse-gen? [_] false) - - (send-event! [_this _event-type _data-lines _opts] - (throw-not-implemented nil :send-event!)) - - (get-lock [_this] - (throw-not-implemented nil :get-lock)) - - (close-sse! [_this] - (throw-not-implemented nil :close-sse!)) - - - Object - (sse-gen? [_] false) - - (send-event! [_this _event-type _data-lines _opts] - (throw-not-implemented Object :send-event!)) - - (get-lock [_this] - (throw-not-implemented Object :get-lock)) - - (close-sse! [_this] - (throw-not-implemented Object :close-sse!))) - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj deleted file mode 100644 index d466b9330..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj +++ /dev/null @@ -1,82 +0,0 @@ -(ns starfederation.datastar.clojure.utils - (:refer-clojure :exclude [assert]) - (:require - [clojure.string :as string]) - (:import - [java.util.concurrent.locks ReentrantLock])) - - -(defmacro assert - "Same as clojure's [[assert]] except that it throws a `clojure.lang.ExceptionInfo`." - {:added "1.0"} - ([x] - (when *assert* - `(when-not ~x - (throw (ex-info (str "Assert failed: " (pr-str '~x)) {}))))) - ([x message] - (when *assert* - `(when-not ~x - (throw (ex-info (str "Assert failed: " ~message "\n" (pr-str '~x)) {})))))) - -(comment - (assert (number? :a))) - -;; ----------------------------------------------------------------------------- -;; Locking utility -;; ----------------------------------------------------------------------------- -(defn reantrant-lock? [l] - (instance? ReentrantLock l)) - -;; Shamelessly adapted from https://github.com/clojure/clojure/blob/clojure-1.12.0/src/clj/clojure/core.clj#L1662 -(defmacro lock! - [x & body] - `(let [lockee# ~x] - (assert (reantrant-lock? ~x)) - (try - (let [^ReentrantLock locklocal# lockee#] - (.lock locklocal#) - (try - ~@body - (finally - (.unlock locklocal#))))))) - -(comment - (macroexpand-1 '(lock! x (do (stuff))))) - -;; ----------------------------------------------------------------------------- -;; Other -;; ----------------------------------------------------------------------------- -(defmacro transient-> [v & body] - `(-> ~v transient ~@body persistent!)) - -(comment - (macroexpand-1 '(transient-> [] (conj! 1) (conj! 2)))) - - -(defn not-empty-string? [s] - (not (string/blank? s))) - - -(defn merge-transient! - "Merge a map `m` into a transient map `tm`. - Returns the transient map without calling [[persistent!]] on it." - [tm m] - (reduce-kv (fn [acc k v] - (assoc! acc k v)) - tm - m)) - - -(defmacro def-clone - "Little utility to clone simple var. It brings their docstring to the clone." - ([src] - (let [dest (-> src name symbol)] - `(def-clone ~dest ~src))) - ([dest src] - (let [src-var (resolve src) - doc (-> src-var meta :doc)] - `(do - (def ~dest ~(symbol src-var)) - (alter-meta! (resolve '~dest) assoc :doc ~doc) - (var ~dest))))) - diff --git a/sdk/clojure/src/bb/tasks.clj b/sdk/clojure/src/bb/tasks.clj deleted file mode 100644 index 42484c1c9..000000000 --- a/sdk/clojure/src/bb/tasks.clj +++ /dev/null @@ -1,76 +0,0 @@ -(ns tasks - (:require - [babashka.tasks :as t] - [clojure.string :as string] - [clojure.edn :as edn])) - - - -(defn prep-libs [] - (t/clojure - "-X:deps prep")) - -(defn classpath [aliases] - (apply str "-M" aliases)) - -(defn base-cli-invocation [aliases main] - (str (classpath aliases) - " -m " - main)) - -(def dev-aliases - [:test - :repl - :malli-schemas - :sdk-brotli]) - -(defn arg->kw [s] - (if (string/starts-with? s ":") - (keyword (subs s 1)) - (keyword s))) - - -(defn dev [& aliases] - (let [aliases (-> dev-aliases - (into aliases) - (into (map arg->kw *command-line-args*)))] - (println "Starting Dev repl with aliases: " aliases) - (t/clojure - (str (base-cli-invocation aliases 'nrepl.cmdline) - " --middleware \"[cider.nrepl/cider-middleware]\"")))) - - -;; ----------------------------------------------------------------------------- -;; Helpers tasks for tests -; ----------------------------------------------------------------------------- -(defn deps-aliases [] - (-> "deps.edn" - slurp - edn/read-string - :aliases)) - - -(def all-aliases (delay (deps-aliases))) - - -(defn named-paths->dirs [as] - (->> as - (mapcat #(get @all-aliases %)) - (map #(str "-d " %)))) - - -(defn lazytest-invocation [aliases named-paths args] - (string/join " " - (concat [(base-cli-invocation aliases 'lazytest.main)] - (named-paths->dirs named-paths) - args - *command-line-args*))) - - -(defn lazytest [aliases paths-aliases & args] - (t/clojure - (lazytest-invocation (into [:test] aliases) - paths-aliases - args))) - - diff --git a/sdk/clojure/src/dev/examples/animation_gzip.clj b/sdk/clojure/src/dev/examples/animation_gzip.clj deleted file mode 100644 index c8f355cd4..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip.clj +++ /dev/null @@ -1,93 +0,0 @@ -(ns examples.animation-gzip - (:require - [examples.animation-gzip.broadcast :as broadcast] - [examples.animation-gzip.handlers :as handlers] - [examples.animation-gzip.rendering :as rendering] - [examples.animation-gzip.state :as state] - [examples.common :as c] - [examples.utils :as u] - [reitit.ring :as rr] - [reitit.ring.middleware.exception :as reitit-exception] - [reitit.ring.middleware.parameters :as reitit-params] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] - [starfederation.datastar.clojure.adapter.http-kit-schemas] - [starfederation.datastar.clojure.adapter.ring :as ring-gen] - [starfederation.datastar.clojure.adapter.ring-schemas] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api-schemas] - [starfederation.datastar.clojure.brotli :as brotli])) - -;; This example let's use play with fat updates and compression -;; to get an idea of the gains compression can help use achieve -;; in terms of network usage. -(broadcast/install-watch!) - - -(defn ->routes [->sse-response opts] - [["/" handlers/home-handler] - ["/ping/:id" {:handler handlers/ping-handler - :middleware [reitit-params/parameters-middleware]}] - ["/random-10" handlers/random-pings-handler] - ["/reset" handlers/reset-handler] - ["/step1" handlers/step-handler] - ["/play" handlers/play-handler] - ["/pause" handlers/pause-handler] - ["/updates" (handlers/->updates-handler ->sse-response opts)] - ["/refresh" handlers/refresh-handler] - ["/resize" handlers/resize-handler] - c/datastar-route]) - - -(defn ->router [->sse-handler opts] - (rr/router (->routes ->sse-handler opts))) - - -(defn ->handler [->sse-response & {:as opts}] - (rr/ring-handler - (->router ->sse-response opts) - (rr/create-default-handler) - {:middleware [reitit-exception/exception-middleware]})) - - -(def handler-http-kit (->handler hk-gen/->sse-response - {hk-gen/write-profile (brotli/->brotli-profile)})) - -(def handler-ring (->handler ring-gen/->sse-response - {ring-gen/write-profile ring-gen/gzip-profile})) - -(defn after-ns-reload [] - (println "rebooting servers") - (u/reboot-hk-server! #'handler-http-kit) - (u/reboot-jetty-server! #'handler-ring {:async? true})) - - -(comment - #_{:clj-kondo/ignore true} - (user/reload!) - :help - :dbg - :rec - :stop - *e - state/!state - state/!conns - (reset! state/!conns #{}) - - (-> state/!state - deref - rendering/page) - (state/resize! 10 10) - (state/resize! 20 20) - (state/resize! 25 25) - (state/resize! 30 30) - (state/resize! 50 50) - (state/reset-state!) - (state/add-random-pings!) - (state/step-state!) - (state/start-animating!) - (u/clear-terminal!) - (u/reboot-hk-server! #'handler-http-kit) - (u/reboot-jetty-server! #'handler-ring {:async? true})) - - - diff --git a/sdk/clojure/src/dev/examples/animation_gzip/animation.clj b/sdk/clojure/src/dev/examples/animation_gzip/animation.clj deleted file mode 100644 index b8d18e0d6..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/animation.clj +++ /dev/null @@ -1,137 +0,0 @@ -(ns examples.animation-gzip.animation - (:require - [clojure.math :as math])) - - -;; ----------------------------------------------------------------------------- -;; Basic math -;; ----------------------------------------------------------------------------- -(defn point [x y] - {:x x :y y}) - -(defn distance [p1 p2] - (let [x1 (:x p1) - y1 (:y p1) - x2 (:x p2) - y2 (:y p2)] - (math/sqrt (+ - (math/pow (- x2 x1) 2) - (math/pow (- y2 y1) 2))))) - - -(defn clamp [low n high] - (min high (max low n))) - -(defn clamp-color [v] - (clamp 0 v 255)) - -(comment - (clamp-color -1) - (clamp-color 100) - (clamp-color 300)) - -;; ----------------------------------------------------------------------------- -;; State management -;; ----------------------------------------------------------------------------- -(defn next-color [current] - (case current - :r :g - :g :b - :b :r)) - - -(def starting-state - {:animator nil - :animation-tick 100 - :clock 0 - :size {:x 10 :y 10} - :color :r - :pings []}) - - - -(def default-ping-duration 20) -(def default-ping-speed 0.5) - - -(defn resize [state x y] - (assoc state :size {:x x :y y})) - - -(defn ->ping [state pos duration speed] - {:clock (:clock state) - :color (:color state) - :duration duration - :speed speed - :traveled 0 - :pos pos}) - - -(defn add-ping - ([state pos] - (add-ping state pos default-ping-duration default-ping-speed)) - ([state pos duration speed] - (-> state - (update :color next-color) - (update :pings conj (->ping state pos duration speed))))) - - -(defn add-random-pings [state n] - (let [size (:size state) - x (:x size) - y (:y size)] - (reduce - (fn [acc _] - (add-ping acc (point (inc (rand-int x)) - (inc (rand-int y))))) - state - (range n)))) - - -(defn keep-ping? [general-clock ping] - (-> (:clock ping) - (+ (:duration ping)) - (- general-clock) - pos?)) - - -(defn traveled-distance [general-clock ping-clock speed] - (let [elapsed-time (- general-clock ping-clock)] - (int (math/floor (* speed elapsed-time))))) - - -(defn update-ping [general-clock ping] - (let [c (:clock ping) - s (:speed ping)] - (assoc ping - :traveled (traveled-distance general-clock c s)))) - -(defn ->x-update-pings [general-clock] - (comp - (filter #(keep-ping? general-clock %)) - (map #(update-ping general-clock %)))) - - -(defn start-animating [state id] - (assoc state :animator id)) - - -(defn stop-animating [state] - (dissoc state :animator)) - - -(defn step-state [state] - (let [new-clock (-> state :clock inc) - pings (:pings state) - x-update-pings (->x-update-pings new-clock) - new-pings (into [] x-update-pings pings)] - (-> state - transient - (assoc! :clock new-clock - :color (next-color (:color state)) - :pings new-pings) - persistent! - (cond-> - (empty? new-pings) stop-animating)))) - - diff --git a/sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj b/sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj deleted file mode 100644 index 94af16359..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj +++ /dev/null @@ -1,27 +0,0 @@ -(ns examples.animation-gzip.broadcast - (:require - [examples.animation-gzip.rendering :as rendering] - [examples.animation-gzip.state :as state] - [starfederation.datastar.clojure.api :as d*])) - - -(defn send-frame! [sse frame] - (try - (d*/patch-elements! sse frame) - (catch Exception e - (println e)))) - - -(defn broadcast-new-frame! [frame] - (let [sses @state/!conns] - (doseq [sse sses] - (send-frame! sse frame)))) - - -(defn install-watch! [] - (add-watch state/!state ::watch - (fn [_k _ref old new] - (when-not (identical? old new) - (let [frame (rendering/render-content new)] - (broadcast-new-frame! frame)))))) - diff --git a/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj b/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj deleted file mode 100644 index 2a3e3c66e..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj +++ /dev/null @@ -1,125 +0,0 @@ -(ns examples.animation-gzip.handlers - (:require - [examples.animation-gzip.rendering :as rendering] - [examples.animation-gzip.state :as state] - [examples.utils :as u] - [ring.util.response :as ruresp] - [starfederation.datastar.clojure.adapter.common :as ac])) - - -(defn home-handler - ([_] - (ruresp/response (rendering/page @state/!state))) - ([req respond _raise] - (respond - (home-handler req)))) - - -(defn ->updates-handler - [->sse-response & {:as opts}] - (fn updates-handler - ([req] - (->sse-response req - (merge opts - {ac/on-open - (fn [sse] - (state/add-conn! sse)) - ac/on-close - (fn on-close - ([sse] - (state/remove-conn! sse)) - ([sse _status] - (on-close sse)))}))) - ([req respond _raise] - (respond - (updates-handler req))))) - - -(def id-regex #"[^-]*-(\d*)-(\d*)") - - -(defn recover-coords [req] - (when-let [[_ x y] (-> req - :path-params - :id - (->> (re-find id-regex)))] - {:x (Integer/parseInt x) - :y (Integer/parseInt y)})) - - -(defn ping-handler - ([req] - (when-let [coords (recover-coords req)] - (println "-- ping " coords) - (state/add-ping! coords)) - {:status 204}) - ([req respond _raise] - (respond (ping-handler req)))) - - -(defn random-pings-handler - ([_req] - (println "-- add pixels") - (state/add-random-pings!) - {:status 204}) - ([req respond _raise] - (respond - (random-pings-handler req)))) - - -(defn reset-handler - ([_req] - (println "-- reseting state") - (state/reset-state!) - {:status 204}) - ([req respond _raise] - (respond (reset-handler req)))) - -(defn step-handler - ([_req] - (println "-- Step 1") - (state/step-state!) - {:status 204}) - ([req respond _raise] - (respond - (step-handler req)))) - - - -(defn play-handler - ([_req] - (println "-- play animation") - (state/start-animating!) - {:status 204}) - ([req respond _raise] - (respond (play-handler req)))) - - -(defn pause-handler - ([_req] - (println "-- pause animation") - (state/stop-animating!) - {:status 204}) - ([req respond _raise] - (respond (pause-handler req)))) - -(defn resize-handler - ([req] - (let [{x "rows" y "columns"} (u/get-signals req)] - (println "-- resize" x y) - (state/resize! x y) - {:status 204})) - ([req respond _raise] - (respond - (resize-handler req)))) - - -(defn refresh-handler - ([_req] - {:status 200 - :headers {"Content-Type" "text/html"} - :body (rendering/render-content @state/!state)}) - ([req respond _raise] - (respond (refresh-handler req)))) - - diff --git a/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj b/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj deleted file mode 100644 index d68ffb765..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj +++ /dev/null @@ -1,183 +0,0 @@ -(ns examples.animation-gzip.rendering - (:require - [clojure.java.io :as io] - [clojure.math :as math] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [dom-top.core :as dt] - [examples.common :as c] - [examples.animation-gzip.animation :as animation] - [starfederation.datastar.clojure.api :as d*])) - -;; ----------------------------------------------------------------------------- -;; Rendering util -;; ----------------------------------------------------------------------------- -(defn color-level [distance] - (case (int distance) - 0 100 - 1 50 - 2 20 - 0)) - - -(defn modify-color ^long [x intensity] - (animation/clamp-color (- x intensity))) - - -(defn compute-intensity [ping pixel-position] - (let [ping-traveled-distance (:traveled ping) - cell-to-ping-distance (animation/distance pixel-position (:pos ping)) - delta (-> (- ping-traveled-distance cell-to-ping-distance) - abs math/floor int)] - (color-level delta))) - - -#_ {:clj-kondo/ignore true} -(defn cell-color [state pos] - (let [general-clock (:clock state)] - (dt/loopr [r 255 - g 255 - b 255] - [ping (:pings state)] - (let [intensity (compute-intensity ping pos)] - (if (pos? intensity) - (case (:color ping) - :r (recur r (modify-color g intensity) (modify-color b intensity)) - :g (recur (modify-color r intensity) g (modify-color b intensity)) - :b (recur (modify-color r intensity) (modify-color g intensity) b)) - (recur r g b))) - [r g b]))) - - - -;; ----------------------------------------------------------------------------- -;; Page generation -;; ----------------------------------------------------------------------------- -(def css (slurp (io/resource "examples/animation_gzip/style.css"))) - -(defn rgb [r g b] - (str "rgb(" r ", " g ", " b")")) - - -(defn cell-style [v] - (str "background-color: "v";}")) - - -(def on-click - "@get(`/ping/${event.srcElement.id}`)") - - -(defn pseudo-pixel [state x y] - (let [c (cell-color state (animation/point x y)) - id (str "px-" x "-" y)] - (hc/compile - [:div.pseudo-pixel {:style (cell-style (apply rgb c)) - :id id} - " "]))) - -(defn grid-style [state] - (let [columns (-> state :size :y)] - (str "grid-template-columns: repeat(" columns ", 1fr)"))) - - -#_ {:clj-kondo/ignore true} -(defn pseudo-canvas [state] - (let [size (:size state) - rows (:x size) - columns (:y size)] - (dt/loopr - [pc (transient [:div.pseudo-canvas {:style (grid-style state) - :data-on-click on-click}])] - [r (range 1 (inc rows)) - c (range 1 (inc columns))] - (recur (conj! pc (pseudo-pixel state r c))) - (persistent! pc)))) - - -(defn left-pane [state] - (hc/compile - [:div#left-pane - (pseudo-canvas state)])) - - - -(defn controls [state] - (hc/compile - [:div - [:h3 "Controls"] - [:ul.h-list - [:li [:button {:data-on-click (d*/sse-get "/refresh")} "refresh"]] - [:li [:button {:data-on-click (d*/sse-get "/reset")} "reset"]] - [:li [:button {:data-on-click (d*/sse-get "/random-10")} "add 10"]] - [:li [:button {:data-on-click (d*/sse-get "/step1")} "step1"]] - (if (:animator state) - (hc/compile - [:li [:button {:data-on-click (d*/sse-get "/pause")} "pause"]]) - (hc/compile - [:li [:button {:data-on-click (d*/sse-get "/play")} "play"]]))] - - [:ul.h-list {:data-signals-rows (-> state :size :x) - :data-signals-columns (-> state :size :y)} - [:li "rows: " [:input {:type "number" - :data-bind "rows" - :data-on-change (d*/sse-post "/resize")}]] - [:li "columns: " [:input {:type "number" - :data-bind "columns" - :data-on-change (d*/sse-post "/resize")}]]]])) - - -(defn log-pane [state] - (hc/compile - [:div#log-pane.stack - [:h3 "State"] - [:div.stack - [:h4 "General state"] - [:pre (pr-str (:size state))] - [:span "clock: " (:clock state)] - [:span "animator:" (:animator state)]] - - (let [pings (:pings state)] - (when (seq pings) - [:div - [:h4 "Pings"] - [:table - [:thead - [:tr - [:th "clock"] [:th "color"] [:th "duration"] [:th "speed"] - [:th "traveled"] [:th "pos"]]] - [:tbody - (for [ping pings] - (hc/compile - [:tr - [:td (:clock ping)] - [:td (:color ping)] - [:td (:duration ping)] - [:td (:speed ping)] - [:td (:traveled ping)] - [:td [:pre (pr-str (:pos ping))]]]))]]]))])) - - - -(defn right-pane [state] - [:div#right-pane.stack.center - (controls state) - (log-pane state)]) - - -(defn content [state] - [:div#main-content.center - (left-pane state) - (right-pane state)]) - - -(defn page [state] - (h/html - (c/page-scaffold - [:div {:data-on-load (d*/sse-get "/updates")} - [:style (h/raw css)] - [:h2.center "lets get something fun going"] - (content state)]))) - - -(defn render-content [state] - (h/html (content state))) diff --git a/sdk/clojure/src/dev/examples/animation_gzip/state.clj b/sdk/clojure/src/dev/examples/animation_gzip/state.clj deleted file mode 100644 index 91e2cad64..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/state.clj +++ /dev/null @@ -1,78 +0,0 @@ -(ns examples.animation-gzip.state - (:require - [examples.animation-gzip.animation :as animation])) - -;; ----------------------------------------------------------------------------- -;; Animation state -;; ----------------------------------------------------------------------------- -(defonce !state (atom animation/starting-state)) - - -(defn reset-state! [] - (reset! !state animation/starting-state)) - -(defn resize! [x y] - (swap! !state animation/resize x y)) - -(defn add-ping! - ([pos] - (swap! !state animation/add-ping pos)) - ([pos duration speed] - (swap! !state animation/add-ping pos duration speed))) - - -(defn add-random-pings! [] - (swap! !state animation/add-random-pings 10)) - - -(defn step-state! [] - (swap! !state animation/step-state)) - - -(defn- try-claiming [current-state id] - (compare-and-set! - !state - current-state - (animation/start-animating current-state id))) - - -(defn- claim-animator-job! [id] - (let [current-state @!state] - (if (:animator current-state) - :already-claimed - (if (try-claiming current-state id) - :claimed - (recur id))))) - - -(defn start-animating! [] - (let [id (random-uuid) - res (claim-animator-job! id)] - (when (= :claimed res) - (future - (loop [] - (let [state @!state] - (when (:animator state) - (step-state!) - (Thread/sleep (long (:animation-tick state))) - (recur)))))))) - - -(defn stop-animating! [] - (swap! !state animation/stop-animating)) - - -;; ----------------------------------------------------------------------------- -;; SSE connections state -;; ----------------------------------------------------------------------------- -(defonce !conns (atom #{})) - - -(defn add-conn! [sse] - (swap! !conns conj sse)) - - -(defn remove-conn! [sse] - (swap! !conns disj sse)) - - diff --git a/sdk/clojure/src/dev/examples/animation_gzip/style.css b/sdk/clojure/src/dev/examples/animation_gzip/style.css deleted file mode 100644 index 85ebe6c55..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/style.css +++ /dev/null @@ -1,59 +0,0 @@ -#main-content { - width: 70%; - display: flex; - align-items: flex-start; - justify-content: space-between; - flex-direction: row; -} - -#right-pane { - width: auto; -} - -#controls { - list-style: none; - display: flex; - flex-direction: row; -} - -.h-list { - list-style: none; - display: flex; - flex-direction: row; - gap: 5px; -} - -h2 { - width: 50%; -} - -.pseudo-canvas { - display: grid; - gap: 0; - border: 1px dotted black; -} - -.pseudo-pixel { - min-width: 10px; - width: 10px; - height: 10px; - min-height: 10px; - padding: 0; - margin: 0; - border: 0; -} - -.stack { - display: flex; - flex-direction: column; -} - -.stack > * + * { - margin-block-start: 1rem; -} - -.center { - box-sizing: content-box; - margin-inline: auto; - max-inline-size: var(--measure); -} diff --git a/sdk/clojure/src/dev/examples/broadcast_http_kit.clj b/sdk/clojure/src/dev/examples/broadcast_http_kit.clj deleted file mode 100644 index 8eb6c597f..000000000 --- a/sdk/clojure/src/dev/examples/broadcast_http_kit.clj +++ /dev/null @@ -1,57 +0,0 @@ -(ns examples.broadcast-http-kit - (:require - [examples.utils :as u] - [reitit.ring :as rr] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open on-close]])) - - -;; Tiny setup for that allows broadcasting events to several curl processes - -(defonce !conns (atom #{})) - -(defn long-connection [req] - (->sse-response req - {on-open - (fn [sse] - (swap! !conns conj sse) - (d*/console-log! sse "'connected'")) - on-close - (fn on-close [sse status-code] - (swap! !conns disj sse) - (println "Connection closed status: " status-code) - (println "remove connection from pool"))})) - - -(def routes - [["/persistent" {:handler long-connection}]]) - - -(def router - (rr/router routes)) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router - default-handler)) - -(defn broadcast-number! [n] - (doseq [conn @!conns] - (try - (d*/console-log! conn (str "n: " n)) - (catch Exception e - (println "Error: " e))))) - - - -;; open several clients: -;; curl -vv http://localhost:8080/persistent -(comment - (-> !conns deref first d*/close-sse!) - (broadcast-number! (rand-int 25)) - (u/clear-terminal!) - (u/reboot-hk-server! #'handler)) - diff --git a/sdk/clojure/src/dev/examples/broadcast_ring.clj b/sdk/clojure/src/dev/examples/broadcast_ring.clj deleted file mode 100644 index f4a83b93e..000000000 --- a/sdk/clojure/src/dev/examples/broadcast_ring.clj +++ /dev/null @@ -1,81 +0,0 @@ -(ns examples.broadcast-ring - (:require - [clojure.string :as string] - [examples.utils :as u] - [reitit.ring :as rr] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open on-close]])) - - -;; Tiny setup for that allows broadcasting events to several curl processes - -(defonce !conns (atom #{})) - - -(defn long-connection - ([req respond _raise] - (respond - (->sse-response req - {on-open - (fn [sse] - (swap! !conns conj sse) - (try - (d*/console-log! sse "'connected with jetty!'") - (catch Exception _ - (d*/close-sse! sse)))) - on-close - (fn on-close [sse] - (swap! !conns disj sse) - (println "Removed connection from pool"))})))) - - -(def routes - [["/persistent" {:handler long-connection}]]) - - -(def router - (rr/router routes)) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router - default-handler)) - -(defn broadcast-number! [n] - (doseq [conn @!conns] - (try - (d*/console-log! conn (str "n: " n)) - (catch Exception _ - (d*/close-sse! conn) - (println "closing connection"))))) - - -(defn broadcast-lines! [n] - (doseq [conn @!conns] - (try - (d*/patch-elements! conn (->> (range 0 n) - (map (fn [x] - (str "-----------------" x "-------------"))) - (string/join "\n"))) - (catch Exception _ - (d*/close-sse! conn) - (println "closing connection"))))) - - - -;; open several clients: -;; curl -vv http://localhost:8081/persistent -(comment - (-> !conns deref first d*/close-sse!) - (reset! !conns #{}) - (broadcast-number! (rand-int 25)) - (broadcast-lines! 1000) - (u/clear-terminal!) - (u/reboot-jetty-server! #'handler {:async? true :output-buffer-size 64}) - (u/reboot-jetty-server! #'handler {:async? true}) - - (u/reboot-rj9a-server! #'handler {:async? true})) - diff --git a/sdk/clojure/src/dev/examples/common.clj b/sdk/clojure/src/dev/examples/common.clj deleted file mode 100644 index 670fd7f63..000000000 --- a/sdk/clojure/src/dev/examples/common.clj +++ /dev/null @@ -1,35 +0,0 @@ -(ns examples.common - (:require - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [ring.util.response :as rur] - [starfederation.datastar.clojure.consts :as consts])) - - -(def cdn-url - (str "https://cdn.jsdelivr.net/gh/starfederation/datastar@" - consts/version - "/bundles/datastar.js")) - -(def datastar-response - (-> (slurp "../../bundles/datastar.js") - rur/response - (rur/content-type "text/javascript"))) - - -(def datastar-route - ["/datastar.js" - (fn ([_req] datastar-response) - ([_req respond _raise] (respond datastar-response)))]) - - -(defn page-scaffold [body] - (hc/compile - [[h/doctype-html5] - [:html - [:head - [:meta {:charset "UTF-8"}] - [:script {:type "module" - :src "/datastar.js"}]] - [:body body]]])) - diff --git a/sdk/clojure/src/dev/examples/data_dsl.clj b/sdk/clojure/src/dev/examples/data_dsl.clj deleted file mode 100644 index 992b55dcb..000000000 --- a/sdk/clojure/src/dev/examples/data_dsl.clj +++ /dev/null @@ -1,85 +0,0 @@ -(ns examples.data-dsl - (:require - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.api :as d*])) - -;; Examples of how one might want to build a higher level api -;; on top of the SDK - -(def example - {:event ::patch-elements - :elements "
hello
" - d*/selector "foo" - d*/patch-mode d*/pm-append - d*/use-view-transition true}) - - - -;; ----------------------------------------------------------------------------- -;; Pure version just for data-lines -;; ----------------------------------------------------------------------------- -(require '[starfederation.datastar.clojure.api.elements :as elements]) - - -(defn sse-event [e] - (case (:event e) - ::patch-elements - (elements/->patch-elements (:elements e) e))) - - -(sse-event example) -; ["selector foo" -; "mergeMode append" -; "useViewTransition true" -; "fragments
hello
"] - - -;; ----------------------------------------------------------------------------- -;; Pure version handling buffer -;; ----------------------------------------------------------------------------- -(require '[starfederation.datastar.clojure.api.sse :as sse]) - -(defn elements->str [e] - (let [buffer (StringBuilder.)] - (sse/write-event! buffer - consts/event-type-patch-elements - (elements/->patch-elements (:fragments e) e) - e) - (str buffer))) - - -(defn event->str [e] - (case (:event e) - ::patch-elements - (elements->str e))) - -(event->str example) -; "event: datastar-merge-fragments\n -; retry: 1000\n -; data: selector foo\n -; data: mergeMode append\n -; data: useViewTransition true\n -; data: fragments
hello
\n\n\n" - -;; ----------------------------------------------------------------------------- -;; Side effecting version -;; ----------------------------------------------------------------------------- -(require '[starfederation.datastar.clojure.adapter.test :as at]) - -;; SSE generator that returns the sse event string instead of sending it -(def sse-gen (at/->sse-gen)) - - - -(defn sse-event! [sse-gen e] - (case (:event e) - ::patch-elements (d*/patch-elements! sse-gen (:fragments e) e))) - - -(sse-event! sse-gen example) -; "event: datastar-merge-fragments\n -; retry: 1000\n -; data: selector foo\n -; data: mergeMode append\n -; data: useViewTransition true\n -; data: fragments
hello
\n\n\n" diff --git a/sdk/clojure/src/dev/examples/faulty_event.clj b/sdk/clojure/src/dev/examples/faulty_event.clj deleted file mode 100644 index 73fc3eee1..000000000 --- a/sdk/clojure/src/dev/examples/faulty_event.clj +++ /dev/null @@ -1,77 +0,0 @@ -(ns examples.faulty-event - (:require - [clojure.pprint :as pp] - [examples.utils :as u] - [reitit.ring :as rr] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open]])) - -;; Testing several ways exception might be caught when using a ring adapter - -(defn faulty-event - ([req] - (->sse-response req - {on-open - (fn [sse] - (d*/with-open-sse sse - (try - (d*/console-log! sse - "dummy val" - {d*/retry-duration :faulty-value}) - (catch Exception _ - (println "Caught the faulty event when sending in sync mode")))))})) - ([req respond raise] - (respond - (->sse-response req - {on-open - (fn [sse] - (d*/with-open-sse sse - (try - (d*/console-log! sse - "dummy val" - {d*/retry-duration :faulty-value}) - (catch Exception e - (raise e)))))})))) - - -(def routes - [["/error" {:handler faulty-event}]]) - - -(def router - (rr/router routes)) - - -(def wrap-print-response - {:name ::print-reponse - :wrap (fn [handler] - (fn - ([req] - (let [response (handler req)] - (pp/pprint response) - response)) - ([req respond raise] - (handler req - #(respond (do - (pp/pprint %) - %)) - #(do - (println "captured the faulty event with raise in async mode") - (raise %))))))}) - - -(def default-handler (rr/create-default-handler)) - -(def handler - (rr/ring-handler router - default-handler - {:middleware [wrap-print-response]})) - -;; curl -vv http://localhost:8081/error -(comment - (u/clear-terminal!) - (u/reboot-jetty-server! #'handler) - (u/reboot-jetty-server! #'handler {:async? true}) - (u/reboot-rj9a-server! #'handler) - (u/reboot-rj9a-server! #'handler {:async? true})) - diff --git a/sdk/clojure/src/dev/examples/form_behavior/core.clj b/sdk/clojure/src/dev/examples/form_behavior/core.clj deleted file mode 100644 index 618f734bf..000000000 --- a/sdk/clojure/src/dev/examples/form_behavior/core.clj +++ /dev/null @@ -1,141 +0,0 @@ -(ns examples.form-behavior.core - (:require - [clojure.java.io :as io] - [examples.common :as c] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [examples.utils :as u] - [ring.util.response :as rur] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as params] - [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - [starfederation.datastar.clojure.api :as d*])) - - -;; Trying out several way we might rightly and wrongly use html forms -;; TODO: still need to figure out some things - -(defn result-area [value] - (hc/compile - [:div {:id "form-result"} - [:span "From signals: " [:span {:data-text "$input1"}]] - [:br] - [:span "from backend: " [:span (str value "- " (random-uuid))]]])) - -(def style (slurp (io/resource "examples/form_behavior/style.css"))) - -(def page - (h/html - (c/page-scaffold - [:div - [:style style] - [:h2 "Form page"] - [:form {:action ""} - [:h3 "D* post form"] - [:label {:for "input1"} "Enter text"] - [:br] - - [:input {:type "text" - :id "input1" - :name "input-1" - :data-bind-input1 true}] - - - [:br] - (result-area "") - - [:h3 "Inside form"] - [:div#buttons - ;; GET - [:div - [:h4 "GET"] - [:button { :data-on-click "@get('/endpoint')"} "get - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@get('/endpoint')"} "get - no ct - type"][:br] - [:button { :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct - no type "][:br] - [:button {:type "button" :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct -type"][:br]] - - ;; POST - [:div - [:h4 "POST"] - [:button { :data-on-click "@post('/endpoint')"} "post - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@post('/endpoint')"} "post - no ct - type"][:br] - [:button { :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct - no type "][:br] - [:button {:type "button" :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct -type"][:br]] - - ;; PUT - [:div - [:h4 "PUT"] - [:button { :data-on-click "@put('/endpoint')"} "put - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@put('/endpoint')"} "put - no ct - type"][:br] - [:button { :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct - no type "][:br] - [:button {:type "button" :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct -type"][:br]] - - ;; PATCH - [:div - [:h4 "PATCH"] - [:button { :data-on-click "@patch('/endpoint')"} "patch - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@patch('/endpoint')"} "patch - no ct - type"][:br] - [:button { :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct - no type "][:br] - [:button {:type "button" :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct -type"][:br]] - - ;; DELETE - [:div - [:h4 "DELETE"] - [:button { :data-on-click "@delete('/endpoint')"} "delete - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@delete('/endpoint')"} "delete - no ct - type"][:br] - [:button { :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct - no type "][:br] - [:button {:type "button" :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct -type"][:br]]]] - [:h3 "Outside form "] - [:button - {:id "get-no-form", :data-on-click "@get('/endpoint')"} - "get no form"] - [:button - {:id "post-no-form", :data-on-click "@post('/endpoint')"} - "post no form"] - - [:br]]))) - - -(defn form - ([_] - (rur/response page)) - ([req respond _] - (respond (form req)))) - - -(defn process-form - ([request] - (let [value (or (some->> (get-in request [:params "input-1"]) - (str "Form value - ")) - (some-> request - u/get-signals - (get "input1") - (->> (str "Signal value - "))))] - (u/clear-terminal!) - (u/pp-request request) - (println "value" value) - (->sse-response request - {on-open - (fn [sse-gen] - (d*/with-open-sse sse-gen - (d*/patch-elements! sse-gen (h/html (result-area value)))))}))) - ([request respond _raise] - (respond (process-form request)))) - - -(def router - (rr/router - [["/" {:handler form}] - ["/endpoint" {:handler process-form}] - ;:parameters {:multipart true} :middleware [mpparams/multipart-middleware]}] - c/datastar-route])) - - -(def handler - (rr/ring-handler router - (rr/create-default-handler) - {:middleware [params/parameters-middleware]})) - - -(comment - (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/form_behavior/style.css b/sdk/clojure/src/dev/examples/form_behavior/style.css deleted file mode 100644 index 3bce4af4f..000000000 --- a/sdk/clojure/src/dev/examples/form_behavior/style.css +++ /dev/null @@ -1,4 +0,0 @@ -#buttons { - display: flex; - gap: 10px; -} diff --git a/sdk/clojure/src/dev/examples/forms/common.clj b/sdk/clojure/src/dev/examples/forms/common.clj deleted file mode 100644 index b770bd501..000000000 --- a/sdk/clojure/src/dev/examples/forms/common.clj +++ /dev/null @@ -1,14 +0,0 @@ -(ns examples.forms.common - (:require - [dev.onionpancakes.chassis.compiler :as hc])) - - -(defn result-area [res-from-signalsl res-from-form] - (hc/compile - [:div {:id "form-result"} - [:span "From signals: " [:span {:data-text "$input1"}]] - [:br] - [:span "from backend signals: " [:span res-from-signalsl]] - [:br] - [:span "from backend form: " [:span res-from-form]]])) - diff --git a/sdk/clojure/src/dev/examples/forms/core.clj b/sdk/clojure/src/dev/examples/forms/core.clj deleted file mode 100644 index 1d21dbf2b..000000000 --- a/sdk/clojure/src/dev/examples/forms/core.clj +++ /dev/null @@ -1,41 +0,0 @@ -(ns examples.forms.core - (:require - [examples.common :as c] - [examples.forms.html :as efh] - [examples.forms.datastar :as efd*] - [dev.onionpancakes.chassis.core :as h] - [examples.utils :as u] - [ring.util.response :as rur] - [reitit.ring :as rr])) - -;; We test here several ways to manage forms, whether the plain HTML way -;; or using D* - -(def home - (h/html - (c/page-scaffold - [[:h1 "Forms Forms Forms"] - [:ul - [:li [:a {:href "/html/get"} "html GET example"]] - [:li [:a {:href "/html/post"}"html POST example"]] - [:li [:a {:href "/datastar/get"}"html GET example"]] - [:li [:a {:href "/datastar/post"}"html POST example"]]]]))) - -(def router - (rr/router - [["/" {:handler (constantly (rur/response home))}] - ["" efh/routes] - ["" efd*/routes] - c/datastar-route])) - - - -(def handler - (rr/ring-handler router (rr/create-default-handler))) - -(defn after-ns-reload [] - (println "rebooting server") - (u/reboot-hk-server! #'handler)) - -(comment - (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/forms/datastar.clj b/sdk/clojure/src/dev/examples/forms/datastar.clj deleted file mode 100644 index 6144cf241..000000000 --- a/sdk/clojure/src/dev/examples/forms/datastar.clj +++ /dev/null @@ -1,91 +0,0 @@ -(ns examples.forms.datastar - (:require - [examples.common :as c] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [examples.utils :as u] - [ring.util.response :as rur] - [reitit.ring.middleware.parameters :as params] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) - - - - -(defn result-area [res-from-form] - (hc/compile - [:div {:id "form-result"} - [:span "From signals: " [:span {:data-text "$input1"}]] - [:br] - [:span "from backend form: " [:span res-from-form]]])) - - -(defn page-get [result] - (h/html - (c/page-scaffold - [:div - [:h2 "Html GET form"] - [:form {:action "" - :data-on-submit "@get('/datastar/get', {contentType: 'form'})"} "submit" - [:input {:type "text" - :id "input1" - :name "input1" - :data-bind-input1 true}] - [:button "submit"]] - (result-area result)]))) - - -(defn get-home [req] - (if (not (d*/datastar-request? req)) - (-> (page-get "") - (rur/response) - (rur/content-type "text/html")) - (let [v (get-in req [:params "input1"])] - (u/clear-terminal!) - (u/pp-request req) - (println "got here " v) - (->sse-response req - {on-open - (fn [sse] - (d*/with-open-sse sse - (d*/patch-elements! sse (h/html (result-area v)))))})))) - - -(defn page-post [result] - (h/html - (c/page-scaffold - [:div - [:h2 "Html POST form !!!!"] - [:form {:action "" :data-on-submit "@post('/datastar/post', {contentType: 'form'})"} - [:input {:type "text" - :id "input1" - :name "input1" - :data-bind-input1 true}] - [:button "submit"]] - (result-area result)]))) - - -(defn post-home [req] - (if (not (d*/datastar-request? req)) - (rur/response (page-post "")) - (let [v (get-in req [:params "input1"])] - (u/clear-terminal!) - (u/pp-request req) - (->sse-response req - {on-open - (fn [sse] - (d*/with-open-sse sse - (d*/patch-elements! sse (h/html (result-area v)))))})))) - - - - -(def routes - ["/datastar" - ["/get" {:handler #'get-home}] - ;:middleware [params/parameters-middleware]}] - ["/post" {:handler #'post-home - :middleware [params/parameters-middleware]}]]) - - - diff --git a/sdk/clojure/src/dev/examples/forms/html.clj b/sdk/clojure/src/dev/examples/forms/html.clj deleted file mode 100644 index e52ce106c..000000000 --- a/sdk/clojure/src/dev/examples/forms/html.clj +++ /dev/null @@ -1,63 +0,0 @@ -(ns examples.forms.html - (:require - [examples.common :as c] - [dev.onionpancakes.chassis.core :as h] - [examples.utils :as u] - [ring.util.response :as rur] - [reitit.ring.middleware.parameters :as params])) - - - -(defn page-get [result] - (h/html - (c/page-scaffold - [:div - [:h2 "Html GET form"] - [:form {:action "" :method "GET"} - [:input {:type "text" - :id "input1" - :name "input1" - :data-bind-input1 true}] - [:button "submit"]] - [:div result]]))) - -(defn get-home [req] - (let [v (get-in req [:params "input1"])] - (u/clear-terminal!) - (u/?req req) - (println "got here " v) - (rur/response (page-get v)))) - - -(defn page-post [result] - (h/html - (c/page-scaffold - [:div - [:h2 "Html POST form !!!!"] - [:form {:action "" :method "POST"} - [:input {:type "text" - :id "input1" - :name "input1" - :data-bind-input1 true}] - [:button "submit"]] - [:div result]]))) - - -(defn post-home [req] - (let [v (get-in req [:params "input1"])] - (u/clear-terminal!) - (u/?req req) - (println "got here " v) - (rur/response (page-post v)))) - - - -(def routes - ["/html" - ["/get" {:handler #'get-home - :middleware [params/parameters-middleware]}] - ["/post" {:handler #'post-home - :middleware [params/parameters-middleware]}]]) - - - diff --git a/sdk/clojure/src/dev/examples/http_kit2/animation.clj b/sdk/clojure/src/dev/examples/http_kit2/animation.clj deleted file mode 100644 index 4ba04432a..000000000 --- a/sdk/clojure/src/dev/examples/http_kit2/animation.clj +++ /dev/null @@ -1,94 +0,0 @@ -(ns examples.http-kit2.animation - (:require - [examples.animation-gzip.broadcast :as broadcast] - [examples.animation-gzip.handlers :as handlers] - [examples.animation-gzip.rendering :as rendering] - [examples.animation-gzip.state :as state] - [examples.common :as c] - [examples.utils :as u] - [reitit.ring :as rr] - [reitit.ring.middleware.exception :as reitit-exception] - [reitit.ring.middleware.parameters :as reitit-params] - [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen] - [starfederation.datastar.clojure.adapter.http-kit-schemas] - [starfederation.datastar.clojure.adapter.ring :as ring-gen] - [starfederation.datastar.clojure.adapter.ring-schemas] - [starfederation.datastar.clojure.api-schemas] - [starfederation.datastar.clojure.brotli :as brotli])) - -;; This example let's use play with fat updates and compression -;; to get an idea of the gains compression can help use achieve -;; in terms of network usage. - -(broadcast/install-watch!) - - -(defn ->routes [->sse-response opts] - [["/" handlers/home-handler] - ["/ping/:id" {:handler handlers/ping-handler - :middleware [reitit-params/parameters-middleware]}] - ["/random-10" handlers/random-pings-handler] - ["/reset" handlers/reset-handler] - ["/step1" handlers/step-handler] - ["/play" handlers/play-handler] - ["/pause" handlers/pause-handler] - ["/updates" {:handler (handlers/->updates-handler ->sse-response opts) - :middleware [[hk-gen/start-responding-middleware]]}] - ["/refresh" handlers/refresh-handler] - ["/resize" handlers/resize-handler] - c/datastar-route]) - - -(defn ->router [->sse-handler opts] - (rr/router (->routes ->sse-handler opts))) - - -(defn ->handler [->sse-response & {:as opts}] - (rr/ring-handler - (->router ->sse-response opts) - (rr/create-default-handler) - {:middleware [reitit-exception/exception-middleware]})) - - -(def handler-http-kit (->handler hk-gen/->sse-response - {hk-gen/write-profile (brotli/->brotli-profile)})) - -(def handler-ring (->handler ring-gen/->sse-response - {ring-gen/write-profile ring-gen/gzip-profile})) - -(defn after-ns-reload [] - (println "rebooting servers") - (u/reboot-hk-server! #'handler-http-kit) - (u/reboot-jetty-server! #'handler-ring {:async? true})) - - -(comment - #_{:clj-kondo/ignore true} - (user/reload!) - :help - :dbg - :rec - :stop - *e - state/!state - state/!conns - (reset! state/!conns #{}) - - (-> state/!state - deref - rendering/page) - (state/resize! 10 10) - (state/resize! 20 20) - (state/resize! 25 25) - (state/resize! 30 30) - (state/resize! 50 50) - (state/reset-state!) - (state/add-random-pings!) - (state/step-state!) - (state/start-animating!) - (u/clear-terminal!) - (u/reboot-hk-server! #'handler-http-kit) - (u/reboot-jetty-server! #'handler-ring {:async? true})) - - - diff --git a/sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj b/sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj deleted file mode 100644 index 6f708ed26..000000000 --- a/sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj +++ /dev/null @@ -1,138 +0,0 @@ -(ns examples.http-kit2.form-behavior - (:require - [clojure.java.io :as io] - [examples.common :as c] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [examples.utils :as u] - [ring.util.response :as rur] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as params] - [starfederation.datastar.clojure.adapter.http-kit2 :refer [->sse-response on-open start-responding-middleware]] - [starfederation.datastar.clojure.api :as d*])) - - -;; Trying out several way we might rightly and wrongly use html forms -;; TODO: still need to figure out some things - -(defn result-area [value] - (hc/compile - [:div {:id "form-result"} - [:span "From signals: " [:span {:data-text "$input1"}]] - [:br] - [:span "from backend: " [:span (str value "- " (random-uuid))]]])) - -(def style (slurp (io/resource "examples/form_behavior/style.css"))) - -(def page - (h/html - (c/page-scaffold - [:div - [:style style] - [:h2 "Form page"] - [:form {:action ""} - [:h3 "D* post form"] - [:label {:for "input1"} "Enter text"] - [:br] - - [:input {:type "text" - :id "input1" - :name "input-1" - :data-bind-input1 true}] - - - [:br] - (result-area "") - - [:h3 "Inside form"] - [:div#buttons - ;; GET - [:div - [:h4 "GET"] - [:button { :data-on-click "@get('/endpoint')"} "get - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@get('/endpoint')"} "get - no ct - type"][:br] - [:button { :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct - no type "][:br] - [:button {:type "button" :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct -type"][:br]] - - ;; POST - [:div - [:h4 "POST"] - [:button { :data-on-click "@post('/endpoint')"} "post - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@post('/endpoint')"} "post - no ct - type"][:br] - [:button { :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct - no type "][:br] - [:button {:type "button" :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct -type"][:br]] - - ;; PUT - [:div - [:h4 "PUT"] - [:button { :data-on-click "@put('/endpoint')"} "put - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@put('/endpoint')"} "put - no ct - type"][:br] - [:button { :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct - no type "][:br] - [:button {:type "button" :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct -type"][:br]] - - ;; PATCH - [:div - [:h4 "PATCH"] - [:button { :data-on-click "@patch('/endpoint')"} "patch - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@patch('/endpoint')"} "patch - no ct - type"][:br] - [:button { :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct - no type "][:br] - [:button {:type "button" :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct -type"][:br]] - - ;; DELETE - [:div - [:h4 "DELETE"] - [:button { :data-on-click "@delete('/endpoint')"} "delete - no ct - no type"][:br] - [:button {:type "button" :data-on-click "@delete('/endpoint')"} "delete - no ct - type"][:br] - [:button { :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct - no type "][:br] - [:button {:type "button" :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct -type"][:br]]]] - [:h3 "Outside form "] - [:button - {:id "get-no-form", :data-on-click "@get('/endpoint')"} - "get no form"] - [:button - {:id "post-no-form", :data-on-click "@post('/endpoint')"} - "post no form"] - - [:br]]))) - - -(defn form - ([_] - (rur/response page)) - ([req respond _] - (respond (form req)))) - - -(defn process-form - ([request] - (let [value (or (some->> (get-in request [:params "input-1"]) - (str "Form value - ")) - (some-> request - u/get-signals - (get "input1") - (->> (str "Signal value - "))))] - (->sse-response request - {on-open - (fn [sse-gen] - (d*/with-open-sse sse-gen - (d*/patch-elements! sse-gen (h/html (result-area value)))))}))) - ([request respond _raise] - (respond (process-form request)))) - - -(def router - (rr/router - [["/" {:handler form}] - ["/endpoint" {:handler process-form - :middleware [start-responding-middleware]}] - c/datastar-route])) - - -(def handler - (rr/ring-handler router - (rr/create-default-handler) - {:middleware [params/parameters-middleware]})) - - -(comment - (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/http_kit_disconnect.clj b/sdk/clojure/src/dev/examples/http_kit_disconnect.clj deleted file mode 100644 index bf9e7e8bd..000000000 --- a/sdk/clojure/src/dev/examples/http_kit_disconnect.clj +++ /dev/null @@ -1,58 +0,0 @@ -(ns examples.http-kit-disconnect - (:require - [examples.utils :as u] - [reitit.ring :as rr] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open on-close]])) - - -;; This is a small experiment to determine the behaviour of -;; Http-kit in the face of the client disconnecting -;; Http-kit somehow detects closed connections on it's own - -(def !conn (atom nil)) - -(defn long-connection [req] - (->sse-response req - {on-open - (fn [sse] - (reset! !conn sse) - (d*/console-log! sse "'connected'")) - on-close - (fn on-close [_ status-code] - (println "-----------------") - (println "Connection closed status: " status-code) - (println "-----------------"))})) - - -(def routes - [["/persistent" {:handler long-connection}]]) - - -(def router - (rr/router routes)) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router - default-handler)) - -(defn send-tiny-event! [] - (d*/console-log! @!conn "'toto'")) - - -;; curl -vv http://localhost:8080/persistent -(comment - (-> !conn deref d*/close-sse!) - (send-tiny-event!) - (d*/console-log! @!conn "'toto'") - - (u/clear-terminal!) - (u/reboot-hk-server! #'handler)) - - - - diff --git a/sdk/clojure/src/dev/examples/jetty_disconnect.clj b/sdk/clojure/src/dev/examples/jetty_disconnect.clj deleted file mode 100644 index 87ce7ddcb..000000000 --- a/sdk/clojure/src/dev/examples/jetty_disconnect.clj +++ /dev/null @@ -1,75 +0,0 @@ -(ns examples.jetty-disconnect - (:require - [examples.utils :as u] - [reitit.ring :as rr] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open on-close]])) - - -;; This is a small experiment to determine the behaviour of -;; ring jetty in the face of the client disconnecting - - -;; 2 tiny events to detect a lost connection or 1 big event -;; Jetty internal buffer has an impact - -(def !conn (atom nil)) - -(def big-message - (let [b (StringBuilder.)] - (doseq [i (range 10000)] - (doto ^StringBuilder b - (.append (str "-------------" i "-----------------\n")))) - (str b))) - - -(defn long-connection [req respond raise] - (try - (respond - (->sse-response req - {on-open - (fn [sse] - (reset! !conn sse) - (d*/console-log! sse "'connected'")) - on-close - (fn [_sse] - (println "Connection lost detected") - (reset! !conn nil))})) - (catch Exception e - (raise e)))) - - -(def routes - [["/persistent" {:handler long-connection}]]) - - -(def router - (rr/router routes)) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router - default-handler)) - -(defn send-tiny-event! [] - (d*/console-log! @!conn "'toto'")) - - -(defn send-big-event! [] - (d*/patch-elements! @!conn big-message)) - -;; curl -vv http://localhost:8081/persistent -(comment - (-> !conn deref d*/close-sse!) - (send-tiny-event!) - (send-big-event!) - - (u/clear-terminal!) - (u/reboot-jetty-server! #'handler {:async? true})) - - - - diff --git a/sdk/clojure/src/dev/examples/malli.clj b/sdk/clojure/src/dev/examples/malli.clj deleted file mode 100644 index f4496d4a5..000000000 --- a/sdk/clojure/src/dev/examples/malli.clj +++ /dev/null @@ -1,35 +0,0 @@ -(ns examples.malli - (:require - [malli.core :as m] - [malli.instrument :as mi] - [malli.dev :as mdev] - [starfederation.datastar.clojure.adapter.test :as at] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api.elements :as e] - [starfederation.datastar.clojure.api.elements-schemas] - [starfederation.datastar.clojure.api.common :as c])) - -;; Testing how instrumentation works and how it's activated -(comment - m/-instrument - (mi/instrument!) - (mi/unstrument!) - (mdev/start!) - (mdev/start! {:exception true}) - (mdev/stop!)) - -(comment - (d*/patch-elements! {} "frag") - (d*/patch-elements! (at/->sse-gen) "frag") - (e/->patch-elements "f" {c/retry-duration :a}) - (e/->patch-elements "f" {c/retry-duration 1022})) - - -(comment - #_{:clj-kondo/ignore true} - (user/reload!) - :help - :dbg - :rec - :stop - :debug) diff --git a/sdk/clojure/src/dev/examples/multiple_fragments.clj b/sdk/clojure/src/dev/examples/multiple_fragments.clj deleted file mode 100644 index 42e46c8a8..000000000 --- a/sdk/clojure/src/dev/examples/multiple_fragments.clj +++ /dev/null @@ -1,81 +0,0 @@ -(ns examples.multiple-fragments - (:require - [examples.common :as c] - [examples.utils :as u] - [dev.onionpancakes.chassis.core :as h] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as reitit-params] - [ring.util.response :as ruresp] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] - [starfederation.datastar.clojure.adapter.ring :as ring-gen])) - - -;; Testing the sending of multiple fragments at once - -(defn res [id val] - [:span {:id id} val]) - - -(def page - (h/html - (c/page-scaffold - [[:h1 "Test page"] - [:input {:type "text" :data-bind-input true}] - [:button {:data-on-click (d*/sse-get "/endpoint")} "Send input"] - [:br] - [:span {:data-text "$input"}] - [:br] - [:div "res: " (res "res-1" "")] - [:div "duplicate res: " (res "res-2" "")]]))) - - -(defn home [_] - (ruresp/response page)) - - -(defn ->elements [input-val] - [(h/html (res "res-1" input-val)) - (h/html (res "res-2" input-val))]) - - -(defn ->endpoint[->sse-response] - (fn [req] - (let [signals (u/get-signals req) - input-val (get signals "input")] - (->sse-response req - {ac/on-open - (fn [sse] - (d*/with-open-sse sse - (d*/patch-elements-seq! sse (->elements input-val))))})))) - - -(defn ->router [->sse-response] - (rr/router - [["/" {:handler home}] - ["/endpoint" {:handler (->endpoint ->sse-response)}] - c/datastar-route])) - - -(def default-handler (rr/create-default-handler)) - - -(defn ->handler [->sse-response] - (rr/ring-handler (->router ->sse-response) - default-handler - {:middleware [reitit-params/parameters-middleware]})) - - - -(def handler-hk (->handler hk-gen/->sse-response)) -(def handler-ring (->handler ring-gen/->sse-response)) - - -(comment - :dbg - :rec - (u/clear-terminal!) - (u/reboot-hk-server! handler-hk) - (u/reboot-rj9a-server! #'handler-ring)) - diff --git a/sdk/clojure/src/dev/examples/redirect.clj b/sdk/clojure/src/dev/examples/redirect.clj deleted file mode 100644 index cb1109405..000000000 --- a/sdk/clojure/src/dev/examples/redirect.clj +++ /dev/null @@ -1,68 +0,0 @@ -(ns examples.redirect - (:require - [examples.common :as c] - [examples.utils :as u] - [dev.onionpancakes.chassis.core :refer [html]] - [reitit.ring :as rr] - [ring.util.response :as ruresp] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) - -;; Redirection example - -(def home-page - (html - (c/page-scaffold - [[:h1 "Test page"] - [:div.#indicator - [:button {:data-on-click (d*/sse-get "/redirect-me")} - "Start redirect"]]]))) - - -(defn home [_] - (ruresp/response home-page)) - - -(def guide-page - (html - (c/page-scaffold - [[:h1 "You have been redirected"] - [:a {:href "/" } "Home"]]))) - - -(defn guide [_] - (ruresp/response guide-page)) - - -(defn redirect-handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (d*/patch-elements! sse - (html [:div#indicator "Redirecting in 3 seconds..."])) - (Thread/sleep 3000) - (d*/redirect! sse "/guide") - (d*/close-sse! sse))})) - - - - -(def router (rr/router - [["/" {:handler home}] - ["/guide" {:handler guide}] - ["/redirect-me" {:handler redirect-handler}] - c/datastar-route])) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router default-handler)) - - - -(comment - (u/reboot-hk-server! #'handler)) - - diff --git a/sdk/clojure/src/dev/examples/remove_fragments.clj b/sdk/clojure/src/dev/examples/remove_fragments.clj deleted file mode 100644 index 9c5fb4797..000000000 --- a/sdk/clojure/src/dev/examples/remove_fragments.clj +++ /dev/null @@ -1,88 +0,0 @@ -(ns dev.examples.remove-fragments - (:require - [examples.common :as c] - [examples.utils :as u] - [dev.onionpancakes.chassis.core :as h] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as reitit-params] - [ring.util.response :as ruresp] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) - - -;; Appending and removing fragments with the D* api - -(def page - (h/html - (c/page-scaffold - [[:h1 "Test page"] - [:input {:type "text" :data-bind-input true :required true}] - [:button {:data-attr-disabled "!$input" - :data-on-click (str (d*/sse-get "/add-fragment") - "; $input = ''")} - "Send input"] - [:br] - [:ul {:id "list"}]]))) - - -(defn home [_] - (ruresp/response page)) - - -(defonce !counter (atom 0)) - - -(defn id! [] - (-> !counter - (swap-vals! inc) - first - (->> (str "id-")))) - -(defn ->fragment [id val] - (h/html - [:li {:id id} - val - [:button {:data-on-click (d*/sse-post (str "/remove-fragment/" id))} "remove me"]])) - - -(defn add-element [req] - (let [signals (u/get-signals req) - input-val (get signals "input")] - (hk-gen/->sse-response req - {hk-gen/on-open - (fn [sse] - (d*/with-open-sse sse - (d*/patch-elements! sse - (->fragment (id!) input-val) - {d*/selector "#list" - d*/patch-mode d*/pm-append})))}))) - - -(defn remove-element [req] - (let [id (-> req :path-params :id)] - (hk-gen/->sse-response req - {hk-gen/on-open - (fn [sse-gen] - (d*/with-open-sse sse-gen - (d*/remove-element! sse-gen (str "#" id))))}))) - - -(def router (rr/router - [["/" {:handler #'home}] - ["/add-fragment" {:handler #'add-element}] - ["/remove-fragment/:id" {:handler #'remove-element}] - c/datastar-route])) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router - default-handler - {:middleware [reitit-params/parameters-middleware]})) - - -(comment - (u/reboot-hk-server! handler)) - diff --git a/sdk/clojure/src/dev/examples/scripts.clj b/sdk/clojure/src/dev/examples/scripts.clj deleted file mode 100644 index ef125ac3a..000000000 --- a/sdk/clojure/src/dev/examples/scripts.clj +++ /dev/null @@ -1,52 +0,0 @@ -(ns examples.scripts - (:require - [examples.common :as c] - [examples.utils :as u] - [dev.onionpancakes.chassis.core :as h] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as reitit-params] - [ring.util.response :as ruresp] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) - -;; Sending scripts and playing with auto-remove - -(def page - (h/html - (c/page-scaffold - [[:h1 "Test page"] - [:button {:data-on-click (d*/sse-get "/endpoint")} - "Say hello!"]]))) - - -(defn home [_] - (ruresp/response page)) - - -(defn endpoint [req] - (hk-gen/->sse-response req - {hk-gen/on-open - (fn [sse] - (d*/with-open-sse sse - (d*/execute-script! sse - "console.log('hello')" - {d*/auto-remove false})))})) - - -(def router (rr/router - [["/" {:handler home}] - ["/endpoint" {:handler endpoint}] - c/datastar-route])) - - -(def default-handler (rr/create-default-handler)) - - -(def handler - (rr/ring-handler router - default-handler - {:middleware [reitit-params/parameters-middleware]})) - - -(comment - (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/snippets.clj b/sdk/clojure/src/dev/examples/snippets.clj deleted file mode 100644 index 98fab68d9..000000000 --- a/sdk/clojure/src/dev/examples/snippets.clj +++ /dev/null @@ -1,53 +0,0 @@ -(ns examples.snippets - (:require - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :refer [on-open]] - [starfederation.datastar.clojure.adapter.test :as at :refer [->sse-response]])) - - -;; Snippets used in the website docs - -(def sse (at/->sse-gen)) - -;; multiple_events -(d*/patch-elements! sse "
...
") -(d*/patch-elements! sse "
...
") -(d*/patch-signals! sse "{answer: '...'}") -(d*/patch-signals! sse "{prize: '...'}") - -;; setup -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) - - -(defn handler [request] - (->sse-response request - {on-open - (fn [sse] - (d*/patch-elements! sse - "
What do you put in a toaster?
") - - (d*/patch-signals! sse "{response: '', answer: 'bread'}"))})) - -(comment - (handler {})) - - ;; multiple_events going deeper -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) - - -#_{:clj-kondo/ignore true} -(defn handler [request] - (->sse-response request - {on-open - (fn [sse] - (d*/patch-elements! sse "
Hello, world!
") - (d*/patch-signals! sse "{foo: {bar: 1}}") - (d*/execute-script! sse "console.log('Success!')"))})) diff --git a/sdk/clojure/src/dev/examples/snippets/load_more.clj b/sdk/clojure/src/dev/examples/snippets/load_more.clj deleted file mode 100644 index 215ae5501..000000000 --- a/sdk/clojure/src/dev/examples/snippets/load_more.clj +++ /dev/null @@ -1,56 +0,0 @@ -#_{:clj-kondo/ignore true} -(ns examples.snippets.load-more - (:require - [dev.onionpancakes.chassis.core :refer [html]] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :refer [on-open]] - [starfederation.datastar.clojure.adapter.test :refer [->sse-response]] - [charred.api :as charred])) - - -(def ^:private bufSize 1024) -(def read-json-str (charred/parse-json-fn {:async? false :bufsize bufSize})) - -(def write-json-str charred/write-json-str) - - - -#_{:clj-kondo/ignore true} -(comment - (require - '[charred.api :as charred] - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - '[some.hiccup.library :refer [html]] - '[some.json.library :refer [read-json-str write-json-str]])) - - -(def max-offset 5) - -(defn handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (let [d*-signals (-> ring-request d*/get-signals read-json-str) - offset (get d*-signals "offset") - limit 1 - new-offset (+ offset limit)] - - (d*/patch-elements! sse - (html [:div "Item " new-offset]) - {d*/selector "#list" - d*/patch-mode d*/pm-append}) - - (if (< new-offset max-offset) - (d*/patch-signals! sse (write-json-str {"offset" new-offset})) - (d*/remove-element! sse "#load-more")) - - (d*/close-sse! sse)))})) - - -(comment - (handler {:request-method :get :query-params {"datastar" "{\"offset\": 1}"}}) - (handler {:request-method :get :query-params {"datastar" "{\"offset\": 2}"}}) - (handler {:request-method :get :query-params {"datastar" "{\"offset\": 3}"}}) - (handler {:request-method :get :query-params {"datastar" "{\"offset\": 4}"}})) - diff --git a/sdk/clojure/src/dev/examples/snippets/polling1.clj b/sdk/clojure/src/dev/examples/snippets/polling1.clj deleted file mode 100644 index b76e65b50..000000000 --- a/sdk/clojure/src/dev/examples/snippets/polling1.clj +++ /dev/null @@ -1,35 +0,0 @@ -#_{:clj-kondo/ignore true} -(ns examples.snippets.polling1 - (:require - [dev.onionpancakes.chassis.core :refer [html]] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :refer [on-open]] - [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) - - -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - '[some.hiccup.library :refer [html]])) - -(import - 'java.time.format.DateTimeFormatter - 'java.time.LocalDateTime) - -(def formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) - -(defn handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (d*/patch-elements! sse - (html [:div#time {:data-on-interval__duration.5s (d*/sse-get "/endpoint")} - (LocalDateTime/.format (LocalDateTime/now) formatter)])) - (d*/close-sse! sse))})) - -(comment - (handler {})) - - diff --git a/sdk/clojure/src/dev/examples/snippets/polling2.clj b/sdk/clojure/src/dev/examples/snippets/polling2.clj deleted file mode 100644 index 12b5c7ce2..000000000 --- a/sdk/clojure/src/dev/examples/snippets/polling2.clj +++ /dev/null @@ -1,44 +0,0 @@ -#_{:clj-kondo/ignore true} -(ns examples.snippets.polling2 - (:require - [dev.onionpancakes.chassis.core :refer [html]] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :refer [on-open]] - [starfederation.datastar.clojure.adapter.test :as at :refer [->sse-response]])) - -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - '[some.hiccup.library :refer [html]])) - -(import - 'java.time.format.DateTimeFormatter - 'java.time.LocalDateTime) - -(def date-time-formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) -(def seconds-formatter (DateTimeFormatter/ofPattern "ss")) - -(defn handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (let [now (LocalDateTime/now) - current-time (LocalDateTime/.format now date-time-formatter) - seconds (LocalDateTime/.format now seconds-formatter) - duration (if (neg? (compare seconds "50")) - "5" - "1")] - (d*/patch-elements! sse - (html [:div#time {(str "data-on-interval__duration." duration "s") - (d*/sse-get "/endpoint")} - current-time])) - - (d*/close-sse! sse)))})) - - -(comment - (handler {})) - - diff --git a/sdk/clojure/src/dev/examples/snippets/redirect1.clj b/sdk/clojure/src/dev/examples/snippets/redirect1.clj deleted file mode 100644 index f5e1716f9..000000000 --- a/sdk/clojure/src/dev/examples/snippets/redirect1.clj +++ /dev/null @@ -1,31 +0,0 @@ -#_{:clj-kondo/ignore true} -(ns examples.snippets.redirect1 - (:require - [dev.onionpancakes.chassis.core :refer [html]] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.test :refer [->sse-response]] - [starfederation.datastar.clojure.adapter.common :refer [on-open]])) - - -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - '[some.hiccup.library :refer [html]])) - - -(defn handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (d*/patch-elements! sse - (html [:div#indicator "Redirecting in 3 seconds..."])) - (Thread/sleep 3000) - (d*/execute-script! sse "window.location = \"/guide\"") - (d*/close-sse! sse))})) - -(comment - (handler {})) - - diff --git a/sdk/clojure/src/dev/examples/snippets/redirect2.clj b/sdk/clojure/src/dev/examples/snippets/redirect2.clj deleted file mode 100644 index 22c7ddede..000000000 --- a/sdk/clojure/src/dev/examples/snippets/redirect2.clj +++ /dev/null @@ -1,33 +0,0 @@ -#_{:clj-kondo/ignore true} -(ns examples.snippets.redirect2 - (:require - [dev.onionpancakes.chassis.core :refer [html]] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :refer [on-open]] - [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) - - -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - '[some.hiccup.library :refer [html]])) - - -(defn handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (d*/patch-elements! sse - (html [:div#indicator "Redirecting in 3 seconds..."])) - (Thread/sleep 3000) - (d*/execute-script! sse - "setTimeout(() => window.location = \"/guide\"") - (d*/close-sse! sse))})) - - -(comment - (handler {})) - - diff --git a/sdk/clojure/src/dev/examples/snippets/redirect3.clj b/sdk/clojure/src/dev/examples/snippets/redirect3.clj deleted file mode 100644 index f76c42f5a..000000000 --- a/sdk/clojure/src/dev/examples/snippets/redirect3.clj +++ /dev/null @@ -1,32 +0,0 @@ -#_{:clj-kondo/ignore true} -(ns examples.snippets.redirect3 - (:require - [dev.onionpancakes.chassis.core :refer [html]] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :refer [on-open]] - [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) - - -#_{:clj-kondo/ignore true} -(comment - (require - '[starfederation.datastar.clojure.api :as d*] - '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - '[some.hiccup.library :refer [html]])) - - -(defn handler [ring-request] - (->sse-response ring-request - {on-open - (fn [sse] - (d*/patch-elements! sse - (html [:div#indicator "Redirecting in 3 seconds..."])) - (Thread/sleep 3000) - (d*/redirect! sse "/guide") - (d*/close-sse! sse))})) - - -(comment - (handler {})) - - diff --git a/sdk/clojure/src/dev/examples/tiny_gzip.clj b/sdk/clojure/src/dev/examples/tiny_gzip.clj deleted file mode 100644 index c9203f001..000000000 --- a/sdk/clojure/src/dev/examples/tiny_gzip.clj +++ /dev/null @@ -1,124 +0,0 @@ -(ns examples.tiny-gzip - (:require - [examples.common :as c] - [examples.utils :as u] - [examples.animation-gzip.brotli :as brotli] - [dev.onionpancakes.chassis.core :as h] - [ring.util.response :as ruresp] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as reitit-params] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] - [starfederation.datastar.clojure.adapter.ring :as ring-gen] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.api :as d*])) - - -;; Here we try to use compression on little update to see if some -;; server buffer holds updates for short fragments. -;; It doesn't seem to be the case, compressing tiny events seems to work fine - - -(defonce !current-val (atom nil)) -(defonce !sses (atom #{})) - - -(defn render-val [current-val] - [:span#val current-val]) - - -(defn page [current-val] - (h/html - (c/page-scaffold - [:div#page {:data-on-load (d*/sse-get "/updates")} - [:h1 "Test page"] - [:input {:type "text" - :data-bind-input true - :data-on-input__debounce.100ms(d*/sse-get "/change-val")}] - [:br] - [:div - (render-val current-val)]]))) - - -(defn home - ([_] - (ruresp/response (page @!current-val))) - ([req respond _] - (respond (home req)))) - -(defn send-val! [sse v] - (try - (d*/patch-elements! sse (h/html (render-val v))) - (catch Exception e - (println e)))) - -(defn broadcast-new-val! [sses v] - (doseq [sse sses] - (send-val! sse v))) - - -(add-watch !current-val ::watch - (fn [_key _ref _old new] - (broadcast-new-val! @!sses new))) - - -(defn ->change-val [->sse-response] - (fn change-val - ([req] - (let [signals (u/get-signals req) - input-val (get signals "input")] - (->sse-response req - {:status 204 - ac/on-open - (fn [sse] - (d*/with-open-sse sse - (reset! !current-val input-val)))}))) - ([req respond _raise] - (respond (change-val req))))) - - -(defn ->updates[->sse-response opts] - (fn updates - ([req] - (->sse-response req - (merge opts - {ac/on-open - (fn [sse] - (swap! !sses conj sse)) - ac/on-close - (fn [sse & _args] - (swap! !sses disj sse))}))) - ([req respond _raise] - (respond (updates req))))) - - -(defn ->router [->sse-response opts] - (rr/router - [["/" {:handler home}] - ["/change-val" {:handler (->change-val ->sse-response) - :middleware [reitit-params/parameters-middleware]}] - ["/updates" {:handler (->updates ->sse-response opts)}] - c/datastar-route])) - -(def default-handler (rr/create-default-handler)) - - -(defn ->handler [->sse-response & {:as opts}] - (rr/ring-handler (->router ->sse-response opts) - default-handler)) - - - -(def handler-hk (->handler hk-gen/->sse-response - hk-gen/write-profile hk-gen/gzip-profile)) -(def handler-ring (->handler ring-gen/->sse-response - ring-gen/write-profile ring-gen/gzip-profile)) - -(comment - :dbg - :rec - (u/clear-terminal!) - !sses - (reset! !sses #{}) - (u/reboot-hk-server! #'handler-hk) - (u/reboot-jetty-server! #'handler-ring {:async? true})) - diff --git a/sdk/clojure/src/dev/examples/utils.clj b/sdk/clojure/src/dev/examples/utils.clj deleted file mode 100644 index e1b190c63..000000000 --- a/sdk/clojure/src/dev/examples/utils.clj +++ /dev/null @@ -1,110 +0,0 @@ -(ns examples.utils - (:require - [charred.api :as charred] - [fireworks.core :refer [?]] - [puget.printer :as pp] - [starfederation.datastar.clojure.api :as d*])) - - -;; ----------------------------------------------------------------------------- -;; Misc utils -;; ----------------------------------------------------------------------------- -(defn clear-terminal! [] - (binding [*out* (java.io.PrintWriter. System/out)] - (print "\033c") - (flush))) - - -(defmacro force-out [& body] - `(binding [*out* (java.io.OutputStreamWriter. System/out)] - ~@body)) - - -(defn pp-request [req] - (-> req - (dissoc :reitit.core/match :reitit.core/router) - pp/pprint - pp/with-color)) - - -(defn ?req [req] - (? (dissoc req :reitit.core/match :reitit.core/router))) - - -(def ^:private bufSize 1024) -(def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) - - - -(defn get-signals [req] - (some-> req d*/get-signals read-json)) - - -(defn rr [sym] - (try - (requiring-resolve sym) - (catch Exception _ - nil))) - -;; ----------------------------------------------------------------------------- -;; httpkit server -;; ----------------------------------------------------------------------------- -(defonce !hk-server (atom nil)) - -(def http-kit-run! (rr 'org.httpkit.server/run-server)) -(def http-kit-stop! (rr 'org.httpkit.server/server-stop!)) - -(defn reboot-hk-server! [handler] - (if-not http-kit-run! - (println "http kit isn't in the classpath") - (swap! !hk-server - (fn [server] - (when server - (http-kit-stop! server)) - (http-kit-run! handler - {:port 8080 - :legacy-return-value? false}))))) - -;; ----------------------------------------------------------------------------- -;; ring jetty server -;; ----------------------------------------------------------------------------- -(defonce !jetty-server (atom nil)) - -(def ring-jetty-run! (rr 'ring.adapter.jetty/run-jetty)) - - -(defn reboot-jetty-server! [handler & {:as opts}] - (if-not ring-jetty-run! - (println "Ring jetty isn't in the classpath") - (swap! !jetty-server - (fn [server] - (when server - (.stop server)) - (ring-jetty-run! handler - (merge - {:port 8081 - :join? false} - opts)))))) - -;; ----------------------------------------------------------------------------- -;; rj9a server -;; ----------------------------------------------------------------------------- -(defonce !rj9a-server (atom nil)) - -(def rj9a-run! (rr 'ring.adapter.jetty9/run-jetty)) - - -(defn reboot-rj9a-server! [handler & {:as opts}] - (if-not rj9a-run! - (println "Ring jetty isn't in the classpath") - (swap! !rj9a-server - (fn [server] - (when server - (.stop server)) - (rj9a-run! handler - (merge - {:port 8082 - :join? false} - opts)))))) - - diff --git a/sdk/clojure/src/dev/user.clj b/sdk/clojure/src/dev/user.clj deleted file mode 100644 index 119296eb0..000000000 --- a/sdk/clojure/src/dev/user.clj +++ /dev/null @@ -1,44 +0,0 @@ -(ns user - (:require - [clojure.repl.deps :as crdeps] - [clojure+.hashp :as hashp] - [clj-reload.core :as reload] - [malli.dev :as mdev])) - - -(alter-var-root #'*warn-on-reflection* (constantly true)) - - -(hashp/install!) - -(reload/init - {:no-reload ['user]}) - - -(defn reload! [] - (reload/reload)) - - -(defn clear-terminal! [] - (binding [*out* (java.io.PrintWriter. System/out)] - (print "\033c") - (flush))) - - -(defmacro force-out [& body] - `(binding [*out* (java.io.OutputStreamWriter. System/out)] - ~@body)) - - -(comment - (mdev/start! {:exception true}) - (mdev/stop!) - (reload!) - *e - (crdeps/sync-deps) - - (-> (System/getProperties) - keys - sort)) - - diff --git a/sdk/clojure/src/test/adapter-common/test/common.clj b/sdk/clojure/src/test/adapter-common/test/common.clj deleted file mode 100644 index 8700c0d5f..000000000 --- a/sdk/clojure/src/test/adapter-common/test/common.clj +++ /dev/null @@ -1,228 +0,0 @@ -(ns test.common - (:require - [clojure.java.io :as io] - [clojure.edn :as edn] - [clojure.string :as string] - [etaoin.api :as ea] - [lazytest.core :as lt] - [lazytest.extensions.matcher-combinators :as mc] - [org.httpkit.client :as http] - [starfederation.datastar.clojure.adapter.test :as test-gen] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api.sse :as sse] - [test.examples.form :as ef] - [test.utils :as u])) - - -(def ^:dynamic *ctx* nil) - - -#_{:clj-kondo/ignore true} -(defn with-server-f - "Http server around fixture. - - Opts: - - `:start!`: fn starting a http server - - `:stop!`: fn stoping the server - - `:get-port`: get the port used by the running server - - other: specific opts for the http server used - " - [handler opts] - (lt/around [f] - (let [get-port (:get-port opts)] - (u/with-server s handler (dissoc opts :get-port) - (binding [*ctx* (assoc *ctx* :port (get-port s))] - (f)))))) - - -(def default-drivers-config - {:chrome {:headless true - :args ["-incognito"]} - :firefox {:headless true - :args ["-private"]}}) - - -(def default-drivers - [:firefox :chrome]) - - -(def custom-config - "Config from \"test-resources/test.config.edn\". - - Keys: - - `:drivers`: vector of driver types to run (kws like `:firefox`... from etaoin) - - `:webdriver-opts`: map of drivers types to drivers opts - (options passed to etaoin driver starting opts) - " - (some-> "test.config.edn" - io/resource - slurp - edn/read-string)) - - -(def drivers-configs - "Merge of the default drivers opts and the custom ones." - (let [custom-opts (:webdriver-opts custom-config)] - (reduce-kv - (fn [acc type opts] - (assoc acc type (merge opts - (get custom-opts type)))) - {} - default-drivers-config))) - - -(defonce drivers - (reduce - (fn [acc driver] - (assoc acc driver (delay (ea/boot-driver driver (get drivers-configs driver))))) - {} - (:drivers custom-config default-drivers))) - - -(defn install-shutdown-hooks! [] - (.addShutdownHook (Runtime/getRuntime) - (Thread. (fn [] - ; Killing web drivers - (println "Killing web drivers") - (doseq [d (vals drivers)] - (when (realized? d) - (try - (ea/quit @d) - (catch Exception _ - (println "Exception killing webdriver"))))) - - ; Killing agents - (println "Killing agents") - (shutdown-agents))))) - -(defonce _ (install-shutdown-hooks!)) - -;; ----------------------------------------------------------------------------- -;; Generic counters tests -;; ----------------------------------------------------------------------------- -(defn run-counters! [driver] - (let [{:keys [port]} *ctx*] - (ea/go driver (u/url port "counters/")) - - (ea/click driver :increment-1) - (ea/click driver :increment-1) - (ea/click driver :decrement-2) - (ea/click driver :increment-3) - - - {:get (ea/get-element-text driver :counter1) - :post (ea/get-element-text driver :counter2) - :signal (ea/get-element-text driver :counter3)})) - - -(def expected-counters {:get "2" - :post "-1" - :signal "1"}) - - -;; ----------------------------------------------------------------------------- -;; form test -;; ----------------------------------------------------------------------------- -(defn do-form! [driver msg button] - (ea/go driver (u/url (:port *ctx*) "form")) - (ea/fill driver ef/input-id msg) - (ea/click driver button) - (ea/clear driver ef/input-id) - (ea/get-element-text driver ef/form-result-id)) - - -(defn run-form-test! [driver] - {:get (do-form! driver "get" ef/get-button-id) - :post (do-form! driver "post" ef/post-button-id)}) - - -(def expected-form-vals - {:get "get" - :post "post"}) - -;; ----------------------------------------------------------------------------- -;; Generic persistent connection tests -;; ----------------------------------------------------------------------------- -(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] - (fn handler - ([req] - (->sse-response req - {ac/on-open (fn [sse-gen] - (deliver !conn sse-gen))})) - ([req respond _raise] - (respond (handler req))))) - - -(defn setup-persistent-see-state - "Setup of the persistent connection test. - We put together an atom to store a sse generator, a countdown latch and a - ring handler hooked to them." - [->sse-response] - (let [!conn (promise) - handler (->persistent-sse-handler ->sse-response !conn)] - {:!conn !conn - :handler handler})) - - -#_{:clj-kondo/ignore true} -(defn persistent-sse-f - "Fixture for the persistent sse test. A server is set up and the state - needed to run the test (see [[setup-persistent-see-state]])." - [->sse-response server-opts] - (lt/around [f] - (let [{:keys [get-port wrap]} server-opts - {:keys [!conn latch handler]} (setup-persistent-see-state ->sse-response) - handler (cond-> handler wrap wrap)] - (u/with-server server handler (dissoc server-opts :get-port) - (binding [*ctx* {:port (get-port server) - :!conn !conn}] - (f)))))) - - -(defn persistent-see-send-events! [sse-gen] - (d*/patch-elements! sse-gen "1") - (d*/patch-elements! sse-gen "2")) - - -(defn run-persistent-sse-test! [] - (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))) - - - -(defn p-sse-status-ok? [response] - (lt/expect (= (:status response) 200))) - - -(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-dot-1 (:headers response)))) - - -(def expected-p-sse-res-body - (let [sse-gen (test-gen/->sse-gen)] - (str (d*/patch-elements! sse-gen "1") - (d*/patch-elements! sse-gen "2")))) - - -(defn p-sse-body-ok? [response] - (lt/expect (= (:body response) expected-p-sse-res-body))) diff --git a/sdk/clojure/src/test/adapter-common/test/examples/common.clj b/sdk/clojure/src/test/adapter-common/test/examples/common.clj deleted file mode 100644 index 5023fe247..000000000 --- a/sdk/clojure/src/test/adapter-common/test/examples/common.clj +++ /dev/null @@ -1,71 +0,0 @@ -(ns test.examples.common - (:require - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [ring.middleware.params :as rmp] - [ring.middleware.multipart-params :as rmpp] - [ring.util.response :as rur] - [reitit.ring :as rr] - [starfederation.datastar.clojure.consts :as consts])) - - - -(defn script [type src] - [:script {:type type :src src}]) - - -(def cdn-url - (str "https://cdn.jsdelivr.net/gh/starfederation/datastar@" - consts/version - "/bundles/datastar.js")) - - -(def datastar-response - (-> (slurp "../../bundles/datastar.js") - rur/response - (rur/content-type "text/javascript"))) - - -(def datastar-route - ["/datastar.js" - (fn ([_req] datastar-response) - ([_req respond _raise] (respond datastar-response)))]) - - - -(def datastar - (script "module" "/datastar.js")) - - -(defn scaffold [content & {:as _}] - (hc/compile - [h/doctype-html5 - [:html - [:head - [:meta {:charset "UTF-8"}] - datastar] - [:body content]]])) - - -;; ----------------------------------------------------------------------------- -;; Common Handler stuff -;; ----------------------------------------------------------------------------- -(def wrap-params - {:name ::wrap-params - :description "Ring param extraction middleware." - :wrap rmp/wrap-params}) - - -(def wrap-mpparams - {:name ::wrap-multipart-params - :description "Ring multipart param extraction middleware." - :wrap rmpp/wrap-multipart-params}) - -(def global-middleware - [[wrap-params]]) - - -(def default-handler - (rr/create-default-handler)) - - diff --git a/sdk/clojure/src/test/adapter-common/test/examples/counter.clj b/sdk/clojure/src/test/adapter-common/test/examples/counter.clj deleted file mode 100644 index 96fbfdcfa..000000000 --- a/sdk/clojure/src/test/adapter-common/test/examples/counter.clj +++ /dev/null @@ -1,111 +0,0 @@ -(ns test.examples.counter - (:require - [test.examples.common :as common] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [ring.util.response :as rur] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.api :as d*] - [test.utils :as u])) - -;; ----------------------------------------------------------------------------- -;; Views -;; ----------------------------------------------------------------------------- -(defn ->->id [prefix] - (fn [s] - (str prefix s))) - -(def ->inc-id (->->id "increment-")) -(def ->dec-id (->->id "decrement-")) - -(defn inc-url [id] - (str "/counters/increment/" id)) - -(defn dec-url [id] - (str "/counters/decrement/" id)) - - -(defn sse-inc [counter-id] - (d*/sse-get (inc-url counter-id))) - -(defn sse-inc-post [counter-id] - (d*/sse-post (inc-url counter-id))) - -(defn signal-inc [counter-id] - (format "$%s += 1" counter-id)) - -(defn sse-dec [counter-id] - (d*/sse-get (dec-url counter-id))) - -(defn sse-dec-post [counter-id] - (d*/sse-post (dec-url counter-id))) - - -(defn signal-dec [counter-id] - (format "$%s -= 1" counter-id)) - - -(defn counter-button [id text action] - (hc/compile - [:button - {:id id - :data-on-click action} - text])) - - -(defn counter [id & {:keys [inc dec] - :or {inc sse-inc - dec sse-dec}}] - (let [counter-id (str "counter" id)] - (hc/compile - [:div {(keyword (str "data-signals-" counter-id)) "0"} - (counter-button (->inc-id id) "inc" (inc counter-id)) - (counter-button (->dec-id id) "dec" (dec counter-id)) - [:span {:id counter-id - :data-text (str "$"counter-id)}]]))) - - -(defn counter-page [] - (common/scaffold - (hc/compile - [:div - [:h2 "Counter page"] - [:div - [:h3 "Server side with get"] - (counter "1")] - [:div - [:h3 "Server side with post"] - (counter "2" :inc sse-inc-post :dec sse-dec-post)] - [:div - [:h3 "Client side"] - (counter "3" :inc signal-inc :dec signal-dec)]]))) - - -(def page (h/html (counter-page))) - - -;; ----------------------------------------------------------------------------- -;; Handlers common logic -;; ----------------------------------------------------------------------------- -(defn counters - ([_] - (rur/response page)) - ([_ respond _] - (respond (rur/response page)))) - - -(defn update-signal* [req f & args] - (let [signals (-> req d*/get-signals u/read-json) - id (-> req :path-params :id) - val (get signals id)] - (format "{'%s':%s}" id (apply f val args)))) - - -(defn ->update-signal [->sse-response] - (fn update-signal [req f & args] - (->sse-response req - {ac/on-open (fn [sse-gen] - (d*/patch-signals! sse-gen (apply update-signal* req f args)) - (d*/close-sse! sse-gen))}))) - - diff --git a/sdk/clojure/src/test/adapter-common/test/examples/form.clj b/sdk/clojure/src/test/adapter-common/test/examples/form.clj deleted file mode 100644 index 21e5c1b58..000000000 --- a/sdk/clojure/src/test/adapter-common/test/examples/form.clj +++ /dev/null @@ -1,87 +0,0 @@ -(ns test.examples.form - (:require - [test.examples.common :as common] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [ring.middleware.multipart-params] - [ring.util.response :as rur] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.api :as d*])) - - -;; ----------------------------------------------------------------------------- -;; Views -;; ----------------------------------------------------------------------------- -(def input-id :input1) -(def get-button-id :get-form) -(def post-button-id :post-form) -(def form-result-id :form-result) - - -(defn form-get [url] - (d*/sse-get url "{contentType: 'form'}")) - - -(defn form-post [url] - (d*/sse-post url "{contentType: 'form'}")) - - -(defn result-area [res] - (hc/compile - [:span {:id form-result-id} res])) - -(defn form-page [] - (common/scaffold - (hc/compile - [:div - [:h2 "Form page"] - [:form {:action ""} - [:h3 "D* post form"] - [:label {:for input-id} "Enter text"] - [:br] - - [:input {:type "text" - :id input-id - :name input-id}] - [:br] - [:button {:id get-button-id - :data-on-click (form-get "/form/endpoint")} - "Form get"] - - [:br] - [:button {:id post-button-id - :data-on-click (form-post "/form/endpoint")} - "Form post"] - - - [:br] - - (result-area "")]]))) - - -(def page (h/html (form-page))) - - -(defn form - ([_] - (rur/response page)) - ([req respond _] - (respond (form req)))) - - -(defn process-endpoint [request ->sse-response] - (let [input-val (get-in request [:params (name input-id)])] - (->sse-response request - {ac/on-open - (fn [sse-gen] - (d*/with-open-sse sse-gen - (d*/patch-elements! sse-gen (h/html (result-area input-val)))))}))) - - -(defn ->endpoint [->sse-response] - (fn endpoint - ([request] - (process-endpoint request ->sse-response)) - ([request respond _raise] - (respond (endpoint request))))) - diff --git a/sdk/clojure/src/test/adapter-common/test/utils.clj b/sdk/clojure/src/test/adapter-common/test/utils.clj deleted file mode 100644 index 258d231fd..000000000 --- a/sdk/clojure/src/test/adapter-common/test/utils.clj +++ /dev/null @@ -1,70 +0,0 @@ -(ns test.utils - (:require - [charred.api :as charred]) - (:import - java.io.StringWriter - java.net.ServerSocket)) - -;; ----------------------------------------------------------------------------- -;; JSON helpers -;; ----------------------------------------------------------------------------- -(def ^:private bufSize 1024) -(def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) - -(def ^:private write-json* (charred/write-json-fn {})) - -(defn- write-json [s] - (let [out (StringWriter.)] - (write-json* out s) - (.toString out))) - -(comment - (-> {"1" 2} - (write-json) - (read-json)) - := {"1" 2}) - - -;; ----------------------------------------------------------------------------- -;; Http servers helpers -;; ----------------------------------------------------------------------------- -(defn free-port! [] - (with-open [socket (ServerSocket. 0)] - (.getLocalPort socket))) - - -(defn url [port path] - (format "http://localhost:%s/%s" port path)) - - - -(defn sanitize-opts [opts] - (-> opts - (update :port #(or % (free-port!))) - (dissoc :start! :stop!))) - - -(defmacro with-server - "Setup a server - - Opts: - - `:start!`: mandatory - - `:stop!`: mandatory " - [server-name handler opts & body] - `(let [opts# ~opts - {start!# :start! - stop!# :stop!} opts# - sanitized-opts# (sanitize-opts opts#) - ~server-name (start!# ~handler sanitized-opts#)] - (try - ~@body - (finally - (stop!# ~server-name))))) - - -(comment - (macroexpand-1 - '(with-server serv h {:port 123456} - (do1) - (do2)))) - diff --git a/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj b/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj deleted file mode 100644 index aecaf88e5..000000000 --- a/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj +++ /dev/null @@ -1,107 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.http-kit.impl-test - (:require - [lazytest.core :as lt :refer [defdescribe it expect]] - [org.httpkit.server :as hk-server] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.test :as at] - [starfederation.datastar.clojure.adapter.http-kit.impl :as impl] - [starfederation.datastar.clojure.adapter.common-test :refer [read-bytes]]) - (:import - [java.io Closeable ByteArrayOutputStream OutputStreamWriter])) - -;; Mock Http-kit channel -(defrecord Channel [^ByteArrayOutputStream baos - !ch-open? - !on-close] - hk-server/Channel - ;; websocket stuff - (open? [_] @!ch-open?) - (websocket? [_] false) - - (on-receive [_ _callback]) - (on-ping [_ _callback]) - - (close [_] - (if @!ch-open? - (do - (vreset! !ch-open? false) - (when-let [on-close @!on-close] - (on-close :whatever)) - true) - false)) - - (on-close [_ callback] - (vreset! !on-close callback)) - - (send! [this data] - (hk-server/send! this data true)) - - (send! [this data close-after-send?] - (cond - (string? data) - (let [^OutputStreamWriter osw (ac/->os-writer baos)] - (doto osw - (.append (str data)) - (.flush))) - - (bytes? data) - (-> baos - (.write (bytes data)))) - - (when close-after-send? - (hk-server/close this)))) - - -(defn ->channel [baos] - (Channel. baos - (volatile! true) - (volatile! nil))) - - -(defn ->sse-gen [baos opts] - (let [c (->channel baos) - send! (impl/->send! c opts)] - (hk-server/on-close c - (fn [status] - (send!) - (when-let [callback (ac/on-close opts)] - (callback c status)))) - (impl/->sse-gen c send!))) - - -(def expected-event-result - (d*/patch-elements! (at/->sse-gen) "msg")) - -(defn send-SSE-event [opts] - (let [baos (ByteArrayOutputStream.)] - (with-open [_baos baos - sse-gen ^Closeable (->sse-gen baos opts)] - (d*/patch-elements! sse-gen "msg" {})) - - (expect - (= (read-bytes baos opts) - expected-event-result)))) - - -(defdescribe simple-test - (it "We can send events using a temp buffer" - (send-SSE-event {})) - - (it "We can send events using a persistent buffered reader" - (send-SSE-event {ac/write-profile ac/buffered-writer-profile})) - - (it "We can send gziped events using a temp buffer" - (send-SSE-event {ac/write-profile ac/gzip-profile :gzip? true})) - - (it "We can send gziped events using a persistent buffered reader" - (send-SSE-event {ac/write-profile ac/gzip-buffered-writer-profile :gzip? true}))) - - -(comment - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'simple-test) - :dbg - :rec) - - diff --git a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj b/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj deleted file mode 100644 index 04e015dbb..000000000 --- a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj +++ /dev/null @@ -1,66 +0,0 @@ -(ns test.examples.http-kit-handler - (:require - [test.examples.common :as common] - [test.examples.counter :as counters] - [test.examples.form :as form] - [reitit.ring :as rr] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) - - -;; ----------------------------------------------------------------------------- -;; Counters -;; ----------------------------------------------------------------------------- -(def update-signal (counters/->update-signal hk-gen/->sse-response)) - - -(defn increment - ([req] - (update-signal req inc)) - ([req respond _raise] - (respond (increment req)))) - - -(defn decrement - ([req] - (update-signal req dec)) - ([req respond _raise] - (respond (decrement req)))) - - -(def counter-routes - ["/counters/" - ["" {:handler #'counters/counters}] - ["increment/:id" #'increment] - ["decrement/:id" #'decrement]]) - - - -;; ----------------------------------------------------------------------------- -;; Form -;; ----------------------------------------------------------------------------- -(def endpoint (form/->endpoint hk-gen/->sse-response)) - - -(def form-routes - ["/form" - ["" {:handler #'form/form}] - ["/endpoint" {:middleware [common/wrap-mpparams] - :handler #'endpoint}]]) - - - - -(def router - (rr/router - [common/datastar-route - counter-routes - form-routes])) - - -(def handler - (rr/ring-handler router - common/default-handler - {:middleware common/global-middleware})) - - - diff --git a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj b/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj deleted file mode 100644 index 377404be6..000000000 --- a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj +++ /dev/null @@ -1,66 +0,0 @@ -(ns test.examples.http-kit-handler2 - (:require - [test.examples.common :as common] - [test.examples.counter :as counters] - [test.examples.form :as form] - [reitit.ring :as rr] - [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen])) - - -;; ----------------------------------------------------------------------------- -;; Counters -;; ----------------------------------------------------------------------------- -(def update-signal (counters/->update-signal hk-gen/->sse-response)) - - -(defn increment - ([req] - (update-signal req inc)) - ([req respond _raise] - (respond (increment req)))) - - -(defn decrement - ([req] - (update-signal req dec)) - ([req respond _raise] - (respond (decrement req)))) - - -(def counter-routes - ["/counters/" - ["" {:handler #'counters/counters}] - ["increment/:id" #'increment] - ["decrement/:id" #'decrement]]) - - - -;; ----------------------------------------------------------------------------- -;; Form -;; ----------------------------------------------------------------------------- -(def endpoint (form/->endpoint hk-gen/->sse-response)) - - -(def form-routes - ["/form" - ["" {:handler #'form/form}] - ["/endpoint" {:middleware [common/wrap-mpparams] - :handler #'endpoint}]]) - - - - -(def router - (rr/router - [common/datastar-route - counter-routes - form-routes])) - - -(def handler - (rr/ring-handler router - common/default-handler - {:middleware (into [[hk-gen/start-responding-middleware]] common/global-middleware)})) - - - diff --git a/sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj b/sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj deleted file mode 100644 index 744778e9e..000000000 --- a/sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj +++ /dev/null @@ -1,74 +0,0 @@ -(ns test.http-kit2-test - (:require - [test.common :as common] - [test.examples.http-kit-handler2 :as hkh] - [lazytest.core :as lt :refer [defdescribe expect it]] - [org.httpkit.server :as hk-server] - [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen])) - -;; ----------------------------------------------------------------------------- -;; HTTP-Kit stuff -;; ----------------------------------------------------------------------------- -(def http-kit-basic-opts - {:start! hk-server/run-server - :stop! hk-server/server-stop! - :get-port hk-server/server-port - :legacy-return-value? false}) - - -;; ----------------------------------------------------------------------------- -(defdescribe counters-test - {:webdriver true - :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - - -(defdescribe counters-test-async - {:webdriver true - :context [(common/with-server-f hkh/handler (assoc http-kit-basic-opts - :ring-async? true))]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - -;; ----------------------------------------------------------------------------- -;; Tests -;; ----------------------------------------------------------------------------- -(defdescribe form-test - {:webdriver true - :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} - (it "manages forms" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-form-test! @driver)] - (expect (= res common/expected-form-vals) (str driver-type)))))) - - -;; ----------------------------------------------------------------------------- -(defdescribe persistent-sse-test - {:context [(common/persistent-sse-f hk-gen/->sse-response - (assoc http-kit-basic-opts - :wrap hk-gen/wrap-start-responding))]} - (it "handles persistent connections" - (let [res (common/run-persistent-sse-test!)] - (expect (map? res)) - (expect (common/p-sse-status-ok? res)) - (expect (common/p-sse-http1-headers-ok? res)) - (expect (common/p-sse-body-ok? res))))) - - -(defdescribe persistent-sse-test-async - {:context [(common/persistent-sse-f hk-gen/->sse-response - (assoc http-kit-basic-opts - :wrap hk-gen/wrap-start-responding - :ring-async? true))]} - (it "handles persistent connections" - (let [res (common/run-persistent-sse-test!)] - (expect (map? res)) - (common/p-sse-status-ok? res) - (common/p-sse-http1-headers-ok? res) - (common/p-sse-body-ok? res)))) - diff --git a/sdk/clojure/src/test/adapter-http-kit/test/http_kit_test.clj b/sdk/clojure/src/test/adapter-http-kit/test/http_kit_test.clj deleted file mode 100644 index 6973eb604..000000000 --- a/sdk/clojure/src/test/adapter-http-kit/test/http_kit_test.clj +++ /dev/null @@ -1,72 +0,0 @@ -(ns test.http-kit-test - (:require - [test.common :as common] - [test.examples.http-kit-handler :as hkh] - [lazytest.core :as lt :refer [defdescribe expect it]] - [org.httpkit.server :as hk-server] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) - -;; ----------------------------------------------------------------------------- -;; HTTP-Kit stuff -;; ----------------------------------------------------------------------------- -(def http-kit-basic-opts - {:start! hk-server/run-server - :stop! hk-server/server-stop! - :get-port hk-server/server-port - :legacy-return-value? false}) - - -;; ----------------------------------------------------------------------------- -(defdescribe counters-test - {:webdriver true - :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - - -(defdescribe counters-test-async - {:webdriver true - :context [(common/with-server-f hkh/handler (assoc http-kit-basic-opts - :ring-async? true))]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - -;; ----------------------------------------------------------------------------- -;; Tests -;; ----------------------------------------------------------------------------- -(defdescribe form-test - {:webdriver true - :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} - (it "manages forms" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-form-test! @driver)] - (expect (= res common/expected-form-vals) (str driver-type)))))) - - -;; ----------------------------------------------------------------------------- -(defdescribe persistent-sse-test - {:context [(common/persistent-sse-f hk-gen/->sse-response - http-kit-basic-opts)]} - (it "handles persistent connections" - (let [res (common/run-persistent-sse-test!)] - (expect (map? res)) - (expect (common/p-sse-status-ok? res)) - (expect (common/p-sse-http1-headers-ok? res)) - (expect (common/p-sse-body-ok? res))))) - - -(defdescribe persistent-sse-test-async - {:context [(common/persistent-sse-f hk-gen/->sse-response - (assoc http-kit-basic-opts - :ring-async? true))]} - (it "handles persistent connections" - (let [res (common/run-persistent-sse-test!)] - (expect (map? res)) - (common/p-sse-status-ok? res) - (common/p-sse-http1-headers-ok? res) - (common/p-sse-body-ok? res)))) - diff --git a/sdk/clojure/src/test/adapter-ring-jetty/test/ring_jetty_test.clj b/sdk/clojure/src/test/adapter-ring-jetty/test/ring_jetty_test.clj deleted file mode 100644 index 4069c08a5..000000000 --- a/sdk/clojure/src/test/adapter-ring-jetty/test/ring_jetty_test.clj +++ /dev/null @@ -1,89 +0,0 @@ -(ns test.ring-jetty-test - (:require - [test.common :as common] - [test.examples.ring-handler :as rh] - [lazytest.core :as lt :refer [defdescribe expect it]] - [ring.adapter.jetty :as jetty] - [starfederation.datastar.clojure.adapter.ring :as jetty-gen]) - (:import - [org.eclipse.jetty.server Server ServerConnector])) - -;; ----------------------------------------------------------------------------- -;; Ring Jetty stuff -;; ----------------------------------------------------------------------------- -(defn stop-jetty! [^Server server] - (.stop server)) - - -(defn jetty-server-port [jetty-server] - (let [connector (-> jetty-server - Server/.getConnectors - seq - first)] - (.getLocalPort ^ServerConnector connector))) - - -(def ring-jetty-basic-opts - {:start! jetty/run-jetty - :stop! stop-jetty! - :get-port jetty-server-port - :join? false}) - - -;; ----------------------------------------------------------------------------- -;; Tests -;; ----------------------------------------------------------------------------- -(defdescribe counters-test - {:webdriver true - :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - - -(defdescribe counters-async-test - {:webdriver true - :context [(common/with-server-f rh/handler - (assoc ring-jetty-basic-opts :async? true))]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - -;; ----------------------------------------------------------------------------- -(defdescribe form-test - {:webdriver true - :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} - (it "manages forms" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-form-test! @driver)] - (expect (= res common/expected-form-vals) (str driver-type)))))) - - -(defdescribe form-test-async - {:webdriver true - :context [(common/with-server-f rh/handler - (assoc ring-jetty-basic-opts :async? true))]} - (it "manages forms" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-form-test! @driver)] - (expect (= res common/expected-form-vals) (str driver-type)))))) - - -;; ----------------------------------------------------------------------------- -(defdescribe persistent-sse-test - "Testing persistent connection, events are sent from outide the ring handler." - {:context [(common/persistent-sse-f jetty-gen/->sse-response - (assoc ring-jetty-basic-opts - :async? true))]} - (it "handles persistent connections" - (let [res (common/run-persistent-sse-test!)] - (expect (map? res)) - (common/p-sse-status-ok? res) - (common/p-sse-http1-headers-ok? res) - (common/p-sse-body-ok? res)))) - - - - diff --git a/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj b/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj deleted file mode 100644 index 4b59520a9..000000000 --- a/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj +++ /dev/null @@ -1,155 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.ring.impl-test - (:require - [lazytest.core :as lt :refer [defdescribe it expect]] - [ring.core.protocols :as p] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.ring.impl :as impl] - [starfederation.datastar.clojure.adapter.test :as at] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common-test :refer [read-bytes]]) - (:import - [java.io ByteArrayOutputStream])) - -;; ----------------------------------------------------------------------------- -;; Testing the gnarly adapter setup by simulating it -;; ----------------------------------------------------------------------------- -(defn ->lock [] (volatile! 0)) -(defn lock! [!l] (vswap! !l inc)) -(defn unlock! [!l] (vswap! !l dec)) - - -(defn throw-key [k] (throw (ex-info "" {:k k}))) -(defn throw-already-used [] (throw-key :already-used-error)) -(defn throw-writter-error [] (throw-key :writer-ctr-error)) -(defn throw-flush-errror [] (throw-key :flush-error)) -(defn throw-on-open [] (throw-key :on-open-error)) - - -(defn set-writer! [!ref behavior] - (case behavior - :ok (vreset! !ref :new-writer) - (throw-writter-error))) - -(defn flush-headers! [v] - (case v - :ok nil - (throw-flush-errror))) - -(defn on-open [v] - (case v - :ok nil - (throw-on-open))) - - -(defn write-body-to-stream-simulation - "Here we mimick the behavior of the initialisation of ring SSE responses - We capture lock, internal state, errors and return value to check several - properties." - [writer set-writer-v flush-v on-open-v] - (let [!l (->lock) - !writer (volatile! :old-writer) - !return (volatile! nil)] - - ;; The code actually mimicking - (try - (lock! !l) - (when writer - (unlock! !l) - (throw-already-used)) - - (let [!error (volatile! nil)] - (try - (set-writer! !writer set-writer-v) - (flush-headers! flush-v) - (vreset! !return :success) - (catch Throwable t - (vreset! !error t)) - (finally - (unlock! !l) - (if-let [e @!error] - (throw e) - (on-open on-open-v))))) - (catch Throwable t - (vreset! !return t))) - - {:lock @!l - :writer @!writer - :return (let [r @!return] - (or - (-> r ex-data :k) - r))})) - - -(defn make-all-cases [] - (for [writer [nil :old-writer] - set-writer [:ok :throw] - flush [:ok :throw] - on-open [:ok :throw]] - [writer set-writer flush on-open])) - - -(defn expected [writer set-writer! flush on-open] - (cond - writer {:lock 0 :writer :old-writer :return :already-used-error} - (= set-writer! :throw) {:lock 0 :writer :old-writer :return :writer-ctr-error} - (= flush :throw) {:lock 0 :writer :new-writer :return :flush-error} - (= on-open :throw) {:lock 0 :writer :new-writer :return :on-open-error} - :else {:lock 0 :writer :new-writer :return :success})) - - -(defn run-test-case [test-case] - {:res (apply write-body-to-stream-simulation test-case) - :expected (apply expected test-case)}) - - -(defn case-coherent? [test-case] - (let [res (run-test-case test-case)] - (= (:res res) (:expected res)))) - - -(defdescribe simulate-write-body-to-stream - (it "manages locks and errors properly" - (doseq [test-case (make-all-cases)] - (expect (case-coherent? test-case) (str test-case))))) - - -;; ----------------------------------------------------------------------------- -;; Basic sending of a SSE event without any server -;; ----------------------------------------------------------------------------- -(def expected-event-result - (d*/patch-elements! (at/->sse-gen) "msg")) - -(defn send-SSE-event [response] - (let [baos (ByteArrayOutputStream.)] - (with-open [sse-gen (impl/->sse-gen) - baos baos] - (p/write-body-to-stream sse-gen response baos) - (d*/patch-elements! sse-gen "msg" {})) - - (expect - (= (read-bytes baos (::impl/opts response)) - expected-event-result)))) - -(defdescribe simple-test - (it "can send events with a using temp buffers" - (send-SSE-event {})) - - (it "can send events with a using a persistent buffered reader" - (send-SSE-event {::impl/opts {ac/write-profile ac/buffered-writer-profile}})) - - (it "can send gziped events with a using temp buffers" - (send-SSE-event {::impl/opts {ac/write-profile ac/gzip-profile - :gzip? true}})) - - (it "can send gziped events with a using a persistent buffered reader" - (send-SSE-event {::impl/opts {ac/write-profile ac/gzip-buffered-writer-profile - :gzip? true}}))) - - - - -(comment - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'simulate-write-body-to-stream) - (ltr/run-test-var #'simple-test)) - diff --git a/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj b/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj deleted file mode 100644 index e27cc89ae..000000000 --- a/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj +++ /dev/null @@ -1,61 +0,0 @@ -(ns test.examples.ring-handler - (:require - [test.examples.common :as common] - [test.examples.counter :as counter] - [test.examples.form :as form] - [starfederation.datastar.clojure.adapter.ring :as jetty-gen] - [reitit.ring :as rr])) - -;; ----------------------------------------------------------------------------- -;; counters -;; ----------------------------------------------------------------------------- -(def update-signal (counter/->update-signal jetty-gen/->sse-response)) - -(defn increment - ([req] - (update-signal req inc)) - ([req respond _] - (respond (update-signal req inc)))) - - -(defn decrement - ([req] - (update-signal req dec)) - ([req respond _] - (respond (update-signal req dec)))) - - -(def counter-routes - ["/counters/" - ["" {:handler #'counter/counters}] - ["increment/:id" #'increment] - ["decrement/:id" #'decrement]]) - - -;; ----------------------------------------------------------------------------- -;; Form -;; ----------------------------------------------------------------------------- -(def endpoint (form/->endpoint jetty-gen/->sse-response)) - - -(def form-routes - ["/form" - ["" {:handler #'form/form}] - ["/endpoint" {:middleware [common/wrap-mpparams] - :handler #'endpoint}]]) - - -(def router - (rr/router - [common/datastar-route - counter-routes - form-routes])) - - -(def handler - (rr/ring-handler router - common/default-handler - {:middleware common/global-middleware})) - - - diff --git a/sdk/clojure/src/test/adapter-rj9a/test/rj9a_test.clj b/sdk/clojure/src/test/adapter-rj9a/test/rj9a_test.clj deleted file mode 100644 index c15cd3b59..000000000 --- a/sdk/clojure/src/test/adapter-rj9a/test/rj9a_test.clj +++ /dev/null @@ -1,94 +0,0 @@ -(ns test.rj9a-test - (:require - [test.common :as common] - [test.examples.ring-handler :as rh] - [lazytest.core :as lt :refer [defdescribe expect it]] - [ring.adapter.jetty9 :as jetty] - [starfederation.datastar.clojure.adapter.ring :as jetty-gen]) - (:import - [org.eclipse.jetty.server Server ServerConnector])) - -;; ----------------------------------------------------------------------------- -;; Ring Jetty stuff -;; ----------------------------------------------------------------------------- -(defn stop-jetty! [^Server server] - (.stop server)) - - -(defn jetty-server-port [jetty-server] - (let [connector (-> jetty-server - Server/.getConnectors - seq - first)] - (.getLocalPort ^ServerConnector connector))) - - -(def ring-jetty-basic-opts - {:start! jetty/run-jetty - :stop! stop-jetty! - :get-port jetty-server-port - :join? false}) - - -;; ----------------------------------------------------------------------------- -;; Tests -;; ----------------------------------------------------------------------------- -(defdescribe counters-test - {:webdriver true - :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - - -(defdescribe counters-async-test - {:webdriver true - :context [(common/with-server-f rh/handler - (assoc ring-jetty-basic-opts :async? true))]} - (it "manages signals" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-counters! @driver)] - (expect (= res common/expected-counters) (str driver-type)))))) - -;; ----------------------------------------------------------------------------- -(defdescribe form-test - {:webdriver true - :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} - (it "manages forms" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-form-test! @driver)] - (expect (= res common/expected-form-vals) (str driver-type)))))) - - -(defdescribe form-test-async - {:webdriver true - :context [(common/with-server-f rh/handler - (assoc ring-jetty-basic-opts :async? true))]} - (it "manages forms" - (doseq [[driver-type driver] common/drivers] - (let [res (common/run-form-test! @driver)] - (expect (= res common/expected-form-vals) (str driver-type)))))) - - -;; ----------------------------------------------------------------------------- -(defdescribe persistent-sse-test - "Testing persistent connection, events are sent from outide the ring handler." - {:context [(common/persistent-sse-f jetty-gen/->sse-response - (assoc ring-jetty-basic-opts - :async? true))]} - (it "handles persistent connections" - (let [res (common/run-persistent-sse-test!)] - (expect (map? res)) - (common/p-sse-status-ok? res) - (common/p-sse-http1-headers-ok? res) - (common/p-sse-body-ok? res)))) - - - -(comment - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'counters-test) - (ltr/run-test-var #'form-test) - (ltr/run-test-var #'persistent-sse-test) - (user/clear-terminal!)) diff --git a/sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj b/sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj deleted file mode 100644 index 0cc88cec4..000000000 --- a/sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj +++ /dev/null @@ -1,87 +0,0 @@ -(ns starfederation.datastar.clojure.brotli-test - (:require - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.test :as at] - [starfederation.datastar.clojure.adapter.common-test :as ct] - [starfederation.datastar.clojure.brotli :as brotli] - [lazytest.core :as lt :refer [defdescribe describe specify expect]]) - (:import - [java.io InputStream ByteArrayOutputStream - ByteArrayInputStream InputStreamReader BufferedReader] - [java.nio.charset StandardCharsets])) - - - -(defn ->input-stream-reader [^InputStream is] - (InputStreamReader. is StandardCharsets/UTF_8)) - - -(defn ->ba [v] - (cond - (bytes? v) - v - - (instance? ByteArrayOutputStream v) - (.toByteArray ^ByteArrayOutputStream v))) - - -(defn read-bytes [ba opts] - (if (:brotli opts) - (brotli/decompress ba) - (-> ba - ->ba - (ByteArrayInputStream.) - (->input-stream-reader) - (BufferedReader.) - (slurp)))) - -(defdescribe reading-bytes - (specify "We can do str -> bytes -> str" - (let [original (str (d*/patch-elements! (at/->sse-gen) "msg"))] - (expect - (= original - (-> original - (.getBytes) - (read-bytes {}))))))) - -;; ----------------------------------------------------------------------------- -;; Tests -;; ----------------------------------------------------------------------------- -(defn simple-round-trip [write-profile] - (let [!res (atom nil) - machinery (ct/->machinery write-profile) - baos (ct/get-baos machinery)] - (with-open [_baos baos - writer (ct/get-writer machinery)] - (ct/append-then-flush writer "some text")) - (reset! !res (-> baos .toByteArray (read-bytes write-profile))) - (expect (= @!res "some text")))) - - - -(defdescribe brotli - (describe "Writing of text with compression" - (specify "We can do a simple round trip" - (simple-round-trip (assoc (brotli/->brotli-profile) - :brotli true))) - - (specify "We can compress several messages" - (let [machinery (ct/->machinery (brotli/->brotli-profile)) - baos (ct/get-baos machinery) - !res (atom [])] - (with-open [writer (ct/get-writer machinery)] - (ct/append-then-flush writer "some text") - (ct/append-then-flush writer "some other text")) - (reset! !res (-> baos .toByteArray (read-bytes {:brotli true}))) - (expect (= @!res "some textsome other text")))))) - - - -(comment - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'reading-bytes) - (ltr/run-test-var #'brotli)) - - - - diff --git a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj deleted file mode 100644 index 1a7a8ed33..000000000 --- a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj +++ /dev/null @@ -1,160 +0,0 @@ -(ns starfederation.datastar.clojure.adapter.common-test - (:require - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.common :as ac] - [starfederation.datastar.clojure.adapter.test :as at] - [lazytest.core :as lt :refer [defdescribe describe specify expect]]) - (:import - [java.io - Writer InputStream ByteArrayOutputStream - ByteArrayInputStream InputStreamReader BufferedReader] - [java.nio.charset StandardCharsets] - [java.util.zip GZIPInputStream])) - - -;; ----------------------------------------------------------------------------- -;; Reading helpers -;; ----------------------------------------------------------------------------- -(defn ->input-stream-reader [^InputStream is] - (InputStreamReader. is StandardCharsets/UTF_8)) - - -(defn ->ba [v] - (cond - (bytes? v) - v - - (instance? ByteArrayOutputStream v) - (.toByteArray ^ByteArrayOutputStream v))) - - -(defn read-bytes [ba opts] - (-> ba - ->ba - (ByteArrayInputStream.) - (cond-> (:gzip? opts) (GZIPInputStream.)) - (->input-stream-reader) - (BufferedReader.) - (slurp))) - - -(defdescribe reading-bytes - (specify "We can do str -> bytes -> str" - (let [original (str (d*/patch-elements! (at/->sse-gen) "msg"))] - (expect - (= original - (-> original - (.getBytes) - (read-bytes {}))))))) - -;; ----------------------------------------------------------------------------- -;; Test helpers -;; ----------------------------------------------------------------------------- -(defn ->machinery [write-profile] - (let [^ByteArrayOutputStream baos (ByteArrayOutputStream.) - {write! ac/write! - wrap ac/wrap-output-stream} write-profile - writer (wrap baos)] - {:write! write! - :writer writer - :baos baos})) - -(defn get-baos ^ByteArrayOutputStream [machinery] - (:baos machinery)) - - -(defn get-writer ^Writer [machinery] - (:writer machinery)) - - -(defn append-then-flush [writer s] - (doto ^Writer writer - (.append (str s)) - (.flush))) - - -;; ----------------------------------------------------------------------------- -;; Tests -;; ----------------------------------------------------------------------------- -(defn simple-round-trip [write-profile] - (let [!res (atom nil) - machinery (->machinery write-profile) - baos (get-baos machinery)] - (with-open [_baos baos - writer (get-writer machinery)] - (append-then-flush writer "some text")) - (reset! !res (-> baos .toByteArray (read-bytes write-profile))) - (expect (= @!res "some text")))) - - -(defn resetless-writes [opts] - (let [!res (atom []) - machinery (->machinery opts)] - (with-open [baos (get-baos machinery) - writer (get-writer machinery)] - - (append-then-flush writer "some text") - (swap! !res conj (-> baos .toByteArray (read-bytes opts))) - - (append-then-flush writer "some other text") - (swap! !res conj (-> baos .toByteArray (read-bytes opts)))) - - (expect (= @!res ["some text" "some textsome other text"])))) - - -(defn writes [opts] - (let [!res (atom []) - machinery (->machinery opts)] - (with-open [baos (get-baos machinery) - writer (get-writer machinery)] - - (append-then-flush writer "some text") - (swap! !res conj (-> baos .toByteArray (read-bytes opts))) - - (.reset baos) - - (append-then-flush writer "some other text") - (swap! !res conj (-> baos .toByteArray (read-bytes opts)))) - (expect (= @!res ["some text" "some other text"])))) - - -(defdescribe normal - (describe "Writing of text without compression" - (specify "We can do a simple round trip" - (simple-round-trip ac/basic-profile)) - - - (describe "We need to be careful about reseting the ouput stream" - (specify "Without reset" - (resetless-writes ac/basic-profile)) - - (specify "With reset" - (writes ac/basic-profile))))) - - -(defdescribe gzip - (describe "Writing of text with compression" - (specify "We can do a simple round trip" - (simple-round-trip (assoc ac/gzip-profile - :gzip? true))) - - (specify "We can compress several messages" - (let [machinery (->machinery ac/gzip-profile) - baos (get-baos machinery) - !res (atom [])] - (with-open [writer (get-writer machinery)] - (append-then-flush writer "some text") - (append-then-flush writer "some other text")) - (reset! !res (-> baos .toByteArray (read-bytes {:gzip? true}))) - (expect (= @!res "some textsome other text")))))) - - -(comment - :dbg - :rec - *e - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'reading-bytes) - (ltr/run-test-var #'normal) - (ltr/run-test-var #'gzip)) - diff --git a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj deleted file mode 100644 index bbfd1bead..000000000 --- a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj +++ /dev/null @@ -1,327 +0,0 @@ -(ns starfederation.datastar.clojure.api-test - (:require - [clojure.string :as string] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.adapter.test :as at] - [lazytest.core :as lt :refer [defdescribe describe it expect specify]])) - - -(def patch-element-t consts/event-type-patch-elements) -(def patch-signals-t consts/event-type-patch-signals) - - -;; ----------------------------------------------------------------------------- -;; Basic event -;; ----------------------------------------------------------------------------- -(defn event-beginning - "Alternate, simplified implementation of - [starfederation.datastar.clojure.api.sse/write-event!]" - [event-type & {id d*/id - retry-duration d*/retry-duration}] - (cond-> [(format "event: %s" event-type)] - id - (conj (format "id: %s" id)) - - (and retry-duration - (> retry-duration 0) - (not= retry-duration consts/default-sse-retry-duration)) - (conj (format "retry: %s" retry-duration)))) - - -(def event-end ["" ""]) - - -(defn event [event-type data-lines & {:as opts}] - (string/join \newline - (concat (event-beginning event-type opts) - data-lines - event-end))) - - -;; ----------------------------------------------------------------------------- -;; Testing that basic send-event! options -;; ----------------------------------------------------------------------------- -(defn basic-test - "Testing the handling of SSE options." - [tested-fn input event-type data-lines] - (describe tested-fn - (lt/expect-it "sends minimal fragment" - (= (tested-fn (at/->sse-gen) input {}) - (event event-type data-lines))) - - - (lt/expect-it "handles ids" - (= (tested-fn (at/->sse-gen) input {d*/id "1"}) - (event event-type data-lines {d*/id "1"}))) - - - (it "handles retry duration" - (expect - (= (tested-fn (at/->sse-gen) input {d*/retry-duration 1}) - (event event-type data-lines {d*/retry-duration 1}))) - - (expect - (= (tested-fn (at/->sse-gen) input {d*/retry-duration 0}) - (event event-type data-lines {d*/retry-duration 0})))) - - - (lt/expect-it "handles both" - (= (tested-fn (at/->sse-gen) input {d*/id "1" d*/retry-duration 1}) - (event event-type data-lines {d*/id "1" d*/retry-duration 1}))))) - - -(defdescribe test-common-sse-opts - (basic-test d*/patch-elements! "" patch-element-t []) - (basic-test d*/patch-elements-seq! [] patch-element-t []) - (basic-test d*/patch-signals! "" patch-signals-t [])) - - -(comment - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'test-common-sse-opts)) - - -;; ----------------------------------------------------------------------------- -;; Patch elements helpers -;; ----------------------------------------------------------------------------- -(defn ->data-line [line-literal val] - (format "data: %s%s" line-literal val)) - -(def basic-selector "#id") -(def selector-line - (->data-line consts/selector-dataline-literal basic-selector)) - -(def test-merge-mode consts/element-patch-mode-after) -(def patch-mode-line - (->data-line consts/mode-dataline-literal test-merge-mode)) - -(def use-view-transition-line - (->data-line consts/use-view-transition-dataline-literal true)) - -(def ->element-line (partial ->data-line consts/elements-dataline-literal)) - -;; ----------------------------------------------------------------------------- -;; Patch elements tests cases -;; ----------------------------------------------------------------------------- -(defn patch-simple-test - "No options give a simple event with the patch data-lines" - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) input {}) - (event patch-element-t expected-datalines)))) - - -(defn patch-selector-test - "We see the selector data-line added." - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) input {d*/selector basic-selector}) - (event patch-element-t (list* selector-line expected-datalines))))) - - -(defn patch-mode-test - "We see the patch mode data-line added." - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) input {d*/patch-mode test-merge-mode}) - (event patch-element-t (list* patch-mode-line expected-datalines))))) - - -(defn patch-vt-false-test - "No view transition on false" - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) input {d*/use-view-transition false}) - (event patch-element-t expected-datalines)))) - -(defn patch-vt-non-bool-test - "No view transition on non boolean." - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) input {d*/use-view-transition :true}) - (event patch-element-t expected-datalines)))) - - -(defn patch-vt-true-test - "View transition line is added on true." - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) input {d*/use-view-transition true}) - (event patch-element-t (list* use-view-transition-line expected-datalines))))) - - -(defn patch-all-options-test - "All options, we see all additional lines." - [tested-patch-fn input expected-datalines] - (expect (= (tested-patch-fn (at/->sse-gen) - input - {d*/selector basic-selector - d*/patch-mode test-merge-mode - d*/use-view-transition true}) - (event patch-element-t - (list* - selector-line - patch-mode-line - use-view-transition-line - expected-datalines))))) - - -;; ----------------------------------------------------------------------------- -;; patch-elements! test definition -;; ----------------------------------------------------------------------------- -(def div-element "
\n hello\n
") -(def div-data - [(->element-line "
") - (->element-line " hello") - (->element-line "
")]) - - - -(defdescribe test-patch-elements! - (describe d*/patch-elements! - (it "handles no options" - (patch-simple-test d*/patch-elements! div-element div-data)) - (it "handles selectors" - (patch-selector-test d*/patch-elements! div-element div-data)) - (it "handles patch modes" - (patch-mode-test d*/patch-elements! div-element div-data)) - - (describe "handles view-transitions" - (specify "no view transition on false" - (patch-vt-false-test d*/patch-elements! div-element div-data)) - (specify "no view transition on non boolean value" - (patch-vt-non-bool-test d*/patch-elements! div-element div-data)) - (specify "view transition on true" - (patch-vt-true-test d*/patch-elements! div-element div-data))) - - (it "handles all options" - (patch-all-options-test d*/patch-elements! div-element div-data)))) - - -(comment - (ltr/run-test-var #'test-patch-elements!)) - -;; ----------------------------------------------------------------------------- -;; patch-elements-seq! test definition -;; ----------------------------------------------------------------------------- -(def multi-elements ["
\n hello\n
" - "
\n world\n
"]) - -(def multi-data - [(->element-line "
") - (->element-line " hello") - (->element-line "
") - (->element-line "
") - (->element-line " world") - (->element-line "
")]) - - -(defdescribe test-patch-elements-seq! - (describe d*/patch-elements-seq! - (it "handles no options" - (patch-simple-test d*/patch-elements-seq! multi-elements multi-data)) - (it "handles selectors" - (patch-selector-test d*/patch-elements-seq! multi-elements multi-data)) - (it "handles patch modes" - (patch-mode-test d*/patch-elements-seq! multi-elements multi-data)) - - (describe "handles view-transitions" - (specify "no view transition on false" - (patch-vt-false-test d*/patch-elements-seq! multi-elements multi-data)) - (specify "no view transition on non boolean value" - (patch-vt-non-bool-test d*/patch-elements-seq! multi-elements multi-data)) - (specify "view transition on true" - (patch-vt-true-test d*/patch-elements-seq! multi-elements multi-data))) - - (it "handles all options" - (patch-all-options-test d*/patch-elements-seq! multi-elements multi-data)))) - - - -(comment - (ltr/run-test-var #'test-patch-elements-seq!)) - - - -;; ----------------------------------------------------------------------------- -;; remove-element! test definition -;; ----------------------------------------------------------------------------- -(def patch-mode-remove-line - (->data-line consts/mode-dataline-literal consts/element-patch-mode-remove)) - -(defdescribe test-remove-element! - (describe d*/remove-element! - (it "produces a well formed event" - (expect (= (d*/remove-element! (at/->sse-gen) "#id") - (event patch-element-t [selector-line patch-mode-remove-line])))))) - -(comment - (ltr/run-test-var #'test-remove-element!)) - - -;; ----------------------------------------------------------------------------- -;; Merge signals -;; ----------------------------------------------------------------------------- -(def test-signals-content "{\"a\":1,\"b\":2,\"c\":{\"d\":1}}") - -(def patch-signals-lines - [(->data-line consts/signals-dataline-literal test-signals-content)]) - - -(def only-if-missing-line - (->data-line consts/only-if-missing-dataline-literal true)) - -(defdescribe test-patch-signals! - (describe d*/patch-signals! - (it "works with no options" - (expect (= (d*/patch-signals! (at/->sse-gen) test-signals-content {}) - (event patch-signals-t patch-signals-lines)))) - - (it "adds only-if-missing line" - (expect (= (d*/patch-signals! (at/->sse-gen) test-signals-content {d*/only-if-missing true}) - (event patch-signals-t - (list* only-if-missing-line - patch-signals-lines))))))) - -(comment - (ltr/run-test-var #'test-patch-signals!)) - -;; ----------------------------------------------------------------------------- -;; Execute scripts -;; ----------------------------------------------------------------------------- -(def script-content "console.log('hello')") - -(defn script-event [script-tag] - (event patch-element-t - [(->data-line consts/selector-dataline-literal "body") - (->data-line consts/mode-dataline-literal consts/element-patch-mode-append) - (->data-line consts/elements-dataline-literal script-tag)])) - -(defdescribe test-execute-script! - (describe d*/execute-script! - (specify "auto-remove is the default behavior" - (expect (= (d*/execute-script! (at/->sse-gen) script-content) - (script-event "")))) - - (specify "we can disable auto-remove" - (expect (= (d*/execute-script! (at/->sse-gen) - script-content - {d*/auto-remove false}) - (script-event "")))) - - (specify "we can disable auto-remove and add attributes" - (expect (= (d*/execute-script! (at/->sse-gen) - script-content - {d*/auto-remove false - d*/attributes {:type "module"}}) - (script-event "")))) - - (specify "we can add attributes auto-remove is there" - (expect (= (d*/execute-script! (at/->sse-gen) - script-content - {d*/attributes {:type "module" :data-something 1}}) - (d*/execute-script! (at/->sse-gen) - script-content - {d*/auto-remove true - d*/attributes {:type "module" :data-something 1}}) - (script-event "")))))) - -(comment - (ltr/run-test-var #'test-execute-script!)) - - diff --git a/sdk/clojure/src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj b/sdk/clojure/src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj deleted file mode 100644 index 69d5bb35c..000000000 --- a/sdk/clojure/src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj +++ /dev/null @@ -1,69 +0,0 @@ -(ns starfederation.datastar.clojure.api-schemas-test - (:require - [lazytest.core :as lt :refer [defdescribe describe expect it]] - [malli.instrument :as mi] - [starfederation.datastar.clojure.adapter.test :as at] - [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api.elements :as elements] - [starfederation.datastar.clojure.api-schemas])) - - -(def with-malli - (lt/around [f] - (mi/instrument!) - (f) - (mi/unstrument!))) - - -(def sse-gen (at/->sse-gen)) - - -(defn get-exception [thunk] - (try - (thunk) - nil - (catch Exception e - e))) - - -(defn get-exception-msg [thunk] - (-> thunk - (get-exception) - ex-message)) - - -(def malli-error-msg - ":malli.core/invalid-input") - - -(def dumy-script "console.log('hello')") - -(def thunk-wrong-script-type #(d*/execute-script! sse-gen :test)) -(def thunk-wrong-option-type #(d*/execute-script! sse-gen dumy-script {d*/auto-remove :test})) - - -(defdescribe test-malli-schemas - (describe "without malli" - (it "error can go through" - (expect (= (thunk-wrong-script-type) - "event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n")) - (expect (= (thunk-wrong-option-type) - "event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n")))) - - (describe "with malli" - {:context [with-malli]} - (it "types are checked" - (let [msg1 (get-exception-msg thunk-wrong-script-type) - msg2 (get-exception-msg thunk-wrong-option-type)] - (expect (= msg1 malli-error-msg)) - (expect (= msg2 malli-error-msg))))) - - (describe "Schemas not required" - (it "doesn't trigger instrumentation" - (expect (= (elements/->patch-elements "" {d*/retry-duration :test}) - []))))) - -(comment - (require '[lazytest.repl :as ltr]) - (ltr/run-test-var #'test-malli-schemas)) - diff --git a/sdk/datastar-sdk-config-v1.json b/sdk/datastar-sdk-config-v1.json new file mode 100644 index 000000000..409225b54 --- /dev/null +++ b/sdk/datastar-sdk-config-v1.json @@ -0,0 +1,76 @@ +{ + "$schema": "./datastar-sdk-config.schema.json-v1.json", + "datastarKey": "datastar", + "defaults": { + "booleans": { + "elementsUseViewTransitions": false, + "patchSignalsOnlyIfMissing": false + }, + "durations": { + "sseRetryDuration": 1000 + } + }, + "datalineLiterals": [ + "selector", + "mode", + "elements", + "useViewTransition", + "signals", + "onlyIfMissing" + ], + "enums": { + "ElementPatchMode": { + "description": "The mode in which an element is patched into the DOM.", + "default": "outer", + "values": [ + { + "value": "outer", + "description": "Morphs the element into the existing element." + }, + { + "value": "inner", + "description": "Replaces the inner HTML of the existing element." + }, + { + "value": "remove", + "description": "Removes the existing element." + }, + { + "value": "replace", + "description": "Replaces the existing element with the new element." + }, + { + "value": "prepend", + "description": "Prepends the element inside to the existing element." + }, + { + "value": "append", + "description": "Appends the element inside the existing element." + }, + { + "value": "before", + "description": "Inserts the element before the existing element." + }, + { + "value": "after", + "description": "Inserts the element after the existing element." + } + ] + }, + "EventType": { + "description": "The type protocol on top of SSE which allows for core pushed based communication between the server and the client.", + "values": [ + { + "name": "PatchElements", + "value": "datastar-patch-elements", + "description": "An event for patching HTML elements into the DOM." + }, + { + "name": "PatchSignals", + "value": "datastar-patch-signals", + "description": "An event for patching signals." + } + ] + } + } +} diff --git a/sdk/datastar-sdk-config-v1.schema.json b/sdk/datastar-sdk-config-v1.schema.json new file mode 100644 index 000000000..5425af55b --- /dev/null +++ b/sdk/datastar-sdk-config-v1.schema.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://datastar.dev/schemas/sdk-config.schema.json", + "title": "Datastar SDK Configuration", + "description": "Configuration schema for Datastar SDK authors", + "type": "object", + "required": ["version", "datastarKey", "defaults", "enums", "datalineLiterals"], + "properties": { + "version": { + "type": "string", + "description": "The version of Datastar", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?$" + }, + "datastarKey": { + "type": "string", + "description": "The key used for datastar attributes", + "default": "datastar" + }, + "defaults": { + "type": "object", + "description": "Default values for various SDK settings", + "properties": { + "booleans": { + "type": "object", + "description": "Boolean default values", + "properties": { + "elementsUseViewTransitions": { + "type": "boolean", + "description": "Should elements be patched using the ViewTransition API?", + "default": false + }, + "patchSignalsOnlyIfMissing": { + "type": "boolean", + "description": "Should a given set of signals patch if they are missing?", + "default": false + } + } + }, + "durations": { + "type": "object", + "description": "Duration default values (in milliseconds)", + "properties": { + "sseRetryDuration": { + "type": "integer", + "description": "The default duration for retrying SSE on connection reset (in milliseconds)", + "default": 1000 + } + } + } + } + }, + "datalineLiterals": { + "type": "array", + "description": "Literal strings used in dataline attributes", + "items": { + "type": "string" + }, + "default": ["selector", "mode", "elements", "useViewTransition", "signals", "onlyIfMissing"] + }, + "enums": { + "type": "object", + "description": "Enum definitions used in the SDK", + "properties": { + "ElementPatchMode": { + "type": "object", + "description": "The mode in which an element is patched into the DOM", + "properties": { + "description": { + "type": "string" + }, + "default": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "object", + "required": ["value", "description"], + "properties": { + "value": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + }, + "EventType": { + "type": "object", + "description": "The type protocol on top of SSE which allows for core pushed based communication between the server and the client", + "properties": { + "description": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "object", + "required": ["value", "description"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/sdk/dotnet/.gitignore b/sdk/dotnet/.gitignore deleted file mode 100644 index 90568b92f..000000000 --- a/sdk/dotnet/.gitignore +++ /dev/null @@ -1,359 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates -[Ss]amples/[Ss]andbox/ -*.sqlite - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ -.DS_Store - -# Rider (JetBrain's cross-platform .NET IDE) working folder -.idea/ - -# Datastar Bundles copied -**/wwwroot/bundles/ diff --git a/sdk/dotnet/Build.ps1 b/sdk/dotnet/Build.ps1 deleted file mode 100644 index a5eedb5a0..000000000 --- a/sdk/dotnet/Build.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -# `Build.ps1 Pack Build` to create the nugets - -[CmdletBinding()] -param ( - [Parameter(HelpMessage="The action to execute.")] - [ValidateSet("Build", "Pack" )] # ,"Test" - [string] $Action = "Build", - - [Parameter(HelpMessage="The msbuild configuration to use.")] - [ValidateSet("Debug", "Release")] - [string] $Configuration = "Debug", - - [switch] $SkipClean -) - -function RunCommand { - param ([string] $CommandExpr) - Write-Verbose " $CommandExpr" - Invoke-Expression $CommandExpr -} - -$rootDir = $PSScriptRoot -$srcDir = Join-Path -Path $rootDir -ChildPath 'src' -# $testDir = Join-Path -Path $rootDir -ChildPath 'test' - -switch ($Action) { - # "Test" { $projectDir = $srcDir } - "Pack" { $projectDir = $srcDir } - Default { $projectDir = $srcDir } -} - -if (!$SkipClean.IsPresent) -{ - RunCommand "dotnet restore $projectDir --force --force-evaluate --nologo --verbosity quiet" - RunCommand "dotnet clean $projectDir -c $Configuration --nologo --verbosity quiet" -} - -switch ($Action) { - #"Test" { RunCommand "dotnet test `"$projectDir`"" } - "Pack" { RunCommand "dotnet pack `"$projectDir`" -c $Configuration --include-symbols --include-source" } - Default { RunCommand "dotnet build `"$projectDir`" -c $Configuration" } -} diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md deleted file mode 100644 index 1a0423aab..000000000 --- a/sdk/dotnet/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# Datastar + dotnet - -[![NuGet Version](https://img.shields.io/nuget/v/Starfederation.Datastar.svg)](https://www.nuget.org/packages/Starfederation.Datastar) - -Real-time Hypermedia first Library and Framework for dotnet - -# HTML Frontend - -```html -
- -
-
- - -
-``` - -# C# Backend - -```csharp -using StarFederation.Datastar; -using StarFederation.Datastar.DependencyInjection; -using System.Text.Json; -using System.Text.Json.Serialization; - -// add as an ASP Service -// allows injection of IDatastarService, to respond to a request with a Datastar friendly ServerSentEvent -// and to read the signals sent by the client -builder.Services.AddDatastar(); - -// displayDate - patching an element -app.MapGet("/displayDate", async (IDatastarService datastarService) => -{ - string today = DateTime.Now.ToString("%y-%M-%d %h:%m:%s"); - await datastarService.PatchElementsAsync($"""
{today}
"""); -}); - -// removeDate - removing an element -app.MapGet("/removeDate", async (IDatastarService datastarService) => { await datastarService.RemoveElementAsync("#date"); }); - -public record MySignals { - [JsonPropertyName("input")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Input { get; init; } = null; - - [JsonPropertyName("output")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Output { get; init; } = null; - - public string Serialize() => ... -} - -// changeOutput - reads the signals, update the Output, and merge back -app.MapPost("/changeOutput", async (IDatastarService datastarService) => ... -{ - MySignals signals = await datastarService.ReadSignalsAsync(); - MySignals newSignals = new() { Output = $"Your Input: {signals.Input}" }; - await datastarService.PatchSignalsAsync(newSignals.Serialize()); -}); -``` - -# Model Binding - -```csharp -public class MySignals { - public string myString { get; set; } = ""; - public int myInt { get; set; } = 0; - public InnerSignals myInner { get; set; } = new(); - - public class InnerSignals { - public string myInnerString { get; set; } = ""; - public int myInnerInt { get; set; } = 0; - } -} - -public IActionResult Test_GetSignals([FromSignals] MySignals signals) => ... - -public IActionResult Test_GetValues([FromSignals] string myString, [FromSignals] int myInt) => ... - -public IActionResult Test_GetInnerPathed([FromSignals(Path = "myInner")] MySignals.InnerSignals myInnerOther) => ... - -public IActionResult Test_GetInnerValues([FromSignals(Path = "myInner.myInnerString")] string myInnerStringOther, [FromSignals(Path = "myInner.myInnerInt")] int myInnerIntOther) => ... - -``` \ No newline at end of file diff --git a/sdk/dotnet/assets/datastar_icon.png b/sdk/dotnet/assets/datastar_icon.png deleted file mode 100644 index 77cd5eb8e..000000000 Binary files a/sdk/dotnet/assets/datastar_icon.png and /dev/null differ diff --git a/sdk/dotnet/csharp/src/Consts.cs b/sdk/dotnet/csharp/src/Consts.cs deleted file mode 100644 index fb045cf62..000000000 --- a/sdk/dotnet/csharp/src/Consts.cs +++ /dev/null @@ -1,75 +0,0 @@ -// This is auto-generated by Datastar. DO NOT EDIT. - -namespace StarFederation.Datastar; - -using System; - -public enum ElementPatchMode -{ - /// Morphs the element into the existing element. - Outer, - /// Replaces the inner HTML of the existing element. - Inner, - /// Removes the existing element. - Remove, - /// Replaces the existing element with the new element. - Replace, - /// Prepends the element inside to the existing element. - Prepend, - /// Appends the element inside the existing element. - Append, - /// Inserts the element before the existing element. - Before, - /// Inserts the element after the existing element. - After, - -} -public enum EventType -{ - /// An event for patching HTML elements into the DOM. - PatchElements, - /// An event for patching signals. - PatchSignals, - -} - -public static class Consts -{ - public const string DatastarKey = "datastar"; - public const string Version = "1.0.0-RC.1"; - - /// Default: TimeSpan.FromMilliseconds 1000 - public static readonly TimeSpan DefaultSseRetryDuration = TimeSpan.FromMilliseconds(1000); - - - /// Default: outer - Morphs the element into the existing element. - public const ElementPatchMode DefaultElementPatchMode = ElementPatchMode.Outer; - - public const bool DefaultElementsUseViewTransitions = false; - public const bool DefaultPatchSignalsOnlyIfMissing = false; - - - public const string DatastarDatalineSelector = "selector"; - public const string DatastarDatalineMode = "mode"; - public const string DatastarDatalineElements = "elements"; - public const string DatastarDatalineUseViewTransition = "useViewTransition"; - public const string DatastarDatalineSignals = "signals"; - public const string DatastarDatalineOnlyIfMissing = "onlyIfMissing"; - - public static string EnumToString( ElementPatchMode enumValue ) => enumValue switch { - ElementPatchMode.Outer => "outer", - ElementPatchMode.Inner => "inner", - ElementPatchMode.Remove => "remove", - ElementPatchMode.Replace => "replace", - ElementPatchMode.Prepend => "prepend", - ElementPatchMode.Append => "append", - ElementPatchMode.Before => "before", - ElementPatchMode.After => "after", - _ => throw new NotImplementedException($"ElementPatchMode.{enumValue}") - }; - public static string EnumToString( EventType enumValue ) => enumValue switch { - EventType.PatchElements => "datastar-patch-elements", - EventType.PatchSignals => "datastar-patch-signals", - _ => throw new NotImplementedException($"EventType.{enumValue}") - }; -} \ No newline at end of file diff --git a/sdk/dotnet/csharp/src/DependencyInjection/Services.cs b/sdk/dotnet/csharp/src/DependencyInjection/Services.cs deleted file mode 100644 index 9ba184eb1..000000000 --- a/sdk/dotnet/csharp/src/DependencyInjection/Services.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Text.Json; -using Microsoft.Extensions.Primitives; -using Microsoft.FSharp.Core; -using Core = StarFederation.Datastar.FSharp; - -namespace StarFederation.Datastar.DependencyInjection; - -public interface IDatastarService -{ - Task StartServerEventStreamAsync(IEnumerable>? additionalHeaders = null); - Task StartServerEventStreamAsync(IEnumerable>? additionalHeaders = null); - - Task PatchElementsAsync(string elements, PatchElementsOptions? options = null); - - Task RemoveElementAsync(string selector, RemoveElementOptions? options = null); - - /// - /// Note: If TType is string then it is assumed that it is an already serialized Signals, otherwise serialize with jsonSerializerOptions - /// - Task PatchSignalsAsync(TType signals, JsonSerializerOptions? jsonSerializerOptions = null, PatchSignalsOptions? patchSignalsOptions = null); - - /// - /// Execute a JS script on the client. Note: Do NOT include "<script>" encapsulation - /// - Task ExecuteScriptAsync(string script, ExecuteScriptOptions? options = null); - - /// - /// Get the serialized signals as a stream - /// - Stream GetSignalsStream(); - - /// - /// Read the signals and return as a serialized string - /// - /// A task that represents the asynchronous read operation. The result contains the serialized signals. - Task ReadSignalsAsync(); - - /// - /// Read the signals and deserialize as a TType - /// - /// A task that represents the asynchronous read and deserialize operation. The result contains the deserialized data. - Task ReadSignalsAsync(JsonSerializerOptions? options = null); -} - -internal class DatastarService(Core.ServerSentEventGenerator serverSentEventGenerator) : IDatastarService -{ - public Task StartServerEventStreamAsync(IEnumerable>? additionalHeaders) => - serverSentEventGenerator.StartServerEventStreamAsync(additionalHeaders ?? []); - - public Task StartServerEventStreamAsync(IEnumerable>? additionalHeaders) => - serverSentEventGenerator.StartServerEventStreamAsync(additionalHeaders?.Select(kvp => new KeyValuePair(kvp.Key, new StringValues(kvp.Value))) ?? []); - - public Task PatchElementsAsync(string elements, PatchElementsOptions? options = null) => - serverSentEventGenerator.PatchElementsAsync(elements, options ?? Core.PatchElementsOptions.Defaults); - - public Task RemoveElementAsync(string selector, RemoveElementOptions? options = null) => - serverSentEventGenerator.RemoveElementAsync(selector, options ?? Core.RemoveElementOptions.Defaults); - - public Task PatchSignalsAsync(TType signals, JsonSerializerOptions? jsonSerializerOptions = null, PatchSignalsOptions? patchSignalsOptions = null) => - serverSentEventGenerator.PatchSignalsAsync(signals as string ?? JsonSerializer.Serialize(signals, jsonSerializerOptions), patchSignalsOptions ?? Core.PatchSignalsOptions.Defaults); - - public Task ExecuteScriptAsync(string script, ExecuteScriptOptions? options = null) => - serverSentEventGenerator.ExecuteScriptAsync(script, options ?? Core.ExecuteScriptOptions.Defaults); - - public Stream GetSignalsStream() => serverSentEventGenerator.GetSignalsStream(); - - public async Task ReadSignalsAsync() - { - string? signals = await serverSentEventGenerator.ReadSignalsAsync(); - return String.IsNullOrEmpty(signals) ? null : signals; - } - - public async Task ReadSignalsAsync(JsonSerializerOptions? jsonSerializerOptions = null) - { - FSharpValueOption read = await serverSentEventGenerator.ReadSignalsAsync(jsonSerializerOptions ?? Core.JsonSerializerOptions.SignalsDefault); - return read.IsSome ? read.Value : default; - } -} \ No newline at end of file diff --git a/sdk/dotnet/csharp/src/DependencyInjection/ServicesProvider.cs b/sdk/dotnet/csharp/src/DependencyInjection/ServicesProvider.cs deleted file mode 100644 index 626c72db0..000000000 --- a/sdk/dotnet/csharp/src/DependencyInjection/ServicesProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Core = StarFederation.Datastar.FSharp; - -namespace StarFederation.Datastar.DependencyInjection; - -public static class ServiceCollectionExtensionMethods -{ - public static IServiceCollection AddDatastar(this IServiceCollection serviceCollection) - { - serviceCollection - .AddHttpContextAccessor() - .AddScoped(svcPvd => - { - IHttpContextAccessor? httpContextAccessor = svcPvd.GetService(); - Core.ServerSentEventGenerator serverSentEventGenerator = new(httpContextAccessor); - return new DatastarService(serverSentEventGenerator); - }); - return serviceCollection; - } -} diff --git a/sdk/dotnet/csharp/src/DependencyInjection/Types.cs b/sdk/dotnet/csharp/src/DependencyInjection/Types.cs deleted file mode 100644 index 4347e96f5..000000000 --- a/sdk/dotnet/csharp/src/DependencyInjection/Types.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.FSharp.Core; -using Core = StarFederation.Datastar.FSharp; - -namespace StarFederation.Datastar.DependencyInjection; - -public class PatchElementsOptions -{ - public string? Selector { get; init; } = null; - public ElementPatchMode PatchMode { get; init; } = Consts.DefaultElementPatchMode; - public bool UseViewTransition { get; init; } = Consts.DefaultElementsUseViewTransitions; - public string? EventId { get; init; } = null; - public TimeSpan Retry { get; init; } = Consts.DefaultSseRetryDuration; - - public static implicit operator FSharpValueOption(PatchElementsOptions options) => ToFSharp(options); - public static implicit operator Core.PatchElementsOptions(PatchElementsOptions options) => ToFSharp(options); - - private static Core.PatchElementsOptions ToFSharp(PatchElementsOptions options) - { - return new Core.PatchElementsOptions( - options.Selector ?? FSharpValueOption.ValueNone, - From(options.PatchMode), - options.UseViewTransition, - options.EventId ?? FSharpValueOption.ValueNone, - options.Retry - ); - - static Core.ElementPatchMode From(ElementPatchMode patchElementsMode) => patchElementsMode switch - { - ElementPatchMode.Inner => Core.ElementPatchMode.Inner, - ElementPatchMode.Outer => Core.ElementPatchMode.Outer, - ElementPatchMode.Prepend => Core.ElementPatchMode.Prepend, - ElementPatchMode.Append => Core.ElementPatchMode.Append, - ElementPatchMode.Before => Core.ElementPatchMode.Before, - ElementPatchMode.After => Core.ElementPatchMode.After, - ElementPatchMode.Remove => Core.ElementPatchMode.Remove, - ElementPatchMode.Replace => Core.ElementPatchMode.Replace, - _ => throw new ArgumentOutOfRangeException(nameof(patchElementsMode), patchElementsMode, null) - }; - } -} - -public class PatchSignalsOptions -{ - public bool OnlyIfMissing { get; init; } = Consts.DefaultPatchSignalsOnlyIfMissing; - public string? EventId { get; init; } = null; - public TimeSpan Retry { get; init; } = Consts.DefaultSseRetryDuration; - - public static implicit operator Core.PatchSignalsOptions(PatchSignalsOptions options) => ToFSharp(options); - public static implicit operator FSharpValueOption(PatchSignalsOptions options) => ToFSharp(options); - - private static Core.PatchSignalsOptions ToFSharp(PatchSignalsOptions options) => new( - options.OnlyIfMissing, - options.EventId ?? FSharpValueOption.ValueNone, - options.Retry); -} - -public class RemoveElementOptions -{ - public bool UseViewTransition { get; init; } = Consts.DefaultElementsUseViewTransitions; - public string? EventId { get; init; } = null; - public TimeSpan Retry { get; init; } = Consts.DefaultSseRetryDuration; - - public static implicit operator Core.RemoveElementOptions(RemoveElementOptions options) => ToFSharp(options); - public static implicit operator FSharpValueOption(RemoveElementOptions options) => ToFSharp(options); - - private static Core.RemoveElementOptions ToFSharp(RemoveElementOptions options) => new( - options.UseViewTransition, - options.EventId ?? FSharpValueOption.ValueNone, - options.Retry); -} - -public class ExecuteScriptOptions -{ - public string? EventId { get; init; } = null; - public TimeSpan Retry { get; init; } = Consts.DefaultSseRetryDuration; - - public static implicit operator Core.ExecuteScriptOptions(ExecuteScriptOptions options) => ToFSharp(options); - public static implicit operator FSharpValueOption(ExecuteScriptOptions options) => ToFSharp(options); - - private static Core.ExecuteScriptOptions ToFSharp(ExecuteScriptOptions options) => new( - options.EventId ?? FSharpValueOption.ValueNone, - options.Retry); -} diff --git a/sdk/dotnet/csharp/src/ModelBinding/FromSignalAttribute.cs b/sdk/dotnet/csharp/src/ModelBinding/FromSignalAttribute.cs deleted file mode 100644 index 3cdc63b9d..000000000 --- a/sdk/dotnet/csharp/src/ModelBinding/FromSignalAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Core = StarFederation.Datastar.FSharp; - -namespace StarFederation.Datastar.ModelBinding; - -public class DatastarSignalsBindingSource(string path, JsonSerializerOptions? jsonSerializerOptions) : BindingSource(BindingSourceName, BindingSourceName, isGreedy: true, isFromRequest: true) -{ - public const string BindingSourceName = "DatastarSignalsSource"; - public string BindingPath { get; } = path; - public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? Core.JsonSerializerOptions.SignalsDefault; -} - -/// -/// FromSignals will collect the values from the signals passed.
-/// - value type or string without path => use parameter name as key into Signals
-/// - value type or string with path => use dot-separated, path name as key into Signals
-/// - reference type without path => deserialize Signals into parameter Type
-/// - reference type with path => deserialize Signals at path into parameter Type
-/// Important: Requests that have the signals in the body (POST, PUT, etc) can only have one [FromSignals] in the parameter list; all following will receive default
-/// Important: When binding to a non-string, reference type; the parameter name is always ignored, the parameter name will not be used as a key into the Signals like value types and strings -///
-[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public class FromSignalsAttribute : Attribute, IBindingSourceMetadata -{ - public string Path { get; set; } = String.Empty; - public JsonSerializerOptions JsonSerializerOptions { get; set; } = Core.JsonSerializerOptions.SignalsDefault; - public BindingSource BindingSource => new DatastarSignalsBindingSource(Path, JsonSerializerOptions); -} \ No newline at end of file diff --git a/sdk/dotnet/csharp/src/ModelBinding/MvcServiceProvider.cs b/sdk/dotnet/csharp/src/ModelBinding/MvcServiceProvider.cs deleted file mode 100644 index 89c287bc2..000000000 --- a/sdk/dotnet/csharp/src/ModelBinding/MvcServiceProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using StarFederation.Datastar.DependencyInjection; - -namespace StarFederation.Datastar.ModelBinding; - -public static class ServiceCollectionExtensionMethods -{ - public static IServiceCollection AddDatastarMvc(this IServiceCollection serviceCollection) - { - // ReSharper disable once SuspiciousTypeConversion.Global - if (!serviceCollection.Any(_ => _.ServiceType == typeof(IDatastarService))) - { - throw new Exception($"{nameof(AddDatastarMvc)} requires that {nameof(StarFederation.Datastar.DependencyInjection.ServiceCollectionExtensionMethods.AddDatastar)} is added first"); - } - - serviceCollection.AddControllers(options => options.ModelBinderProviders.Insert(0, new SignalsModelBinderProvider())); - return serviceCollection; - } -} diff --git a/sdk/dotnet/csharp/src/ModelBinding/SignalsModelBinder.cs b/sdk/dotnet/csharp/src/ModelBinding/SignalsModelBinder.cs deleted file mode 100644 index 7cd027582..000000000 --- a/sdk/dotnet/csharp/src/ModelBinding/SignalsModelBinder.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; -using StarFederation.Datastar.DependencyInjection; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace StarFederation.Datastar.ModelBinding; - -public class SignalsModelBinder(ILogger logger, IDatastarService signalsReader) : IModelBinder -{ - public async Task BindModelAsync(ModelBindingContext bindingContext) - { - DatastarSignalsBindingSource signalBindingSource = (bindingContext.BindingSource as DatastarSignalsBindingSource)!; - - // Get signals into a JsonDocument - JsonDocument doc; - try - { - doc = await ReadSignalsToJsonDocument(bindingContext); - } - catch (JsonException ex) when (ex is { LineNumber: 0, BytePositionInLine: 0 }) - { - logger.LogWarning("Empty Signals. Is it possible you have multiple [FromSignals] for a not-GET request?"); - bindingContext.Result = ModelBindingResult.Failed(); - return; - } - catch - { - bindingContext.Result = ModelBindingResult.Failed(); - return; - } - - try - { - if (bindingContext.ModelType.IsValueType || bindingContext.ModelType == typeof(string)) - { - // SignalsPath: use the name of the field in the method or the one passed in the attribute - string signalsPath = String.IsNullOrEmpty(signalBindingSource.BindingPath) ? bindingContext.FieldName : signalBindingSource.BindingPath; - - object? value = doc.RootElement.GetValueFromPath(signalsPath, bindingContext.ModelType, signalBindingSource.JsonSerializerOptions) - ?? (bindingContext.ModelType.IsValueType ? Activator.CreateInstance(bindingContext.ModelType) : null); - bindingContext.Result = ModelBindingResult.Success(value); - } - else - { - object? value; - if (String.IsNullOrEmpty(signalBindingSource.BindingPath)) - { - value = doc.Deserialize(bindingContext.ModelType, signalBindingSource.JsonSerializerOptions); - } - else - { - value = doc.RootElement.GetValueFromPath(signalBindingSource.BindingPath, bindingContext.ModelType, signalBindingSource.JsonSerializerOptions); - } - - bindingContext.Result = ModelBindingResult.Success(value); - } - } - catch - { - bindingContext.Result = ModelBindingResult.Failed(); - } - } - - private async ValueTask ReadSignalsToJsonDocument(ModelBindingContext bindingContext) - { - return bindingContext.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Get - ? JsonDocument.Parse(await signalsReader.ReadSignalsAsync() ?? String.Empty) - : await JsonDocument.ParseAsync(signalsReader.GetSignalsStream()); - } -} - -public class SignalsModelBinderProvider : IModelBinderProvider -{ - public IModelBinder? GetBinder(ModelBinderProviderContext context) - => context?.BindingInfo?.BindingSource?.DisplayName == DatastarSignalsBindingSource.BindingSourceName - ? new BinderTypeModelBinder(typeof(SignalsModelBinder)) - : null; - -} \ No newline at end of file diff --git a/sdk/dotnet/csharp/src/StarFederation.Datastar.csproj b/sdk/dotnet/csharp/src/StarFederation.Datastar.csproj deleted file mode 100644 index c898d4d7d..000000000 --- a/sdk/dotnet/csharp/src/StarFederation.Datastar.csproj +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώ - - - StarFederation.Datastar - net8.0;net9.0 - enable - enable - StarFederation.Datastar - 1.0.0-beta.7 - StarFederation.Datastar - Greg Holden and contributors - SDK for ServerSentEvents and convenience methods for Datastar - https://github.com/starfederation/datastar - datastar_icon.png - https://github.com/starfederation/datastar - datastar;datastar-sharp;asp.net core;asp.net;.net core;routing;web;csharp - true - - - - - - - - - - - - - True - - datastar_icon.png - - - - diff --git a/sdk/dotnet/csharp/src/Utility.cs b/sdk/dotnet/csharp/src/Utility.cs deleted file mode 100644 index 76db3cf05..000000000 --- a/sdk/dotnet/csharp/src/Utility.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Diagnostics; -using System.Text.Json; -// ReSharper disable PossibleMultipleEnumeration - -namespace StarFederation.Datastar; - -internal static class Utilities -{ - public static JsonElement? Get(this JsonElement element, string name) => - element.ValueKind != JsonValueKind.Null && element.ValueKind != JsonValueKind.Undefined && element.TryGetProperty(name, out JsonElement value) - ? value - : null; - - public static JsonElement? Get(this JsonElement element, int index) => - element.ValueKind == JsonValueKind.Null || element.ValueKind == JsonValueKind.Undefined ? null : - index < element.GetArrayLength() ? element[index] : null; - - /// - /// Given a dot-separated path into a JSON structure, will return the JsonElement or null - /// - /// the head of the tree - /// dot-separated path - /// JsonElement if found; null if the path cannot be followed - public static JsonElement? GetFromPath(this JsonElement jsonElement, string path) - { - JsonElement? GetFromPathCore(JsonElement jElement, IEnumerable pathElements) - { - if (!pathElements.Any()) - { - return null; - } - - while (true) - { - if (pathElements.Count() == 1) - { - return jElement.Get(pathElements.First()); - } - if (jElement.Get(pathElements.First()) is not { } childElement) - { - return null; - } - jElement = childElement; - pathElements = pathElements.Skip(1); - } - } - return GetFromPathCore(jsonElement, path.Split('.')); - } - - /// - /// Given a dot-separated path into a JSON structure, will return the value or null - /// - /// the head of the tree - /// dot-separated path - /// the type to convert the value into - /// options to the serializer - /// value if found; null if the path cannot be followed - public static object? GetValueFromPath(this JsonElement jsonElement, string path, Type jsonElementType, JsonSerializerOptions jsonSerializerOptions) - { - JsonElement? childJsonElement = GetFromPath(jsonElement, path); - return childJsonElement?.Deserialize(jsonElementType, jsonSerializerOptions); - } - - public static Tuple AsTuple(this KeyValuePair keyValuePair) => new(keyValuePair.Key, keyValuePair.Value); - public static Tuple AsTuple(this (TKey, TValue) keyValuePair) => new(keyValuePair.Item1, keyValuePair.Item2); -} \ No newline at end of file diff --git a/sdk/dotnet/fsharp/src/.gitattributes b/sdk/dotnet/fsharp/src/.gitattributes deleted file mode 100644 index 0c8ffec21..000000000 --- a/sdk/dotnet/fsharp/src/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -Consts.fs linguist-generated=true \ No newline at end of file diff --git a/sdk/dotnet/fsharp/src/Consts.fs b/sdk/dotnet/fsharp/src/Consts.fs deleted file mode 100644 index 3349dcdfa..000000000 --- a/sdk/dotnet/fsharp/src/Consts.fs +++ /dev/null @@ -1,70 +0,0 @@ -// This is auto-generated by Datastar. DO NOT EDIT. - -namespace StarFederation.Datastar.FSharp - -open System - -type ElementPatchMode = -/// Morphs the element into the existing element. -| Outer -/// Replaces the inner HTML of the existing element. -| Inner -/// Removes the existing element. -| Remove -/// Replaces the existing element with the new element. -| Replace -/// Prepends the element inside to the existing element. -| Prepend -/// Appends the element inside the existing element. -| Append -/// Inserts the element before the existing element. -| Before -/// Inserts the element after the existing element. -| After - -type EventType = -/// An event for patching HTML elements into the DOM. -| PatchElements -/// An event for patching signals. -| PatchSignals - - -module Consts = - let [] DatastarKey = "datastar" - let [] Version = "1.0.0-RC.1" - - /// Default: TimeSpan.FromMilliseconds 1000 - let DefaultSseRetryDuration = TimeSpan.FromMilliseconds 1000 - - - /// Default: outer - Morphs the element into the existing element. - let DefaultElementPatchMode = Outer - - let [] DefaultElementsUseViewTransitions = false - let [] DefaultPatchSignalsOnlyIfMissing = false - - - let [] DatastarDatalineSelector = "selector" - let [] DatastarDatalineMode = "mode" - let [] DatastarDatalineElements = "elements" - let [] DatastarDatalineUseViewTransition = "useViewTransition" - let [] DatastarDatalineSignals = "signals" - let [] DatastarDatalineOnlyIfMissing = "onlyIfMissing" - - module ElementPatchMode = - let inline toString this = - match this with - | ElementPatchMode.Outer -> "outer" - | ElementPatchMode.Inner -> "inner" - | ElementPatchMode.Remove -> "remove" - | ElementPatchMode.Replace -> "replace" - | ElementPatchMode.Prepend -> "prepend" - | ElementPatchMode.Append -> "append" - | ElementPatchMode.Before -> "before" - | ElementPatchMode.After -> "after" - - module EventType = - let inline toString this = - match this with - | EventType.PatchElements -> "datastar-patch-elements" - | EventType.PatchSignals -> "datastar-patch-signals" \ No newline at end of file diff --git a/sdk/dotnet/fsharp/src/ServerSentEvent.fs b/sdk/dotnet/fsharp/src/ServerSentEvent.fs deleted file mode 100644 index 364c3845c..000000000 --- a/sdk/dotnet/fsharp/src/ServerSentEvent.fs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StarFederation.Datastar.FSharp - -open System -open System.Buffers -open System.Text - -module internal ServerSentEvent = - let private eventPrefix = "event: "B - let private idPrefix = "id: "B - let private retryPrefix = "retry: "B - let private dataPrefix = "data: "B - - let inline private writeUtf8String (str:string) (writer:IBufferWriter) = - let span = writer.GetSpan(Encoding.UTF8.GetByteCount(str)) - let bytesWritten = Encoding.UTF8.GetBytes(str.AsSpan(), span) - writer.Advance(bytesWritten) - writer - - let inline private writeUtf8Literal (bytes:byte[]) (writer:IBufferWriter) = - let span = writer.GetSpan(bytes.Length) - bytes.AsSpan().CopyTo(span) - writer.Advance(bytes.Length) - writer - - let inline writeNewline (writer:IBufferWriter) = - let span = writer.GetSpan(1) - span[0] <- 10uy // '\n' - writer.Advance(1) - - let inline sendEventType eventType writer = - writer - |> writeUtf8Literal eventPrefix - |> writeUtf8String (Consts.EventType.toString eventType) - |> writeNewline - - let inline sendId id writer = - match id with - | ValueSome idValue -> - writer - |> writeUtf8Literal idPrefix - |> writeUtf8String idValue - |> writeNewline - | _ -> () - - let inline sendRetry (retry:TimeSpan) writer = - if retry <> Consts.DefaultSseRetryDuration then - writer - |> writeUtf8Literal retryPrefix - |> writeUtf8String (retry.TotalMilliseconds.ToString()) - |> writeNewline - - let inline sendDataLine dataLine writer = - writer - |> writeUtf8Literal dataPrefix - |> writeUtf8String dataLine - |> writeNewline diff --git a/sdk/dotnet/fsharp/src/ServerSentEventGenerator.fs b/sdk/dotnet/fsharp/src/ServerSentEventGenerator.fs deleted file mode 100644 index 5bc947b2e..000000000 --- a/sdk/dotnet/fsharp/src/ServerSentEventGenerator.fs +++ /dev/null @@ -1,268 +0,0 @@ -namespace StarFederation.Datastar.FSharp - -open System.Collections.Concurrent -open System.Collections.Generic -open System.IO -open System.Text -open System.Text.Json -open System.Threading -open System.Threading.Tasks -open Microsoft.AspNetCore.Http -open Microsoft.Extensions.Primitives -open StarFederation.Datastar.FSharp.Utility - -[] -type ServerSentEventGenerator(httpContextAccessor:IHttpContextAccessor) = - let httpRequest = httpContextAccessor.HttpContext.Request - let httpResponse = httpContextAccessor.HttpContext.Response - let mutable _startResponseTask : Task = null - let _startResponseLock = obj() - let _eventQueue = ConcurrentQueue Task>() - - static member StartServerEventStreamAsync(httpResponse:HttpResponse, additionalHeaders:KeyValuePair seq, cancellationToken:CancellationToken) = - let task = backgroundTask { - httpResponse.Headers.ContentType <- "text/event-stream" - if (httpResponse.HttpContext.Request.Protocol = HttpProtocol.Http11) then - httpResponse.Headers.Connection <- "keep-alive" - for KeyValue(name, content) in additionalHeaders do - match httpResponse.Headers.TryGetValue(name) with - | false, _ -> httpResponse.Headers.Add(name, content) - | true, _ -> () - do! httpResponse.StartAsync(cancellationToken) - return! httpResponse.BodyWriter.FlushAsync(cancellationToken) - } - task :> Task - - static member PatchElementsAsync(httpResponse:HttpResponse, elements:string, options:PatchElementsOptions, cancellationToken:CancellationToken) = - let writer = httpResponse.BodyWriter - writer |> ServerSentEvent.sendEventType PatchElements - writer |> ServerSentEvent.sendId options.EventId - writer |> ServerSentEvent.sendRetry options.Retry - - match options.Selector with - | ValueSome selector -> writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineSelector} {Selector.value selector}" - | _ -> () - - if options.PatchMode <> Consts.DefaultElementPatchMode then - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineMode} {Consts.ElementPatchMode.toString options.PatchMode}" - - if options.UseViewTransition <> Consts.DefaultElementsUseViewTransitions then - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineUseViewTransition} %A{options.UseViewTransition}" - - for segment in String.splitLinesToSegments elements do - writer |> ServerSentEvent.sendDataLine (String.buildDataLine Consts.DatastarDatalineElements segment) - - writer |> ServerSentEvent.writeNewline - - writer.FlushAsync(cancellationToken).AsTask() :> Task - - static member RemoveElementAsync(httpResponse:HttpResponse, selector:Selector, options:RemoveElementOptions, cancellationToken:CancellationToken) = - let writer = httpResponse.BodyWriter - writer |> ServerSentEvent.sendEventType PatchElements - writer |> ServerSentEvent.sendId options.EventId - writer |> ServerSentEvent.sendRetry options.Retry - - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineSelector} {selector |> Selector.value}" - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineMode} {ElementPatchMode.Remove |> Consts.ElementPatchMode.toString}" - - if options.UseViewTransition <> Consts.DefaultElementsUseViewTransitions then - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineUseViewTransition} %A{options.UseViewTransition}" - - writer |> ServerSentEvent.writeNewline - - writer.FlushAsync(cancellationToken).AsTask() :> Task - - static member PatchSignalsAsync(httpResponse:HttpResponse, signals:Signals, options:PatchSignalsOptions, cancellationToken:CancellationToken) = - let writer = httpResponse.BodyWriter - writer |> ServerSentEvent.sendEventType PatchSignals - writer |> ServerSentEvent.sendId options.EventId - writer |> ServerSentEvent.sendRetry options.Retry - - if options.OnlyIfMissing <> Consts.DefaultPatchSignalsOnlyIfMissing then - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineOnlyIfMissing} %A{options.OnlyIfMissing}" - - for segment in String.splitLinesToSegments (Signals.value signals) do - writer |> ServerSentEvent.sendDataLine (String.buildDataLine Consts.DatastarDatalineSignals segment) - - writer |> ServerSentEvent.writeNewline - - writer.FlushAsync(cancellationToken).AsTask() :> Task - - static member ExecuteScriptAsync(httpResponse:HttpResponse, script:string, options:ExecuteScriptOptions, cancellationToken:CancellationToken) = - let writer = httpResponse.BodyWriter - writer |> ServerSentEvent.sendEventType PatchElements - writer |> ServerSentEvent.sendId options.EventId - writer |> ServerSentEvent.sendRetry options.Retry - - writer |> ServerSentEvent.sendDataLine $"{Consts.DatastarDatalineElements} " - - writer |> ServerSentEvent.writeNewline - - writer.FlushAsync(cancellationToken).AsTask() :> Task - - static member GetSignalsStream(httpRequest:HttpRequest) = - match httpRequest.Method with - | System.Net.WebRequestMethods.Http.Get -> - match httpRequest.Query.TryGetValue(Consts.DatastarKey) with - | true, stringValues when stringValues.Count > 0 -> (new MemoryStream(Encoding.UTF8.GetBytes(stringValues[0])) :> Stream) - | _ -> Stream.Null - | _ -> httpRequest.Body - - static member ReadSignalsAsync(httpRequest:HttpRequest, cancellationToken:CancellationToken) = - task { - match httpRequest.Method with - | System.Net.WebRequestMethods.Http.Get -> - match httpRequest.Query.TryGetValue(Consts.DatastarKey) with - | true, stringValues when stringValues.Count > 0 -> return (stringValues[0] |> Signals.create) - | _ -> return Signals.empty - | _ -> - try - use readResult = new StreamReader(httpRequest.Body) - let! signals = readResult.ReadToEndAsync(cancellationToken) - return (signals |> Signals.create) - with _ -> return Signals.empty - } - - static member ReadSignalsAsync<'T>(httpRequest:HttpRequest, jsonSerializerOptions:JsonSerializerOptions, cancellationToken:CancellationToken) = - task { - try - match httpRequest.Method with - | System.Net.WebRequestMethods.Http.Get -> - match httpRequest.Query.TryGetValue(Consts.DatastarKey) with - | true, stringValues when stringValues.Count > 0 -> - return ValueSome (JsonSerializer.Deserialize<'T>(stringValues[0], jsonSerializerOptions)) - | _ -> - return ValueNone - | _ -> - let! t = JsonSerializer.DeserializeAsync<'T>(httpRequest.Body, jsonSerializerOptions, cancellationToken) - return (ValueSome t) - with _ -> return ValueNone - } - - member this.StartServerEventStreamAsync(additionalHeaders, cancellationToken) = - lock _startResponseLock (fun () -> - if _startResponseTask = null - then _startResponseTask <- ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, additionalHeaders, cancellationToken) - ) - _startResponseTask - - member private this.SendEventAsync(sendEventTask:unit -> Task, cancellationToken:CancellationToken) = - task { - _eventQueue.Enqueue(sendEventTask) - do! - if _startResponseTask <> null - then _startResponseTask - else ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, Seq.empty, cancellationToken) - let (_, sendEventTask') = _eventQueue.TryDequeue() - return! sendEventTask' () - } - - member this.PatchElementsAsync(elements, options, cancellationToken) = - let sendTask = fun () -> ServerSentEventGenerator.PatchElementsAsync(httpResponse, elements, options, cancellationToken) - this.SendEventAsync (sendTask, cancellationToken) :> Task - - member this.RemoveElementAsync(selector, options, cancellationToken) = - let sendTask = fun () -> ServerSentEventGenerator.RemoveElementAsync(httpResponse, selector, options, cancellationToken) - this.SendEventAsync (sendTask, cancellationToken) :> Task - - member this.PatchSignalsAsync(signals, options, cancellationToken) = - let sendTask = fun () -> ServerSentEventGenerator.PatchSignalsAsync(httpResponse, signals, options, cancellationToken) - this.SendEventAsync (sendTask, cancellationToken) :> Task - - member this.ExecuteScriptAsync(script, options, cancellationToken) = - let sendTask = fun () -> ServerSentEventGenerator.ExecuteScriptAsync(httpResponse, script, options, cancellationToken) - this.SendEventAsync (sendTask, cancellationToken) :> Task - - member this.GetSignalsStream() = - ServerSentEventGenerator.GetSignalsStream(httpRequest) - - member this.ReadSignalsAsync(cancellationToken) : Task = - ServerSentEventGenerator.ReadSignalsAsync(httpRequest, cancellationToken) - - member this.ReadSignalsAsync<'T>(jsonSerializerOptions, cancellationToken) = - ServerSentEventGenerator.ReadSignalsAsync<'T>(httpRequest, jsonSerializerOptions, cancellationToken) - - // - // SHORT HAND METHODS - // - static member StartServerEventStreamAsync(httpResponse, additionalHeaders) = - ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, additionalHeaders, httpResponse.HttpContext.RequestAborted) - static member StartServerEventStreamAsync(httpResponse) = - ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, Seq.empty, httpResponse.HttpContext.RequestAborted) - static member StartServerEventStreamAsync(httpResponse, cancellationToken) = - ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, Seq.empty, cancellationToken) - - static member PatchElementsAsync(httpResponse, elements, options) = - ServerSentEventGenerator.PatchElementsAsync(httpResponse, elements, options, httpResponse.HttpContext.RequestAborted) - static member PatchElementsAsync(httpResponse, elements) = - ServerSentEventGenerator.PatchElementsAsync(httpResponse, elements, PatchElementsOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - static member RemoveElementAsync(httpResponse, selector, options) = - ServerSentEventGenerator.RemoveElementAsync(httpResponse, selector, options, httpResponse.HttpContext.RequestAborted) - static member RemoveElementAsync(httpResponse, selector) = - ServerSentEventGenerator.RemoveElementAsync(httpResponse, selector, RemoveElementOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - static member PatchSignalsAsync(httpResponse, signals, options) = - ServerSentEventGenerator.PatchSignalsAsync(httpResponse, signals, options, httpResponse.HttpContext.RequestAborted) - static member PatchSignalsAsync(httpResponse, signals) = - ServerSentEventGenerator.PatchSignalsAsync(httpResponse, signals, PatchSignalsOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - static member ExecuteScriptAsync(httpResponse, script, options) = - ServerSentEventGenerator.ExecuteScriptAsync(httpResponse, script, options, httpResponse.HttpContext.RequestAborted) - static member ExecuteScriptAsync(httpResponse, script) = - ServerSentEventGenerator.ExecuteScriptAsync(httpResponse, script, ExecuteScriptOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - static member ReadSignalsAsync(httpRequest) = - ServerSentEventGenerator.ReadSignalsAsync(httpRequest, cancellationToken=httpRequest.HttpContext.RequestAborted) - static member ReadSignalsAsync<'T>(httpRequest, jsonSerializerOptions) = - ServerSentEventGenerator.ReadSignalsAsync<'T>(httpRequest, jsonSerializerOptions, httpRequest.HttpContext.RequestAborted) - static member ReadSignalsAsync<'T>(httpRequest) = - ServerSentEventGenerator.ReadSignalsAsync<'T>(httpRequest, JsonSerializerOptions.SignalsDefault, httpRequest.HttpContext.RequestAborted) - - member this.StartServerEventStreamAsync(additionalHeaders) = - ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, additionalHeaders, httpResponse.HttpContext.RequestAborted) - member this.StartServerEventStreamAsync(cancellationToken:CancellationToken) = - ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, Seq.empty, cancellationToken) - member this.StartServerEventStreamAsync() = - ServerSentEventGenerator.StartServerEventStreamAsync(httpResponse, Seq.empty, httpResponse.HttpContext.RequestAborted) - - member this.PatchElementsAsync(elements, options) = - ServerSentEventGenerator.PatchElementsAsync(httpResponse, elements, options, httpResponse.HttpContext.RequestAborted) - member this.PatchElementsAsync(elements, cancellationToken) = - ServerSentEventGenerator.PatchElementsAsync(httpResponse, elements, PatchElementsOptions.Defaults, cancellationToken) - member this.PatchElementsAsync(elements) = - ServerSentEventGenerator.PatchElementsAsync(httpResponse, elements, PatchElementsOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - member this.RemoveElementAsync(selector, options) = - ServerSentEventGenerator.RemoveElementAsync(httpResponse, selector, options, httpResponse.HttpContext.RequestAborted) - member this.RemoveElementAsync(selector, cancellationToken) = - ServerSentEventGenerator.RemoveElementAsync(httpResponse, selector, RemoveElementOptions.Defaults, cancellationToken) - member this.RemoveElementAsync(selector) = - ServerSentEventGenerator.RemoveElementAsync(httpResponse, selector, RemoveElementOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - member this.PatchSignalsAsync(signals, options) = - ServerSentEventGenerator.PatchSignalsAsync(httpResponse, signals, options, httpResponse.HttpContext.RequestAborted) - member this.PatchSignalsAsync(signals, cancellationToken) = - ServerSentEventGenerator.PatchSignalsAsync(httpResponse, signals, PatchSignalsOptions.Defaults, cancellationToken) - member this.PatchSignalsAsync(signals) = - ServerSentEventGenerator.PatchSignalsAsync(httpResponse, signals, PatchSignalsOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - member this.ExecuteScriptAsync(script, options) = - ServerSentEventGenerator.ExecuteScriptAsync(httpResponse, script, options, httpResponse.HttpContext.RequestAborted) - member this.ExecuteScriptAsync(script, cancellationToken) = - ServerSentEventGenerator.ExecuteScriptAsync(httpResponse, script, ExecuteScriptOptions.Defaults, cancellationToken) - member this.ExecuteScriptAsync(script) = - ServerSentEventGenerator.ExecuteScriptAsync(httpResponse, script, ExecuteScriptOptions.Defaults, httpResponse.HttpContext.RequestAborted) - - member this.ReadSignalsAsync(): Task = - ServerSentEventGenerator.ReadSignalsAsync(httpRequest, httpRequest.HttpContext.RequestAborted) - member this.ReadSignalsAsync<'T>(jsonSerializerOptions) = - ServerSentEventGenerator.ReadSignalsAsync<'T>(httpRequest, jsonSerializerOptions, httpRequest.HttpContext.RequestAborted) - member this.ReadSignalsAsync<'T>(cancellationToken) = - ServerSentEventGenerator.ReadSignalsAsync<'T>(httpRequest, JsonSerializerOptions.SignalsDefault, cancellationToken) - member this.ReadSignalsAsync<'T>() = - ServerSentEventGenerator.ReadSignalsAsync<'T>(httpRequest, JsonSerializerOptions.SignalsDefault, httpRequest.HttpContext.RequestAborted) diff --git a/sdk/dotnet/fsharp/src/StarFederation.Datastar.FSharp.fsproj b/sdk/dotnet/fsharp/src/StarFederation.Datastar.FSharp.fsproj deleted file mode 100644 index 35fc12e10..000000000 --- a/sdk/dotnet/fsharp/src/StarFederation.Datastar.FSharp.fsproj +++ /dev/null @@ -1,59 +0,0 @@ - - - StarFederation.Datastar.FSharp - 1.0.0-beta.2 - StarFederation.Datastar.FSharp - disabled - - - SDK for ServerSentEvents and convenience methods for Datastar - Greg Holden and contributors - en-CA - - - embedded - Library - true - false - true - - - StarFederation.Datastar.FSharp - datastar;datastar-sharp;fsharp;functional;asp.net core;asp.net;.net core;routing;web;csharp - https://github.com/starfederation/datastar - MIT - datastar_icon.png - README.md - true - git - https://github.com/starfederation/datastar - - - true - true - true - StarFederation.Datastar.FSharp - net8.0;net9.0 - - - - - - - - - true - datastar_icon.png - $(PackageIconUrl) - - - README.md - - - - - - - - - diff --git a/sdk/dotnet/fsharp/src/Types.fs b/sdk/dotnet/fsharp/src/Types.fs deleted file mode 100644 index f75198ecc..000000000 --- a/sdk/dotnet/fsharp/src/Types.fs +++ /dev/null @@ -1,122 +0,0 @@ -namespace StarFederation.Datastar.FSharp - -open System -open System.Collections.Generic -open System.Text.Json -open System.Text.Json.Nodes -open System.Text.RegularExpressions -open StarFederation.Datastar.FSharp.Utility - -/// -/// Signals read to and from Datastar on the front end -/// -type Signals = string - -/// -/// A dotted path into Signals to access a key/value pair -/// -type SignalPath = string - -/// -/// An HTML selector name -/// -type Selector = string - -[] -type PatchElementsOptions = - { Selector: Selector voption - PatchMode: ElementPatchMode - UseViewTransition: bool - EventId: string voption - Retry: TimeSpan } - with - static member Defaults = - { Selector = ValueNone - PatchMode = Consts.DefaultElementPatchMode - UseViewTransition = Consts.DefaultElementsUseViewTransitions - EventId = ValueNone - Retry = Consts.DefaultSseRetryDuration } - -[] -type RemoveElementOptions = - { UseViewTransition: bool - EventId: string voption - Retry: TimeSpan } - with - static member Defaults = - { UseViewTransition = Consts.DefaultElementsUseViewTransitions - EventId = ValueNone - Retry = Consts.DefaultSseRetryDuration } - -[] -type PatchSignalsOptions = - { OnlyIfMissing: bool - EventId: string voption - Retry: TimeSpan } - with - static member Defaults = - { OnlyIfMissing = Consts.DefaultPatchSignalsOnlyIfMissing - EventId = ValueNone - Retry = Consts.DefaultSseRetryDuration } - -[] -type ExecuteScriptOptions = - { EventId: string voption; Retry: TimeSpan } - with - static member Defaults = - { EventId = ValueNone - Retry = Consts.DefaultSseRetryDuration } - -module JsonSerializerOptions = - let SignalsDefault = - let options = JsonSerializerOptions() - options.PropertyNameCaseInsensitive <- true - options - -module Signals = - let inline value (signals:Signals) : string = signals - let create (signalsString:string) = Signals signalsString - let tryCreate (signalsString:string) = - try - let _ = JsonObject.Parse(signalsString) - ValueSome (Signals signalsString) - with _ -> ValueNone - let empty = Signals "{ }" - -module SignalPath = - let inline value (signalPath:SignalPath) = signalPath - let isValidKey (signalPathKey:string) = - signalPathKey |> String.isPopulated && signalPathKey.ToCharArray() |> Seq.forall (fun chr -> Char.IsLetter chr || Char.IsNumber chr || chr = '_') - let isValid (signalPathString:string) = signalPathString.Split('.') |> Array.forall isValidKey - let tryCreate (signalPathString:string) = - if isValid signalPathString - then ValueSome (SignalPath signalPathString) - else ValueNone - let sp (signalPathString:string) = - if isValid signalPathString - then SignalPath signalPathString - else failwith $"{signalPathString} is not a valid signal path" - let create = sp - let kebabValue signals = signals |> value |> String.toKebab - let keys (signalPath:SignalPath) = signalPath.Split('.') - let createJsonNodePathToValue<'T> signalPath (signalValue:'T) = - signalPath - |> keys - |> Seq.rev - |> Seq.fold (fun json key -> - JsonObject([ KeyValuePair (key, json) ]) :> JsonNode - ) (JsonValue.Create(signalValue) :> JsonNode) - -module Selector = - let inline value (selector:Selector) = selector - let regex = Regex(@"[#.][-_]?[_a-zA-Z]+(?:\w|\\.)*|(?<=\s+|^)(?:\w+|\*)|\[[^\s""'=<>`]+?(?`]+))?\]|:[\w-]+(?:\(.*\))?", RegexOptions.Compiled) - let isValid (selectorString:string) = regex.IsMatch selectorString - let tryCreate (selectorString:string) = - if isValid selectorString - then ValueSome (Selector selectorString) - else ValueNone - let sel (selectorString:string) = - if isValid selectorString - then Selector selectorString - else failwith $"{selectorString} is not a valid selector" - let create = sel diff --git a/sdk/dotnet/fsharp/src/Utility.fs b/sdk/dotnet/fsharp/src/Utility.fs deleted file mode 100644 index 79b73c519..000000000 --- a/sdk/dotnet/fsharp/src/Utility.fs +++ /dev/null @@ -1,48 +0,0 @@ -namespace StarFederation.Datastar.FSharp - -module internal Utility = - open System - open System.Text - open Microsoft.Extensions.Primitives - - module internal String = - let dotSeparator = [| '.' |] - let newLines = [| "\r\n"; "\n"; "\r" |] - let newLineChars = [| '\r'; '\n' |] - - // New zero-allocation version using StringTokenizer - let inline splitToSegments (separatorChars:char[]) (text:string) = - StringTokenizer(text, separatorChars) - |> Seq.filter (fun segment -> segment.Length > 0) - - let inline splitLinesToSegments (text:string) = - splitToSegments newLineChars text - - let buildDataLine (prefix:string) (segment:StringSegment) = - String.Create(prefix.Length + segment.Length + 1, (prefix, segment), fun span (prefix, segment) -> - let mutable pos = 0 - prefix.AsSpan().CopyTo(span.Slice(pos)) - pos <- pos + prefix.Length - span.[pos] <- ' ' - pos <- pos + 1 - segment.AsSpan().CopyTo(span.Slice(pos)) - ) - - let buildDataLinesFromSegments (prefix:string) (content:string) = - splitLinesToSegments content - |> Seq.map (buildDataLine prefix) - |> Seq.toArray - |> StringValues - - let isPopulated = (String.IsNullOrWhiteSpace >> not) - - let toKebab (pascalString:string) = - let sb = StringBuilder(pascalString.Length * 2) - let chars = pascalString.ToCharArray() - for i = 0 to chars.Length - 1 do - let chr = chars.[i] - if Char.IsUpper(chr) && i > 0 then - sb.Append('-').Append(Char.ToLower(chr)) |> ignore - else - sb.Append(Char.ToLower(chr)) |> ignore - sb.ToString() diff --git a/sdk/go/README.md b/sdk/go/README.md deleted file mode 100644 index 50e0d309c..000000000 --- a/sdk/go/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Go SDK for 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. - -## Installation - -```sh -go get -u github.com/starfederation/datastar/sdk/go/datastar@v1.0.0-beta.11 -``` - -## Examples - -- [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. diff --git a/sdk/go/datastar/.gitattributes b/sdk/go/datastar/.gitattributes deleted file mode 100644 index 4f7542402..000000000 --- a/sdk/go/datastar/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -consts.go linguist-generated=true \ No newline at end of file diff --git a/sdk/go/datastar/consts.go b/sdk/go/datastar/consts.go deleted file mode 100644 index 0bdb8cbea..000000000 --- a/sdk/go/datastar/consts.go +++ /dev/null @@ -1,97 +0,0 @@ -// This is auto-generated by Datastar. DO NOT EDIT. - -package datastar - -import "time" - -const ( - DatastarKey = "datastar" - Version = "1.0.0-RC.1" - VersionClientByteSize = 31267 - VersionClientByteSizeBrotli = 10668 - - //region Default durations - - // The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. - DefaultSseRetryDuration = 1000 * time.Millisecond - - //endregion Default durations - - //region Default strings - - - //endregion Default strings - - //region Dataline literals - SelectorDatalineLiteral = "selector " - ModeDatalineLiteral = "mode " - ElementsDatalineLiteral = "elements " - UseViewTransitionDatalineLiteral = "useViewTransition " - SignalsDatalineLiteral = "signals " - OnlyIfMissingDatalineLiteral = "onlyIfMissing " - //endregion Dataline literals -) - -var ( - //region Default booleans - - // Should elements be patched using the ViewTransition API? - DefaultElementsUseViewTransitions = false - - // Should a given set of signals patch if they are missing? - DefaultPatchSignalsOnlyIfMissing = false - - //endregion Default booleans -) - -//region Enums - -//region The mode in which an element is patched into the DOM. -type ElementPatchMode string - -const ( - // Default value for ElementPatchMode - // Morphs the element into the existing element. - DefaultElementPatchMode = ElementPatchModeOuter - - // Morphs the element into the existing element. - ElementPatchModeOuter ElementPatchMode = "outer" - - // Replaces the inner HTML of the existing element. - ElementPatchModeInner ElementPatchMode = "inner" - - // Removes the existing element. - ElementPatchModeRemove ElementPatchMode = "remove" - - // Replaces the existing element with the new element. - ElementPatchModeReplace ElementPatchMode = "replace" - - // Prepends the element inside to the existing element. - ElementPatchModePrepend ElementPatchMode = "prepend" - - // Appends the element inside the existing element. - ElementPatchModeAppend ElementPatchMode = "append" - - // Inserts the element before the existing element. - ElementPatchModeBefore ElementPatchMode = "before" - - // Inserts the element after the existing element. - ElementPatchModeAfter ElementPatchMode = "after" - -) -//endregion ElementPatchMode - -//region The type protocol on top of SSE which allows for core pushed based communication between the server and the client. -type EventType string - -const ( - // An event for patching HTML elements into the DOM. - EventTypePatchElements EventType = "datastar-patch-elements" - - // An event for patching signals. - EventTypePatchSignals EventType = "datastar-patch-signals" - -) -//endregion EventType - -//endregion Enums \ No newline at end of file diff --git a/sdk/go/datastar/elements-sugar.go b/sdk/go/datastar/elements-sugar.go deleted file mode 100644 index 4b7266387..000000000 --- a/sdk/go/datastar/elements-sugar.go +++ /dev/null @@ -1,212 +0,0 @@ -package datastar - -import ( - "context" - "fmt" - "io" - - "github.com/valyala/bytebufferpool" -) - -// ValidElementPatchModes is a list of valid element patch modes. -var ValidElementPatchModes = []ElementPatchMode{ - ElementPatchModeOuter, - ElementPatchModeInner, - ElementPatchModeRemove, - ElementPatchModePrepend, - ElementPatchModeAppend, - ElementPatchModeBefore, - ElementPatchModeAfter, - ElementPatchModeReplace, -} - -// ElementPatchModeFromString converts a string to a [ElementPatchMode]. -func ElementPatchModeFromString(s string) (ElementPatchMode, error) { - switch s { - case "outer": - return ElementPatchModeOuter, nil - case "inner": - return ElementPatchModeInner, nil - case "remove": - return ElementPatchModeRemove, nil - case "prepend": - return ElementPatchModePrepend, nil - case "append": - return ElementPatchModeAppend, nil - case "before": - return ElementPatchModeBefore, nil - case "after": - return ElementPatchModeAfter, nil - case "replace": - return ElementPatchModeReplace, nil - default: - return "", fmt.Errorf("invalid element merge type: %s", s) - } -} - -// WithModeOuter creates a PatchElementOption that merges elements using the outer mode. -func WithModeOuter() PatchElementOption { - return WithMode(ElementPatchModeOuter) -} - -// WithModeInner creates a PatchElementOption that merges elements using the inner mode. -func WithModeInner() PatchElementOption { - return WithMode(ElementPatchModeInner) -} - -// WithModeRemove creates a PatchElementOption that removes elements from the DOM. -func WithModeRemove() PatchElementOption { - return WithMode(ElementPatchModeRemove) -} - -// WithModePrepend creates a PatchElementOption that merges elements using the prepend mode. -func WithModePrepend() PatchElementOption { - return WithMode(ElementPatchModePrepend) -} - -// WithModeAppend creates a PatchElementOption that merges elements using the append mode. -func WithModeAppend() PatchElementOption { - return WithMode(ElementPatchModeAppend) -} - -// WithModeBefore creates a PatchElementOption that merges elements using the before mode. -func WithModeBefore() PatchElementOption { - return WithMode(ElementPatchModeBefore) -} - -// WithModeAfter creates a PatchElementOption that merges elements using the after mode. -func WithModeAfter() PatchElementOption { - return WithMode(ElementPatchModeAfter) -} - -// WithModeReplace creates a PatchElementOption that replaces elements without morphing. -// This mode does not use morphing and will completely replace the element, resetting any related state. -func WithModeReplace() PatchElementOption { - return WithMode(ElementPatchModeReplace) -} - -// WithSelectorID is a convenience wrapper for [WithSelector] option -// equivalent to calling `WithSelector("#"+id)`. -func WithSelectorID(id string) PatchElementOption { - return WithSelector("#" + id) -} - -// WithViewTransitions enables the use of view transitions when merging elements. -func WithViewTransitions() PatchElementOption { - return func(o *patchElementOptions) { - o.UseViewTransitions = true - } -} - -// WithoutViewTransitions disables the use of view transitions when merging elements. -func WithoutViewTransitions() PatchElementOption { - return func(o *patchElementOptions) { - o.UseViewTransitions = false - } -} - -// PatchElementf is a convenience wrapper for [PatchElements] option -// equivalent to calling `PatchElements(fmt.Sprintf(format, args...))`. -func (sse *ServerSentEventGenerator) PatchElementf(format string, args ...any) error { - return sse.PatchElements(fmt.Sprintf(format, args...)) -} - -// 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 -} - -// PatchElementTempl is a convenience adaptor of [sse.PatchElements] for [TemplComponent]. -func (sse *ServerSentEventGenerator) PatchElementTempl(c TemplComponent, opts ...PatchElementOption) error { - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - if err := c.Render(sse.Context(), buf); err != nil { - return fmt.Errorf("failed to patch element: %w", err) - } - if err := sse.PatchElements(buf.String(), opts...); err != nil { - return fmt.Errorf("failed to patch element: %w", err) - } - return nil -} - -// 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 -} - -// PatchElementGostar is a convenience adaptor of [sse.PatchElements] for [GoStarElementRenderer]. -func (sse *ServerSentEventGenerator) PatchElementGostar(child GoStarElementRenderer, opts ...PatchElementOption) error { - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - if err := child.Render(buf); err != nil { - return fmt.Errorf("failed to render: %w", err) - } - if err := sse.PatchElements(buf.String(), opts...); err != nil { - return fmt.Errorf("failed to patch element: %w", err) - } - 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...)) -} - -// RemoveElement is a convenience method for removing elements from the DOM. -// It uses PatchElements with the remove mode and the specified selector. -func (sse *ServerSentEventGenerator) RemoveElement(selector string, opts ...PatchElementOption) error { - // Prepend the remove mode option - allOpts := append([]PatchElementOption{WithModeRemove(), WithSelector(selector)}, opts...) - return sse.PatchElements("", allOpts...) -} - -// RemoveElementf is a convenience wrapper for RemoveElement that formats the selector string -// using the provided format and arguments similar to fmt.Sprintf. -func (sse *ServerSentEventGenerator) RemoveElementf(selectorFormat string, args ...any) error { - selector := fmt.Sprintf(selectorFormat, args...) - return sse.RemoveElement(selector) -} - -// RemoveElementByID is a convenience wrapper for RemoveElement that removes an element by its ID. -// Equivalent to calling RemoveElement("#"+id). -func (sse *ServerSentEventGenerator) RemoveElementByID(id string) error { - return sse.RemoveElement("#" + id) -} diff --git a/sdk/go/datastar/elements.go b/sdk/go/datastar/elements.go deleted file mode 100644 index eeb67258b..000000000 --- a/sdk/go/datastar/elements.go +++ /dev/null @@ -1,121 +0,0 @@ -package datastar - -import ( - "fmt" - "strings" - "time" -) - -// patchElementOptions holds the configuration data for [PatchElementOption]s used -// for initialization of [sse.PatchElements] event. -type patchElementOptions struct { - EventID string - RetryDuration time.Duration - Selector string - Mode ElementPatchMode - UseViewTransitions bool -} - -// PatchElementOption configures the [sse.PatchElements] event initialization. -type PatchElementOption func(*patchElementOptions) - -// WithPatchElementsEventID configures an optional event ID for the elements patch 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 WithPatchElementsEventID(id string) PatchElementOption { - return func(o *patchElementOptions) { - 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) PatchElementOption { - selector := fmt.Sprintf(selectorFormat, args...) - return WithSelector(selector) -} - -// WithSelector specifies the [CSS selector] for HTML elements that an element 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) PatchElementOption { - return func(o *patchElementOptions) { - o.Selector = selector - } -} - -// WithMode overrides the [DefaultElementPatchMode] for the element. -// Choose a valid [ElementPatchMode]. -func WithMode(merge ElementPatchMode) PatchElementOption { - return func(o *patchElementOptions) { - o.Mode = merge - } -} - -// WithUseViewTransitions specifies whether to use [view transitions] when merging elements. -// -// [view transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API -func WithUseViewTransitions(useViewTransition bool) PatchElementOption { - return func(o *patchElementOptions) { - o.UseViewTransitions = useViewTransition - } -} - -// WithRetryDuration overrides the [DefaultSseRetryDuration] for the element patch event. -func WithRetryDuration(retryDuration time.Duration) PatchElementOption { - return func(o *patchElementOptions) { - o.RetryDuration = retryDuration - } -} - -// PatchElements sends HTML elements to the client to update the DOM tree with. -func (sse *ServerSentEventGenerator) PatchElements(elements string, opts ...PatchElementOption) error { - options := &patchElementOptions{ - EventID: "", - RetryDuration: DefaultSseRetryDuration, - Selector: "", - Mode: ElementPatchModeOuter, - } - for _, opt := range opts { - opt(options) - } - - sendOptions := make([]SSEEventOption, 0, 2) - if options.EventID != "" { - sendOptions = append(sendOptions, WithSSEEventId(options.EventID)) - } - if options.RetryDuration > 0 { - sendOptions = append(sendOptions, WithSSERetryDuration(options.RetryDuration)) - } - - dataRows := make([]string, 0, 4) - if options.Selector != "" { - dataRows = append(dataRows, SelectorDatalineLiteral+options.Selector) - } - if options.Mode != ElementPatchModeOuter { - dataRows = append(dataRows, ModeDatalineLiteral+string(options.Mode)) - } - if options.UseViewTransitions { - dataRows = append(dataRows, UseViewTransitionDatalineLiteral+"true") - } - - if elements != "" { - parts := strings.Split(elements, "\n") - for _, part := range parts { - dataRows = append(dataRows, ElementsDatalineLiteral+part) - } - } - - if err := sse.Send( - EventTypePatchElements, - dataRows, - sendOptions..., - ); err != nil { - return fmt.Errorf("failed to send elements: %w", err) - } - - return nil -} diff --git a/sdk/go/datastar/elements_test.go b/sdk/go/datastar/elements_test.go deleted file mode 100644 index 396da8245..000000000 --- a/sdk/go/datastar/elements_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package datastar - -import "testing" - -func TestAllValidElementMergeTypes(t *testing.T) { - var err error - for _, validType := range ValidElementPatchModes { - if _, err = ElementPatchModeFromString(string(validType)); err != nil { - t.Errorf("Expected %v to be a valid element merge type, but it was rejected: %v", validType, err) - } - } - - if _, err = ElementPatchModeFromString(""); err == nil { - t.Errorf("Expected an empty string to be an invalid element merge type, but it was accepted") - } - - if _, err = ElementPatchModeFromString("fakeType"); err == nil { - t.Errorf("Expected a fake type to be an invalid element merge type, but it was accepted") - } -} diff --git a/sdk/go/datastar/execute-script.go b/sdk/go/datastar/execute-script.go deleted file mode 100644 index cec799654..000000000 --- a/sdk/go/datastar/execute-script.go +++ /dev/null @@ -1,336 +0,0 @@ -package datastar - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "time" -) - -// executeScriptOptions hold script options that will be translated to [SSEEventOptions]. -type executeScriptOptions struct { - EventID string - AutoRemove *bool - Attributes []string - RetryDuration time.Duration -} - -// 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) { - o.EventID = id - } -} - -// WithExecuteScriptRetryDuration overrides the [DefaultSseRetryDuration] for this script -// execution only. -func WithExecuteScriptRetryDuration(retryDuration time.Duration) ExecuteScriptOption { - return func(o *executeScriptOptions) { - o.RetryDuration = retryDuration - } -} - -// WithExecuteScriptAutoRemove requires the client to eliminate the script element after its execution. -func WithExecuteScriptAutoRemove(autoremove bool) ExecuteScriptOption { - return func(o *executeScriptOptions) { - o.AutoRemove = &autoremove - } -} - -// WithExecuteScriptAttributes sets the script element attributes. -// Each attribute should be a complete key="value" pair (e.g., `type="module"`). -func WithExecuteScriptAttributes(attributes ...string) ExecuteScriptOption { - 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") - } - attributes := make([]string, 0, len(kvs)/2) - for i := 0; i < len(kvs); i += 2 { - attribute := fmt.Sprintf(`%s="%s"`, kvs[i], kvs[i+1]) - attributes = append(attributes, attribute) - } - return WithExecuteScriptAttributes(attributes...) -} - -// ExecuteScript runs a script in the client browser by using PatchElements to send a ") - - // Use PatchElements to send the script - patchOpts := []PatchElementOption{ - WithSelector("body"), - WithModeAppend(), - } - if options.EventID != "" { - patchOpts = append(patchOpts, WithPatchElementsEventID(options.EventID)) - } - if options.RetryDuration > 0 { - patchOpts = append(patchOpts, WithRetryDuration(options.RetryDuration)) - } - - return sse.PatchElements(sb.String(), patchOpts...) -} - -// 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...) -} - -// 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 - Bubbles bool - Cancelable bool - Composed bool -} - -// 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) { - o.EventID = id - } -} - -// WithDispatchCustomEventRetryDuration overrides the [DefaultSseRetryDuration] for one custom event. -func WithDispatchCustomEventRetryDuration(retryDuration time.Duration) DispatchCustomEventOption { - return func(o *dispatchCustomEventOptions) { - o.RetryDuration = retryDuration - } -} - -// WithDispatchCustomEventSelector replaces the default custom event target `document` with a -// [CSS selector]. If the selector matches multiple HTML elements, the event will be dispatched -// from each one. For example, if the selector is `#my-element`, the event will be dispatched -// from the element with the ID `my-element`. If the selector is `main > section`, the event will be dispatched -// from each `
` element which is a direct child of the `
` element. -// -// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors -func WithDispatchCustomEventSelector(selector string) DispatchCustomEventOption { - return func(o *dispatchCustomEventOptions) { - 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) { - 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) { - 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) { - 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") - } - - detailsJSON, err := json.Marshal(detail) - if err != nil { - return fmt.Errorf("failed to marshal detail: %w", err) - } - - const defaultSelector = "document" - options := dispatchCustomEventOptions{ - EventID: "", - RetryDuration: DefaultSseRetryDuration, - Selector: defaultSelector, - Bubbles: true, - Cancelable: true, - Composed: true, - } - - for _, opt := range opts { - opt(&options) - } - - elementsJS := `[document]` - if options.Selector != "" && options.Selector != defaultSelector { - elementsJS = fmt.Sprintf(`document.querySelectorAll(%q)`, options.Selector) - } - - js := fmt.Sprintf(` -const elements = %s - -const event = new CustomEvent(%q, { - bubbles: %t, - cancelable: %t, - composed: %t, - detail: %s, -}); - -elements.forEach((element) => { - element.dispatchEvent(event); -}); - `, - elementsJS, - eventName, - options.Bubbles, - options.Cancelable, - options.Composed, - string(detailsJSON), - ) - - executeOptions := make([]ExecuteScriptOption, 0) - if options.EventID != "" { - executeOptions = append(executeOptions, WithExecuteScriptEventID(options.EventID)) - } - if options.RetryDuration != 0 { - executeOptions = append(executeOptions, WithExecuteScriptRetryDuration(options.RetryDuration)) - } - - return sse.ExecuteScript(js, executeOptions...) - -} - -// 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 { - wrappedURLs[i] = fmt.Sprintf(`"%s"`, url) - } - script := fmt.Sprintf(` -{ - "prefetch": [ - { - "source": "list", - "urls": [ - %s - ] - } - ] -} - `, strings.Join(wrappedURLs, ",\n\t\t\t\t")) - return sse.ExecuteScript( - script, - WithExecuteScriptAutoRemove(false), - WithExecuteScriptAttributes(`type="speculationrules"`), - ) -} diff --git a/sdk/go/datastar/signals-sugar.go b/sdk/go/datastar/signals-sugar.go deleted file mode 100644 index 6c1496ed1..000000000 --- a/sdk/go/datastar/signals-sugar.go +++ /dev/null @@ -1,42 +0,0 @@ -package datastar - -import ( - "encoding/json" - "fmt" -) - -// MarshalAndPatchSignals is a convenience method for [see.PatchSignals]. -// It marshals a given signals struct into JSON and -// emits a [EventTypePatchSignals] event. -func (sse *ServerSentEventGenerator) MarshalAndPatchSignals(signals any, opts ...PatchSignalsOption) error { - b, err := json.Marshal(signals) - if err != nil { - panic(err) - } - if err := sse.PatchSignals(b, opts...); err != nil { - return fmt.Errorf("failed to patch signals: %w", err) - } - - return nil -} - -// MarshalAndPatchSignalsIfMissing is a convenience method for [see.MarshalAndPatchSignals]. -// It is equivalent to calling [see.MarshalAndPatchSignals] with [see.WithOnlyIfMissing(true)] option. -func (sse *ServerSentEventGenerator) MarshalAndPatchSignalsIfMissing(signals any, opts ...PatchSignalsOption) error { - if err := sse.MarshalAndPatchSignals( - signals, - append(opts, WithOnlyIfMissing(true))..., - ); err != nil { - return fmt.Errorf("failed to patch signals if missing: %w", err) - } - return nil -} - -// PatchSignalsIfMissingRaw is a convenience method for [see.PatchSignals]. -// It is equivalent to calling [see.PatchSignals] with [see.WithOnlyIfMissing(true)] option. -func (sse *ServerSentEventGenerator) PatchSignalsIfMissingRaw(signalsJSON string) error { - if err := sse.PatchSignals([]byte(signalsJSON), WithOnlyIfMissing(true)); err != nil { - return fmt.Errorf("failed to patch signals if missing: %w", err) - } - return nil -} diff --git a/sdk/go/datastar/signals.go b/sdk/go/datastar/signals.go deleted file mode 100644 index 44de526fc..000000000 --- a/sdk/go/datastar/signals.go +++ /dev/null @@ -1,120 +0,0 @@ -package datastar - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/valyala/bytebufferpool" -) - -// patchSignalsOptions holds configuration options for patching signals. -type patchSignalsOptions struct { - EventID string - RetryDuration time.Duration - OnlyIfMissing bool -} - -// PatchSignalsOption configures one [EventTypePatchSignals] event. -type PatchSignalsOption func(*patchSignalsOptions) - -// WithPatchSignalsEventID configures an optional event ID for the signals patch 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 WithPatchSignalsEventID(id string) PatchSignalsOption { - return func(o *patchSignalsOptions) { - o.EventID = id - } -} - -// WithPatchSignalsRetryDuration overrides the [DefaultSseRetryDuration] for signal patching. -func WithPatchSignalsRetryDuration(retryDuration time.Duration) PatchSignalsOption { - return func(o *patchSignalsOptions) { - o.RetryDuration = retryDuration - } -} - -// WithOnlyIfMissing instructs the client to only patch signals if they are missing. -func WithOnlyIfMissing(onlyIfMissing bool) PatchSignalsOption { - return func(o *patchSignalsOptions) { - o.OnlyIfMissing = onlyIfMissing - } -} - -// PatchSignals sends a [EventTypePatchSignals] to the client. -// Requires a JSON-encoded payload. -func (sse *ServerSentEventGenerator) PatchSignals(signalsContents []byte, opts ...PatchSignalsOption) error { - options := &patchSignalsOptions{ - EventID: "", - RetryDuration: DefaultSseRetryDuration, - OnlyIfMissing: false, - } - for _, opt := range opts { - opt(options) - } - - dataRows := make([]string, 0, 32) - if options.OnlyIfMissing { - dataRows = append(dataRows, OnlyIfMissingDatalineLiteral+strconv.FormatBool(options.OnlyIfMissing)) - } - lines := bytes.Split(signalsContents, newLineBuf) - for _, line := range lines { - dataRows = append(dataRows, SignalsDatalineLiteral+string(line)) - } - - sendOptions := make([]SSEEventOption, 0, 2) - if options.EventID != "" { - sendOptions = append(sendOptions, WithSSEEventId(options.EventID)) - } - if options.RetryDuration != DefaultSseRetryDuration { - sendOptions = append(sendOptions, WithSSERetryDuration(options.RetryDuration)) - } - - if err := sse.Send( - EventTypePatchSignals, - dataRows, - sendOptions..., - ); err != nil { - return fmt.Errorf("failed to send patch signals: %w", err) - } - 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 - - if r.Method == "GET" { - dsJSON := r.URL.Query().Get(DatastarKey) - if dsJSON == "" { - return nil - } else { - dsInput = []byte(dsJSON) - } - } else { - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - if _, err := buf.ReadFrom(r.Body); err != nil { - if err == http.ErrBodyReadAfterClose { - return fmt.Errorf("body already closed, are you sure you created the SSE ***AFTER*** the ReadSignals? %w", err) - } - return fmt.Errorf("failed to read body: %w", err) - } - dsInput = buf.Bytes() - } - - if err := json.Unmarshal(dsInput, signals); err != nil { - return fmt.Errorf("failed to unmarshal: %w", err) - } - return nil -} diff --git a/sdk/go/datastar/sse-compression.go b/sdk/go/datastar/sse-compression.go deleted file mode 100644 index 5a8b8a872..000000000 --- a/sdk/go/datastar/sse-compression.go +++ /dev/null @@ -1,294 +0,0 @@ -package datastar - -import ( - "strings" - - "github.com/CAFxX/httpcompression/contrib/andybalholm/brotli" - "github.com/CAFxX/httpcompression/contrib/compress/gzip" - "github.com/CAFxX/httpcompression/contrib/compress/zlib" - "github.com/CAFxX/httpcompression/contrib/klauspost/zstd" - zstd_opts "github.com/klauspost/compress/zstd" - - "github.com/CAFxX/httpcompression" -) - -// CompressionStrategy indicates the strategy for selecting the compression algorithm. -type CompressionStrategy string - -const ( - // ClientPriority indicates that the client's preferred compression algorithm - // should be used if possible. - ClientPriority CompressionStrategy = "client_priority" - - // ServerPriority indicates that the server's preferred compression algorithm - // should be used. - ServerPriority CompressionStrategy = "server_priority" - - // Forced indicates that the first provided compression - // algorithm must be used regardless of client or server preferences. - Forced CompressionStrategy = "forced" -) - -// Compressor pairs a [httpcompression.CompressorProvider] -// with an encoding HTTP content type. -type Compressor struct { - Encoding string - Compressor httpcompression.CompressorProvider -} - -// compressionOptions holds all the data for server-sent events -// message compression configuration initiated by [CompressionOption]s. -type compressionOptions struct { - CompressionStrategy CompressionStrategy - ClientEncodings []string - Compressors []Compressor -} - -// CompressionOption configures server-sent events -// message compression. -type CompressionOption func(*compressionOptions) - -// GzipOption configures the Gzip compression algorithm. -type GzipOption func(*gzip.Options) - -// WithGzipLevel determines the algorithm's compression level. -// Higher values result in smaller output at the cost of higher CPU usage. -// -// Choose one of the following levels: -// - [gzip.NoCompression] -// - [gzip.BestSpeed] -// - [gzip.BestCompression] -// - [gzip.DefaultCompression] -// - [gzip.HuffmanOnly] -func WithGzipLevel(level int) GzipOption { - return func(opts *gzip.Options) { - opts.Level = level - } -} - -// WithGzip appends a [Gzip] compressor to the list of compressors. -// -// [Gzip]: https://en.wikipedia.org/wiki/Gzip -func WithGzip(opts ...GzipOption) CompressionOption { - return func(cfg *compressionOptions) { - // set default options - options := gzip.Options{ - Level: gzip.DefaultCompression, - } - // Apply all provided options. - for _, opt := range opts { - opt(&options) - } - - gzipCompressor, _ := gzip.New(options) - - compressor := Compressor{ - Encoding: gzip.Encoding, - Compressor: gzipCompressor, - } - - cfg.Compressors = append(cfg.Compressors, compressor) - } -} - -// DeflateOption configures the Deflate compression algorithm. -type DeflateOption func(*zlib.Options) - -// WithDeflateLevel determines the algorithm's compression level. -// Higher values result in smaller output at the cost of higher CPU usage. -// -// Choose one of the following levels: -// - [zlib.NoCompression] -// - [zlib.BestSpeed] -// - [zlib.BestCompression] -// - [zlib.DefaultCompression] -// - [zlib.HuffmanOnly] -func WithDeflateLevel(level int) DeflateOption { - return func(opts *zlib.Options) { - opts.Level = level - } -} - -// WithDeflateDictionary sets the dictionary used by the algorithm. -// This can improve compression ratio for repeated data. -func WithDeflateDictionary(dict []byte) DeflateOption { - return func(opts *zlib.Options) { - opts.Dictionary = dict - } -} - -// WithDeflate appends a [Deflate] compressor to the list of compressors. -// -// [Deflate]: https://en.wikipedia.org/wiki/Deflate -func WithDeflate(opts ...DeflateOption) CompressionOption { - return func(cfg *compressionOptions) { - options := zlib.Options{ - Level: zlib.DefaultCompression, - } - - for _, opt := range opts { - opt(&options) - } - - zlibCompressor, _ := zlib.New(options) - - compressor := Compressor{ - Encoding: zlib.Encoding, - Compressor: zlibCompressor, - } - - cfg.Compressors = append(cfg.Compressors, compressor) - } -} - -// BrotliOption configures the Brotli compression algorithm. -type BrotliOption func(*brotli.Options) - -// WithBrotliLevel determines the algorithm's compression level. -// Higher values result in smaller output at the cost of higher CPU usage. -// Fastest compression level is 0. Best compression level is 11. -// Defaults to 6. -func WithBrotliLevel(level int) BrotliOption { - return func(opts *brotli.Options) { - opts.Quality = level - } -} - -// WithBrotliLGWin the sliding window size for Brotli compression -// algorithm. Select a value between 10 and 24. -// Defaults to 0, indicating automatic window size selection based on compression quality. -func WithBrotliLGWin(lgwin int) BrotliOption { - return func(opts *brotli.Options) { - opts.LGWin = lgwin - } -} - -// WithBrotli appends a [Brotli] compressor to the list of compressors. -// -// [Brotli]: https://en.wikipedia.org/wiki/Brotli -func WithBrotli(opts ...BrotliOption) CompressionOption { - return func(cfg *compressionOptions) { - options := brotli.Options{ - Quality: brotli.DefaultCompression, - } - - for _, opt := range opts { - opt(&options) - } - - brotliCompressor, _ := brotli.New(options) - - compressor := Compressor{ - Encoding: brotli.Encoding, - Compressor: brotliCompressor, - } - - cfg.Compressors = append(cfg.Compressors, compressor) - } -} - -// WithZstd appends a [Zstd] compressor to the list of compressors. -// -// [Zstd]: https://en.wikipedia.org/wiki/Zstd -func WithZstd(opts ...zstd_opts.EOption) CompressionOption { - return func(cfg *compressionOptions) { - zstdCompressor, _ := zstd.New(opts...) - - compressor := Compressor{ - Encoding: zstd.Encoding, - Compressor: zstdCompressor, - } - - cfg.Compressors = append(cfg.Compressors, compressor) - } -} - -// WithClientPriority sets the compression strategy to [ClientPriority]. -// The compression algorithm will be selected based on the -// client's preference from the list of included compressors. -func WithClientPriority() CompressionOption { - return func(cfg *compressionOptions) { - cfg.CompressionStrategy = ClientPriority - } -} - -// WithServerPriority sets the compression strategy to [ServerPriority]. -// The compression algorithm will be selected based on the -// server's preference from the list of included compressors. -func WithServerPriority() CompressionOption { - return func(cfg *compressionOptions) { - cfg.CompressionStrategy = ServerPriority - } -} - -// WithForced sets the compression strategy to [Forced]. -// The first compression algorithm will be selected -// from the list of included compressors. -func WithForced() CompressionOption { - return func(cfg *compressionOptions) { - cfg.CompressionStrategy = Forced - } -} - -// WithCompression adds compression to server-sent event stream. -func WithCompression(opts ...CompressionOption) SSEOption { - return func(sse *ServerSentEventGenerator) { - cfg := &compressionOptions{ - CompressionStrategy: ClientPriority, - ClientEncodings: parseEncodings(sse.acceptEncoding), - } - - // apply options - for _, opt := range opts { - opt(cfg) - } - - // set defaults - if len(cfg.Compressors) == 0 { - WithBrotli()(cfg) - WithZstd()(cfg) - WithGzip()(cfg) - WithDeflate()(cfg) - } - - switch cfg.CompressionStrategy { - case ClientPriority: - for _, clientEnc := range cfg.ClientEncodings { - for _, comp := range cfg.Compressors { - if comp.Encoding == clientEnc { - sse.w = comp.Compressor.Get(sse.w) - sse.encoding = comp.Encoding - return - } - } - } - case ServerPriority: - for _, comp := range cfg.Compressors { - for _, clientEnc := range cfg.ClientEncodings { - if comp.Encoding == clientEnc { - sse.w = comp.Compressor.Get(sse.w) - sse.encoding = comp.Encoding - return - } - } - } - case Forced: - if len(cfg.Compressors) > 0 { - sse.w = cfg.Compressors[0].Compressor.Get(sse.w) - sse.encoding = cfg.Compressors[0].Encoding - } - } - } -} - -func parseEncodings(header string) []string { - parts := strings.Split(header, ",") - var tokens []string - for _, part := range parts { - token := strings.SplitN(strings.TrimSpace(part), ";", 2)[0] - if token != "" { - tokens = append(tokens, token) - } - } - return tokens -} diff --git a/sdk/go/datastar/sse.go b/sdk/go/datastar/sse.go deleted file mode 100644 index ad31b4ab1..000000000 --- a/sdk/go/datastar/sse.go +++ /dev/null @@ -1,212 +0,0 @@ -package datastar - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "sync" - "time" - - "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 - w io.Writer - rc *http.ResponseController - shouldLogPanics bool - encoding string - 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) - - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Content-Type", "text/event-stream") - if r.ProtoMajor == 1 { - w.Header().Set("Connection", "keep-alive") - } - - sseHandler := &ServerSentEventGenerator{ - ctx: r.Context(), - mu: &sync.Mutex{}, - w: w, - rc: rc, - shouldLogPanics: true, - acceptEncoding: r.Header.Get("Accept-Encoding"), - } - - // apply options - for _, opt := range opts { - opt(sseHandler) - } - - // set compression encoding - if sseHandler.encoding != "" { - w.Header().Set("Content-Encoding", sseHandler.encoding) - } - - // flush headers - if err := rc.Flush(); err != nil { - // Below panic is a deliberate choice as it should never occur and is an environment issue. - // https://crawshaw.io/blog/go-and-sqlite - // In Go, errors that are part of the standard operation of a program are returned as values. - // Programs are expected to handle errors. - panic(fmt.Sprintf("response writer failed to flush: %v", err)) - } - - 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 -} - -// serverSentEventData holds event configuration data for -// [SSEEventOption]s. -type serverSentEventData struct { - Type EventType - EventID string - Data []string - RetryDuration time.Duration -} - -// 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) { - e.EventID = id - } -} - -// WithSSERetryDuration overrides the [DefaultSseRetryDuration] for -// one server-sent event. -func WithSSERetryDuration(retryDuration time.Duration) SSEEventOption { - return func(e *serverSentEventData) { - e.RetryDuration = retryDuration - } -} - -var ( - eventLinePrefix = []byte("event: ") - idLinePrefix = []byte("id: ") - retryLinePrefix = []byte("retry: ") - dataLinePrefix = []byte("data: ") -) - -func writeJustError(w io.Writer, b []byte) (err error) { - _, err = w.Write(b) - 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{ - Type: eventType, - Data: dataLines, - RetryDuration: DefaultSseRetryDuration, - } - - // apply options - for _, opt := range opts { - opt(&evt) - } - - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - // write event type - if err := errors.Join( - writeJustError(buf, eventLinePrefix), - writeJustError(buf, []byte(evt.Type)), - writeJustError(buf, newLineBuf), - ); err != nil { - return fmt.Errorf("failed to write event type: %w", err) - } - - // write id if needed - if evt.EventID != "" { - if err := errors.Join( - writeJustError(buf, idLinePrefix), - writeJustError(buf, []byte(evt.EventID)), - writeJustError(buf, newLineBuf), - ); err != nil { - return fmt.Errorf("failed to write id: %w", err) - } - } - - // write retry if needed - if evt.RetryDuration.Milliseconds() > 0 && evt.RetryDuration.Milliseconds() != DefaultSseRetryDuration.Milliseconds() { - retry := int(evt.RetryDuration.Milliseconds()) - retryStr := strconv.Itoa(retry) - if err := errors.Join( - writeJustError(buf, retryLinePrefix), - writeJustError(buf, []byte(retryStr)), - writeJustError(buf, newLineBuf), - ); err != nil { - return fmt.Errorf("failed to write retry: %w", err) - } - } - - // write data lines - for _, d := range evt.Data { - if err := errors.Join( - writeJustError(buf, dataLinePrefix), - writeJustError(buf, []byte(d)), - writeJustError(buf, newLineBuf), - ); err != nil { - return fmt.Errorf("failed to write data: %w", err) - } - } - - // write double newlines to separate events - if err := writeJustError(buf, doubleNewLineBuf); err != nil { - return fmt.Errorf("failed to write newline: %w", err) - } - - // copy the buffer to the response writer - if _, err := buf.WriteTo(sse.w); err != nil { - return fmt.Errorf("failed to write to response writer: %w", err) - } - - // flush the write if its a compressing writer - if f, ok := sse.w.(flusher); ok { - if err := f.Flush(); err != nil { - return fmt.Errorf("failed to flush compressing writer: %w", err) - } - } - - if err := sse.rc.Flush(); err != nil { - return fmt.Errorf("failed to flush data: %w", err) - } - - // log.Print(NewLine + buf.String()) - return nil -} diff --git a/sdk/go/datastar/types.go b/sdk/go/datastar/types.go deleted file mode 100644 index d3991a73c..000000000 --- a/sdk/go/datastar/types.go +++ /dev/null @@ -1,15 +0,0 @@ -package datastar - -const ( - NewLine = "\n" - DoubleNewLine = "\n\n" -) - -var ( - newLineBuf = []byte(NewLine) - doubleNewLineBuf = []byte(DoubleNewLine) -) - -type flusher interface { - Flush() error -} diff --git a/sdk/go/examples/basic/main.go b/sdk/go/examples/basic/main.go deleted file mode 100644 index 0ee05200f..000000000 --- a/sdk/go/examples/basic/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "log/slog" - "net/http" - "time" - - "github.com/starfederation/datastar/sdk/go/datastar" -) - -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(` - - - - - - - - - - - - - - `, 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(`%s`, hexString, hexString, hexString) - - sse.PatchElements(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())) - } -} diff --git a/sdk/go/examples/hotreload/main.go b/sdk/go/examples/hotreload/main.go deleted file mode 100644 index 4e58345b9..000000000 --- a/sdk/go/examples/hotreload/main.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "fmt" - "log/slog" - "net/http" - "sync" - - "github.com/starfederation/datastar/sdk/go/datastar" -) - -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()") - }) - - // 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(` - - - - - - - - - - -
-

- 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. -

-
- - - - `, 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 -} diff --git a/sdk/haskell/CHANGELOG.md b/sdk/haskell/CHANGELOG.md deleted file mode 100644 index 2877c6c2e..000000000 --- a/sdk/haskell/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# Changelog - -1.0.0-beta.1 2025-02-05 diff --git a/sdk/haskell/LICENSE.md b/sdk/haskell/LICENSE.md deleted file mode 100644 index 1bfeaf9ad..000000000 --- a/sdk/haskell/LICENSE.md +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) Henry Laxen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/sdk/haskell/Makefile b/sdk/haskell/Makefile deleted file mode 100755 index d0c4ffec3..000000000 --- a/sdk/haskell/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -BASE=~/datastar-haskell - -.PHONY: all -all: - @echo "This is just a big case statement for the following actions" - @echo "test - run the tests, with doctest" - @echo "watch - run ghciwatch, to point out your mistakes in real time" - @echo "build - build everything" - @echo "haddock - generate the Haddock documentation files" - @echo "clean - delete the dist-newstyle directory" - @echo "tags - generate a TAGS file, if you are not an emacs user, you should be" - @echo "demo - run the demo, have a look" - @echo "hello - run the ubiquitous hello world program, this time in color" -.PHONY: -test: - cabal repl lib:datastar --with-compiler=doctest 2>&1 | tee /tmp/datastar.log -#cabal repl lib:datastar --with-compiler=doctest --repl-options=--verbose 2>&1 | tee /tmp/e.log -.PHONY: -watch: - ghciwatch --clear -.PHONY: -build: - cabal build -.PHONY: -haddock: - cabal haddock --enable-documentation -.PHONY: -clean: - rm -rf dist-newstyle -.PHONY: -tags: - pushd $(BASE)/src ; find . -name "*.hs" | grep -v "dist-newstyle" | xargs ghc-tags --etags -f $(BASE)/TAGS ; popd -.PHONY: -demo: - cabal run exe:datastar-demo 2>&1 | tee /tmp/datastar.log -hello: - cabal run exe:hello-world 2>&1 | tee /tmp/datastar.log - diff --git a/sdk/haskell/README.md b/sdk/haskell/README.md deleted file mode 100644 index 53828b325..000000000 --- a/sdk/haskell/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Haskell SDK for Datastar - -After familiarizing yourself with the functionality of Datastar, this -haskell interface basically comes down to a few main functions -specified in the Datastar sdK - - mergeFragments :: Text -> Selector -> MergeMode -> FragmentOptions -> Options -> Text - removeFragments :: Selector -> FragmentOptions -> Options -> Text - mergeSignals :: Text -> Bool -> Options -> Text - removeSignals :: [Text] -> Options -> Text - executeScript :: Text -> Text -> Bool -> Options -> Text - send :: Text -> Snap () -- !!Only for Snap web server!! - readSignals :: Snap (Request, Value) -- !!Only for Snap web server!! - -However, most likely you will usually take the defaults, which means -you will actually type: - - withDefaults :: EventType -> Text -> Text - withDefaults MergeFragments txt - withDefaults RemoveFragments txt - withDefaults MergeSignals txt - withDefaults RemoveSignals txt - withDefaults ExecuteScript txt - -Additionally you, dear user, will need to implement a web server -dependent function named **send** that sends the text you created to -the client. I have included a sample implementation for the Snap -server, in the ServerSentEventGenerator.Server.Snap module, please -have a look at it. If you implement a similar module for you server -of choice, please create a pull request so I can include it. - -You will also need a small function called isHttpVersion1_1 which -returns True if you server speaks Http Version 1.1, and False -otherwise. This is needed to produce the correct headers for sse -events. - -You will notice a Bool named debug, in the -ServerSentEventGenerator.Server.Snap module which is currently set to -False. Setting it to True will enable debug messages printed to -stdout so you can see what is being sent to the client (web browser) - -The readSignals function probably does not do what you expect. It -returns a Data.Aeson Object, which is generally useless for Haskell -programmers. You are better off reading the from your server and -dealing with it directly. - -Finally, the demo, which you can try out by typing "./run" in the -top level directory and pointing your browser to: - http://localhost:8000/ -will give you a simple demo of some Datastar features and show that -streaming SSE events to the browser work. - -The code itself is extensively documented, with many doctest examples -that show up in the Haddock files. - -Best wishes, -Henry Laxen -nadine.and.henry@pobox.com -http://nadineloveshenry.com/ - diff --git a/sdk/haskell/cabal.project b/sdk/haskell/cabal.project deleted file mode 100644 index c19a1bac2..000000000 --- a/sdk/haskell/cabal.project +++ /dev/null @@ -1,3 +0,0 @@ -constraints: HsOpenSSL == 0.11.7.2 -packages: . - diff --git a/sdk/haskell/datastar.cabal b/sdk/haskell/datastar.cabal deleted file mode 100644 index a6be12686..000000000 --- a/sdk/haskell/datastar.cabal +++ /dev/null @@ -1,119 +0,0 @@ -cabal-version: 3.12 -name: datastar -version: 1.0.0.0 -synopsis: Interface for Server Sent Events using the Datastar framework. -description: See https://data-star.dev/ for full information. -license-file: LICENSE.md -author: Henry Laxen -maintainer: nadine.and.henry@pobox.com -category: Language -build-type: Simple -extra-doc-files: CHANGELOG.md - -common warnings - ghc-options: - -O0 - -Wall - -ddump-minimal-imports - -funbox-strict-fields - -fwarn-tabs - -fwrite-ide-info - -haddock - -threaded - -common deps - build-depends: - base - , aeson - , bytestring - , containers - , data-default - , io-streams - , neat-interpolation - , snap - , snap-core - , template-haskell - , text - , utf8-string - , vector - -common default-extensions - default-extensions: - ConstraintKinds - , FlexibleInstances - , FunctionalDependencies - , GeneralizedNewtypeDeriving - , MultiParamTypeClasses - , NoImplicitPrelude - , OverloadedStrings - , QuasiQuotes - , RecordWildCards - , ScopedTypeVariables - , TemplateHaskell - , TypeSynonymInstances - -library - import: deps,warnings,default-extensions - exposed-modules: - ServerSentEventGenerator - , ServerSentEventGenerator.Class - , ServerSentEventGenerator.Constants - , ServerSentEventGenerator.Types - , ServerSentEventGenerator.Server.Snap - other-modules: - ServerSentEventGenerator.Internal - hs-source-dirs: src - default-language: Haskell2010 - - -Executable datastar-demo - import: deps,warnings,default-extensions - default-language: Haskell2010 - build-depends: - datastar - , time - other-modules: - ServerSentEventGenerator - , ServerSentEventGenerator.Class - , ServerSentEventGenerator.Constants - , ServerSentEventGenerator.Types - , ServerSentEventGenerator.Internal - , ServerSentEventGenerator.Server.Snap - hs-source-dirs: src,src/demo - main-is: Main.hs - -Executable datastar-test - import: deps,warnings,default-extensions - default-language: Haskell2010 - build-depends: - html-entities - , datastar - , time - other-modules: - ServerSentEventGenerator - , ServerSentEventGenerator.Server.Snap - , ServerSentEventGenerator.Class - , ServerSentEventGenerator.Constants - , ServerSentEventGenerator.Types - , ServerSentEventGenerator.Internal - hs-source-dirs: src,src/test - main-is: Main.hs - -Executable hello-world - import: deps,warnings,default-extensions - default-language: Haskell2010 - build-depends: - aeson - , relude - , scientific - , datastar - other-modules: - ServerSentEventGenerator - , ServerSentEventGenerator.Server.Snap - , ServerSentEventGenerator.Class - , ServerSentEventGenerator.Constants - , ServerSentEventGenerator.Types - , ServerSentEventGenerator.Internal - hs-source-dirs: src,../../examples/haskell/hello-world - main-is: Main.hs - default-extensions: diff --git a/sdk/haskell/run b/sdk/haskell/run deleted file mode 100755 index 0cb3eb7b0..000000000 --- a/sdk/haskell/run +++ /dev/null @@ -1 +0,0 @@ -cabal run exe:datastar-demo 2>&1 \ No newline at end of file diff --git a/sdk/haskell/src/ServerSentEventGenerator.hs b/sdk/haskell/src/ServerSentEventGenerator.hs deleted file mode 100644 index 0d8a9b69d..000000000 --- a/sdk/haskell/src/ServerSentEventGenerator.hs +++ /dev/null @@ -1,307 +0,0 @@ -module ServerSentEventGenerator ( - AutoRemove(..) - , EventType(..) - , FragmentOptions(..) - , HttpVersion(..) - , MergeMode(..) - , Options(..) - , Prompt(..) - , SSEapp(..) - , SSEstream - , Selector(..) - , executeScript - , mergeFragments - , mergeSignals - , removeFragments - , removeSignals - , sendPure - , singleThreaded - , sseHeaders - , test - , withDefaults - -- $setup - ) - -where - -import Data.ByteString.Builder ( Builder ) -import Data.Default ( Default(..) ) -import Prelude -import Data.Text ( Text, lines, unlines ) -import ServerSentEventGenerator.Class - ( HttpVersion(..), Prompt(..) ) -import ServerSentEventGenerator.Constants -import ServerSentEventGenerator.Internal -import ServerSentEventGenerator.Types - --- $setup --- >>> import Data.Text --- >>> import Data.Maybe --- >>> import Control.Exception - -sseHeaders :: HttpVersion m => m Builder -sendPure :: EventType -> [Text] -> Options -> Text -mergeFragments :: Text -> Selector -> MergeMode -> FragmentOptions -> Options -> Text -removeFragments :: Selector -> FragmentOptions -> Options -> Text -mergeSignals :: Text -> Bool -> Options -> Text -removeSignals :: [Text] -> Options -> Text -executeScript :: Text -> Text -> AutoRemove -> Options -> Text - --- | The sseHeaders output if the server uses Http Version 1.1 --- >>> sseHeaders --- "HTTP/1.1 200 OK\nCache-control: no-cache\nContent-type: text/event-stream\nConnection: keep-alive\n\n" - -sseHeaders = do - b <- isHttpVersion1_1 - let headers = if b then sseHeaders1_1 else sseHeaders2 - return (headers <> "\n") - where - sseHeaders2 = "HTTP/1.1 200 OK\nCache-control: no-cache\nContent-type: text/event-stream\n" - sseHeaders1_1 = sseHeaders2 <> "Connection: keep-alive\n" - --- | All server sent events can contain and Event Id and a Retry Duration as an option --- This works, because if the options are equal to their defaults, they will --- be removed from the output -{- | >>> :{ -do - let - sampleDataLines :: [Text] - sampleDataLines = ["data: line 1","data: line 2"] - them = [ - sendPure MergeFragments sampleDataLines (O "id1" 100) ] - test them -:} -event: datastar-merge-fragments -id: id1 -retry: 100 -data: line 1 -data: line 2 - --} - -sendPure eventType dataLines options = mconcat (buildLines (a:b:dataLines)) <> "\n\n" - where - a = cEvent <> cSColon <> prompt eventType - b = prompt options - -{- | >>> :{ -do - let - sampleDataLines :: Text - sampleDataLines = "line 1\nline 2" - them = [ - mergeFragments sampleDataLines def def def def - , mergeFragments sampleDataLines (SEL "#id") def def def - , mergeFragments sampleDataLines (SEL "#id") Inner def def - , mergeFragments sampleDataLines (SEL "#id") Inner (FO 1 False) def - , mergeFragments sampleDataLines (SEL "#id") Inner (FO 1 True) (O "abc123" 10) ] - test them -:} -event: datastar-merge-fragments -data: fragments line 1 -data: fragments line 2 - -event: datastar-merge-fragments -data: selector #id -data: fragments line 1 -data: fragments line 2 - -event: datastar-merge-fragments -data: selector #id -data: mergeMode inner -data: fragments line 1 -data: fragments line 2 - -event: datastar-merge-fragments -data: selector #id -data: mergeMode inner -data: fragments line 1 -data: fragments line 2 - -event: datastar-merge-fragments -id: abc123 -retry: 10 -data: selector #id -data: mergeMode inner -data: useViewTransition true -data: fragments line 1 -data: fragments line 2 - --} - -mergeFragments fragments selector mode fragOptions = sendPure MergeFragments (buildLines (a:b:c:d)) - where - a = prompt selector - b = withDefault cMergeMode (prompt (def :: MergeMode)) (prompt mode) - c = prompt fragOptions - d = withList cFragments fragments - -{- | >>> :{ -do - let - rt1 :: IO () - rt2,rt3,rt4,rt5 :: Text - rt1 = test [removeFragments def def def] `catch` - (\(e :: ServerSentEventGeneratorExceptions) -> print e) - rt2 = removeFragments (SEL "#id") def def - rt3 = removeFragments (SEL "#id") (FO 1 False) def - rt4 = removeFragments (SEL "#id") (FO 1 True) def - rt5 = removeFragments (SEL "#id") (FO 1 False) (O "abc123" 10) - rt1 >> test [rt2,rt3,rt4,rt5] -:} -The selector field is required in RemoveFragment -event: datastar-remove-fragments -data: selector #id - -event: datastar-remove-fragments -data: selector #id - -event: datastar-remove-fragments -data: selector #id -data: useViewTransition true - -event: datastar-remove-fragments -id: abc123 -retry: 10 -data: selector #id - --} - -removeFragments selector fragOptions = sendPure RemoveFragments (buildLines [a,b]) - where - s = prompt selector - a = if s == def then bug RemoveFragmentSelectorIsMissing else s - b = prompt fragOptions - -{- | >>> :{ -do - let - testMergeSignal :: Text - testMergeSignal = "{\"a\":\"b\",\"c\":true,\"d\":1}" - mst1 = test [mergeSignals def def def] `catch` - (\(e :: ServerSentEventGeneratorExceptions) -> print e) - them = [ - mergeSignals testMergeSignal False def - , mergeSignals testMergeSignal True (O "abc123" 10) ] - mst1 >> test them -:} -The selector field is required in MergeSignals -event: datastar-merge-signals -data: signals {"a":"b","c":true,"d":1} - -event: datastar-merge-signals -id: abc123 -retry: 10 -data: signals {"a":"b","c":true,"d":1} -data: onlyIfMissing true - --} - -mergeSignals signals onlyIfMissing = sendPure MergeSignals (buildLines [a,b]) - where - a = if signals == mempty - then bug SignalsSelectorIsMissing - else withDefault cSignals "" signals - b = withDefault cOnlyIfMissing (prompt cDefaultOnlyIfMissing) (prompt onlyIfMissing) - -{- | >>> :{ -do - let - testRemoveSignal = ["velocity.x", "velocity.y", "position"] :: [Text] - them = [ - removeSignals [] def - , removeSignals testRemoveSignal def - , removeSignals testRemoveSignal (O "abc123" 10) ] - test them -:} -event: datastar-remove-signals - -event: datastar-remove-signals -data: paths velocity.x -data: paths velocity.y -data: paths position - -event: datastar-remove-signals -id: abc123 -retry: 10 -data: paths velocity.x -data: paths velocity.y -data: paths position - --} - -removeSignals paths = sendPure RemoveSignals (buildLines c) - where - c = Prelude.map (prefixed cPaths) paths - -{- | >>> :{ -do - let - testScript = "window.location = \"https://data-star.dev\"" :: Text - testAttributes = "type text/javascript" :: Text - them = [ - executeScript "" "" (Auto True) def - , executeScript testScript "" (Auto False) def - , executeScript testScript testAttributes (Auto False) def - , executeScript testScript testAttributes def (O "abc123" 10) ] - test them -:} -event: datastar-execute-script - -event: datastar-execute-script -data: autoRemove false -data: script window.location = "https://data-star.dev" - -event: datastar-execute-script -data: attributes type text/javascript -data: autoRemove false -data: script window.location = "https://data-star.dev" - -event: datastar-execute-script -id: abc123 -retry: 10 -data: attributes type text/javascript -data: script window.location = "https://data-star.dev" - --} - -executeScript script attributes autoRemove = sendPure ExecuteScript (buildLines (a <> (b:c))) - where - filtered = filter (/= cDefaultAttributes) (Data.Text.lines attributes) - a = withList cAttributes (Data.Text.unlines filtered) - b = withDefault cAutoRemove (prompt (def :: AutoRemove)) (prompt autoRemove) - c = withList cScript script - -withDefaults :: EventType -> Text -> Text -withDefaults MergeFragments txt = mergeFragments txt def def def def -withDefaults RemoveFragments txt = removeFragments (SEL txt) def def -withDefaults MergeSignals txt = mergeSignals txt def def -withDefaults RemoveSignals txt = removeSignals (Data.Text.lines txt) def -withDefaults ExecuteScript txt = executeScript txt def def def -{- | >>> :{ -do - let - them = [ - withDefaults MergeFragments "abc123" - , withDefaults RemoveFragments "abc123" - , withDefaults MergeSignals "abc123" - , withDefaults RemoveSignals "abc123" - , withDefaults ExecuteScript "abc123" ] - test them -:} -event: datastar-merge-fragments -data: fragments abc123 - -event: datastar-remove-fragments -data: selector abc123 - -event: datastar-merge-signals -data: signals abc123 - -event: datastar-remove-signals -data: paths abc123 - -event: datastar-execute-script -data: script abc123 - --} diff --git a/sdk/haskell/src/ServerSentEventGenerator/Class.hs b/sdk/haskell/src/ServerSentEventGenerator/Class.hs deleted file mode 100644 index 82f39b152..000000000 --- a/sdk/haskell/src/ServerSentEventGenerator/Class.hs +++ /dev/null @@ -1,45 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -module ServerSentEventGenerator.Class where - -import Data.Default ( Default(..) ) -import Prelude -import Data.Text ( Text ) -import Data.Text.IO ( putStr ) -import Data.String - -type StringLike a = (Eq a, IsString a, Monoid a) - -class Monad m => HttpVersion m where - -- | Are we running Http Version 1.1? Needed to send out the correct headers - -- This needs to be implemented and depends on with web server you are using - -- The default is to just return True - isHttpVersion1_1 :: m Bool - isHttpVersion1_1 = return True - -instance HttpVersion IO - -class Monad m => Send m where - send :: Text -> m () - -instance Send IO where - send = Data.Text.IO.putStr - -instance Default Text where - def = "" - -class Prompt a where - prompt :: a -> Text - --- | I need a way to go from a Datastar type to a StringLike thing that can be --- sent to the browser. The Prompt class lets me do things like --- prompt MergeFragments = "datastar-merge-fragments" without specifying --- the exact type of the StringLike "datastar-merge-fragments" I also use it --- to special case the output for Options - -instance Prompt Bool where - prompt True = "true" - prompt False = "false" - -instance Prompt Int where - prompt = fromString . show - diff --git a/sdk/haskell/src/ServerSentEventGenerator/Constants.hs b/sdk/haskell/src/ServerSentEventGenerator/Constants.hs deleted file mode 100644 index c0aa71570..000000000 --- a/sdk/haskell/src/ServerSentEventGenerator/Constants.hs +++ /dev/null @@ -1,98 +0,0 @@ --- This is auto-generated by Datastar. DO NOT EDIT. -module ServerSentEventGenerator.Constants where - -import Prelude -import ServerSentEventGenerator.Class (StringLike) - --- taken from consts.ts --- why? --- functions must start with a lower case letter --- I could have used a type class, but it would have required --- a function call in front of each data constructor, so I --- decided to just use the prefix "c" - -cDatastarKey :: StringLike a => a -cDatastarKey = "datastar" -cVersion :: StringLike a => a -cVersion = "1.0.0-beta.3" -cVersionClientByteSize :: Int -cVersionClientByteSize = 36872 -cVersionClientByteSizeGzip :: Int -cVersionClientByteSizeGzip = 13691 -cDefaultSseRetryDuration :: Int -cDefaultSseRetryDuration = 1000 -- milliseconds -cExecuteScriptAttributes :: StringLike a => a -cExecuteScriptAttributes = "type module" -cSelector :: StringLike a => a -cSelector = "selector" -cMergeMode :: StringLike a => a -cMergeMode = "mergeMode" -cFragments :: StringLike a => a -cFragments = "fragments" -cUseViewTransition :: StringLike a => a -cUseViewTransition = "useViewTransition" -cSignals :: StringLike a => a -cSignals = "signals" -cOnlyIfMissing :: StringLike a => a -cOnlyIfMissing = "onlyIfMissing" -cPaths :: StringLike a => a -cPaths = "paths" -cScript :: StringLike a => a -cScript = "script" -cAttributes :: StringLike a => a -cAttributes = "attributes" -cAutoRemove :: StringLike a => a -cAutoRemove = "autoRemove" -cDefaultFragmentsUseViewTransitions :: Bool -cDefaultFragmentsUseViewTransitions = False -cDefaultMergeSignalsOnlyIfMissing :: Bool -cDefaultMergeSignalsOnlyIfMissing = False -cDefaultExecuteScriptAutoRemove :: Bool -cDefaultExecuteScriptAutoRemove = True -cMorph :: StringLike a => a -cMorph = "morph" -cInner :: StringLike a => a -cInner = "inner" -cOuter :: StringLike a => a -cOuter = "outer" -cPrepend :: StringLike a => a -cPrepend = "prepend" -cAppend :: StringLike a => a -cAppend = "append" -cBefore :: StringLike a => a -cBefore = "before" -cAfter :: StringLike a => a -cAfter = "after" -cUpsertAttributes :: StringLike a => a -cUpsertAttributes = "upsertAttributes" -cMergeFragments :: StringLike a => a -cMergeFragments = "datastar-merge-fragments" -cMergeSignals :: StringLike a => a -cMergeSignals = "datastar-merge-signals" -cRemoveFragments :: StringLike a => a -cRemoveFragments = "datastar-remove-fragments" -cRemoveSignals :: StringLike a => a -cRemoveSignals = "datastar-remove-signals" -cExecuteScript :: StringLike a => a -cExecuteScript = "datastar-execute-script" --- Added by Henry -cData :: StringLike a => a -cData = "data" -cEvent :: StringLike a => a -cEvent = "event" -cEventId :: StringLike a => a -cEventId = "id" -cRetryDuration :: StringLike a => a -cRetryDuration = "retry" -cSColon :: StringLike a => a -cSColon = ": " -cSpace :: StringLike a => a -cSpace = " " -cDefaultSelector :: StringLike a => a -cDefaultSelector = "" -cDefaultEventId :: StringLike a => a -cDefaultEventId = "" -cDefaultOnlyIfMissing :: Bool -cDefaultOnlyIfMissing = False -cDefaultAttributes :: StringLike a => a -cDefaultAttributes = "" \ No newline at end of file diff --git a/sdk/haskell/src/ServerSentEventGenerator/Internal.hs b/sdk/haskell/src/ServerSentEventGenerator/Internal.hs deleted file mode 100644 index 6b1c4a591..000000000 --- a/sdk/haskell/src/ServerSentEventGenerator/Internal.hs +++ /dev/null @@ -1,90 +0,0 @@ -module ServerSentEventGenerator.Internal where - -import Control.Concurrent ( newMVar, putMVar, takeMVar ) -import Control.Exception ( bracket ) -import Data.String ( IsString ) -import Prelude -import Data.Text ( Text, lines ) -import ServerSentEventGenerator.Class ( StringLike ) -import ServerSentEventGenerator.Constants - ( cData, cSColon, cSpace ) -import qualified Data.Text.IO ( putStr ) - - --- | Combines a list of Texts into a single Text, using the same mechanism --- as the more commonly known functions unWords or unLines. A line feed is --- inserted between each builder in the list. Empty builders are removed, so --- there are no blank lines. - -buildLines :: (Eq a, Monoid a, IsString a) => [a] -> [a] -buildLines texts = if (mconcat texts) == mempty then [] else [go mempty texts] - where - go acc [] = acc - go acc [x] = x <> acc - go acc [x,y] = case [x,y] of - ["",z] -> z <> acc - [z,""] -> z <> acc - [u,v] -> u <> "\n" <> v <> acc - go acc (b:bs) = if b == mempty then go acc bs else b <> "\n" <> go acc bs - -{- | >>> :{ -do - let - wa = "a" :: Text - wb = "b" :: Text - prefix = "prefix" :: Text - enclose :: Text -> Text - enclose x = "[" <> x <> "]\n" - them = map enclose [ - withDefault prefix wa wa - , withDefault prefix wa wa - , withDefault prefix wb wa - , withDefault prefix wa wb - , withDefault prefix "" "" - , withDefault prefix wa "" - , withDefault prefix "" wa ] - test them - :} -[] -[] -[data: prefix a] -[data: prefix b] -[] -[] -[data: prefix a] --} - -prefixed :: StringLike a => a -> a -> a -prefixed name = ( (cData <> cSColon <> name <> cSpace) <> ) - -withDefault :: StringLike a => a -> a -> a -> a -withDefault dStarEvent defaultValue value = - if value == defaultValue || value == mempty - then mempty - else prefixed dStarEvent value - --- | Insert "data: " and the given text in front of each element of the list --- | >>> withList "fragments" ["l1","l2"] --- ["data: fragments l1","data: fragments l2"] - -withList :: Text -> Text -> [Text] -withList name = Prelude.map (prefixed name) . Data.Text.lines - --- | Handy little helpers to watch the result of sending stuff through sse - --- | send a list of text to stdout -test :: [Text] -> IO () -test = mapM_ ps - --- send a line of text to stdout -ps :: Text -> IO () -ps = Data.Text.IO.putStr - --- | Make sure the IO action runs without interference from other threads - -singleThreaded :: IO () -> IO () -singleThreaded action = bracket - (newMVar ()) - (\mvar -> putMVar mvar ()) - (\mvar -> takeMVar mvar >> action) - diff --git a/sdk/haskell/src/ServerSentEventGenerator/Server/Snap.hs b/sdk/haskell/src/ServerSentEventGenerator/Server/Snap.hs deleted file mode 100644 index 0f2695ccd..000000000 --- a/sdk/haskell/src/ServerSentEventGenerator/Server/Snap.hs +++ /dev/null @@ -1,154 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -{-# OPTIONS_GHC -Wno-missing-signatures #-} - -module ServerSentEventGenerator.Server.Snap - ( - runSSE - , sendInApp - , send - , readSignals - ) where - -import Control.Concurrent - ( threadDelay, forkIO, killThread, myThreadId, ThreadId ) -import Control.Exception - ( SomeException, handle, throwIO, Exception(displayException) ) -import Control.Monad ( forever ) -import Control.Monad.IO.Class ( MonadIO(liftIO) ) -import Data.Aeson ( decode, decodeStrictText, Value ) -import Data.ByteString.Builder ( Builder, hPutBuilder, stringUtf8 ) -import Data.ByteString.Builder.Extra ( flush ) -import Prelude -import Data.Text ( Text ) -import Data.Text.Encoding ( encodeUtf8Builder ) -import ServerSentEventGenerator - ( HttpVersion(..), - singleThreaded, - SSEapp(..), - SSEstream, - sseHeaders ) -import ServerSentEventGenerator.Class () -import Snap - ( Snap, - Request(rqVersion), - escapeHttp, - getParam, - getRequest, - readRequestBody, - getHeader ) -import System.IO ( stdout ) -import qualified System.IO.Streams as Streams ( write ) -import qualified Data.Text as T ( pack ) -import qualified Data.Text.Encoding as T ( decodeUtf8 ) - -type Tickle = (Int -> Int) -> IO () - --- set to True to see the text sent to the client on stdout as well, --- VERY handy for debugging your Datastar code - -debug :: Bool -debug = False - -pb :: Builder -> IO () -pb x = if debug then singleThreaded (hPutBuilder stdout x) else return () - -runSSE :: SSEapp -> Snap () -runSSE (SSEapp app) = do - request <- Snap.getRequest - let lastId = getHeader "Last-Event-ID" request - headers <- sseHeaders - Snap.escapeHttp $ \tickle _ writeEnd -> do - singleThreaded $ pb ("Enter runSSE, Last-Event-ID: " <> (stringUtf8 . show) lastId <> "\n") - pingThreadId <-forkIO (ping tickle writeEnd) - handle (handleException pingThreadId "runSSE") $ do - singleThreaded $ do - pb headers - Streams.write (Just headers) writeEnd - Streams.write (Just flush) writeEnd - app writeEnd - killThread pingThreadId - Streams.write Nothing writeEnd - -sendInApp :: Text -> SSEstream -> IO () -sendInApp x writeEnd = singleThreaded $ do - let bs = encodeUtf8Builder x - pb bs - Streams.write (Just bs) writeEnd - Streams.write (Just flush) writeEnd - -send :: Text -> Snap () -send txt = do - runSSE (SSEapp f) - where - f = sendInApp txt - -ping :: Tickle -> SSEstream -> IO () -ping tickle writeEnd = forever $ do - pingThreadId <- myThreadId - singleThreaded $ pb ("PING: " <> (stringUtf8 . show) pingThreadId) - handle (handleException pingThreadId "ping") $ do - singleThreaded $ do - Streams.write (Just ":\n\n") writeEnd - Streams.write (Just flush) writeEnd - tickle (max 60) - threadDelay (11 * 1000 * 1000) - -instance HttpVersion Snap where - isHttpVersion1_1 = do - version <- rqVersion <$> getRequest - return (version == (1,1)) - -handleException :: ThreadId -> String -> SomeException -> IO () -handleException t s e = do - killThread t - putStrLn (s <> ": " <> displayException e) - throwIO e - -{- | >>> :{ -do - signalsAsJsonIO (Nothing, "{\"key1\":{\"one\":1,\"two\":2},\"key2\":\"string\"}") >>= print - signalsAsJsonIO (Just "[{\"key1\":{\"one\":1,\"two\":2},\"key2\":\"string\"}]", "") >>= print -:} -Object (fromList [("key1",Object (fromList [("one",Number 1.0),("two",Number 2.0)])),("key2",String "string")]) -Array [Object (fromList [("key1",Object (fromList [("one",Number 1.0),("two",Number 2.0)])),("key2",String "string")])] --} - --- the OPTIONS_GHC -Wno-missing-signatures is above for this function --- signalsAsJsonIO :: (Maybe (Map ByteString [ByteString]), ByteString) -> IO Value -signalsAsJsonIO (mbDS,body) = do - -- if its url encoded, then we only care about one key, named "datastar", - -- and the value is a bytestring encoded list with one element - case mbDS of - Nothing -> do - let result = decode body :: Maybe Value - maybe (throwIO (JsonBodyException (T.pack . show $ body) )) return result - Just ds -> do - let txt = T.decodeUtf8 ds - case ((decodeStrictText txt) :: Maybe Value) of - Nothing -> throwIO $ JsonDecodingException txt - Just v -> return v - -signalsAsJson :: Snap Value -signalsAsJson = do - mbDS <- getParam "datastar" - body <- readRequestBody 1024 - liftIO $ signalsAsJsonIO (mbDS,body) - --- | Get the current request, and return the value (as per Aeson) of the datastar parameter --- which may be in the query parameters for a GET, PuT, etc request, or in the request body --- if we have a POST request - -readSignals :: Snap (Request, Value) -readSignals = do - req <- getRequest - value <- signalsAsJson - return (req,value) - -data SnapExceptions = - JsonBodyException !Text - | JsonTextTooShort !Text - | JsonDecodingException !Text - - deriving Show -instance Exception SnapExceptions - diff --git a/sdk/haskell/src/ServerSentEventGenerator/Types.hs b/sdk/haskell/src/ServerSentEventGenerator/Types.hs deleted file mode 100644 index c865245b2..000000000 --- a/sdk/haskell/src/ServerSentEventGenerator/Types.hs +++ /dev/null @@ -1,149 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -module ServerSentEventGenerator.Types where - -import Control.Exception ( throw, Exception ) -import Data.ByteString.Builder ( Builder ) -import Data.Default ( Default(..) ) -import Data.Text ( Text ) -import Prelude -import ServerSentEventGenerator.Class ( Prompt(..) ) -import ServerSentEventGenerator.Constants -import ServerSentEventGenerator.Internal - ( buildLines, withDefault ) -import qualified System.IO.Streams as Streams ( OutputStream ) - - -type SSEstream = Streams.OutputStream Builder -newtype SSEapp = SSEapp (SSEstream -> IO ()) - -data Options = O { - eventId :: Text - , retryDuration :: Int - } deriving (Show) - -instance Default Options where - def = O { - eventId = mempty - , retryDuration = cDefaultSseRetryDuration - } - -instance Prompt Options where - prompt options = - let - eI = eventId options - eR = retryDuration options - a = if eI == cDefaultEventId then mempty else cEventId <> cSColon <> eI - b = if eR == cDefaultSseRetryDuration then mempty else cRetryDuration <> cSColon <> (prompt eR) - in mconcat . buildLines $ [a,b] - -newtype Selector = SEL {unSelector :: Text} - deriving (Show, Semigroup, Monoid, Eq) - -instance Default Selector where - def = SEL mempty - -instance Prompt Selector where - prompt (SEL x) = withDefault cSelector cDefaultSelector x - --- | A sum of the possible Datastar specific events that can be sent - -data EventType = - MergeFragments - | RemoveFragments - | MergeSignals - | RemoveSignals - | ExecuteScript - deriving (Eq, Show) - -instance Default EventType - where def = MergeFragments - -instance Prompt EventType where - prompt MergeFragments = cMergeFragments - prompt RemoveFragments = cRemoveFragments - prompt MergeSignals = cMergeSignals - prompt RemoveSignals = cRemoveSignals - prompt ExecuteScript = cExecuteScript - - -data FunctionExecuteScript = FunctionExecuteScript { - eType :: EventType - , eScript :: Text - , eAttributes :: Text - , eAutoRemove :: Bool - , eOptions :: Options - } deriving Show - --- | A sum of the possible Datastar specific merge modes that can be sent - -data MergeMode = - Morph - | Inner - | Outer - | Prepend - | Append - | Before - | After - | UpsertAttributes - deriving (Eq, Show) - -instance Default MergeMode - where def = Morph - -instance Prompt MergeMode where - prompt Morph = cMorph - prompt Inner = cInner - prompt Outer = cOuter - prompt Prepend = cPrepend - prompt Append = cAppend - prompt Before = cBefore - prompt After = cAfter - prompt UpsertAttributes = cUpsertAttributes - -data FragmentOptions = FO { - , useViewTransition :: Bool - } deriving (Show) - --- | the MergeFragments and RemoveFragment data types share these options - -instance Default FragmentOptions where - def = FO { - useViewTransition = cDefaultFragmentsUseViewTransitions - } - -instance Prompt FragmentOptions where - prompt (FO a b) = mconcat . buildLines $ [ - , withDefault cUseViewTransition (prompt cDefaultFragmentsUseViewTransitions) (prompt b) - ] - -newtype AutoRemove = Auto Bool - deriving (Eq, Show) - -instance Default AutoRemove where - def = Auto True - -instance Prompt AutoRemove where - prompt (Auto True) = "true" - prompt (Auto False) = "false" - -instance Show ServerSentEventGeneratorExceptions where - show BuildLineDataIMissing = "buildLine was call with nothing to build" - show RemoveFragmentSelectorIsMissing = "The selector field is required in RemoveFragment" - show SignalsSelectorIsMissing = "The selector field is required in MergeSignals" - show RemoveSignalsPathIsMissing = "The path field is required in RemoveSignals" - show RemoveSignalsPathIsEmpty = "The path field cannot be an empty list" - show ExecuteScriptIsMissing = "The script field is required in ExecuteScript" - -bug :: Exception e => e -> a -bug = throw - -data ServerSentEventGeneratorExceptions = - BuildLineDataIMissing - | RemoveFragmentSelectorIsMissing - | SignalsSelectorIsMissing - | RemoveSignalsPathIsMissing - | RemoveSignalsPathIsEmpty - | ExecuteScriptIsMissing - -instance Exception ServerSentEventGeneratorExceptions - diff --git a/sdk/haskell/src/demo/Main.hs b/sdk/haskell/src/demo/Main.hs deleted file mode 100644 index 663a3ad5c..000000000 --- a/sdk/haskell/src/demo/Main.hs +++ /dev/null @@ -1,136 +0,0 @@ -module Main where - -import Control.Applicative ( Alternative((<|>)) ) -import Control.Concurrent ( threadDelay ) -import Control.Monad ( foldM_ ) -import Control.Monad.IO.Class ( MonadIO(liftIO) ) -import Data.Default ( Default(def) ) -import Data.Maybe ( fromMaybe ) -import Prelude -import Data.Text ( Text ) -import Data.Time ( getCurrentTime ) -import ServerSentEventGenerator -import Snap -import Snap.Util.FileServe ( serveDirectory ) -import ServerSentEventGenerator.Server.Snap -import System.IO - ( stdout, hSetBuffering, stderr, BufferMode(NoBuffering) ) -import qualified Data.Text as T - ( concatMap, pack, singleton, unpack ) -import qualified Data.Text.IO as T - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - hSetBuffering stderr NoBuffering - indexText <- T.readFile "src/demo/www/index.html" - let - mbPort = getPort (defaultConfig :: Config Snap a) - newConfig = setPort (fromMaybe 8000 mbPort) (defaultConfig :: Config Snap a) - conf <- commandLineConfig newConfig - print conf - simpleHttpServe conf (site indexText) - -site :: Text -> Snap () -site indexText = - ifTop (writeText indexText) <|> - route [ - ("favicon.ico" , return ()) - , ("feed" , handlerFeed) - , ("keats" , handlerKeats) - , ("signals" , handlerSignals) - , ("clear" , handlerClear) - , ("test" , handlerSignals) - ] <|> serveDirectory "demo/www" - -handlerSignals :: Snap () -handlerSignals = do - ps "handlerSignals" - req <- T.pack . show <$> getRequest - body <- T.pack . show <$> readRequestBody 1024 - params <- T.pack . show <$> getParams - let - output = mconcat [ - "
"
-      , "\nRequest\n"
-      , req
-      , "\nParams\n"
-      , params
-      , "\nBody\n"
-      , body
-      , "\nEnd\n"
-      , "
" - ] - ds = mergeFragments (output) (SEL "#signals") Inner def def - send ds - -handlerClear :: Snap () -handlerClear = send $ (mergeFragments "
" (SEL "#signals") Inner def def) - -handlerFeed :: Snap () -handlerFeed = do - runSSE (SSEapp f) - where - f :: SSEstream -> IO () - f w = do - let x10times = [1..10] :: [Int] - putStrLn "Write 10 times" - mapM_ (writeNow w) x10times - writeBoth sleeping w - sleep 70 - putStrLn "Wake up" - putStrLn "Write 10 times" - mapM_ (writeNow w) x10times - writeBoth allDone w - sendInApp removeDstar w - writeNow :: SSEstream -> Int -> IO () - writeNow w n = do - now <- getCurrentTime >>= - return . T.pack . ((Prelude.replicate n '.') <> ) . show - sendInApp (feedDstar now) w - threadDelay (1 * 1000 * 1000) - writeBoth x w = putStrLn (T.unpack x) >> sendInApp (feedDstar x) w - sleeping = "Sleeping for 70 seconds, but continuing to ping" - allDone = "All Done" - feedDstar :: Text -> Text - feedDstar x = mergeFragments ("
" <> x <> "
") def def def def - removeDstar :: Text - removeDstar = removeFragments (SEL "#explain") (FO 5000 def) def - -handlerKeats :: Snap () -handlerKeats = do - liftIO $ putStrLn "Keats" - ode <- liftIO $ T.readFile "src/demo/www/keats.txt" - runSSE (SSEapp (f ode)) - where - f :: Text -> SSEstream -> IO () - f ode w = foldM_ (\x -> foldSlowly w x) mempty (T.unpack ode) - keatsDstar :: Text -> Text - keatsDstar x = mergeFragments ("
" <> textToHtml x <> "
") (SEL "#keats") Inner def def - foldSlowly :: SSEstream -> Text -> Char -> IO Text - foldSlowly w b c = do - pause - let s = b <> (T.singleton c) - sendInApp (keatsDstar s) w - return s - -pause :: IO () -pause = threadDelay (10 * 100 * 100 `div` 2) - -sleep :: Int -> IO () -sleep n = threadDelay (n * 1000 * 1000) - -textToHtml :: Text -> Text -textToHtml = T.concatMap escape - where - escape ' ' = T.pack " " - escape '\n' = T.pack "
\n" - escape '<' = T.pack "<" - escape '>' = T.pack ">" - escape '&' = T.pack "&" - escape c = T.singleton c - --- - -ps :: Text -> Snap () -ps = liftIO . T.putStrLn diff --git a/sdk/haskell/src/demo/www/index.html b/sdk/haskell/src/demo/www/index.html deleted file mode 100644 index 908d1f370..000000000 --- a/sdk/haskell/src/demo/www/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - Haskell + Datastar Example - - - -

Haskell + Datastar Example

-
-
- - - - - - -
signals output goes here
-
- If you want see see what is being sent to the browser, edit - the Snap.hs file and set the debug constant to True -
- -
- When you click on the "Feed with morph button, -
There is an empty div below whose text will be replaced -
with the current time 10 times -
after that we will sleep for 70 seconds, during which time -
the pings will continue so we don't time out. Then we will again -
replace the div with the current time 10 times. After that this -
text will be removed with a RemoveFragments command -
-
-
feed goes here
-
-
- When you click the John Keats button, his beautiful - Ode to a Nightingale -
Will be slowly printed below, as if someone were typing it -
on a keyboard. -
- -
- Ode to a Nightingale goes here -
-
- - diff --git a/sdk/haskell/src/demo/www/keats.txt b/sdk/haskell/src/demo/www/keats.txt deleted file mode 100644 index 011ac77f3..000000000 --- a/sdk/haskell/src/demo/www/keats.txt +++ /dev/null @@ -1,10 +0,0 @@ -Darkling I listen; and, for many a time - I have been half in love with easeful Death, -Call'd him soft names in many a mused rhyme, - To take into the air my quiet breath; -Now more than ever seems it rich to die, - To cease upon the midnight with no pain, - While thou art pouring forth thy soul abroad - In such an ecstasy! - Still wouldst thou sing, and I have ears in vainβ€” - To thy high requiem become a sod. diff --git a/sdk/haskell/src/test/Main.hs b/sdk/haskell/src/test/Main.hs deleted file mode 100644 index 265b57e54..000000000 --- a/sdk/haskell/src/test/Main.hs +++ /dev/null @@ -1,50 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -module Main where - -import Data.Default ( Default(def) ) -import NeatInterpolation ( trimming ) -import Prelude -import ServerSentEventGenerator - ( test, - executeScript, - mergeFragments, - mergeSignals, - removeFragments, - removeSignals ) -import ServerSentEventGenerator.Types - ( AutoRemove(Auto), - FragmentOptions(FO), - MergeMode(Morph, Append), - Options(O), - Selector(SEL) ) - -main :: IO () -main = do - let - yourOptions = O "event1" 2000 - oneTwo = [trimming|{"one":1,"two":2}|] - firstSecond = [trimming|{"one":"first\\n signal","two":"second signal"}|] - paths1 = ["one"] - paths2 = ["one", "two.alpha"] - script1 = "console.log('hello')" - script2 = "if (true) {\n console.log('hello');\n}" - attributes1 = "type: text/javascript\nblocking: false" - attributes2 = "type: module" - test [ - mergeFragments "
Merge
" (SEL "div") Append (FO 1000 True) yourOptions - , mergeFragments "
Merge
" def Morph (FO 1000 True) def - , mergeFragments "
\n Merge\n
" def def def def - , mergeFragments "
Merge
" def def def def - , removeFragments (SEL "#target") (FO 2000 True) yourOptions - , removeFragments (SEL "#target") (FO 300 False) def - , removeFragments (SEL "#target") def def - , mergeSignals oneTwo True yourOptions - , mergeSignals firstSecond True def - , mergeSignals oneTwo def def - , removeSignals paths2 yourOptions - , removeSignals paths1 def - , executeScript script1 attributes1 (Auto False) yourOptions - , executeScript script1 attributes2 (Auto True) def - , executeScript script2 def def def - ] - diff --git a/sdk/java/README.md b/sdk/java/README.md deleted file mode 100644 index 17dac0573..000000000 --- a/sdk/java/README.md +++ /dev/null @@ -1,172 +0,0 @@ -# Datastar Java SDK - -This package provides a Java SDK for working with [Datastar](https://data-star.dev/). - -## License - -This package is licensed for free under the MIT License. - -## Requirements - -This package requires Java 17 or later. - -## Installation - -Install using Maven by adding the following to your `pom.xml`: - -```xml - - com.starfederation - datastar - 1.0.0 - - - - - github - https://maven.pkg.github.com/starfederation/datastar - - -``` - -## Usage - -### Import Statements - -```java -import starfederation.datastar.utils.ServerSentEventGenerator; -``` - -### Set up Server-Sent Events Generator - -You will want to set up the ServerSentEventGenerator to be able to handle the framework you choose to use. Adapters are provided for various framework requests and responses in the SDK. - -```java -AbstractResponseAdapter responseAdapter = new HttpServletResponseAdapter(response); - -ServerSentEventGenerator generator = new ServerSentEventGenerator(responseAdapter); -``` - -Once the generator is set up you can use it to send events. Let's utilize this event for the next few examples. - -```java -PatchElements event = PatchElements.builder() - .selector("#test") - .data("
test
") - .build(); -``` - -#### Sending Events - -This is how you would send an event with default [HTTP/SSE options](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). The default options include a `monotonically increasing integer counter` for event ids and a retry duration of `1000ms`. - -```java -generator.send(event); -``` - -#### Custom Event IDs and Retry Durations -If you want to modify these you can call send and pass the arguments you want for each accordingly. - -```java -// custom event id -generator.send(event, "custom-id"); - -// custom retry duration -generator.send(event, 2000); - -// both custom -generator.send(event, "custom-id", 2000); -``` - -### Events, Event Options, Examples - -There are a few different event types in Datastar and each come with their own options. You can check out the [SDK Guide](https://github.com/starfederation/datastar/blob/develop/sdk/README.md) for a review of all of them. - -#### Example: Patch Elements into the DOM - -This event is used to patch elements into the DOM. The data is a string that represents the HTML to be patched into the DOM. - -##### Options - -- `selector` (string) The CSS selector to use to insert the element. If not provided or empty, Datastar **will** default to using the `id` attribute of the element. -- `mode` (ElementPatchMode) The mode to use when patching the element into the DOM. If not provided the Datastar client side **_will_** default to `outer`. -- `useViewTransition` Whether to use view transitions, if not provided the Datastar client side **_will_** default to `false`. - -```java -PatchElements event = PatchElements.builder() - .selector("#feed") - .mode(ElementPatchMode.Append) - .useViewTransition(true) - .data("
\n1\n
") - .build(); -``` - -#### Example: Removing HTML Elements from the DOM - -This event is used to remove elements from the DOM. - -##### Options - -- `selector` (string) The CSS selector to use to remove the elements. If not provided or empty, Datastar **will** default to using the `id` attribute of the element. -- `useViewTransition` Whether to use view transitions, if not provided the Datastar client side **_will_** default to `false`. - -```java -PatchElements event = PatchElements.builder() - .selector("#feed") - .mode(ElementPatchMode.Remove) - .useViewTransition(true) - .build(); -``` - -#### Example: Patching/Updating Signals - -This event is used to patch data into the signals object on the client side. The data is a JSON object that represents the data to be patched into the signals object. - -###### Options - -- `onlyIfMissing` (boolean) Whether to patch the signals only if they do not already exist. If not provided, the Datastar client side will default to false, which will cause the data to be patched into the signals. - -```java -PatchSignals event = PatchSignals.builder() - .data("{\"key\": \"value\"}") - .onlyIfMissing(true) - .build(); -``` - -#### Example: Removing Signals - -For a signal to be removed, the signal values must be set to `null`. The signals can be nested, and therefore to remove a leaf value, you have to provide the path to the leaf. - -```java -PatchSignals event = PatchSignals.builder() - .data("{\"user\": {\"name\":null, \"email\": null}}") - .build(); -``` - -#### Example: Executing Scripts - -This event is used to execute a script on the client side. The script is a string that represents the script to be executed. The script can be any valid JavaScript code. The script will be executed in the context of the client side. - -###### Options - -- `autoRemove` Whether to remove the script after execution, if not provided the Datastar client side will default to true. -- `attributes` A line separated list of attributes to add to the script element, if not provided the Datastar client side will default to type module. Each item in the array should be a string in the format key value. - -```java -ExecuteScript event = ExecuteScript.builder() - .script("console.log('Hello World');") - .autoRemove(false) - .attributes("type='module'") - .build(); -``` - -#### Example: Reading Signals - -This will read signals from a request and merge them into the signals object on the backend. The request is an object that represents the request to read the signals from. The store is a DataStore object that represents the store to merge the signals into. - -Often you would want to have a Datastore created on your backend to keep track of any signals in Datastar. Here is an example of how you would create a DataStore as well as read signals from a request. - -```java -DataStore store = new DataStore(); -SignalReader.readSignals(request, store); -``` diff --git a/sdk/java/core/pom.xml b/sdk/java/core/pom.xml deleted file mode 100644 index 894acbeaa..000000000 --- a/sdk/java/core/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - 4.0.0 - - dev.data-star - datastar-java-sdk-suite - 1.0.0 - - - datastar-java-sdk-core - jar - - Datastar Java SDK Core - The core library for the Datastar Java SDK suite - - - - github - https://maven.pkg.github.com/rphumulock/datastar - - - - - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.databind.version} - - - \ No newline at end of file diff --git a/sdk/java/core/src/main/java/starfederation/datastar/Consts.java b/sdk/java/core/src/main/java/starfederation/datastar/Consts.java deleted file mode 100644 index 1354b3e90..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/Consts.java +++ /dev/null @@ -1,31 +0,0 @@ -package starfederation.datastar; - -import starfederation.datastar.enums.ElementPatchMode; - -/** - * This is auto-generated by Datastar. DO NOT EDIT. - */ -public final class Consts { - public static final String DATASTAR_KEY = "datastar"; - public static final String VERSION = "1.0.0-RC.1"; - - // The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. - public static final int DEFAULT_SSE_RETRY_DURATION = 1000; - - // Should elements be patched using the ViewTransition API? - public static final boolean DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS = false; - - // Should a given set of signals patch if they are missing? - public static final boolean DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING = false; - - // The mode in which an element is patched into the DOM. - public static final ElementPatchMode DEFAULT_ELEMENT_PATCH_MODE = ElementPatchMode.Outer; - - // Dataline literals. - public static final String SELECTOR_DATALINE_LITERAL = "selector "; - public static final String MODE_DATALINE_LITERAL = "mode "; - public static final String ELEMENTS_DATALINE_LITERAL = "elements "; - public static final String USE_VIEW_TRANSITION_DATALINE_LITERAL = "useViewTransition "; - public static final String SIGNALS_DATALINE_LITERAL = "signals "; - public static final String ONLY_IF_MISSING_DATALINE_LITERAL = "onlyIfMissing "; -} \ No newline at end of file diff --git a/sdk/java/core/src/main/java/starfederation/datastar/adapters/request/AbstractRequestAdapter.java b/sdk/java/core/src/main/java/starfederation/datastar/adapters/request/AbstractRequestAdapter.java deleted file mode 100644 index 66a52f719..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/adapters/request/AbstractRequestAdapter.java +++ /dev/null @@ -1,13 +0,0 @@ -package starfederation.datastar.adapters.request; - -import java.io.BufferedReader; -import java.io.IOException; - -public abstract class AbstractRequestAdapter implements RequestAdapter { - protected BufferedReader reader; - - @Override - public BufferedReader getReader() throws IOException { - return reader; - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/adapters/request/RequestAdapter.java b/sdk/java/core/src/main/java/starfederation/datastar/adapters/request/RequestAdapter.java deleted file mode 100644 index 511ba4632..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/adapters/request/RequestAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package starfederation.datastar.adapters.request; - -import java.io.BufferedReader; -import java.io.IOException; - -public interface RequestAdapter { - /** - * Gets the HTTP method (e.g., GET, POST) of the request. - * - * @return The HTTP method as a string. - */ - String getMethod(); - - /** - * Retrieves a query parameter by name. - * - * @param name The name of the query parameter. - * @return The value of the query parameter, or null if not present. - */ - String getParameter(String name); - - /** - * Retrieves a BufferedReader to read the request body. - * - * @return A BufferedReader for the request body. - * @throws IOException If an error occurs during reading. - */ - BufferedReader getReader() throws IOException; -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/adapters/response/AbstractResponseAdapter.java b/sdk/java/core/src/main/java/starfederation/datastar/adapters/response/AbstractResponseAdapter.java deleted file mode 100644 index 60a83a2a9..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/adapters/response/AbstractResponseAdapter.java +++ /dev/null @@ -1,33 +0,0 @@ -package starfederation.datastar.adapters.response; - -import java.io.PrintWriter; - -public abstract class AbstractResponseAdapter implements ResponseAdapter { - - protected PrintWriter writer; - - @Override - public PrintWriter getWriter() throws Exception { - return writer; - } - - @Override - public void setHeader(String name, String value) { - throw new UnsupportedOperationException("setHeader is not implemented for this response type."); - } - - @Override - public void setContentType(String contentType) { - setHeader("Content-Type", contentType); - } - - @Override - public void setCharacterEncoding(String encoding) { - setHeader("Content-Encoding", encoding); - } - - @Override - public void setStatus(int status) { - throw new UnsupportedOperationException("setStatus is not implemented for this response type."); - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/adapters/response/ResponseAdapter.java b/sdk/java/core/src/main/java/starfederation/datastar/adapters/response/ResponseAdapter.java deleted file mode 100644 index 5be9d30a3..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/adapters/response/ResponseAdapter.java +++ /dev/null @@ -1,42 +0,0 @@ -package starfederation.datastar.adapters.response; - -import java.io.PrintWriter; - -public interface ResponseAdapter { - /** - * Provides a `PrintWriter` for writing content to the response. - * - * @return The `PrintWriter` for the response. - * @throws Exception if the writer cannot be retrieved. - */ - PrintWriter getWriter() throws Exception; - - /** - * Sets a header on the response. - * - * @param name The name of the header. - * @param value The value of the header. - */ - void setHeader(String name, String value); - - /** - * Sets the content type of the response. - * - * @param contentType The MIME type of the response content. - */ - void setContentType(String contentType); - - /** - * Sets the character encoding for the response. - * - * @param encoding The character encoding to use. - */ - void setCharacterEncoding(String encoding); - - /** - * Sets the HTTP status code for the response. - * - * @param status The HTTP status code. - */ - void setStatus(int status); -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/enums/ElementPatchMode.java b/sdk/java/core/src/main/java/starfederation/datastar/enums/ElementPatchMode.java deleted file mode 100644 index bd077547d..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/enums/ElementPatchMode.java +++ /dev/null @@ -1,42 +0,0 @@ -package starfederation.datastar.enums; - -/** - * This is auto-generated by Datastar. DO NOT EDIT. - */ -public enum ElementPatchMode { - - // Morphs the element into the existing element. - Outer("outer"), - - // Replaces the inner HTML of the existing element. - Inner("inner"), - - // Removes the existing element. - Remove("remove"), - - // Replaces the existing element with the new element. - Replace("replace"), - - // Prepends the element inside to the existing element. - Prepend("prepend"), - - // Appends the element inside the existing element. - Append("append"), - - // Inserts the element before the existing element. - Before("before"), - - // Inserts the element after the existing element. - After("after"); - - private final String value; - - ElementPatchMode(String value) { - this.value = value; - } - - @Override - public String toString() { - return value; - } -} \ No newline at end of file diff --git a/sdk/java/core/src/main/java/starfederation/datastar/enums/EventType.java b/sdk/java/core/src/main/java/starfederation/datastar/enums/EventType.java deleted file mode 100644 index 6fa1fc8f7..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/enums/EventType.java +++ /dev/null @@ -1,24 +0,0 @@ -package starfederation.datastar.enums; - -/** - * This is auto-generated by Datastar. DO NOT EDIT. - */ -public enum EventType { - - // An event for patching HTML elements into the DOM. - PatchElements("datastar-patch-elements"), - - // An event for patching signals. - PatchSignals("datastar-patch-signals"); - - private final String value; - - EventType(String value) { - this.value = value; - } - - @Override - public String toString() { - return value; - } -} \ No newline at end of file diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/AbstractBuilder.java b/sdk/java/core/src/main/java/starfederation/datastar/events/AbstractBuilder.java deleted file mode 100644 index 48e3efb2f..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/AbstractBuilder.java +++ /dev/null @@ -1,5 +0,0 @@ -package starfederation.datastar.events; - -public abstract class AbstractBuilder { - abstract T build(); -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/AbstractDatastarEvent.java b/sdk/java/core/src/main/java/starfederation/datastar/events/AbstractDatastarEvent.java deleted file mode 100644 index 4913c6e14..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/AbstractDatastarEvent.java +++ /dev/null @@ -1,31 +0,0 @@ -package starfederation.datastar.events; - -import starfederation.datastar.enums.EventType; - -import java.util.List; -import java.util.Objects; - -sealed public abstract class AbstractDatastarEvent implements DatastarEvent permits PatchElements, PatchSignals, - ExecuteScript, CustomEvent { - - private final String[] dataLines; - - protected AbstractDatastarEvent(EventType eventType, List dataLines) { - Objects.requireNonNull(eventType, "Event type cannot be null"); - Objects.requireNonNull(dataLines, "Data lines cannot be null"); - if (dataLines.isEmpty()) { - throw new IllegalArgumentException("Data lines cannot be empty"); - } - this.dataLines = dataLines.toArray(String[]::new); - } - - @Override - public String[] getDataLines() { - return dataLines; - } - - @Override - public String toString() { - return "event: %s\n%s".formatted(getEventType(), String.join("\n", dataLines)); - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/CustomEvent.java b/sdk/java/core/src/main/java/starfederation/datastar/events/CustomEvent.java deleted file mode 100644 index 91c416fc9..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/CustomEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package starfederation.datastar.events; - -import starfederation.datastar.enums.EventType; - -import java.util.List; - -public abstract non-sealed class CustomEvent extends AbstractDatastarEvent { - - protected CustomEvent(EventType eventType, List dataLines) { - super(eventType, dataLines); - } - -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/DatastarEvent.java b/sdk/java/core/src/main/java/starfederation/datastar/events/DatastarEvent.java deleted file mode 100644 index 45810fde2..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/DatastarEvent.java +++ /dev/null @@ -1,26 +0,0 @@ -package starfederation.datastar.events; - -import starfederation.datastar.enums.EventType; - -sealed interface DatastarEvent permits AbstractDatastarEvent { - - /** - * Returns the event type. - */ - EventType getEventType(); - - /** - * Returns the data lines for the event. - */ - String[] getDataLines(); - - /** - * Returns the builder for the event. - */ - - static AbstractBuilder builder() { - throw new IllegalStateException("the builder method should be overridden to use the appropriate builder"); - } - -} - diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/ExecuteScript.java b/sdk/java/core/src/main/java/starfederation/datastar/events/ExecuteScript.java deleted file mode 100644 index c8b26c8ef..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/ExecuteScript.java +++ /dev/null @@ -1,86 +0,0 @@ -package starfederation.datastar.events; - -import starfederation.datastar.enums.EventType; - -import java.util.ArrayList; -import java.util.List; - -import static starfederation.datastar.Consts.*; - -public final class ExecuteScript extends AbstractDatastarEvent { - - private ExecuteScript(EventType eventType, List dataLines) { - super(eventType, dataLines); - } - - @Override - public EventType getEventType() { - return EventType.PatchElements; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder extends AbstractBuilder { - private String script; - private boolean autoRemove = true; // Default - private String attributes = ""; // Default - - /** - * JavaScript to execute on the client. Do not wrap in HTML-script tags. - * @param script valid JavaScript - * @return a builder for fluent configuration - */ - public Builder script(String script) { - if (script == null || script.isBlank()) { - throw new IllegalArgumentException("Script cannot be null or empty"); - } - this.script = script; - return this; - } - private Builder() { - } - - public Builder autoRemove(boolean autoRemove) { - this.autoRemove = autoRemove; - return this; - } - - public Builder attributes(String attributes) { - if (attributes != null && !attributes.isBlank()) { - this.attributes = attributes.trim(); - } - return this; - } - - @Override - public ExecuteScript build() { - if (script == null || script.isBlank()) { - throw new IllegalArgumentException("Script cannot be null or empty"); - } - - var wrappedScript = new StringBuilder(" dataLines = new ArrayList<>(); - - // Add attributes if not default - if (attributes != null && !attributes.isBlank()) { - wrappedScript.append(' ').append(attributes); - } - - // Add autoRemove - if (autoRemove) { - wrappedScript.append(" data-effect=\"el.remove()\""); - } - - wrappedScript.append(">").append(script).append(""); - - // Add script - wrappedScript.toString().lines() - .filter(line -> !line.isBlank()) - .forEach(line -> dataLines.add(ELEMENTS_DATALINE_LITERAL + line)); - - return new ExecuteScript(EventType.PatchElements, dataLines); - } - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/PatchElements.java b/sdk/java/core/src/main/java/starfederation/datastar/events/PatchElements.java deleted file mode 100644 index 2acb1017d..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/PatchElements.java +++ /dev/null @@ -1,87 +0,0 @@ -package starfederation.datastar.events; - -import starfederation.datastar.enums.ElementPatchMode; -import starfederation.datastar.enums.EventType; - -import java.util.ArrayList; -import java.util.List; - -import static starfederation.datastar.Consts.*; - -public final class PatchElements extends AbstractDatastarEvent { - - private PatchElements(EventType eventType, List dataLines) { - super(eventType, dataLines); - } - - @Override - public EventType getEventType() { - return EventType.PatchElements; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder extends AbstractBuilder { - private String selector; - private ElementPatchMode mode = DEFAULT_ELEMENT_PATCH_MODE; // Default - private boolean useViewTransition = DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS; // Default - private String rawData; - - private Builder() { - } - - public Builder selector(String selector) { - this.selector = selector; - return this; - } - - public Builder mode(ElementPatchMode mode) { - this.mode = mode; - return this; - } - - public Builder useViewTransition(boolean useViewTransition) { - this.useViewTransition = useViewTransition; - return this; - } - - public Builder data(String rawData) { - this.rawData = rawData; - return this; - } - - @Override - public PatchElements build() { - if (mode != ElementPatchMode.Remove && (rawData == null || rawData.isBlank())) { - throw new IllegalArgumentException("Data cannot be null or empty"); - } - - List dataLines = new ArrayList<>(); - - // Add selector - if (selector != null && !selector.isEmpty()) { - dataLines.add(SELECTOR_DATALINE_LITERAL + selector.trim()); - } - - // Add mergeMode if not default - if (mode != DEFAULT_ELEMENT_PATCH_MODE) { - dataLines.add(MODE_DATALINE_LITERAL + mode); - } - - // Add useViewTransition if true - if (useViewTransition != DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS) { - dataLines.add(USE_VIEW_TRANSITION_DATALINE_LITERAL + useViewTransition); - } - - // Add raw data as fragments - if (rawData!= null) - rawData.lines() - .filter(line -> !line.isBlank()) - .forEach(line -> dataLines.add(ELEMENTS_DATALINE_LITERAL + line)); - - return new PatchElements(EventType.PatchElements, dataLines); - } - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/events/PatchSignals.java b/sdk/java/core/src/main/java/starfederation/datastar/events/PatchSignals.java deleted file mode 100644 index 4b2d4e327..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/events/PatchSignals.java +++ /dev/null @@ -1,58 +0,0 @@ -package starfederation.datastar.events; - -import starfederation.datastar.enums.EventType; - -import java.util.ArrayList; -import java.util.List; - -import static starfederation.datastar.Consts.*; - -public final class PatchSignals extends AbstractDatastarEvent { - - private PatchSignals(EventType eventType, List dataLines) { - super(eventType, dataLines); - } - - @Override - public EventType getEventType() { - return EventType.PatchSignals; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder extends AbstractBuilder { - private String data; - private boolean onlyIfMissing = DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING; // Default - - public Builder data(String data) { - this.data = data; - return this; - } - - public Builder onlyIfMissing(boolean onlyIfMissing) { - this.onlyIfMissing = onlyIfMissing; - return this; - } - - @Override - public PatchSignals build() { - if (data == null || data.isBlank()) { - throw new IllegalArgumentException("Data cannot be null or empty"); - } - - List dataLines = new ArrayList<>(); - - // Add onlyIfMissing if true - if (onlyIfMissing != DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING) { - dataLines.add(ONLY_IF_MISSING_DATALINE_LITERAL + onlyIfMissing); - } - - // Add signals data - dataLines.add(SIGNALS_DATALINE_LITERAL + data); - - return new PatchSignals(EventType.PatchSignals, dataLines); - } - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/utils/DataStore.java b/sdk/java/core/src/main/java/starfederation/datastar/utils/DataStore.java deleted file mode 100644 index c392e10bd..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/utils/DataStore.java +++ /dev/null @@ -1,110 +0,0 @@ -package starfederation.datastar.utils; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * A simple data store for managing key-value pairs with serialization/deserialization support. - */ -public class DataStore { - - private final ConcurrentMap store = new ConcurrentHashMap<>(); - private final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Adds a key-value pair to the store. - * - * @param key The key (must be non-null and non-blank). - * @param value The value (can be null). - */ - public void put(String key, Object value) { - if (key == null) { - throw new NullPointerException("Key cannot be null"); - } - if (key.trim().isEmpty()) { - throw new IllegalArgumentException("Key cannot be blank"); - } - store.put(key, value); - } - - /** - * Retrieves the value associated with the given key, cast to the specified type. - * - * @param key The key. - * @param clazz The expected type of the value. - * @return The value associated with the key, or null if not found. - * @throws IllegalArgumentException if the value is not of the expected type. - */ - public T get(String key, Class clazz) { - Object value = store.get(key); - if (value == null) { - return null; - } - if (!clazz.isInstance(value)) { - throw new IllegalArgumentException("Value for key '" + key + "' is not of type " + clazz.getSimpleName()); - } - return clazz.cast(value); - } - - /** - * Adds all entries from the given map to the store. - * - * @param data The map of key-value pairs. - */ - public void putAll(Map data) { - if (data == null) { - throw new NullPointerException("Data map cannot be null"); - } - store.putAll(data); - } - - /** - * Gets the underlying ConcurrentMap store. - * Note: This provides direct access to the internal store. - * Use with caution as it bypasses type checking. - * - * @return The underlying ConcurrentMap store. - */ - public ConcurrentMap getStore() { - return store; - } - - /** - * Serializes the store to a JSON string. - * - * @return The JSON representation of the store. - */ - public String toJson() { - try { - return objectMapper.writeValueAsString(store); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to serialize DataStore to JSON", e); - } - } - - /** - * Deserializes the given JSON string into the store. - * - * @param json The JSON string. - * @throws IllegalArgumentException if the JSON is invalid. - */ - public void fromJson(String json) { - try { - Map data = objectMapper.readValue(json, new TypeReference<>() {}); - store.clear(); - store.putAll(data); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Invalid JSON: " + json, e); - } - } - - @Override - public String toString() { - return "DataStore{store=" + store + '}'; - } -} \ No newline at end of file diff --git a/sdk/java/core/src/main/java/starfederation/datastar/utils/ServerSentEventGenerator.java b/sdk/java/core/src/main/java/starfederation/datastar/utils/ServerSentEventGenerator.java deleted file mode 100644 index fcf912813..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/utils/ServerSentEventGenerator.java +++ /dev/null @@ -1,110 +0,0 @@ -package starfederation.datastar.utils; - -import starfederation.datastar.adapters.response.AbstractResponseAdapter; -import starfederation.datastar.events.AbstractDatastarEvent; - -import java.io.Closeable; -import java.io.PrintWriter; -import java.util.concurrent.atomic.AtomicLong; - -public class ServerSentEventGenerator implements Closeable { - - private final PrintWriter writer; - private static final AtomicLong COUNTER = new AtomicLong(-1); - - /** - * Initializes the Server-Sent Event generator. - * - * @param response the response adapter to send the event to. - */ - public ServerSentEventGenerator(AbstractResponseAdapter response) { - if (response == null) { - throw new IllegalArgumentException("Response adapter cannot be null."); - } - - try { - // Set headers for SSE - response.setContentType("text/event-stream"); - response.setCharacterEncoding("UTF-8"); - response.setHeader("Cache-Control", "no-cache"); - response.setHeader("Connection", "keep-alive"); - - this.writer = response.getWriter(); - this.writer.flush(); - } catch (Exception e) { - throw new RuntimeException("Failed to initialize ServerSentEventGenerator.", e); - } - } - - /** - * Sends a Datastar event to the client. - * - * @param event the event to send - * @param id the event id - */ - public synchronized void send(AbstractDatastarEvent event, String id, int retry) { - if (event == null) { - throw new IllegalArgumentException("Event cannot be null."); - } - - StringBuilder output = new StringBuilder(); - - // Write the event type - output.append("event: ").append(event.getEventType()).append("\n"); - output.append("id: ").append(id).append("\n"); - if(retry > 0) { - output.append("retry: ").append(retry).append("\n"); - } - // Write the data lines - for (String line : event.getDataLines()) { - output.append("data: ").append(line).append("\n"); - } - - // Add a blank line to separate events - output.append("\n"); - - // Send the event - writer.print(output); - writer.flush(); - } - - /** - * Sends a Datastar event to the client with a monotonically increasing long ID - * and default retry duration. - * - * @param event the event to send - */ - public synchronized void send(AbstractDatastarEvent event) { - send(event, String.valueOf(COUNTER.incrementAndGet()), -1); - } - - /** - * Sends a Datastar event to the client with a specified ID and default retry - * duration. - * - * @param event the event to send - * @param id the event id - */ - public synchronized void send(AbstractDatastarEvent event, String id) { - send(event, id, -1); - } - - /** - * Sends a Datastar event to the client with a monotonically increasing long ID - * and specified retry duration. - * - * @param event the event to send - * @param retry the retry duration - */ - public synchronized void send(AbstractDatastarEvent event, int retry) { - send(event, String.valueOf(COUNTER.incrementAndGet()), retry); - } - - /** - * Closes the writer when finished. - */ - @Override - public synchronized void close() { - writer.close(); - } -} diff --git a/sdk/java/core/src/main/java/starfederation/datastar/utils/SignalReader.java b/sdk/java/core/src/main/java/starfederation/datastar/utils/SignalReader.java deleted file mode 100644 index e3fd18672..000000000 --- a/sdk/java/core/src/main/java/starfederation/datastar/utils/SignalReader.java +++ /dev/null @@ -1,63 +0,0 @@ -package starfederation.datastar.utils; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import starfederation.datastar.adapters.request.RequestAdapter; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentMap; - -import static starfederation.datastar.Consts.DATASTAR_KEY; - -public class SignalReader { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Parses incoming data from the browser and unmarshals it into the given store object. - * - * @param requestAdapter The request adapter wrapping the incoming request. - * @param store A Map that will hold the parsed data. - * @throws IOException If an error occurs during reading or parsing. - */ - public static void readSignals(RequestAdapter requestAdapter, ConcurrentMap store) throws IOException { - Objects.requireNonNull(store, "store cannot be null"); - Objects.requireNonNull(requestAdapter, "requestAdapter cannot be null"); - - String data; - - if ("GET".equalsIgnoreCase(requestAdapter.getMethod())) { - // Handle GET requests by parsing the `datastar` query parameter - data = requestAdapter.getParameter(DATASTAR_KEY); - if (data == null || data.isBlank()) { - throw new IllegalArgumentException("Missing 'datastar' query parameter in GET request."); - } - } else { - // Handle other methods by reading the request body - StringBuilder requestBody = new StringBuilder(); - try (BufferedReader reader = requestAdapter.getReader()) { - String line; - while ((line = reader.readLine()) != null) { - requestBody.append(line); - } - } - - data = requestBody.toString(); - if (data.isEmpty()) { - throw new IllegalArgumentException("Request body cannot be empty."); - } - } - - // Parse the JSON data into the store - try { - Map parsedData = objectMapper.readValue(data, new TypeReference<>() { - }); - store.putAll(parsedData); - } catch (IOException e) { - throw new IOException("Failed to parse JSON data.", e); - } - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/DataStoreTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/DataStoreTest.java deleted file mode 100644 index 24f506dfc..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/DataStoreTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import starfederation.datastar.utils.DataStore; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -class DataStoreTest { - - private DataStore dataStore; - - @BeforeEach - void setUp() { - dataStore = new DataStore(); - } - - @Test - void putShouldAddKeyValuePair() { - dataStore.put("key1", "value1"); - assertEquals("value1", dataStore.get("key1", String.class)); - } - - @Test - void putShouldThrowExceptionForNullKey() { - assertThrows(NullPointerException.class, () -> dataStore.put(null, "value")); - } - - @Test - void putShouldThrowExceptionForBlankKey() { - assertThrows(IllegalArgumentException.class, () -> dataStore.put(" ", "value")); - } - - @Test - void getShouldReturnNullForNonExistentKey() { - assertNull(dataStore.get("nonexistent", String.class)); - } - - @Test - void getShouldThrowExceptionForTypeMismatch() { - dataStore.put("key1", "stringValue"); - assertThrows(IllegalArgumentException.class, () -> dataStore.get("key1", Integer.class)); - } - - @Test - void putAllShouldAddAllEntries() { - Map data = new HashMap<>(); - data.put("key1", "value1"); - data.put("key2", 123); - data.put("key3", true); - - dataStore.putAll(data); - - assertEquals("value1", dataStore.get("key1", String.class)); - assertEquals(123, dataStore.get("key2", Integer.class)); - assertEquals(true, dataStore.get("key3", Boolean.class)); - } - - @Test - void toJsonShouldSerializeStoreToJson() { - dataStore.put("key1", "value1"); - dataStore.put("key2", 42); - dataStore.put("key3", true); - - String expectedJson = "{\"key1\":\"value1\",\"key2\":42,\"key3\":true}"; - String actualJson = dataStore.toJson(); - - assertEquals(expectedJson, actualJson); - } - - @Test - void fromJsonShouldDeserializeJsonToStore() { - String json = "{\"key1\":\"value1\",\"key2\":42,\"key3\":true}"; - - dataStore.fromJson(json); - - assertEquals("value1", dataStore.get("key1", String.class)); - assertEquals(42, dataStore.get("key2", Integer.class)); - assertEquals(true, dataStore.get("key3", Boolean.class)); - } - - @Test - void fromJsonShouldThrowExceptionForInvalidJson() { - String invalidJson = "{invalid json}"; - - assertThrows(IllegalArgumentException.class, () -> dataStore.fromJson(invalidJson)); - } - - @Test - void toStringShouldReturnCorrectRepresentation() { - dataStore.put("key1", "value1"); - dataStore.put("key2", 42); - - String expectedString = "DataStore{store={key1=value1, key2=42}}"; - assertEquals(expectedString, dataStore.toString()); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/ExecuteScriptTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/ExecuteScriptTest.java deleted file mode 100644 index 735482098..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/ExecuteScriptTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.Test; -import starfederation.datastar.enums.EventType; -import starfederation.datastar.events.ExecuteScript; - -import static org.junit.jupiter.api.Assertions.*; - -class ExecuteScriptTest { - - @Test - void builderShouldGenerateCorrectEvent() { - ExecuteScript event = ExecuteScript.builder() - .script("console.log('Hello World');") - .autoRemove(false) - .attributes("type='module'") - .build(); - - String[] expectedDataLines = { - "elements " - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - assertEquals(EventType.PatchElements, event.getEventType()); - } - - @Test - void builderShouldExcludeDefaultValues() { - ExecuteScript event = ExecuteScript.builder() - .script("console.log('Hello World');") - .build(); - - String[] expectedDataLines = { - "elements " - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - } - - @Test - void builderShouldThrowExceptionForNullScript() { - assertThrows(IllegalArgumentException.class, () -> ExecuteScript.builder().build()); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/MergeSignalsTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/MergeSignalsTest.java deleted file mode 100644 index 8443dc57c..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/MergeSignalsTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.Test; -import starfederation.datastar.enums.EventType; -import starfederation.datastar.events.PatchSignals; - -import static org.junit.jupiter.api.Assertions.*; - -class MergeSignalsTest { - - @Test - void builderShouldGenerateCorrectEvent() { - PatchSignals event = PatchSignals.builder() - .data("{\"key\": \"value\"}") - .onlyIfMissing(true) - .build(); - - String[] expectedDataLines = { - "onlyIfMissing true", - "signals {\"key\": \"value\"}" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - assertEquals(EventType.PatchSignals, event.getEventType()); - } - - @Test - void builderShouldExcludeDefaultValues() { - PatchSignals event = PatchSignals.builder() - .data("{\"key\": \"value\"}") - .build(); - - String[] expectedDataLines = { - "signals {\"key\": \"value\"}" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - } - - @Test - void builderShouldThrowExceptionForNullData() { - assertThrows(IllegalArgumentException.class, () -> PatchSignals.builder().build()); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/PatchElementsTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/PatchElementsTest.java deleted file mode 100644 index 7112a10df..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/PatchElementsTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.Test; -import starfederation.datastar.enums.EventType; -import starfederation.datastar.enums.ElementPatchMode; -import starfederation.datastar.events.PatchElements; - -import static org.junit.jupiter.api.Assertions.*; - -class PatchElementsTest { - - @Test - void builderShouldGenerateCorrectEvent() { - PatchElements event = PatchElements.builder() - .selector("#feed") - .mode(ElementPatchMode.Append) - .useViewTransition(true) - .data("
\n1\n
") - .build(); - - String[] expectedDataLines = { - "selector #feed", - "mode append", - "useViewTransition true", - "elements
", - "elements 1", - "elements
" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - assertEquals(EventType.PatchElements, event.getEventType()); - } - - @Test - void builderShouldExcludeDefaultValues() { - PatchElements event = PatchElements.builder() - .data("
\n1\n
") - .build(); - - String[] expectedDataLines = { - "elements
", - "elements 1", - "elements
" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - } - - @Test - void builderShouldThrowExceptionForNullData() { - assertThrows(IllegalArgumentException.class, () -> PatchElements.builder().build()); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/RemoveFragmentsTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/RemoveFragmentsTest.java deleted file mode 100644 index 04ebecc53..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/RemoveFragmentsTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.Test; - -import starfederation.datastar.enums.ElementPatchMode; -import starfederation.datastar.enums.EventType; -import starfederation.datastar.events.PatchElements; - -import static org.junit.jupiter.api.Assertions.*; - -class RemoveFragmentsTest { - - @Test - void builderShouldGenerateCorrectEvent() { - PatchElements event = PatchElements.builder() - .selector("#feed") - .mode(ElementPatchMode.Remove) - .useViewTransition(true) - .build(); - - String[] expectedDataLines = { - "selector #feed", - "mode remove", - "useViewTransition true" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - assertEquals(EventType.PatchElements, event.getEventType()); - } - - @Test - void builderShouldExcludeDefaultValues() { - PatchElements event = PatchElements.builder() - .selector("#feed") - .mode(ElementPatchMode.Remove) - .build(); - - String[] expectedDataLines = { - "selector #feed", - "mode remove" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - } - - @Test - void builderShouldThrowExceptionForNullSelector() { - assertThrows(IllegalArgumentException.class, () -> PatchElements.builder().build()); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/RemoveSignalsTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/RemoveSignalsTest.java deleted file mode 100644 index e1d91818c..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/RemoveSignalsTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.Test; -import starfederation.datastar.enums.EventType; -import starfederation.datastar.events.PatchSignals; - -import static org.junit.jupiter.api.Assertions.*; - -class RemoveSignalsTest { - - @Test - void builderShouldGenerateCorrectEvent() { - PatchSignals event = PatchSignals.builder() - .data("{\"user\": {\"name\": null, \"email\": null}}") - .build(); - - String[] expectedDataLines = { - "signals {\"user\": {\"name\": null, \"email\": null}}" - }; - - assertArrayEquals(expectedDataLines, event.getDataLines()); - assertEquals(EventType.PatchSignals, event.getEventType()); - } - - @Test - void builderShouldThrowExceptionForEmptyPaths() { - assertThrows(IllegalArgumentException.class, () -> PatchSignals.builder().build()); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/ServerSentEventGeneratorTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/ServerSentEventGeneratorTest.java deleted file mode 100644 index cde5aa8c8..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/ServerSentEventGeneratorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opentest4j.AssertionFailedError; -import starfederation.datastar.adapters.response.AbstractResponseAdapter; -import starfederation.datastar.events.PatchElements; -import starfederation.datastar.utils.ServerSentEventGenerator; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.reflect.Field; -import java.util.concurrent.atomic.AtomicLong; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - -class ServerSentEventGeneratorTest { - private static final AbstractResponseAdapter mockResponse; - private static final StringWriter stringWriter; - private static final ServerSentEventGenerator generator; - - static { - stringWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stringWriter); - mockResponse = Mockito.mock(AbstractResponseAdapter.class); - try { - when(mockResponse.getWriter()).thenReturn(printWriter); - } catch (Exception e) { - throw new RuntimeException(e); - } - generator = new ServerSentEventGenerator(mockResponse); - } - - @AfterAll - static void end() { - generator.close(); - } - - @AfterEach - void tearDown() throws Exception { - resetCounter(); - stringWriter.getBuffer().setLength(0); - - } - private void resetCounter() throws Exception { - Field counterField = ServerSentEventGenerator.class.getDeclaredField("COUNTER"); - counterField.setAccessible(true); - AtomicLong counter = (AtomicLong) counterField.get(null); - counter.set(-1); - counterField.setAccessible(false); - } - - @Test - void sendShouldWriteValidEvent() { - PatchElements event = PatchElements.builder() - .selector("#test") - .data("
test
") - .build(); - - generator.send(event); - - String expectedOutput = """ - event: datastar-patch-elements - id: 0 - data: selector #test - data: elements
test
- - """; - - assertEquals(expectedOutput, stringWriter.toString()); - } - - @Test - void sendWithIdShouldWriteValidEvent() { - PatchElements event = PatchElements.builder() - .selector("#test") - .data("
test
") - .build(); - - generator.send(event, "custom-id"); - - String expectedOutput = """ - event: datastar-patch-elements - id: custom-id - data: selector #test - data: elements
test
- - """; - - assertEquals(expectedOutput, stringWriter.toString()); - } - - @Test - void sendShouldGenerateIncrementingIds() { - PatchElements event1 = PatchElements.builder() - .selector("#test1") - .data("
test1
") - .build(); - - PatchElements event2 = PatchElements.builder() - .selector("#test2") - .data("
test2
") - .build(); - - generator.send(event1); - generator.send(event2); - - String expectedOutput = """ - event: datastar-patch-elements - id: 0 - data: selector #test1 - data: elements
test1
- - event: datastar-patch-elements - id: 1 - data: selector #test2 - data: elements
test2
- - """; - - assertEquals(expectedOutput, stringWriter.toString()); - } - - @Test - void sendShouldThrowExceptionForNullEvent() { - assertThrows(IllegalArgumentException.class, () -> generator.send(null)); - } - - @Test - void constructorShouldThrowExceptionForNullResponse() { - assertThrows(IllegalArgumentException.class, () -> { - try (ServerSentEventGenerator ignored = new ServerSentEventGenerator(null)) { - throw new AssertionFailedError(); - } - }); - } -} diff --git a/sdk/java/core/src/test/java/starfederation/datastar/unit/SignalReaderTest.java b/sdk/java/core/src/test/java/starfederation/datastar/unit/SignalReaderTest.java deleted file mode 100644 index 73cef4a3d..000000000 --- a/sdk/java/core/src/test/java/starfederation/datastar/unit/SignalReaderTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package starfederation.datastar.unit; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import starfederation.datastar.Consts; -import starfederation.datastar.adapters.request.RequestAdapter; -import starfederation.datastar.utils.SignalReader; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class SignalReaderTest { - - @Mock - private RequestAdapter mockRequest; - - private final ConcurrentMap store = new ConcurrentHashMap<>(); - - @AfterEach - void tearDown() { - store.clear(); - } - - @Test - void readSignalsShouldParseGetRequest() throws Exception { - when(mockRequest.getMethod()).thenReturn("GET"); - when(mockRequest.getParameter("datastar")).thenReturn("{\"key1\":\"value1\",\"key2\":42}"); - - SignalReader.readSignals(mockRequest, store); - - assertEquals(2, store.size()); - assertEquals("value1", store.get("key1")); - assertEquals(42, store.get("key2")); - } - - @Test - void readSignalsShouldParsePostRequest() throws Exception { - when(mockRequest.getMethod()).thenReturn("POST"); - when(mockRequest.getReader()).thenReturn(new BufferedReader(new StringReader("{\"key1\":\"value1\",\"key2\":42}"))); - - SignalReader.readSignals(mockRequest, store); - - assertEquals(2, store.size()); - assertEquals("value1", store.get("key1")); - assertEquals(42, store.get("key2")); - } - - @Test - void readSignalsShouldThrowExceptionForNullStore() { - assertThrows(NullPointerException.class, () -> SignalReader.readSignals(mockRequest, null)); - } - - @Test - void readSignalsShouldThrowExceptionForNullRequestAdapter() { - assertThrows(NullPointerException.class, () -> SignalReader.readSignals(null, store)); - } - - @Test - void readSignalsShouldThrowExceptionForMissingQueryParameter() throws Exception { - when(mockRequest.getMethod()).thenReturn("GET"); - when(mockRequest.getParameter(Consts.DATASTAR_KEY)).thenReturn(null); - - assertThrows(IllegalArgumentException.class, () -> SignalReader.readSignals(mockRequest, store)); - } - - @Test - void readSignalsShouldThrowExceptionForEmptyPostBody() throws Exception { - when(mockRequest.getMethod()).thenReturn("POST"); - when(mockRequest.getReader()).thenReturn(new BufferedReader(new StringReader(""))); - - assertThrows(IllegalArgumentException.class, () -> SignalReader.readSignals(mockRequest, store)); - } - - @Test - void readSignalsShouldThrowExceptionForInvalidJson() throws Exception { - when(mockRequest.getMethod()).thenReturn("POST"); - when(mockRequest.getReader()).thenReturn(new BufferedReader(new StringReader("{invalid json}"))); - - assertThrows(IOException.class, () -> SignalReader.readSignals(mockRequest, store)); - } -} diff --git a/sdk/java/datastar-java-sdk-jaxrs/pom.xml b/sdk/java/datastar-java-sdk-jaxrs/pom.xml deleted file mode 100644 index f4895a030..000000000 --- a/sdk/java/datastar-java-sdk-jaxrs/pom.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - 4.0.0 - datastar-java-sdk-jaxrs - 1.0.0 - jar - - dev.data-star - datastar-java-sdk-suite - 1.0.0 - - - Datastar Java SDK JAX-RS plugin - JAX-RS plugin for the Datastar SDK - - - 4.0.0 - 6.1.0 - - - - - github - https://maven.pkg.github.com/rphumulock/datastar - - - - - - - jakarta.ws.rs - jakarta.ws.rs-api - ${jakarta.ws-rs-api.version} - provided - - - - jakarta.servlet - jakarta.servlet-api - ${jakarta.servlet.api.version} - provided - - - dev.data-star - datastar-java-sdk-core - ${project.version} - compile - - - diff --git a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/request/HttpServletRequestAdapter.java b/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/request/HttpServletRequestAdapter.java deleted file mode 100644 index d3e9753f3..000000000 --- a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/request/HttpServletRequestAdapter.java +++ /dev/null @@ -1,32 +0,0 @@ -package starfederation.datastar.adapters.request; - -import jakarta.servlet.http.HttpServletRequest; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.Objects; - -public class HttpServletRequestAdapter extends AbstractRequestAdapter { - - private final HttpServletRequest request; - - public HttpServletRequestAdapter(HttpServletRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - this.request = request; - } - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getParameter(String name) { - return request.getParameter(name); - } - - @Override - public BufferedReader getReader() throws IOException { - return request.getReader(); - } -} diff --git a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/request/JaxRsRequestAdapter.java b/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/request/JaxRsRequestAdapter.java deleted file mode 100644 index 241624674..000000000 --- a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/request/JaxRsRequestAdapter.java +++ /dev/null @@ -1,58 +0,0 @@ -package starfederation.datastar.adapters.request; - -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.HttpHeaders; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.charset.UnsupportedCharsetException; -import java.util.Objects; - -/** - * Adapter to abstract HTTP request handling in JAX-RS. - */ -public class JaxRsRequestAdapter extends AbstractRequestAdapter { - - @Context - public ContainerRequestContext requestContext; - - @Override - public String getMethod() { - return requestContext.getMethod(); - } - - @Override - public String getParameter(String name) { - Objects.requireNonNull(name, "Parameter name cannot be null"); - return requestContext.getUriInfo().getQueryParameters().getFirst(name); - } - - @Override - public BufferedReader getReader() { - return new BufferedReader(new InputStreamReader(requestContext.getEntityStream(), getCharacterEncoding())); - } - - - /** - * Determines the character encoding from the Content-Type header. - * - * @return The determined or default character encoding. - */ - private Charset getCharacterEncoding() { - String contentType = requestContext.getHeaderString(HttpHeaders.CONTENT_TYPE); - String charsetFieldName = "charset="; - if (contentType != null && contentType.contains(charsetFieldName)) { - String charset = contentType.substring(contentType.indexOf(charsetFieldName) + charsetFieldName.length()).trim(); - try { - return Charset.forName(charset); - } catch (UnsupportedCharsetException ignored) { - } - - } - return StandardCharsets.UTF_8; - - } -} diff --git a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/response/HttpServletResponseAdapter.java b/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/response/HttpServletResponseAdapter.java deleted file mode 100644 index d5bf5fa45..000000000 --- a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/response/HttpServletResponseAdapter.java +++ /dev/null @@ -1,23 +0,0 @@ -package starfederation.datastar.adapters.response; - -import jakarta.servlet.http.HttpServletResponse; - -public class HttpServletResponseAdapter extends AbstractResponseAdapter { - - private final HttpServletResponse response; - - public HttpServletResponseAdapter(HttpServletResponse response) throws Exception { - this.response = response; - this.writer = response.getWriter(); - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } - - @Override - public void setStatus(int status) { - response.setStatus(status); - } -} diff --git a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/response/JaxRsResponseAdapter.java b/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/response/JaxRsResponseAdapter.java deleted file mode 100644 index e3e40135d..000000000 --- a/sdk/java/datastar-java-sdk-jaxrs/src/main/java/starfederation/datastar/adapters/response/JaxRsResponseAdapter.java +++ /dev/null @@ -1,37 +0,0 @@ -package starfederation.datastar.adapters.response; - -import jakarta.ws.rs.sse.Sse; -import jakarta.ws.rs.sse.SseEventSink; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; - -public class JaxRsResponseAdapter extends AbstractResponseAdapter { - private final SseEventSink eventSink; - private final Sse sse; - - public JaxRsResponseAdapter(SseEventSink eventSink, Sse sse) { - this.eventSink = eventSink; - this.sse = sse; - StringWriter stringWriter = new StringWriter(); - this.writer = new PrintWriter(stringWriter, true); - } - - @Override - public void setHeader(String name, String value) { - } - - @Override - public void setStatus(int status) { - } - - public void sendEvent(String eventName, String data) throws IOException { - if (eventSink != null && !eventSink.isClosed()) { - eventSink.send(sse.newEventBuilder() - .name(eventName) - .data(data) - .build()); - } - } -} diff --git a/sdk/java/datastar-java-sdk-jaxrs/src/test/java/starfederation/datastar/adapter/unit/JaxRsRequestAdapterTest.java b/sdk/java/datastar-java-sdk-jaxrs/src/test/java/starfederation/datastar/adapter/unit/JaxRsRequestAdapterTest.java deleted file mode 100644 index d7dd8a399..000000000 --- a/sdk/java/datastar-java-sdk-jaxrs/src/test/java/starfederation/datastar/adapter/unit/JaxRsRequestAdapterTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package starfederation.datastar.adapter.unit; - -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.UriInfo; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import starfederation.datastar.adapters.request.JaxRsRequestAdapter; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.when; - -class JaxRsRequestAdapterTest { - - @Mock - private ContainerRequestContext mockRequestContext; - - @Mock - private UriInfo mockUriInfo; - - private JaxRsRequestAdapter adapter; - private AutoCloseable closeable; - - @BeforeEach - void setUp() { - closeable = MockitoAnnotations.openMocks(this); - - adapter = new JaxRsRequestAdapter(); - adapter.requestContext = mockRequestContext; - - // Mock URI Info - when(mockRequestContext.getUriInfo()).thenReturn(mockUriInfo); - } - - @AfterEach - void tearDown() throws Exception { - closeable.close(); - } - - @Test - void getMethodShouldReturnHttpMethod() { - when(mockRequestContext.getMethod()).thenReturn("POST"); - - String method = adapter.getMethod(); - - assertEquals("POST", method); - } - - @Test - void getReaderShouldReturnBufferedReaderWithUtf8EncodingWhenNoCharsetSpecified() throws Exception { - when(mockRequestContext.getHeaderString(HttpHeaders.CONTENT_TYPE)).thenReturn("application/json"); - InputStream bodyStream = new ByteArrayInputStream("{\"key\":\"value\"}".getBytes(StandardCharsets.UTF_8)); - when(mockRequestContext.getEntityStream()).thenReturn(bodyStream); - - assertNotNull(adapter.getReader()); - assertEquals("{\"key\":\"value\"}", adapter.getReader().readLine()); - } -} diff --git a/sdk/java/pom.xml b/sdk/java/pom.xml deleted file mode 100644 index 14f7fea29..000000000 --- a/sdk/java/pom.xml +++ /dev/null @@ -1,188 +0,0 @@ - - - 4.0.0 - dev.data-star - datastar-java-sdk-suite - 1.0.0 - pom - - Datastar Java SDK Suite - The suite of libraries for the Datastar Java SDK - - - core - datastar-java-sdk-jaxrs - - - - - starfederation - StarFederation Dev Team - no-reply@data-star.dev - StarFederation - https://data-star.dev/ - - - - - - MIT - repo - https://mit-license.org/ - - - - - - github - https://maven.pkg.github.com/starfederation/datastar - - - - - scm:git:git@github.com:starfederation/datastar.git - scm:git:git@github.com:starfederation/datastar.git - https://github.com/starfederation/datastar - - - - 17 - UTF-8 - UTF-8 - false - - 3.2.1 - 3.11.0 - 3.5.2 - 3.7.1 - 3.10.1 - 3.2.7 - - 5.11.4 - 5.14.2 - 2.18.2 - - - - - - release-deploy - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven.gpg.plugin.version} - - - sign-artifacts - verify - - sign - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven.javadoc.plugin.version} - - - attach-javadocs - verify - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - ${maven.source.plugin.version} - - - package - - jar - - - - - - - - - - - - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} - test - - - - - org.mockito - mockito-core - ${mockito.version} - test - - - - - org.mockito - mockito-junit-jupiter - ${mockito.version} - test - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven.compiler.plugin.version} - - ${maven.compiler.release} - - - - org.apache.maven.plugins - maven-dependency-plugin - ${maven.dependency.plugin.version} - - - - properties - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven.surefire.plugin.version} - - - -javaagent:@{org.mockito:mockito-core:jar} - -Xshare:off - - - - - - \ No newline at end of file diff --git a/sdk/php/.gitattributes b/sdk/php/.gitattributes deleted file mode 100644 index 21c2001d5..000000000 --- a/sdk/php/.gitattributes +++ /dev/null @@ -1,11 +0,0 @@ -# Do not export those files in the Composer archive (lighter dependency) -/.gitattributes export-ignore -/.github/ export-ignore -/.gitignore export-ignore -/ecs.php export-ignore -/phpstan.neon export-ignore -/tests/ export-ignore -/SUBTREE.md export-ignore - -# Auto detect text files and perform LF normalization -* text=auto diff --git a/sdk/php/.github/PULL_REQUEST_TEMPLATE.md b/sdk/php/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 07aa49b63..000000000 --- a/sdk/php/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,4 +0,0 @@ -**Please do not submit any pull requests here – they will be closed!** - -This repository is a read-only subset of the Datastar monorepo. Please open your pull request there instead: -https://github.com/starfederation/datastar diff --git a/sdk/php/.gitignore b/sdk/php/.gitignore deleted file mode 100644 index 7acc7aa31..000000000 --- a/sdk/php/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -.idea -composer.lock -/.ddev -/vendor diff --git a/sdk/php/LICENSE.md b/sdk/php/LICENSE.md deleted file mode 100644 index 9bbefb710..000000000 --- a/sdk/php/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -Copyright Β© Star Federation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sdk/php/README.md b/sdk/php/README.md deleted file mode 100644 index 1a2e76524..000000000 --- a/sdk/php/README.md +++ /dev/null @@ -1,73 +0,0 @@ -[![Stable Version](https://img.shields.io/packagist/v/starfederation/datastar-php?label=stable)]((https://packagist.org/packages/starfederation/datastar-php)) -[![Total Downloads](https://img.shields.io/packagist/dt/starfederation/datastar-php)](https://packagist.org/packages/starfederation/datastar-php) - -

- -# Datastar PHP SDK - -This package provides a PHP SDK for working with [Datastar](https://data-star.dev/). - -## License - -This package is licensed for free under the MIT License. - -## Requirements - -This package requires PHP 8.1 or later. - -## Installation - -Install using composer. - -```shell -composer require starfederation/datastar-php -``` - -## Usage - -```php -use starfederation\datastar\enums\EventType; -use starfederation\datastar\enums\ElementPatchMode; -use starfederation\datastar\ServerSentEventGenerator; - -// Creates a new `ServerSentEventGenerator` instance. -$sse = new ServerSentEventGenerator(); - -// Sends the response headers. -// If your framework has its own way of sending response headers, manually send the headers returned by `ServerSentEventGenerator::headers()` instead. -$sse->sendHeaders(); - -// Patches elements into the DOM. -$sse->patchElements('
', [ - 'selector' => '#my-div', - 'mode' => ElementPatchMode::Append, - 'useViewTransition' => true, -]); - -// Patches elements into the DOM. -$sse->removeElements('#my-div', [ - 'useViewTransition' => true, -]); - -// Patches signals. -$sse->patchSignals('{foo: 123}', [ - 'onlyIfMissing' => true, -]); - -// Executes JavaScript in the browser. -$sse->executeScript('console.log("Hello, world!")', [ - 'autoRemove' => true, - 'attributes' => [ - 'type' => 'application/javascript', - ], -]); - -// Redirects the browser by setting the location to the provided URI. -$sse->location('/guide'); -``` - -```php -use starfederation\datastar\ServerSentEventGenerator; - -$signals = ServerSentEventGenerator::readSignals(); -``` diff --git a/sdk/php/composer.json b/sdk/php/composer.json deleted file mode 100644 index b8236703c..000000000 --- a/sdk/php/composer.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "starfederation/datastar-php", - "description": "A PHP SDK for working with Datastar.", - "type": "library", - "license": "mit", - "require": { - "php": ">=8.1" - }, - "require-dev": { - "craftcms/ecs": "dev-main", - "craftcms/phpstan": "dev-main", - "pestphp/pest": "^3.5" - }, - "autoload": { - "psr-4": { - "starfederation\\datastar\\": "src/" - } - }, - "scripts": { - "ecs": "ecs check --ansi --fix", - "phpstan": "phpstan --memory-limit=1G", - "test": "vendor/bin/pest" - }, - "config": { - "optimize-autoloader": true, - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } - }, - "authors": [ - { - "name": "Ben Croker", - "homepage": "https://putyourlightson.com/", - "role": "Author" - } - ], - "support": { - "docs": "https://github.com/starfederation/datastar-php", - "source": "https://github.com/starfederation/datastar-php", - "issues": "https://github.com/starfederation/datastar-php/issues" - } -} \ No newline at end of file diff --git a/sdk/php/ecs.php b/sdk/php/ecs.php deleted file mode 100644 index 2d61b3c13..000000000 --- a/sdk/php/ecs.php +++ /dev/null @@ -1,18 +0,0 @@ -paths([ - __DIR__ . '/src', - __DIR__ . '/tests', - __FILE__, - ]); - $ecsConfig->skip([ - __DIR__ . '/src/Consts.php', - __DIR__ . '/src/enums', - ]); - $ecsConfig->parallel(); - $ecsConfig->sets([SetList::CRAFT_CMS_4]); -}; diff --git a/sdk/php/phpstan.neon b/sdk/php/phpstan.neon deleted file mode 100644 index 91e645304..000000000 --- a/sdk/php/phpstan.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - level: 5 - paths: - - src diff --git a/sdk/php/src/.gitattributes b/sdk/php/src/.gitattributes deleted file mode 100644 index 8e18efe96..000000000 --- a/sdk/php/src/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -Constants.php linguist-generated=true -enums/ElementPatchMode.php linguist-generated=true -enums/EventType.php linguist-generated=true \ No newline at end of file diff --git a/sdk/php/src/Consts.php b/sdk/php/src/Consts.php deleted file mode 100644 index 1cb54368c..000000000 --- a/sdk/php/src/Consts.php +++ /dev/null @@ -1,34 +0,0 @@ -eventType = $eventType; - $this->data = $data; - $this->eventId = $eventId; - $this->retryDuration = $retryDuration; - } -} diff --git a/sdk/php/src/ServerSentEventGenerator.php b/sdk/php/src/ServerSentEventGenerator.php deleted file mode 100644 index fc9a6ce6b..000000000 --- a/sdk/php/src/ServerSentEventGenerator.php +++ /dev/null @@ -1,142 +0,0 @@ - 'no-cache', - 'Content-Type' => 'text/event-stream', - // Disable buffering for Nginx. - // https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering - 'X-Accel-Buffering' => 'no', - ]; - - // Connection-specific headers are only allowed in HTTP/1.1. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection - if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1') { - $headers['Connection'] = 'keep-alive'; - } - - return $headers; - } - - /** - * Returns the signals sent in the incoming request. - */ - public static function readSignals(): array - { - $input = $_GET[Consts::DATASTAR_KEY] ?? file_get_contents('php://input'); - $signals = $input ? json_decode($input, true) : []; - - return is_array($signals) ? $signals : []; - } - - /** - * Constructor. - */ - public function __construct() - { - // Abort the process if the client closes the connection. - ignore_user_abort(false); - } - - /** - * Sends the response headers, if not already sent. - */ - public function sendHeaders(): void - { - if (headers_sent()) { - return; - } - - foreach (static::headers() as $name => $value) { - header("$name: $value"); - } - } - - /** - * Patches HTML elements into the DOM and returns the resulting output. - * - * @param array{ - * selector?: string|null, - * mode?: ElementPatchMode|string|null, - * useViewTransition?: bool|null, - * eventId?: string|null, - * retryDuration?: int|null, - * } $options - */ - public function patchElements(string $elements, array $options = []): string - { - return $this->sendEvent(new PatchElements($elements, $options)); - } - - /** - * Patches signals and returns the resulting output. - */ - public function patchSignals(array|string $signals, array $options = []): string - { - return $this->sendEvent(new PatchSignals($signals, $options)); - } - - /** - * Removes elements from the DOM and returns the resulting output. - * - * @param array{ - * eventId?: string|null, - * retryDuration?: int|null, - * } $options - */ - public function removeElements(string $selector, array $options = []): string - { - return $this->sendEvent(new RemoveElements($selector, $options)); - } - - /** - * Executes JavaScript in the browser and returns the resulting output. - */ - public function executeScript(string $script, array $options = []): string - { - return $this->sendEvent(new ExecuteScript($script, $options)); - } - - /** - * Redirects the browser by setting the location to the provided URI and returns the resulting output. - */ - public function location(string $uri, array $options = []): string - { - return $this->sendEvent(new Location($uri, $options)); - } - - /** - * Sends an event, flushes the output buffer and returns the resulting output. - */ - protected function sendEvent(EventInterface $event): string - { - $output = $event->getOutput(); - echo $output; - - if (ob_get_contents()) { - ob_end_flush(); - } - flush(); - - return $output; - } -} diff --git a/sdk/php/src/enums/ElementPatchMode.php b/sdk/php/src/enums/ElementPatchMode.php deleted file mode 100644 index 63823fdd9..000000000 --- a/sdk/php/src/enums/ElementPatchMode.php +++ /dev/null @@ -1,34 +0,0 @@ -eventId)) { - $options['eventId'] = $this->eventId; - } - - if (!empty($this->retryDuration) && $this->retryDuration != Consts::DEFAULT_SSE_RETRY_DURATION) { - $options['retryDuration'] = $this->retryDuration; - } - - return $options; - } - - /** - * @inerhitdoc - */ - public function getBooleanAsString(bool $value): string - { - return $value ? 'true' : 'false'; - } - - /** - * @inerhitdoc - */ - public function getDataLine(string $datalineLiteral, string|int $value = ''): string - { - return 'data: ' . $datalineLiteral . $value; - } - - /** - * @inerhitdoc - */ - public function getMultiDataLines(string $datalineLiteral, string $data): array - { - $dataLines = []; - $lines = explode("\n", trim($data)); - - foreach ($lines as $line) { - $dataLines[] = $this->getDataLine($datalineLiteral, $line); - } - - return $dataLines; - } - - /** - * @inerhitdoc - */ - public function getOutput(): string - { - $options = $this->getOptions(); - $eventData = new ServerSentEventData( - $this->getEventType(), - $this->getDataLines(), - $options['eventId'] ?? null, - $options['retryDuration'] ?? Consts::DEFAULT_SSE_RETRY_DURATION, - ); - - foreach ($options as $key => $value) { - $eventData->$key = $value; - } - - $output = ['event: ' . $eventData->eventType->value]; - - if ($eventData->eventId !== null) { - $output[] = 'id: ' . $eventData->eventId; - } - - if ($eventData->retryDuration !== Consts::DEFAULT_SSE_RETRY_DURATION) { - $output[] = 'retry: ' . $eventData->retryDuration; - } - - foreach ($eventData->data as $line) { - $output[] = $line; - } - - return implode("\n", $output) . "\n\n"; - } -} diff --git a/sdk/php/src/events/ExecuteScript.php b/sdk/php/src/events/ExecuteScript.php deleted file mode 100644 index 8094b2d7b..000000000 --- a/sdk/php/src/events/ExecuteScript.php +++ /dev/null @@ -1,63 +0,0 @@ -script = $script; - - foreach ($options as $key => $value) { - $this->$key = $value; - } - } - - /** - * @inerhitdoc - */ - public function getEventType(): EventType - { - return EventType::PatchElements; - } - - /** - * @inerhitdoc - */ - public function getDataLines(): array - { - $dataLines = []; - $dataLines[] = $this->getDataLine(Consts::SELECTOR_DATALINE_LITERAL, 'body'); - $dataLines[] = $this->getDataLine(Consts::MODE_DATALINE_LITERAL, ElementPatchMode::Append->value); - - $elements = 'attributes as $key => $value) { - $elements .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; - } - - if ($this->autoRemove) { - $elements .= ' ' . 'data-effect="el.remove()"'; - } - - $elements .= '>' . $this->script . ''; - - return array_merge( - $dataLines, - $this->getMultiDataLines(Consts::ELEMENTS_DATALINE_LITERAL, $elements), - ); - } -} diff --git a/sdk/php/src/events/Location.php b/sdk/php/src/events/Location.php deleted file mode 100644 index d339d9128..000000000 --- a/sdk/php/src/events/Location.php +++ /dev/null @@ -1,18 +0,0 @@ -script = "setTimeout(() => window.location = '$uri')"; - - foreach ($options as $key => $value) { - $this->$key = $value; - } - } -} diff --git a/sdk/php/src/events/PatchElements.php b/sdk/php/src/events/PatchElements.php deleted file mode 100644 index 1812cac66..000000000 --- a/sdk/php/src/events/PatchElements.php +++ /dev/null @@ -1,80 +0,0 @@ -elements = $elements; - - foreach ($options as $key => $value) { - if ($key === 'mode') { - $value = $this->getMode($value); - } - - $this->$key = $value; - } - } - - /** - * @inerhitdoc - */ - public function getEventType(): EventType - { - return EventType::PatchElements; - } - - /** - * @inerhitdoc - */ - public function getDataLines(): array - { - $dataLines = []; - - if (!empty($this->selector)) { - $dataLines[] = $this->getDataLine(Consts::SELECTOR_DATALINE_LITERAL, $this->selector); - } - - if ($this->mode !== Consts::DEFAULT_ELEMENT_PATCH_MODE) { - $dataLines[] = $this->getDataLine(Consts::MODE_DATALINE_LITERAL, $this->mode->value); - } - - if ($this->useViewTransition !== Consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS) { - $dataLines[] = $this->getDataLine(Consts::USE_VIEW_TRANSITION_DATALINE_LITERAL, $this->getBooleanAsString($this->useViewTransition)); - } - - return array_merge( - $dataLines, - $this->getMultiDataLines(Consts::ELEMENTS_DATALINE_LITERAL, $this->elements), - ); - } - - private function getMode(ElementPatchMode|string $value): ElementPatchMode - { - $value = is_string($value) ? ElementPatchMode::tryFrom($value) : $value; - - if ($value === null) { - $enumValues = array_map(fn($case) => '`' . $case->value . '`', ElementPatchMode::cases()); - - throw new Exception('An invalid value was passed into `mode`. The value must be one of: ' . implode(', ', $enumValues) . '.'); - } - - return $value; - } -} diff --git a/sdk/php/src/events/PatchSignals.php b/sdk/php/src/events/PatchSignals.php deleted file mode 100644 index 350621a62..000000000 --- a/sdk/php/src/events/PatchSignals.php +++ /dev/null @@ -1,53 +0,0 @@ -signals = $signals; - - foreach ($options as $key => $value) { - $this->$key = $value; - } - } - - /** - * @inerhitdoc - */ - public function getEventType(): EventType - { - return EventType::PatchSignals; - } - - /** - * @inerhitdoc - */ - public function getDataLines(): array - { - $dataLines = []; - - if ($this->onlyIfMissing !== Consts::DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING) { - $dataLines[] = $this->getDataLine(Consts::ONLY_IF_MISSING_DATALINE_LITERAL, $this->getBooleanAsString($this->onlyIfMissing)); - } - - $data = is_array($this->signals) ? json_encode($this->signals) : $this->signals; - - return array_merge( - $dataLines, - $this->getMultiDataLines(Consts::SIGNALS_DATALINE_LITERAL, $data), - ); - } -} diff --git a/sdk/php/src/events/RemoveElements.php b/sdk/php/src/events/RemoveElements.php deleted file mode 100644 index 89aac3b2d..000000000 --- a/sdk/php/src/events/RemoveElements.php +++ /dev/null @@ -1,52 +0,0 @@ -selector = $selector; - - foreach ($options as $key => $value) { - $this->$key = $value; - } - } - - /** - * @inerhitdoc - */ - public function getEventType(): EventType - { - return EventType::PatchElements; - } - - /** - * @inerhitdoc - */ - public function getDataLines(): array - { - $dataLines = [ - $this->getDataLine(Consts::SELECTOR_DATALINE_LITERAL, $this->selector), - $this->getDataLine(Consts::MODE_DATALINE_LITERAL, ElementPatchMode::Remove->value), - ]; - - if ($this->useViewTransition !== Consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS) { - $dataLines[] = $this->getDataLine(Consts::USE_VIEW_TRANSITION_DATALINE_LITERAL, $this->getBooleanAsString($this->useViewTransition)); - } - - return $dataLines; - } -} diff --git a/sdk/php/tests/README.md b/sdk/php/tests/README.md deleted file mode 100644 index 42c536ebd..000000000 --- a/sdk/php/tests/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Testing - -## Static Analysis - -```shell -composer phpstan -``` - -## Easy Coding Standard - -```shell -composer ecs -``` - -## Unit Tests - -```shell -composer test -``` diff --git a/sdk/php/tests/Unit/PatchElementsTest.php b/sdk/php/tests/Unit/PatchElementsTest.php deleted file mode 100644 index 93f810b66..000000000 --- a/sdk/php/tests/Unit/PatchElementsTest.php +++ /dev/null @@ -1,58 +0,0 @@ -content
'; - $event = new PatchElements($content, [ - 'mode' => $mode, - ]); - expect($event->getDataLines()) - ->toBe([ - 'data: mode append', - 'data: elements ' . $content, - ]); -})->with([ - 'enum' => ElementPatchMode::Append, - 'string' => ElementPatchMode::Append->value, -]); - -test('Options are correctly output', function() { - $content = '
content
'; - $event = new PatchElements($content, [ - 'selector' => 'selector', - 'mode' => ElementPatchMode::Append, - 'useViewTransition' => true, - ]); - expect($event->getDataLines()) - ->toBe([ - 'data: selector selector', - 'data: mode append', - 'data: useViewTransition true', - 'data: elements ' . $content, - ]); -}); - -test('Default options are not output', function() { - $content = '
content
'; - $event = new PatchElements($content, [ - 'selector' => '', - 'mode' => ElementPatchMode::Outer, - 'useViewTransition' => false, - ]); - expect($event->getDataLines()) - ->toBe([ - 'data: elements ' . $content, - ]); -}); - -test('Multi-line content is correctly output', function() { - $content = '
content
'; - $event = new PatchElements("\n" . $content . "\n" . $content . "\n"); - expect($event->getDataLines()) - ->toBe([ - 'data: elements ' . $content, - 'data: elements ' . $content, - ]); -}); diff --git a/sdk/php/tests/Unit/PatchSignalsTest.php b/sdk/php/tests/Unit/PatchSignalsTest.php deleted file mode 100644 index 696f976a3..000000000 --- a/sdk/php/tests/Unit/PatchSignalsTest.php +++ /dev/null @@ -1,45 +0,0 @@ - true, - ]); - expect($event->getDataLines()) - ->toBe([ - 'data: onlyIfMissing true', - 'data: signals {x: 1}', - ]); -}); - -test('Default options are not output', function() { - $content = '{x: 1}'; - $event = new PatchSignals($content, [ - 'onlyIfMissing' => false, - ]); - expect($event->getDataLines()) - ->toBe([ - 'data: signals {x: 1}', - ]); -}); - -test('Multi-line content is correctly output', function() { - $content = '{x: 1}'; - $event = new PatchSignals("\n" . $content . "\n" . $content . "\n"); - expect($event->getDataLines()) - ->toBe([ - 'data: signals {x: 1}', - 'data: signals {x: 1}', - ]); -}); - -test('Signals can be passed in as an array', function() { - $signals = ['x' => 1]; - $event = new PatchSignals($signals); - expect($event->getDataLines()) - ->toBe([ - 'data: signals ' . json_encode($signals), - ]); -}); diff --git a/sdk/python/README.md b/sdk/python/README.md deleted file mode 100644 index 0c0a00f0a..000000000 --- a/sdk/python/README.md +++ /dev/null @@ -1,152 +0,0 @@ -# datastar-py - -The `datastar-py` package provides backend helpers for the [Datastar](https://data-star.dev) JS library. - -Datastar sends responses back to the browser using SSE. This allows the backend to -send any number of events, from zero to infinity in response to a single request. - -`datastar-py` has helpers for creating those responses, formatting the events, -reading signals from the frontend, and generating the data-* HTML attributes. - -The event generator can be used with any framework. There are also custom -helpers included for the following frameworks: - -* [Django](https://www.djangoproject.com/) -* [FastAPI](https://fastapi.tiangolo.com/) -* [FastHTML](https://fastht.ml/) -* [Litestar](https://litestar.dev/) -* [Quart](https://quart.palletsprojects.com/en/stable/) -* [Sanic](https://sanic.dev/en/) -* [Starlette](https://www.starlette.io/) - -## Event Generation Helpers - -To use `datastar-py`, import the SSE generator in your app and then use -it in your route handler: - -```python -from datastar_py import ServerSentEventGenerator as SSE - - -# ... various app setup. -# The example below is for the Quart framework, and is only using the event generation helpers. - -@app.route("/updates") -async def updates(): - async def time_updates(): - while True: - yield SSE.patch_elements( - [f"""{datetime.now().isoformat()}"""] - ) - await asyncio.sleep(1) - yield SSE.patch_signals({"currentTime": f"{datetime.now().isoformat()}"}) - await asyncio.sleep(1) - - response = await make_response(time_updates(), SSE_HEADERS) - response.timeout = None - return response -``` - -## Response Helpers - -A datastar response consists of 0..N datastar events. There are response -classes included to make this easy in all of the supported frameworks. - -The following examples will work across all supported frameworks when the -response class is imported from the appropriate framework package. -e.g. `from datastar_py.quart import DatastarResponse` The containing functions -are not shown here, as they will differ per framework. - -```python -from datastar_py import ServerSentEventGenerator as SSE - -# 0 events, a 204 -return DatastarResponse() -# 1 event -return DatastarResponse(SSE.patch_elements("
")) -# 2 events -return DatastarResponse([ - SSE.patch_elements("
"), - SSE.patch_signals({"mysignal": "myval"}), -]) - -# N events, a long lived stream (for all frameworks but sanic) -async def updates(): - while True: - yield SSE.patch_elements("
") - await asyncio.sleep(1) -return DatastarResponse(updates()) - -# A long lived stream for sanic -response = await datastar_respond(request) -# which is just a helper for the following -# response = await request.respond(DatastarResponse()) -while True: - await response.send(SSE.patch_elements("
")) - await asyncio.sleep(1) -``` - -### Response Decorator -To make returning a `DatastarResponse` simpler, there is a decorator -`datastar_response` available that automatically wraps a function result in -`DatastarResponse`. It works on async and regular functions and generator -functions. The main use case is when using a generator function, as you can -avoid a second generator function inside your response function. The decorator -works the same for any of the supported frameworks, and should be used under -any routing decorator from the framework. - -```python -from datastar_py.sanic import datastar_response, ServerSentEventGenerator as SSE - -@app.get('/my_route') -@datastar_response -def my_route(request): - while True: - yield SSE.patch_elements("
") - await asyncio.sleep(1) -``` - -## Signal Helpers -The current state of the datastar signals is included by default in every -datastar request. A helper is included to load those signals for each -framework. `read_signals` - -```python -from datastar_py.quart import read_signals - -@app.route("/updates") -async def updates(): - signals = await read_signals() -``` - -## Attribute Generation Helper -Datastar allows HTML generation to be done on the backend. datastar-py includes -a helper to generate data-* attributes in your HTML with IDE completion and -type checking. It can be used with many different HTML generation libraries. - -```python -from datastar_py import attribute_generator as data - -# htpy -button(data.on("click", "console.log('clicked')").debounce(1000).stop)["My Button"] -# FastHTML -Button("My Button", **data.on("click", "console.log('clicked')").debounce(1000).stop) -# After next release of FastHTML you don't have to unpack the datastar helpers e.g. -Button("My Button", data.on("click", "console.log('clicked')").debounce(1000).stop) -# f-strings -f"" -# Jinja, but no editor completion :( - -``` - -When using datastar with a different alias, you can instantiate the class yourself. - -```python -from datastar_py.attributes import AttributeGenerator - -data = AttributeGenerator(alias="data-star-") - -# htmy (htmy will transform _ into - unless the attribute starts with _, which will be stripped) -data = AttributeGenerator(alias="_data-") -html.button("My Button", **data.on("click", "console.log('clicked')").debounce("1s").stop) -``` \ No newline at end of file diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml deleted file mode 100644 index 7254ec701..000000000 --- a/sdk/python/pyproject.toml +++ /dev/null @@ -1,74 +0,0 @@ -[project] -name = "datastar-py" -description = "Helper functions and classes for the Datastar library (https://data-star.dev/)" -version = "0.5.0" -readme = "README.md" -authors = [ - { name = "Felix Ingram", email = "f.ingram@gmail.com" }, - { name = "Lucian Knock", email = "git@lucianknock.com" }, - { name = "Chase Sterling", email = "chase.sterling@gmail.com" } -] -requires-python = ">=3.9" -dependencies = [] -license = {text = "MIT"} -keywords = ["datastar", "django", "fastapi", "fasthtml", "flask", "litestar", "quart", "sanic", "starlette", "html"] -classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", -] - -[project.urls] -Documentation = "https://github.com/starfederation/datastar/blob/develop/sdk/python/README.md" -GitHub = "https://github.com/starfederation/datastar" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - "django>=4.2.17", - "fastapi[standard]>=0.115.4", - "python-fasthtml>=0.12.0; python_version > '3.10'", - "flask[async]>=3.0.3", - "quart>=0.19.9", - "sanic>=24.6.0", - "starlette>=0.46.1", - "uvicorn>=0.32.1", - "litestar>=2.15.2", -] - -[tool.ruff] -line-length = 99 -exclude = [ - "src/datastar_py/consts.py", -] -[tool.ruff.lint] -select = [ - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade - "UP", - # flake8-bugbear - "B", - # flake8-simplify - "SIM", - # isort - "I", - # Annotations - "ANN", - # Ruff specific - "RUF", -] -fixable = ["ALL"] diff --git a/sdk/python/sdk-test.py b/sdk/python/sdk-test.py deleted file mode 100644 index 7e22c77c2..000000000 --- a/sdk/python/sdk-test.py +++ /dev/null @@ -1,54 +0,0 @@ -# /// script -# requires-python = ">=3.13" -# dependencies = [ -# "datastar-py", -# "sanic", -# ] -# [tool.uv.sources] -# datastar-py = { path = "." } -# /// - -""" -Runs a test server that the SDK tests can be run against. -1. Start this server with `uv run sdk-test.py` -2. Move to the sdk/tests folder. -3. Run `test-all.sh http://127.0.0.1:8000` to run the tests. -""" - -import re - -from sanic import Request, Sanic - -from datastar_py import ServerSentEventGenerator as SSE -from datastar_py.sanic import DatastarResponse, read_signals -from datastar_py.sse import DatastarEvent - -app = Sanic("datastar-sdk-test") - - -@app.route("/test", methods=["GET", "POST"]) -async def test_route(request: Request) -> None: - signals = await read_signals(request) - events: list[dict] = signals["events"] - - response = await request.respond(response=DatastarResponse()) - - for event in events: - await response.send(build_event(event)) - - -def build_event(input: dict) -> DatastarEvent: - event_type = input.pop("type") - signals_raw = input.pop("signals-raw", None) - kwargs = {camel_to_snake(k): v for k, v in input.items()} - if signals_raw: - kwargs["signals"] = signals_raw - return getattr(SSE, camel_to_snake(event_type))(**kwargs) - - -def camel_to_snake(text: str) -> str: - return re.sub(r"(.)([A-Z])", r"\1_\2", text).lower() - - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000) diff --git a/sdk/python/src/datastar_py/__init__.py b/sdk/python/src/datastar_py/__init__.py deleted file mode 100644 index 70c5f2790..000000000 --- a/sdk/python/src/datastar_py/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import json -from collections.abc import Mapping -from typing import Any - -from .attributes import attribute_generator -from .sse import SSE_HEADERS, ServerSentEventGenerator - -__all__ = ["SSE_HEADERS", "ServerSentEventGenerator", "attribute_generator"] - - -def _read_signals( - method: str, headers: Mapping[str, str], params: Mapping, body: str | bytes -) -> dict[str, Any] | None: - if "Datastar-Request" not in headers: - return None - if method == "GET": - data = params.get("datastar") - elif headers.get("Content-Type") == "application/json": - data = body - else: - return None - return json.loads(data) if data else None diff --git a/sdk/python/src/datastar_py/attributes.py b/sdk/python/src/datastar_py/attributes.py deleted file mode 100644 index 73b155499..000000000 --- a/sdk/python/src/datastar_py/attributes.py +++ /dev/null @@ -1,738 +0,0 @@ -from __future__ import annotations - -import json -import re -from collections.abc import Iterable, Iterator, Mapping -from itertools import chain -from typing import Literal, Self, TypeAlias, TypeVar, Union, overload - -__all__ = ["attribute_generator"] - -JSEvent = Literal[ - "abort", - "afterprint", - "animationend", - "animationiteration", - "animationstart", - "beforeprint", - "beforeunload", - "blur", - "canplay", - "canplaythrough", - "change", - "click", - "contextmenu", - "copy", - "cut", - "dblclick", - "drag", - "dragend", - "dragenter", - "dragleave", - "dragover", - "dragstart", - "drop", - "durationchange", - "ended", - "error", - "focus", - "focusin", - "focusout", - "fullscreenchange", - "fullscreenerror", - "hashchange", - "input", - "invalid", - "keydown", - "keypress", - "keyup", - "load", - "loadeddata", - "loadedmetadata", - "loadstart", - "message", - "mousedown", - "mouseenter", - "mouseleave", - "mousemove", - "mouseover", - "mouseout", - "mouseup", - "mousewheel", - "offline", - "online", - "open", - "pagehide", - "pageshow", - "paste", - "pause", - "play", - "playing", - "popstate", - "progress", - "ratechange", - "resize", - "reset", - "scroll", - "search", - "seeked", - "seeking", - "select", - "show", - "stalled", - "storage", - "submit", - "suspend", - "timeupdate", - "toggle", - "touchcancel", - "touchend", - "touchmove", - "touchstart", - "transitionend", - "unload", - "volumechange", - "waiting", - "wheel", -] - - -SignalValue: TypeAlias = Union[ - str, - int, - float, - bool, - dict[str, "SignalValue"], - list["SignalValue"], - None, -] - - -class AttributeGenerator: - def __init__(self, alias: str = "data-") -> None: - """A helper which can generate all the Datastar attributes. - - :param alias: The prefix for all attributes. Defaults to `data-`. - """ - self._alias: str = alias - - def signals( - self, - signals_dict: Mapping[str, SignalValue] | None = None, - /, - *, - expressions_: bool = False, - **signals: SignalValue, - ) -> SignalsAttr: - """Patch one or more signals into the existing signals. - - :param signals_dict: A dictionary of signals to patch. - :param expressions_: If True, the values of the signals will be evaluated as expressions - rather than literals. - """ - signals = {**(signals_dict if signals_dict else {}), **signals} - val = _js_object(signals) if expressions_ else json.dumps(signals) - return SignalsAttr(value=val, alias=self._alias) - - def computed(self, computed_dict: Mapping | None = None, /, **computed: str) -> BaseAttr: - """Create signals that are computed based on an expression.""" - computed = {**(computed_dict if computed_dict else {}), **computed} - first, *rest = ( - BaseAttr("computed", key=sig, value=expr, alias=self._alias) - for sig, expr in computed.items() - ) - first._other_attrs = rest - return first - - def effect(self, expression: str) -> BaseAttr: - """Execute an expression when any referenced signals change.""" - return BaseAttr("effect", value=expression, alias=self._alias) - - @property - def ignore(self) -> IgnoreAttr: - """Tell Datastar to ignore data-* attributes on the element.""" - return IgnoreAttr(alias=self._alias) - - def attr(self, attr_dict: Mapping | None = None, /, **attrs: str) -> BaseAttr: - """Set the value of any HTML attributes to expressions, and keep them in sync.""" - attrs = {**(attr_dict if attr_dict else {}), **attrs} - return BaseAttr("attr", value=_js_object(attrs), alias=self._alias) - - def bind(self, signal_name: str) -> BaseAttr: - """Set up two-way data binding between a signal and an element's value.""" - return BaseAttr("bind", value=signal_name, alias=self._alias) - - def class_(self, class_dict: Mapping | None = None, /, **classes: str) -> BaseAttr: - """Add or removes classes to or from an element based on expressions.""" - classes = {**(class_dict if class_dict else {}), **classes} - return BaseAttr("class", value=_js_object(classes), alias=self._alias) - - @overload - def on(self, event: Literal["interval"], expression: str) -> OnIntervalAttr: ... - @overload - def on(self, event: Literal["load"], expression: str) -> OnLoadAttr: ... - @overload - def on(self, event: Literal["intersect"], expression: str) -> OnIntersectAttr: ... - @overload - def on(self, event: Literal["raf"], expression: str) -> OnRafAttr: ... - @overload - def on(self, event: Literal["resize"], expression: str) -> OnResizeAttr: ... - @overload - def on(self, event: Literal["signal-patch"], expression: str) -> OnSignalPatchAttr: ... - @overload - def on(self, event: JSEvent | str, expression: str) -> OnAttr: ... - def on( - self, event: str, expression: str - ) -> ( - OnAttr - | OnIntervalAttr - | OnLoadAttr - | OnIntersectAttr - | OnRafAttr - | OnResizeAttr - | OnSignalPatchAttr - ): - """Execute an expression when an event occurs.""" - if event == "interval": - return OnIntervalAttr(value=expression, alias=self._alias) - if event == "load": - return OnLoadAttr(value=expression, alias=self._alias) - if event == "raf": - return OnRafAttr(value=expression, alias=self._alias) - if event == "resize": - return OnResizeAttr(value=expression, alias=self._alias) - if event == "intersect": - return OnIntersectAttr(value=expression, alias=self._alias) - if event == "signal-patch": - return OnSignalPatchAttr(value=expression, alias=self._alias) - return OnAttr(key=event, value=expression, alias=self._alias) - - def on_interval(self, expression: str) -> OnIntervalAttr: - """Execute an expression at a regular interval.""" - return OnIntervalAttr(value=expression, alias=self._alias) - - def on_load(self, expression: str) -> OnLoadAttr: - """Execute an expression when the element is loaded into the DOM.""" - return OnLoadAttr(value=expression, alias=self._alias) - - def on_intersect(self, expression: str) -> OnIntersectAttr: - """Execute an expression when the element intersects with the viewport.""" - return OnIntersectAttr(value=expression, alias=self._alias) - - def on_raf(self, expression: str) -> OnRafAttr: - """(PRO) Execute an expression on every requestAnimationFrame event.""" - return OnRafAttr(value=expression, alias=self._alias) - - def on_signal_patch( - self, expression: str, include: str | None = None, exclude: str | None = None - ) -> OnSignalPatchAttr: - """Execute an expression when a signal patch taxes plase.""" - attr = OnSignalPatchAttr(value=expression, alias=self._alias) - if include or exclude: - attr.filter(include, exclude) - return attr - - def on_resize(self, expression: str) -> OnResizeAttr: - """(PRO) Execute an expression each time the element's dimensions change.""" - return OnResizeAttr(value=expression, alias=self._alias) - - @property - def persist(self) -> PersistAttr: - """(PRO) Persist signals in local storage.""" - return PersistAttr(alias=self._alias) - - def ref(self, signal_name: str) -> BaseAttr: - """Create a signal which references the element on which the attribute is placed.""" - return BaseAttr("ref", value=signal_name, alias=self._alias) - - def replace_url(self, url_expression: str) -> BaseAttr: - """(PRO) Replace the URL in the browser without replacing the page.""" - return BaseAttr("replace-url", value=url_expression, alias=self._alias) - - def show(self, expression: str) -> BaseAttr: - """Show or hides an element based on whether an expression evaluates to true or false.""" - return BaseAttr("show", value=expression, alias=self._alias) - - def text(self, expression: str) -> BaseAttr: - """Bind the text content of an element to an expression.""" - return BaseAttr("text", value=expression, alias=self._alias) - - def indicator(self, signal_name: str) -> BaseAttr: - """Create a signal whose value is true while an SSE request is in flight.""" - return BaseAttr("indicator", value=signal_name, alias=self._alias) - - def custom_validity(self, expression: str) -> BaseAttr: - """(PRO) Set the validity message for an element based on an expression.""" - return BaseAttr("custom-validity", value=expression, alias=self._alias) - - @property - def scroll_into_view(self) -> ScrollIntoViewAttr: - """(PRO) Scrolls the element into view.""" - return ScrollIntoViewAttr(alias=self._alias) - - def view_transition(self, expression: str) -> BaseAttr: - """(PRO) Set the view-transition-name style attribute explicitly.""" - return BaseAttr("view-transition", value=expression, alias=self._alias) - - @property - def json_signals(self) -> BaseAttr: - """Create a signal that contains the JSON representation of the signals.""" - return BaseAttr("json-signals", alias=self._alias) - - @property - def ignore_morph(self) -> BaseAttr: - """Do not overwrite this element or its children when morphing.""" - return BaseAttr("ignore-morph", alias=self._alias) - - def preserve_attr(self, attrs: str | Iterable[str]) -> BaseAttr: - """Preserve the client side state for specified attribute(s) when morphing.""" - value = attrs if isinstance(attrs, str) else " ".join(attrs) - return BaseAttr("preserve-attrs", value=value, alias=self._alias) - - @property - def query_string(self) -> QueryStringAttr: - """(PRO) Sync the query string with signal values.""" - return QueryStringAttr(alias=self._alias) - - -class BaseAttr(Mapping): - _attr: str - - def __init__( - self, - attr: str | None = None, - /, - *, - key: str | None = None, - value: str | Literal[True] = True, - alias: str = "data-", - ) -> None: - if attr: - self._attr: str = attr - self._key: str | None = None - self._mods: dict[str, list[str]] = {} - self._other_attrs: list[BaseAttr] = [] - self._value: str | Literal[True] = value - self._alias: str = alias - if key: - self._to_kebab_key(key) - - def __call__(self) -> Self: - # Because some attributes and modifiers do not need to be called, - # allow calling them anyway so that all attributes allow parens. - return self - - def _full_key(self) -> str: - key = f"{self._alias}{self._attr}" - if self._key: - key += f"-{self._key}" - for mod, values in self._mods.items(): - key += f"__{mod}" - if values: - key += f".{'.'.join(values)}" - return key - - def _to_kebab_key(self, key_name: str) -> None: - if "-" in key_name: - kebab_name, from_case = key_name.lower(), "kebab" - elif "_" in key_name: - kebab_name, from_case = key_name.lower().replace("_", "-"), "snake" - elif key_name[0].isupper(): - kebab_name, from_case = ( - re.sub(r"((? str | Literal[True]: - if key == self._full_key(): - return self._value - for attr in self._other_attrs: - if key == attr._full_key(): - return attr._value - raise KeyError(key) - - def __len__(self) -> int: - return len(self._other_attrs) + 1 - - def __iter__(self) -> Iterator[str]: - return chain((self._full_key(),), *self._other_attrs) - - def __str__(self) -> str: - r = _escape(self._full_key()) - if isinstance(self._value, str): - r += f'="{_escape(self._value)}"' - if self._other_attrs: - other = " ".join(str(o) for o in self._other_attrs) - r += f" {other}" - return r - - __html__ = __str__ - - -TAttr = TypeVar("TAttr", bound=BaseAttr) - - -class TimingMod: - def debounce( - self: TAttr, - wait: int | float | str, - *, - leading: bool = False, - notrail: bool = False, - ) -> TAttr: - """Debounce the event listener. - - :param wait: The minimum interval between events. - :param leading: If True, the event listener will be called on the leading edge of the - wait time. - :param notrail: If True, the event listener will not be called on the trailing edge of the - wait time. - """ - self._mods["debounce"] = [str(wait)] - if leading: - self._mods["debounce"].append("leading") - if notrail: - self._mods["debounce"].append("notrail") - return self - - def throttle( - self: TAttr, - wait: int | float | str, - *, - noleading: bool = False, - trail: bool = False, - ) -> TAttr: - """Throttle the event listener. - - :param wait: The minimum interval between events. - :param noleading: If true, the event listener will not be called on the leading edge of the - wait time. - :param trail: If true, the event listener will be called on the trailing edge of the - wait time. - """ - self._mods["throttle"] = [str(wait)] - if noleading: - self._mods["throttle"].append("noleading") - if trail: - self._mods["throttle"].append("trail") - return self - - -class ViewtransitionMod: - @property - def viewtransition(self: TAttr) -> TAttr: - """Wrap the expression in document.startViewTransition().""" - self._mods["view-transition"] = [] - return self - - -class SignalsAttr(BaseAttr): - _attr = "signals" - - @property - def ifmissing(self) -> Self: - """Only set signals that do not already exist.""" - self._mods["ifmissing"] = [] - return self - - -class IgnoreAttr(BaseAttr): - _attr = "ignore" - - @property - def self(self) -> Self: - """Only ignore the element itself, not its descendants.""" - self._mods["self"] = [] - return self - - -class OnAttr(BaseAttr, TimingMod, ViewtransitionMod): - _attr = "on" - - @property - def once(self) -> Self: - """Only trigger the event listener once.""" - self._mods["once"] = [] - return self - - @property - def passive(self) -> Self: - """Do not call preventDefault on the event listener.""" - self._mods["passive"] = [] - return self - - @property - def capture(self) -> Self: - """Use a capture event listener.""" - self._mods["capture"] = [] - return self - - @property - def window(self) -> Self: - """Attach the event listener to the window element.""" - self._mods["window"] = [] - return self - - @property - def outside(self) -> Self: - """Trigger when the event is outside the element.""" - self._mods["outside"] = [] - return self - - @property - def prevent(self) -> Self: - """Call preventDefault on the event listener.""" - self._mods["prevent"] = [] - return self - - @property - def stop(self) -> Self: - """Call stopPropagation on the event listener.""" - self._mods["stop"] = [] - return self - - @property - def trust(self) -> Self: - """Run even when isTrusted property on the event is false.""" - self._mods["trust"] = [] - return self - - -class PersistAttr(BaseAttr): - _attr = "persist" - - def __call__(self, signal_names: str | Iterable[str] | None = None) -> Self: - if not signal_names: - return self - if isinstance(signal_names, str): - self._value = signal_names - else: - self._value = " ".join(signal_names) - return self - - @property - def session(self) -> Self: - """Persist signals in session storage.""" - self._mods["session"] = [] - return self - - -class JsonSignalsAttr(BaseAttr): - _attr = "json-signals" - - def __call__(self, include: str | None = None, exclude: str | None = None) -> Self: - if include or exclude: - filter_object = {} - if include: - filter_object["include"] = include - if exclude: - filter_object["exclude"] = exclude - self._value = json.dumps(filter_object) - return self - - @property - def terse(self) -> Self: - """Output without extra whitespace.""" - self._mods["terse"] = [] - return self - - -class ScrollIntoViewAttr(BaseAttr): - _attr = "scroll-into-view" - - @property - def smooth(self) -> Self: - """Animate scrolling smoothly.""" - self._mods["smooth"] = [] - return self - - @property - def instant(self) -> Self: - """Scroll instantly.""" - self._mods["instant"] = [] - return self - - @property - def auto(self) -> Self: - """Let scrolling be determined by the computed scroll-behavior CSS property.""" - self._mods["auto"] = [] - return self - - @property - def hstart(self) -> Self: - """Scroll to the left of the element.""" - self._mods["hstart"] = [] - return self - - @property - def hcenter(self) -> Self: - """Scroll to the horizontal center of the element.""" - self._mods["hcenter"] = [] - return self - - @property - def hend(self) -> Self: - """Scroll to the right of the element.""" - self._mods["hend"] = [] - return self - - @property - def hnearest(self) -> Self: - """Scroll to the nearest horizontal edge of the element.""" - self._mods["hnearest"] = [] - return self - - @property - def vstart(self) -> Self: - """Scroll to the top of the element.""" - self._mods["vstart"] = [] - return self - - @property - def vcenter(self) -> Self: - """Scroll to the vertical center of the element.""" - self._mods["vcenter"] = [] - return self - - @property - def vend(self) -> Self: - """Scroll to the bottom of the element.""" - self._mods["vend"] = [] - return self - - @property - def vnearest(self) -> Self: - """Scroll to the nearest vertical edge of the element.""" - self._mods["vnearest"] = [] - return self - - @property - def focus(self) -> Self: - """Focus the element after scrolling.""" - self._mods["focus"] = [] - return self - - -class OnIntersectAttr(BaseAttr, TimingMod, ViewtransitionMod): - @property - def once(self) -> Self: - """Only trigger the event listener once.""" - self._mods["once"] = [] - return self - - @property - def half(self) -> Self: - """Trigger the event listener when half the element enters the viewport.""" - self._mods["half"] = [] - return self - - @property - def full(self) -> Self: - """Trigger the event listener when the full element is visible.""" - self._mods["full"] = [] - return self - - -class OnIntervalAttr(BaseAttr, ViewtransitionMod): - _attr = "on-interval" - - def duration(self, duration: int | float | str, *, leading: bool = False) -> Self: - """Set the interval duration.""" - self._mods["duration"] = [str(duration)] - if leading: - self._mods["duration"].append("leading") - return self - - -class OnLoadAttr(BaseAttr, ViewtransitionMod): - _attr = "on-load" - - def delay(self, delay: int | float | str) -> Self: - """Delay the event listener.""" - self._mods["delay"] = [str(delay)] - return self - - @property - def once(self) -> Self: - """Only trigger the event listener once.""" - self._mods["once"] = [] - return self - - -class OnRafAttr(BaseAttr, TimingMod): - _attr = "on-raf" - - -class OnSignalPatchAttr(BaseAttr, TimingMod): - _attr = "on-signal-patch" - - def filter(self, include: str | None = None, exclude: str | None = None) -> Self: - """Filter the signal patch events.""" - if include or exclude: - filter_object = {} - if include: - filter_object["include"] = include - if exclude: - filter_object["exclude"] = exclude - self._other_attrs = [ - BaseAttr("on-signal-patch-filter", value=json.dumps(filter_object)) - ] - return self - - -class OnResizeAttr(BaseAttr, TimingMod): - _attr = "on-resize" - - -class QueryStringAttr(BaseAttr): - _attr = "query-string" - - def __call__(self, include: str | None = None, exclude: str | None = None) -> Self: - if include or exclude: - filter_object = {} - if include: - filter_object["include"] = include - if exclude: - filter_object["exclude"] = exclude - self._value = json.dumps(filter_object) - return self - - @property - def history(self) -> Self: - self._mods["history"] = [] - return self - - -def _escape(s: str) -> str: - return ( - s.replace("&", "&") - .replace("'", "'") - .replace('"', """) - .replace(">", ">") - .replace("<", "<") - ) - - -def _js_object(obj: dict) -> str: - """Create a JS object where the values are expressions rather than strings.""" - return ( - "{" - + ", ".join( - f"{json.dumps(k)}: {_js_object(v) if isinstance(v, dict) else v}" - for k, v in obj.items() - ) - + "}" - ) - - -attribute_generator = AttributeGenerator() diff --git a/sdk/python/src/datastar_py/consts.py b/sdk/python/src/datastar_py/consts.py deleted file mode 100644 index 56cbdcc43..000000000 --- a/sdk/python/src/datastar_py/consts.py +++ /dev/null @@ -1,90 +0,0 @@ -# This is auto-generated by Datastar. DO NOT EDIT. -from enum import Enum - - -# TODO: Remove this and import builtin when python 3.10 support ends -class StrEnum(str, Enum): - __str__ = str.__str__ - __format__ = str.__format__ - - -# region Enums - - -# region The mode in which an element is patched into the DOM. -class ElementPatchMode(StrEnum): - # Morphs the element into the existing element. - OUTER = "outer" - - # Replaces the inner HTML of the existing element. - INNER = "inner" - - # Removes the existing element. - REMOVE = "remove" - - # Replaces the existing element with the new element. - REPLACE = "replace" - - # Prepends the element inside to the existing element. - PREPEND = "prepend" - - # Appends the element inside the existing element. - APPEND = "append" - - # Inserts the element before the existing element. - BEFORE = "before" - - # Inserts the element after the existing element. - AFTER = "after" - - -# endregion ElementPatchMode - - -# region The type protocol on top of SSE which allows for core pushed based communication between the server and the client. -class EventType(StrEnum): - # An event for patching HTML elements into the DOM. - PATCH_ELEMENTS = "datastar-patch-elements" - - # An event for patching signals. - PATCH_SIGNALS = "datastar-patch-signals" - - -# endregion EventType - - -# endregion Enums - -DATASTAR_KEY = "datastar" -VERSION = "1.0.0-RC.1" - -# region Default durations - -# The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. -DEFAULT_SSE_RETRY_DURATION = 1000 - -# endregion Default durations - -# region Default strings - - -# endregion Default strings - -# region Dataline literals -SELECTOR_DATALINE_LITERAL = "selector" -MODE_DATALINE_LITERAL = "mode" -ELEMENTS_DATALINE_LITERAL = "elements" -USE_VIEW_TRANSITION_DATALINE_LITERAL = "useViewTransition" -SIGNALS_DATALINE_LITERAL = "signals" -ONLY_IF_MISSING_DATALINE_LITERAL = "onlyIfMissing" -# endregion Dataline literals - -# region Default booleans - -# Should elements be patched using the ViewTransition API? -DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS = False - -# Should a given set of signals patch if they are missing? -DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING = False - -# endregion Default booleans \ No newline at end of file diff --git a/sdk/python/src/datastar_py/django.py b/sdk/python/src/datastar_py/django.py deleted file mode 100644 index c1b88fbb8..000000000 --- a/sdk/python/src/datastar_py/django.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Mapping -from functools import wraps -from typing import Any, Callable, ParamSpec - -from django.http import HttpRequest -from django.http import StreamingHttpResponse as _StreamingHttpResponse - -from . import _read_signals -from .sse import SSE_HEADERS, DatastarEvent, DatastarEvents, ServerSentEventGenerator - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ServerSentEventGenerator", - "read_signals", -] - - -class DatastarResponse(_StreamingHttpResponse): - """Respond with 0..N `DatastarEvent`s""" - - default_headers: dict[str, str] = SSE_HEADERS.copy() - - def __init__( - self, - content: DatastarEvents = None, - *, - status: int | None = None, - headers: Mapping[str, str] | None = None, - ) -> None: - if not content: - status = status or 204 - content = tuple() - else: - headers = {**self.default_headers, **(headers or {})} - if isinstance(content, DatastarEvent): - content = (content,) - super().__init__(content, status=status, headers=headers) - - -P = ParamSpec("P") - - -def datastar_response( - func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents], -) -> Callable[P, Awaitable[DatastarResponse]]: - """A decorator which wraps a function result in DatastarResponse. - - Can be used on a sync or async function or generator function. - """ - - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse: - r = func(*args, **kwargs) - if isinstance(r, Awaitable): - return DatastarResponse(await r) - return DatastarResponse(r) - - return wrapper - - -def read_signals(request: HttpRequest) -> dict[str, Any] | None: - return _read_signals(request.method, request.headers, request.GET, request.body) diff --git a/sdk/python/src/datastar_py/fastapi.py b/sdk/python/src/datastar_py/fastapi.py deleted file mode 100644 index 9368f5018..000000000 --- a/sdk/python/src/datastar_py/fastapi.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Annotated, Any, Union - -from fastapi import Depends - -from .sse import SSE_HEADERS, ServerSentEventGenerator -from .starlette import DatastarResponse, datastar_response, read_signals - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ReadSignals", - "ServerSentEventGenerator", - "datastar_response", - "read_signals", -] - - -ReadSignals = Annotated[Union[dict[str, Any], None], Depends(read_signals)] diff --git a/sdk/python/src/datastar_py/fasthtml.py b/sdk/python/src/datastar_py/fasthtml.py deleted file mode 100644 index eb0d24e48..000000000 --- a/sdk/python/src/datastar_py/fasthtml.py +++ /dev/null @@ -1,10 +0,0 @@ -from .sse import SSE_HEADERS, ServerSentEventGenerator -from .starlette import DatastarResponse, datastar_response, read_signals - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ServerSentEventGenerator", - "datastar_response", - "read_signals", -] diff --git a/sdk/python/src/datastar_py/litestar.py b/sdk/python/src/datastar_py/litestar.py deleted file mode 100644 index f9a5b696c..000000000 --- a/sdk/python/src/datastar_py/litestar.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Mapping -from functools import wraps -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ParamSpec, -) - -from litestar.response import Stream - -from . import _read_signals -from .sse import SSE_HEADERS, DatastarEvent, DatastarEvents, ServerSentEventGenerator - -if TYPE_CHECKING: - from litestar import Request - from litestar.background_tasks import BackgroundTask, BackgroundTasks - from litestar.types import ResponseCookies - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ServerSentEventGenerator", - "read_signals", -] - - -class DatastarResponse(Stream): - """Respond with 0..N `DatastarEvent`s""" - - default_headers: dict[str, str] = SSE_HEADERS.copy() - - def __init__( - self, - content: DatastarEvents = None, - *, - background: BackgroundTask | BackgroundTasks | None = None, - cookies: ResponseCookies | None = None, - headers: Mapping[str, str] | None = None, - status_code: int | None = None, - # Enables this to be used as a response_class - **_, # noqa: ANN003 - ) -> None: - if not content: - status_code = status_code or 204 - content = tuple() - else: - headers = {**self.default_headers, **(headers or {})} - if isinstance(content, DatastarEvent): - content = (content,) - super().__init__( - content, - background=background, - cookies=cookies, - headers=headers, - status_code=status_code, - ) - - -P = ParamSpec("P") - - -def datastar_response( - func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents], -) -> Callable[P, Awaitable[DatastarResponse]]: - """A decorator which wraps a function result in DatastarResponse. - - Can be used on a sync or async function or generator function. - """ - - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse: - r = func(*args, **kwargs) - if isinstance(r, Awaitable): - return DatastarResponse(await r) - return DatastarResponse(r) - - wrapper.__annotations__["return"] = "DatastarResponse" - return wrapper - - -async def read_signals(request: Request) -> dict[str, Any] | None: - return _read_signals( - request.method, request.headers, request.query_params, await request.body() - ) diff --git a/sdk/python/src/datastar_py/py.typed b/sdk/python/src/datastar_py/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/sdk/python/src/datastar_py/quart.py b/sdk/python/src/datastar_py/quart.py deleted file mode 100644 index 13e0cab91..000000000 --- a/sdk/python/src/datastar_py/quart.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Mapping -from functools import wraps -from inspect import isasyncgen, isasyncgenfunction, isgenerator -from typing import Any, Callable, ParamSpec - -from quart import Response, copy_current_request_context, request, stream_with_context - -from . import _read_signals -from .sse import SSE_HEADERS, DatastarEvents, ServerSentEventGenerator - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ServerSentEventGenerator", - "read_signals", -] - - -class DatastarResponse(Response): - """Respond with 0..N `DatastarEvent`s""" - - default_headers: dict[str, str] = SSE_HEADERS.copy() - - def __init__( - self, - content: DatastarEvents = None, - status: int | None = None, - headers: Mapping[str, str] | None = None, - ) -> None: - if not content: - status = status or 204 - else: - headers = {**self.default_headers, **(headers or {})} - super().__init__(content, status=status, headers=headers) - if isgenerator(content) or isasyncgen(content): - self.timeout = None - - -P = ParamSpec("P") - - -def datastar_response( - func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents], -) -> Callable[P, Awaitable[DatastarResponse]]: - """A decorator which wraps a function result in DatastarResponse. - - Can be used on a sync or async function or generator function. - """ - - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse: - if isasyncgenfunction(func): - return DatastarResponse(stream_with_context(func)(*args, **kwargs)) - return DatastarResponse(await copy_current_request_context(func)(*args, **kwargs)) - - wrapper.__annotations__["return"] = "DatastarResponse" - return wrapper - - -async def read_signals() -> dict[str, Any] | None: - return _read_signals(request.method, request.headers, request.args, await request.get_data()) diff --git a/sdk/python/src/datastar_py/sanic.py b/sdk/python/src/datastar_py/sanic.py deleted file mode 100644 index 970066bc0..000000000 --- a/sdk/python/src/datastar_py/sanic.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Collection, Mapping -from functools import wraps -from inspect import isasyncgen, isgenerator -from typing import Any, Callable, ParamSpec - -from sanic import HTTPResponse, Request - -from . import _read_signals -from .sse import SSE_HEADERS, DatastarEvent, DatastarEvents, ServerSentEventGenerator - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ServerSentEventGenerator", - "datastar_respond", - "read_signals", -] - - -class DatastarResponse(HTTPResponse): - default_headers: dict[str, str] = SSE_HEADERS.copy() - - def __init__( - self, - content: DatastarEvent | Collection[DatastarEvent] | None = None, - status: int | None = None, - headers: Mapping[str, str] | None = None, - ) -> None: - if not content: - status = status or 204 - super().__init__( - content, status=status or 200, headers={**self.default_headers, **(headers or {})} - ) - - async def send( - self, - event: DatastarEvent | None = None, - end_stream: bool | None = None, - ) -> None: - if event and self.status == 204: - # When the response is created with no content, it's set to a 204 by default - # if we end up streaming to it, change the status code to 200 before sending. - self.status = 200 - await super().send(event, end_stream=end_stream) - - -async def datastar_respond( - request: Request, *, status: int = 200, headers: Mapping[str, str] | None = None -) -> DatastarResponse: - return await request.respond(DatastarResponse(status=status, headers=headers)) - - -P = ParamSpec("P") - - -def datastar_response( - func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents], -) -> Callable[P, Awaitable[DatastarResponse | None]]: - """A decorator which wraps a function result in DatastarResponse. - - Can be used on a sync or async function or generator function. - """ - - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse | None: - r = func(*args, **kwargs) - if isinstance(r, Awaitable): - return DatastarResponse(await r) - if isasyncgen(r): - request = args[0] - response = await request.respond(response=DatastarResponse()) - async for event in r: - await response.send(event) - await response.eof() - return None - if isgenerator(r): - request = args[0] - response = await request.respond(response=DatastarResponse()) - for event in r: - await response.send(event) - await response.eof() - return None - return DatastarResponse(r) - - wrapper.__annotations__["return"] = "DatastarResponse" - return wrapper - - -async def read_signals(request: Request) -> dict[str, Any] | None: - return _read_signals(request.method, request.headers, request.args, request.body) diff --git a/sdk/python/src/datastar_py/sse.py b/sdk/python/src/datastar_py/sse.py deleted file mode 100644 index 84668451d..000000000 --- a/sdk/python/src/datastar_py/sse.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import annotations - -import json -from collections.abc import AsyncIterable, Iterable, Mapping -from itertools import chain -from typing import Literal, Protocol, TypeAlias, Union, overload, runtime_checkable - -import datastar_py.consts as consts -from datastar_py.attributes import _escape - -SSE_HEADERS: dict[str, str] = { - "Cache-Control": "no-cache", - "Content-Type": "text/event-stream", - "X-Accel-Buffering": "no", -} - - -@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 DatastarEvent(str): - pass - - -# 0..N datastar events -DatastarEvents: TypeAlias = Union[ - DatastarEvent, Iterable[DatastarEvent], AsyncIterable[DatastarEvent], None -] - - -class ServerSentEventGenerator: - __slots__ = () - - @classmethod - def _send( - cls, - event_type: consts.EventType, - data_lines: list[str], - event_id: str | None = None, - retry_duration: int | None = None, - ) -> DatastarEvent: - prefix = [f"event: {event_type}"] - - if event_id: - prefix.append(f"id: {event_id}") - - if retry_duration and retry_duration != consts.DEFAULT_SSE_RETRY_DURATION: - prefix.append(f"retry: {retry_duration}") - - data_lines = [f"data: {line}" for line in data_lines] - - return DatastarEvent("\n".join(chain(prefix, data_lines)) + "\n\n") - - @overload - @classmethod - def patch_elements( - cls, - *, - selector: str, - mode: Literal[consts.ElementPatchMode.REMOVE], - use_view_transitions: bool | None = None, - event_id: str | None = None, - retry_duration: int | None = None, - ) -> DatastarEvent: ... - @overload - @classmethod - def patch_elements( - cls, - elements: str | _HtmlProvider, - selector: str | None = None, - mode: consts.ElementPatchMode | None = None, - use_view_transitions: bool | None = None, - event_id: str | None = None, - retry_duration: int | None = None, - ) -> DatastarEvent: ... - @classmethod - def patch_elements( - cls, - elements: str | _HtmlProvider | None = None, - selector: str | None = None, - mode: consts.ElementPatchMode | None = None, - use_view_transition: bool | None = None, - event_id: str | None = None, - retry_duration: int | None = None, - ) -> DatastarEvent: - if isinstance(elements, _HtmlProvider): - elements = elements.__html__() - data_lines = [] - if mode and mode != "outer": # TODO: Should there be a constant for this? - data_lines.append(f"{consts.MODE_DATALINE_LITERAL} {mode}") - if selector: - data_lines.append(f"{consts.SELECTOR_DATALINE_LITERAL} {selector}") - if ( - use_view_transition is not None - and use_view_transition != consts.DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS - ): - data_lines.append( - f"{consts.USE_VIEW_TRANSITION_DATALINE_LITERAL} {_js_bool(use_view_transition)}" - ) - - if elements: - data_lines.extend( - f"{consts.ELEMENTS_DATALINE_LITERAL} {x}" for x in elements.splitlines() - ) - - return ServerSentEventGenerator._send( - consts.EventType.PATCH_ELEMENTS, - data_lines, - event_id, - retry_duration, - ) - - @classmethod - def remove_elements( - cls, selector: str, event_id: str | None = None, retry_duration: int | None = None - ) -> DatastarEvent: - return ServerSentEventGenerator.patch_elements( - selector=selector, - mode=consts.ElementPatchMode.REMOVE, - event_id=event_id, - retry_duration=retry_duration, - ) - - @classmethod - def patch_signals( - cls, - signals: dict | str, - event_id: str | None = None, - only_if_missing: bool | None = None, - retry_duration: int | None = None, - ) -> DatastarEvent: - data_lines = [] - if ( - only_if_missing is not None - and only_if_missing != consts.DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING - ): - data_lines.append( - f"{consts.ONLY_IF_MISSING_DATALINE_LITERAL} {_js_bool(only_if_missing)}" - ) - - signals_str = ( - signals if isinstance(signals, str) else json.dumps(signals, separators=(",", ":")) - ) - data_lines.extend( - f"{consts.SIGNALS_DATALINE_LITERAL} {line}" for line in signals_str.splitlines() - ) - - return ServerSentEventGenerator._send( - consts.EventType.PATCH_SIGNALS, data_lines, event_id, retry_duration - ) - - @classmethod - def execute_script( - cls, - script: str, - auto_remove: bool = True, - attributes: Mapping[str, str] | list[str] | None = None, - event_id: str | None = None, - retry_duration: int | None = None, - ) -> DatastarEvent: - attribute_string = "" - if auto_remove: - attribute_string += ' data-effect="el.remove()"' - if attributes: - if isinstance(attributes, Mapping): - attribute_string += " " + " ".join( - f'{_escape(k)}="{_escape(v)}"' for k, v in attributes.items() - ) - else: - attribute_string += " " + " ".join(attributes) - script_tag = f"{script}" - - return ServerSentEventGenerator.patch_elements( - script_tag, - mode=consts.ElementPatchMode.APPEND, - selector="body", - event_id=event_id, - retry_duration=retry_duration, - ) - - @classmethod - def redirect(cls, location: str) -> DatastarEvent: - return cls.execute_script(f"setTimeout(() => window.location = '{location}')") - - -def _js_bool(b: bool) -> str: - return "true" if b else "false" diff --git a/sdk/python/src/datastar_py/starlette.py b/sdk/python/src/datastar_py/starlette.py deleted file mode 100644 index 6fc3a4bc1..000000000 --- a/sdk/python/src/datastar_py/starlette.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Mapping -from functools import wraps -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ParamSpec, -) - -from starlette.requests import Request -from starlette.responses import StreamingResponse as _StreamingResponse - -from . import _read_signals -from .sse import SSE_HEADERS, DatastarEvent, DatastarEvents, ServerSentEventGenerator - -if TYPE_CHECKING: - from starlette.background import BackgroundTask - -__all__ = [ - "SSE_HEADERS", - "DatastarResponse", - "ServerSentEventGenerator", - "read_signals", -] - - -class DatastarResponse(_StreamingResponse): - """Respond with 0..N `DatastarEvent`s""" - - default_headers: dict[str, str] = SSE_HEADERS.copy() - - def __init__( - self, - content: DatastarEvents = None, - status_code: int | None = None, - headers: Mapping[str, str] | None = None, - background: BackgroundTask | None = None, - ) -> None: - if not content: - status_code = status_code or 204 - content = tuple() - else: - status_code = status_code or 200 - headers = {**self.default_headers, **(headers or {})} - if isinstance(content, DatastarEvent): - content = (content,) - super().__init__(content, status_code=status_code, headers=headers, background=background) - - -P = ParamSpec("P") - - -def datastar_response( - func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents], -) -> Callable[P, Awaitable[DatastarResponse]]: - """A decorator which wraps a function result in DatastarResponse. - - Can be used on a sync or async function or generator function. - """ - - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse: - r = func(*args, **kwargs) - if isinstance(r, Awaitable): - return DatastarResponse(await r) - return DatastarResponse(r) - - wrapper.__annotations__["return"] = "DatastarResponse" - return wrapper - - -async def read_signals(request: Request) -> dict[str, Any] | None: - return _read_signals( - request.method, request.headers, request.query_params, await request.body() - ) diff --git a/sdk/ruby/.gitignore b/sdk/ruby/.gitignore deleted file mode 100644 index b04a8c840..000000000 --- a/sdk/ruby/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -/.bundle/ -/.yardoc -/_yardoc/ -/coverage/ -/doc/ -/pkg/ -/spec/reports/ -/tmp/ - -# rspec failure tracking -.rspec_status diff --git a/sdk/ruby/.rspec b/sdk/ruby/.rspec deleted file mode 100644 index 34c5164d9..000000000 --- a/sdk/ruby/.rspec +++ /dev/null @@ -1,3 +0,0 @@ ---format documentation ---color ---require spec_helper diff --git a/sdk/ruby/Gemfile b/sdk/ruby/Gemfile deleted file mode 100644 index 90c02e23e..000000000 --- a/sdk/ruby/Gemfile +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' - -# Specify your gem's dependencies in datastar.gemspec -gemspec - -gem 'rake', '~> 13.0' - -gem 'rspec', '~> 3.0' - -gem 'debug' -gem 'logger' - -group :test do - # Async to test Datastar::AsyncExecutor - gem 'async' - # Puma to host test server - gem 'puma' -end diff --git a/sdk/ruby/Gemfile.lock b/sdk/ruby/Gemfile.lock deleted file mode 100644 index 03bb42483..000000000 --- a/sdk/ruby/Gemfile.lock +++ /dev/null @@ -1,85 +0,0 @@ -PATH - remote: . - specs: - datastar (1.0.0.pre.1) - json - logger - rack (>= 3.1.14) - -GEM - remote: https://rubygems.org/ - specs: - async (2.21.3) - console (~> 1.29) - fiber-annotation - io-event (~> 1.7) - metrics (~> 0.12) - traces (~> 0.15) - console (1.29.2) - fiber-annotation - fiber-local (~> 1.1) - json - date (3.4.1) - debug (1.10.0) - irb (~> 1.10) - reline (>= 0.3.8) - diff-lcs (1.5.1) - fiber-annotation (0.2.0) - fiber-local (1.1.0) - fiber-storage - fiber-storage (1.0.0) - io-console (0.8.0) - io-event (1.7.5) - irb (1.15.1) - pp (>= 0.6.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - json (2.9.1) - logger (1.7.0) - metrics (0.12.1) - nio4r (2.7.4) - pp (0.6.2) - prettyprint - prettyprint (0.2.0) - psych (5.2.3) - date - stringio - puma (6.6.0) - nio4r (~> 2.0) - rack (3.1.14) - rake (13.2.1) - rdoc (6.11.0) - psych (>= 4.0.0) - reline (0.6.0) - io-console (~> 0.5) - rspec (3.13.0) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) - rspec-core (3.13.2) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-support (3.13.2) - stringio (3.1.2) - traces (0.15.2) - -PLATFORMS - arm64-darwin-24 - ruby - -DEPENDENCIES - async - datastar! - debug - logger - puma - rake (~> 13.0) - rspec (~> 3.0) - -BUNDLED WITH - 2.5.23 diff --git a/sdk/ruby/LICENSE.md b/sdk/ruby/LICENSE.md deleted file mode 100644 index 0bbd9ab61..000000000 --- a/sdk/ruby/LICENSE.md +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) Ismael Celis - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/sdk/ruby/README.md b/sdk/ruby/README.md deleted file mode 100644 index 547316f9a..000000000 --- a/sdk/ruby/README.md +++ /dev/null @@ -1,374 +0,0 @@ -# Datastar Ruby SDK - -Implement the [Datastart SSE procotocol](https://data-star.dev/reference/sse_events) in Ruby. It can be used in any Rack handler, and Rails controllers. - -## Installation - -Add this gem to your `Gemfile` - -```bash -gem 'datastar' -``` - -Or point your `Gemfile` to the source - -```bash -gem 'datastar', git: 'https://github.com/starfederation/datastar', glob: 'sdk/ruby/*.gemspec' -``` - -## Usage - -### Initialize the Datastar dispatcher - -In your Rack handler or Rails controller: - -```ruby -#Β Rails controllers, as well as Sinatra and others, -# already have request and response objects. -# `view_context` is optional and is used to render Rails templates. -# Or view components that need access to helpers, routes, or any other context. - -datastar = Datastar.new(request:, response:, view_context:) - -# In a Rack handler, you can instantiate from the Rack env -datastar = Datastar.from_rack_env(env) -``` - -### Sending updates to the browser - -There are two ways to use this gem in HTTP handlers: - -* One-off responses, where you want to send a single update down to the browser. -* Streaming responses, where you want to send multiple updates down to the browser. - -#### One-off update: - -```ruby -datastar.patch_elements(%(

Hello, World!

)) -``` -In this mode, the response is closed after the fragment is sent. - -#### Streaming updates - -```ruby -datastar.stream do |sse| - sse.patch_elements(%(

Hello, World!

)) - # Streaming multiple updates - 100.times do |i| - sleep 1 - sse.patch_elements(%(

Hello, World #{i}!

)) - end -end -``` -In this mode, the response is kept open until `stream` blocks have finished. - -#### Concurrent streaming blocks - -Multiple `stream` blocks will be launched in threads/fibers, and will run concurrently. -Their updates are linearized and sent to the browser as they are produced. - -```ruby -# Stream to the browser from two concurrent threads -datastar.stream do |sse| - 100.times do |i| - sleep 1 - sse.patch_elements(%(

#{i}!

)) - end -end - -datastar.stream do |sse| - 1000.times do |i| - sleep 0.1 - sse.patch_elements(%(

#{i}!

)) - end -end -``` - -See the [examples](https://github.com/starfederation/datastar/tree/main/examples/ruby) directory. - -### Datastar methods - -All these methods are available in both the one-off and the streaming modes. - -#### `patch_elements` -See https://data-star.dev/reference/sse_events#datastar-patch-elements - -```ruby -sse.patch_elements(%(
\nhello\n
)) - -# or a Phlex view object -sse.patch_elements(UserComponent.new) - -# Or pass options -sse.patch_elements( - %(
\nhello\n
), - mode: 'append' -) -``` - -You can patch multiple elements at once by passing an array of elements (or components): - -```ruby -sse.patch_elements([ - %(
\nhello\n
), - %(
\nworld\n
) -]) -``` - -#### `remove_elements` - -Sugar on top of `#patch_elements` - See https://data-star.dev/reference/sse_events#datastar-patch-elements - -```ruby -sse.remove_elements('#users') -``` - -#### `patch_signals` - See https://data-star.dev/reference/sse_events#datastar-patch-signals - -```ruby -sse.patch_signals(count: 4, user: { name: 'John' }) -``` - -#### `remove_signals` - -Sugar on top of `#patch_signals` - -```ruby -sse.remove_signals(['user.name', 'user.email']) -``` - -#### `execute_script` - -Sugar on top of `#patch_elements`. Appends a temporary `" - - options[Consts::SELECTOR_DATALINE_LITERAL] = 'body' - options[Consts::MODE_DATALINE_LITERAL] = Consts::ElementPatchMode::APPEND - - patch_elements(script_tag, options) - end - - def redirect(url) - execute_script %(setTimeout(() => { window.location = '#{url}' })) - end - - def write(buffer) - buffer << MSG_END - @stream << buffer - end - - private - - attr_reader :view_context, :stream - - # Support Phlex components - # And Rails' #render_in interface - def render_element(element) - if element.respond_to?(:render_in) - element.render_in(view_context) - elsif element.respond_to?(:call) - element.call(view_context:) - else - element - end - end - - def build_options(options, buffer) - options.each do |k, v| - k = camelize(k) - if (sse_key = SSE_OPTION_MAPPING[k]) - default_value = OPTION_DEFAULTS[sse_key] - buffer << "#{sse_key}: #{v}\n" unless v == default_value - elsif v.is_a?(Hash) - v.each do |kk, vv| - buffer << "data: #{k} #{kk} #{vv}\n" - end - elsif v.is_a?(Array) - if k == Consts::SELECTOR_DATALINE_LITERAL - buffer << "data: #{k} #{v.join(', ')}\n" - else - buffer << "data: #{k} #{v.join(' ')}\n" - end - else - default_value = OPTION_DEFAULTS[k] - buffer << "data: #{k} #{v}\n" unless v == default_value - end - end - end - - def camelize_keys(options) - options.each.with_object({}) do |(key, value), acc| - value = camelize_keys(value) if value.is_a?(Hash) - acc[camelize(key)] = value - end - end - - def camelize(str) - str.to_s.split('_').map.with_index { |word, i| i == 0 ? word : word.capitalize }.join - end - - # Take a string, split it by newlines, - # and write each line as a separate data line - def multi_data_lines(data, buffer, key) - lines = data.to_s.split("\n") - lines.each do |line| - buffer << "data: #{key} #{line}\n" - end - end - - def set_nested_value(hash, path, value) - # Navigate to the parent hash using all but the last segment - parent = path[0...-1].reduce(hash) do |current_hash, key| - current_hash[key] ||= {} - end - - # Set the final key to the value - parent[path.last] = value - end - end -end diff --git a/sdk/ruby/lib/datastar/version.rb b/sdk/ruby/lib/datastar/version.rb deleted file mode 100644 index 4bb542aca..000000000 --- a/sdk/ruby/lib/datastar/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module Datastar - VERSION = '1.0.0.pre.1' -end diff --git a/sdk/ruby/sig/datastar.rbs b/sdk/ruby/sig/datastar.rbs deleted file mode 100644 index f6630627f..000000000 --- a/sdk/ruby/sig/datastar.rbs +++ /dev/null @@ -1,4 +0,0 @@ -module Datastar - VERSION: String - # See the writing guide of rbs: https://github.com/ruby/rbs#guides -end diff --git a/sdk/ruby/spec/dispatcher_spec.rb b/sdk/ruby/spec/dispatcher_spec.rb deleted file mode 100644 index ed8780349..000000000 --- a/sdk/ruby/spec/dispatcher_spec.rb +++ /dev/null @@ -1,579 +0,0 @@ -# frozen_string_literal: true - -class TestSocket - attr_reader :lines, :open - def initialize - @lines = [] - @open = true - end - - def <<(line) - @lines << line - end - - def close = @open = false - - def split_lines - @lines.join.split("\n") - end -end - -RSpec.describe Datastar::Dispatcher do - include DispatcherExamples - - subject(:dispatcher) { Datastar.new(request:, response:, view_context:) } - - let(:request) { build_request('/events') } - let(:response) { Rack::Response.new(nil, 200) } - let(:view_context) { double('View context') } - - describe '#initialize' do - it 'sets Content-Type to text/event-stream' do - expect(dispatcher.response['Content-Type']).to eq('text/event-stream') - end - - it 'sets Cache-Control to no-cache' do - expect(dispatcher.response['Cache-Control']).to eq('no-cache') - end - - it 'sets Connection to keep-alive' do - expect(dispatcher.response['Connection']).to eq('keep-alive') - end - - it 'sets X-Accel-Buffering: no for NGinx and other proxies' do - expect(dispatcher.response['X-Accel-Buffering']).to eq('no') - end - - it 'does not set Connection header if not HTTP/1.1' do - request.env['SERVER_PROTOCOL'] = 'HTTP/2.0' - expect(dispatcher.response['Connection']).to be_nil - end - end - - specify '.from_rack_env' do - dispatcher = Datastar.from_rack_env(request.env) - - expect(dispatcher.response['Content-Type']).to eq('text/event-stream') - expect(dispatcher.response['Cache-Control']).to eq('no-cache') - expect(dispatcher.response['Connection']).to eq('keep-alive') - end - - specify '#sse?' do - expect(dispatcher.sse?).to be(true) - - request = build_request('/events', headers: { 'HTTP_ACCEPT' => 'application/json' }) - dispatcher = Datastar.new(request:, response:, view_context:) - expect(dispatcher.sse?).to be(false) - - request = build_request('/events', headers: { 'HTTP_ACCEPT' => 'text/event-stream,application/json' }) - dispatcher = Datastar.new(request:, response:, view_context:) - expect(dispatcher.sse?).to be(true) - end - - describe '#patch_elements' do - it 'produces a streameable response body with D* elements' do - dispatcher.patch_elements %(
\nhello\n
\n) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq(["event: datastar-patch-elements\ndata: elements
\ndata: elements hello\ndata: elements
\n\n"]) - end - - it 'takes D* options' do - dispatcher.patch_elements( - %(
\nhello\n
), - id: 72, - retry_duration: 2000, - use_view_transition: true, - ) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\nid: 72\nretry: 2000\ndata: useViewTransition true\ndata: elements
\ndata: elements hello\ndata: elements
\n\n)]) - end - - it 'omits retry if using default value' do - dispatcher.patch_elements( - %(
\nhello\n
\n), - id: 72, - retry_duration: 1000 - ) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\nid: 72\ndata: elements
\ndata: elements hello\ndata: elements
\n\n)]) - end - - it 'works with #call(view_context:) interfaces' do - template_class = Class.new do - def self.call(view_context:) = %(
\n#{view_context}\n
\n) - end - - dispatcher.patch_elements( - template_class, - id: 72, - retry_duration: 2000 - ) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.lines).to eq([%(event: datastar-patch-elements\nid: 72\nretry: 2000\ndata: elements
\ndata: elements #{view_context}\ndata: elements
\n\n)]) - end - - it 'works with #render_in(view_context, &) interfaces' do - template_class = Class.new do - def self.render_in(view_context) = %(
\n#{view_context}\n
\n) - end - - dispatcher.patch_elements( - template_class, - id: 72, - retry_duration: 2000 - ) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.lines).to eq([%(event: datastar-patch-elements\nid: 72\nretry: 2000\ndata: elements
\ndata: elements #{view_context}\ndata: elements
\n\n)]) - end - - it 'accepts an array of elements' do - dispatcher.patch_elements([ - %(
Hello
), - %(
Bye
) - ]) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq(["event: datastar-patch-elements\ndata: elements
Hello
\ndata: elements
Bye
\n\n"]) - end - end - - describe '#remove_elements' do - it 'produces D* patch elements with "remove" mode' do - dispatcher.remove_elements('#list-item-1') - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\ndata: mode remove\ndata: selector #list-item-1\n\n)]) - end - - it 'takes D* options' do - dispatcher.remove_elements('#list-item-1', id: 72) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\nid: 72\ndata: mode remove\ndata: selector #list-item-1\n\n)]) - end - - it 'takes an array of selectors' do - dispatcher.remove_elements(%w[#item1 #item2]) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\ndata: mode remove\ndata: selector #item1, #item2\n\n)]) - end - end - - describe '#patch_signals' do - it 'produces a streameable response body with D* signals' do - dispatcher.patch_signals %({ "foo": "bar" }) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-signals\ndata: signals { "foo": "bar" }\n\n)]) - end - - it 'takes a Hash of signals' do - dispatcher.patch_signals(foo: 'bar') - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-signals\ndata: signals {"foo":"bar"}\n\n)]) - end - - it 'takes D* options' do - dispatcher.patch_signals({foo: 'bar'}, event_id: 72, retry_duration: 2000, only_if_missing: true) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-signals\nid: 72\nretry: 2000\ndata: onlyIfMissing true\ndata: signals {"foo":"bar"}\n\n)]) - end - - it 'takes a (JSON encoded) string as signals' do - signals = <<~JSON - { - "foo": "bar", - "age": 42 - } - JSON - dispatcher.patch_signals(signals) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.split_lines).to eq([ - %(event: datastar-patch-signals), - %(data: signals {), - %(data: signals "foo": "bar",), - %(data: signals "age": 42), - %(data: signals }), - ]) - end - end - - describe '#remove_signals' do - it 'sets signal values to null via #patch_signals' do - dispatcher.remove_signals ['user.name', 'user.email'] - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-signals\ndata: signals {"user":{"name":null,"email":null}}\n\n)]) - end - - it 'takes D* options' do - dispatcher.remove_signals 'user.name', event_id: 72, retry_duration: 2000 - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-signals\nid: 72\nretry: 2000\ndata: signals {"user":{"name":null}}\n\n)]) - end - end - - describe '#execute_script' do - it 'appends a \n\n)]) - end - - it 'takes D* options' do - dispatcher.execute_script %(alert('hello')), event_id: 72, auto_remove: false - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\nid: 72\ndata: selector body\ndata: mode append\ndata: elements \n\n)]) - end - - it 'takes attributes Hash' do - dispatcher.execute_script %(alert('hello')), attributes: { type: 'text/javascript', title: 'alert' } - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n)]) - end - - it 'accepts camelized string options' do - dispatcher.execute_script( - %(console.log('hello');), - 'eventId' => 'event1', - 'retryDuration' => 2000, - 'attributes' => { - 'type' => 'text/javascript', - 'blocking' => false - }, - 'autoRemove' => false - ) - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.split_lines).to eq([ - %(event: datastar-patch-elements), - %(id: event1), - %(retry: 2000), - %(data: selector body), - %(data: mode append), - %(data: elements ) - ]) - end - end - - describe '#redirect' do - it 'sends an execute_script event with a window.location change' do - dispatcher.redirect '/guide' - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines).to eq([%(event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n)]) - end - end - - describe '#signals' do - context 'with POST request' do - specify 'Rails parsed parameters' do - request = build_request( - '/events', - method: 'POST', - headers: { - 'action_dispatch.request.request_parameters' => { 'event' => { 'foo' => 'bar' } } - } - ) - - dispatcher = Datastar.new(request:, response:) - expect(dispatcher.signals).to eq({ 'foo' => 'bar' }) - end - - specify 'no signals in Rails parameters' do - request = build_request( - '/events', - method: 'POST', - headers: { - 'action_dispatch.request.request_parameters' => {} - } - ) - - dispatcher = Datastar.new(request:, response:) - expect(dispatcher.signals).to eq({}) - end - - specify 'JSON request with signals in body' do - request = build_request( - '/events', - method: 'POST', - content_type: 'application/json', - body: %({ "foo": "bar" }) - ) - - dispatcher = Datastar.new(request:, response:) - expect(dispatcher.signals).to eq({ 'foo' => 'bar' }) - end - - specify 'multipart form request' do - request = build_request( - '/events', - method: 'POST', - content_type: 'multipart/form-data', - body: 'user[name]=joe&user[email]=joe@email.com' - ) - - dispatcher = Datastar.new(request:, response:) - expect(dispatcher.signals).to eq('user' => { 'name' => 'joe', 'email' => 'joe@email.com' }) - end - end - - context 'with GET request' do - specify 'with signals in ?datastar=[JSON signals]' do - query = %({"foo":"bar"}) - request = build_request( - %(/events?datastar=#{URI.encode_uri_component(query)}), - method: 'GET', - ) - - dispatcher = Datastar.new(request:, response:) - expect(dispatcher.signals).to eq('foo' => 'bar') - end - - specify 'with no signals' do - request = build_request( - %(/events), - method: 'GET', - ) - - dispatcher = Datastar.new(request:, response:) - expect(dispatcher.signals).to eq({}) - end - end - end - - describe '#stream' do - it 'writes multiple events to socket' do - socket = TestSocket.new - dispatcher.on_error do |ex| - raise ex - end - dispatcher.stream do |sse| - sse.patch_elements %(
\nhello\n
) - sse.patch_signals(foo: 'bar') - end - - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines.size).to eq(2) - expect(socket.lines[0]).to eq("event: datastar-patch-elements\ndata: elements
\ndata: elements hello\ndata: elements
\n\n") - expect(socket.lines[1]).to eq("event: datastar-patch-signals\ndata: signals {\"foo\":\"bar\"}\n\n") - end - - it 'returns a Rack array response' do - status, headers, _body = dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - expect(status).to eq(200) - expect(headers['content-type']).to eq('text/event-stream') - expect(headers['cache-control']).to eq('no-cache') - expect(headers['connection']).to eq('keep-alive') - end - - context 'with multiple streams' do - let(:executor) { Datastar.config.executor } - - describe 'default thread-based executor' do - it_behaves_like 'a dispatcher handling concurrent streams' - end - - describe 'Async-based executor' do - around do |example| - Sync do - example.run - end - end - - let(:executor) { Datastar::AsyncExecutor.new } - it_behaves_like 'a dispatcher handling concurrent streams' - end - end - - specify ':heartbeat enabled' do - dispatcher = Datastar.new(request:, response:, heartbeat: 0.001) - connected = true - block_called = false - dispatcher.on_client_disconnect { |conn| connected = false } - - socket = TestSocket.new - allow(socket).to receive(:<<).with("\n").and_raise(Errno::EPIPE, 'Socket closed') - - dispatcher.stream do |sse| - sleep 10 - block_called = true - end - - dispatcher.response.body.call(socket) - expect(connected).to be(false) - expect(block_called).to be(false) - end - - specify ':heartbeat disabled' do - dispatcher = Datastar.new(request:, response:, heartbeat: false) - connected = true - block_called = false - dispatcher.on_client_disconnect { |conn| connected = false } - - socket = TestSocket.new - allow(socket).to receive(:<<).with("\n").and_raise(Errno::EPIPE, 'Socket closed') - - dispatcher.stream do |sse| - sleep 0.001 - block_called = true - end - - dispatcher.response.body.call(socket) - expect(connected).to be(true) - expect(block_called).to be(true) - end - - specify '#signals' do - request = build_request( - %(/events), - method: 'POST', - content_type: 'multipart/form-data', - body: 'user[name]=joe&user[email]=joe@email.com' - ) - - dispatcher = Datastar.new(request:, response:) - signals = nil - - dispatcher.stream do |sse| - signals = sse.signals - end - socket = TestSocket.new - dispatcher.response.body.call(socket) - - expect(signals['user']['name']).to eq('joe') - end - - specify '#on_connect' do - connected = false - dispatcher.on_connect { |conn| connected = true } - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(connected).to be(true) - end - - specify '#on_client_disconnect' do - events = [] - dispatcher - .on_connect { |conn| events << true } - .on_client_disconnect { |conn| events << false } - - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - socket = TestSocket.new - allow(socket).to receive(:<<).and_raise(Errno::EPIPE, 'Socket closed') - - dispatcher.response.body.call(socket) - expect(events).to eq([true, false]) - end - - specify '#check_connection triggers #on_client_disconnect' do - events = [] - dispatcher - .on_connect { |conn| events << true } - .on_client_disconnect { |conn| events << false } - - dispatcher.stream do |sse| - sse.check_connection! - end - socket = TestSocket.new - allow(socket).to receive(:<<).with("\n").and_raise(Errno::EPIPE, 'Socket closed') - - dispatcher.response.body.call(socket) - expect(events).to eq([true, false]) - end - - specify '#on_server_disconnect' do - events = [] - dispatcher - .on_connect { |conn| events << true } - .on_server_disconnect { |conn| events << false } - - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - socket = TestSocket.new - - dispatcher.response.body.call(socket) - expect(events).to eq([true, false]) - end - - specify '#on_error' do - allow(Datastar.config.logger).to receive(:error) - errors = [] - dispatcher.on_error { |ex| errors << ex } - - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - socket = TestSocket.new - allow(socket).to receive(:<<).and_raise(ArgumentError, 'Invalid argument') - - dispatcher.response.body.call(socket) - expect(errors.first).to be_a(ArgumentError) - expect(Datastar.config.logger).to have_received(:error).with(/ArgumentError \(Invalid argument\):/) - end - - specify 'with global on_error' do - errs = [] - Datastar.config.on_error { |ex| errs << ex } - socket = TestSocket.new - allow(socket).to receive(:<<).and_raise(ArgumentError, 'Invalid argument') - - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - dispatcher.response.body.call(socket) - expect(errs.first).to be_a(ArgumentError) - end - end - - private - - def build_request(path, method: 'GET', body: nil, content_type: 'application/json', accept: 'text/event-stream', headers: {}) - headers = { - 'HTTP_ACCEPT' => accept, - 'CONTENT_TYPE' => content_type, - 'REQUEST_METHOD' => method, - Rack::RACK_INPUT => body ? StringIO.new(body) : nil - }.merge(headers) - - Rack::Request.new(Rack::MockRequest.env_for(path, headers)) - end -end diff --git a/sdk/ruby/spec/spec_helper.rb b/sdk/ruby/spec/spec_helper.rb deleted file mode 100644 index e6d18e0f5..000000000 --- a/sdk/ruby/spec/spec_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'datastar' -require 'rack' -require 'datastar/async_executor' -require 'debug' -require_relative './support/dispatcher_examples' - -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' - - # Disable RSpec exposing methods globally on `Module` and `main` - config.disable_monkey_patching! - - config.expect_with :rspec do |c| - c.syntax = :expect - end -end diff --git a/sdk/ruby/spec/support/dispatcher_examples.rb b/sdk/ruby/spec/support/dispatcher_examples.rb deleted file mode 100644 index b5bc13a72..000000000 --- a/sdk/ruby/spec/support/dispatcher_examples.rb +++ /dev/null @@ -1,53 +0,0 @@ -module DispatcherExamples - RSpec.shared_examples 'a dispatcher handling concurrent streams' do - it 'spawns multiple streams in threads, triggering callbacks only once' do - disconnects = [] - - dispatcher = Datastar - .new(request:, response:, executor:) - .on_server_disconnect { |_| disconnects << true } - .on_error { |err| puts err.backtrace.join("\n") } - - dispatcher.stream do |sse| - sleep 0.01 - sse.patch_elements %(
\nhello\n
\n) - end - - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(socket.open).to be(false) - expect(socket.lines.size).to eq(2) - expect(socket.lines[0]).to eq("event: datastar-patch-signals\ndata: signals {\"foo\":\"bar\"}\n\n") - expect(socket.lines[1]).to eq("event: datastar-patch-elements\ndata: elements
\ndata: elements hello\ndata: elements
\n\n") - expect(disconnects).to eq([true]) - end - - it 'catches exceptions raised from threads' do - Thread.report_on_exception = false - errs = [] - - dispatcher = Datastar - .new(request:, response:, executor:) - .on_error { |err| errs << err } - - dispatcher.stream do |sse| - sleep 0.01 - raise ArgumentError, 'Invalid argument' - end - - dispatcher.stream do |sse| - sse.patch_signals(foo: 'bar') - end - - socket = TestSocket.new - dispatcher.response.body.call(socket) - expect(errs.first).to be_a(ArgumentError) - Thread.report_on_exception = true - end - end -end - diff --git a/sdk/rust/.gitignore b/sdk/rust/.gitignore deleted file mode 100644 index f2f9e58ec..000000000 --- a/sdk/rust/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -Cargo.lock \ No newline at end of file diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml deleted file mode 100644 index 5e29ac6dd..000000000 --- a/sdk/rust/Cargo.toml +++ /dev/null @@ -1,97 +0,0 @@ -[package] -authors = [ - "Johnathan Stevers ", - "Glen Henri J. De Cauwsemaecker ", -] -categories = ["web-programming"] -description = "Datastar SDK for Rust" -documentation = "https://docs.rs/datastar" -edition = "2024" -homepage = "https://data-star.dev" -keywords = ["datastar", "backend", "axum", "rocket"] -license = "MIT OR Apache-2.0" -name = "datastar" -readme = "README.md" -repository = "https://github.com/starfederation/datastar" -version = "0.3.0" -rust-version = "1.85.0" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[features] -axum = ["dep:axum", "dep:serde", "dep:serde_json"] -http2 = [] -rocket = ["dep:rocket"] -tracing = ["dep:tracing"] - -[dependencies] -axum = { version = "0.8.4", default-features = false, optional = true, features = [ - "query", - "tokio", - "json", -] } -rocket = { version = "0.5.1", default-features = false, optional = true } -serde = { version = "1", default-features = false, optional = true, features = [ - "derive", -] } -serde_json = { version = "1", default-features = false, optional = true, features = [ - "std", -] } -tracing = { version = "0.1.41", optional = true } - -[dev-dependencies] -async-stream = { version = "0.3.6", default-features = false } -axum = { version = "0.8.4" } -reqwest = { version = "0.12.22", features = ["json", "stream"] } -rocket = { version = "0.5.1", features = ["json"] } -serde = { version = "1", default-features = false, features = ["derive"] } -serde_json = { version = "1", default-features = false, features = ["std"] } -tokio = { version = "1.46.1", features = ["full"] } -tokio-stream = { version = "0.1.17" } -tracing = { version = "0.1.41" } -tracing-test = { version = "0.2.5" } - -[lints.rust] -unreachable_pub = "deny" -elided_lifetimes_in_paths = "allow" -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } - -[lints.clippy] -all = { level = "warn", priority = -1 } -todo = "warn" -empty_enum = "warn" -enum_glob_use = "warn" -mem_forget = "warn" -unused_self = "warn" -filter_map_next = "warn" -needless_continue = "warn" -needless_borrow = "warn" -match_wildcard_for_single_variants = "warn" -if_let_mutex = "warn" -await_holding_lock = "warn" -imprecise_flops = "warn" -suboptimal_flops = "warn" -lossy_float_literal = "warn" -rest_pat_in_fully_bound_structs = "warn" -fn_params_excessive_bools = "warn" -exit = "warn" -inefficient_to_string = "warn" -linkedlist = "warn" -macro_use_imports = "warn" -option_option = "warn" -verbose_file_reads = "warn" -unnested_or_patterns = "warn" -str_to_string = "warn" -type_complexity = "allow" - -[profile.dev] -opt-level = 1 - -[profile.dev.package."*"] -opt-level = 3 - -[profile.release] -codegen-units = 1 -lto = "thin" diff --git a/sdk/rust/Makefile b/sdk/rust/Makefile deleted file mode 100644 index 68d37bbbd..000000000 --- a/sdk/rust/Makefile +++ /dev/null @@ -1,59 +0,0 @@ -.PHONY: all - -all: - @echo "Usage:" - @echo "fmt - run the rust formatter" - @echo "sort - sort TOML dependencies" - @echo "lint - combine fmt+sort" - @echo "check - shallow check of rust code (pre-compile)" - @echo "clippy - run clippy checks" - @echo "doc - doc checks" - @echo "hack - test feature matrix compatibility" - @echo "test - run all unit and doc tests" - @echo "qa - combine lint+check+clippy+doc+hack+test" - @echo "detect-unused-deps - detect unused deps for removal" - @echo "hello-axum - run hello-world example using the Axum framework" - @echo "test-suite-axum - run test-suite example runner using the Axum framework" - @echo "hello-rocket - run hello-world example using the Rocket framework" -.PHONY: - -fmt: - cargo fmt --all - -sort: - cargo sort --grouped - -lint: fmt sort - -check: - cargo check --all-targets --all-features - -clippy: - cargo clippy --all-targets --all-features - -doc: - RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links" cargo doc --all-features --no-deps - -hack: - cargo hack check --each-feature --no-dev-deps --workspace - -test: - cargo test --all-features --workspace - -qa: lint check clippy doc test - cd ../../examples/rust/axum/hello-world && make qa - cd ../../examples/rust/axum/test-suite && make qa - cd ../../examples/rust/rocket/hello-world && make qa - -detect-unused-deps: - # https://github.com/bnjbvr/cargo-machete - cargo machete --skip-target-dir - -hello-axum: - cd ../../examples/rust/axum/hello-world && make run - -test-suite-axum: - cd ../../examples/rust/axum/test-suite && make run - -hello-rocket: - cd ../../examples/rust/rocket/hello-world && make run diff --git a/sdk/rust/README.md b/sdk/rust/README.md deleted file mode 100644 index e4551d017..000000000 --- a/sdk/rust/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Datastar Rust SDK - -An implementation of the [Datastar] SDK in Rust -with framework integration for [Axum] and [Rocket]. - -[Rama](https://github.com/plabayo/rama) has its own SDK implementation defined as a [Rama module for Datastar](https://ramaproxy.org/docs/rama/http/sse/datastar/index.html) as can be seen in action in [this example](https://github.com/plabayo/rama/blob/main/examples/http_sse_datastar_hello.rs). - -# Usage - -Examples for the Rust sdk can be found in [`../../examples/rust`](../../examples/rust), where -you find examples that you can run youself for the supported -frameworks [Axum] and [Rocket]. - -[Datastar]: https://data-star.dev -[Axum]: https://github.com/tokio-rs/axum -[Rocket]: https://github.com/rwf2/rocket diff --git a/sdk/rust/src/axum.rs b/sdk/rust/src/axum.rs deleted file mode 100644 index fe3071d9f..000000000 --- a/sdk/rust/src/axum.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Axum integration for Datastar. - -use { - crate::{ - consts::{self, DATASTAR_REQ_HEADER_STR}, - prelude::{DatastarEvent, PatchElements, PatchSignals}, - }, - axum::{ - Json, - body::Bytes, - extract::{FromRequest, OptionalFromRequest, Query, Request}, - http::{self}, - response::{IntoResponse, Response, sse::Event}, - }, - serde::{Deserialize, de::DeserializeOwned}, - std::fmt::Write, -}; - -impl PatchElements { - /// Write this [`PatchElements`] into an Axum SSE [`Event`]. - pub fn write_as_axum_sse_event(&self) -> Event { - self.as_datastar_event().write_as_axum_sse_event() - } -} - -impl PatchSignals { - /// Write this [`PatchSignals`] into an Axum SSE [`Event`]. - pub fn write_as_axum_sse_event(&self) -> Event { - self.as_datastar_event().write_as_axum_sse_event() - } -} - -impl DatastarEvent { - /// Turn this [`DatastarEvent`] into an Axum SSE [`Event`]. - pub fn write_as_axum_sse_event(&self) -> Event { - let event = Event::default().event(self.event.as_str()); - - let event = if self.retry.as_millis() != (consts::DEFAULT_SSE_RETRY_DURATION as u128) { - event.retry(self.retry) - } else { - event - }; - - let event = match self.id.as_deref() { - Some(id) => event.id(id), - None => event, - }; - - let mut data = String::with_capacity( - (self.data.iter().map(|s| s.len()).sum::() + self.data.len()).saturating_sub(1), - ); - - let mut sep = ""; - for line in self.data.iter() { - // Assumption: std::fmt::write does not fail ever for [`String`]. - let _ = write!(&mut data, "{sep}{line}"); - sep = "\n"; - } - - event.data(data) - } -} - -#[derive(Deserialize)] -struct DatastarParam { - datastar: serde_json::Value, -} - -/// [`ReadSignals`] is a request extractor that reads datastar signals from the request. -/// -/// # Examples -/// -/// ``` -/// use datastar::axum::ReadSignals; -/// use serde::Deserialize; -/// -/// #[derive(Deserialize)] -/// struct Signals { -/// foo: String, -/// bar: i32, -/// } -/// -/// async fn handler(ReadSignals(signals): ReadSignals) { -/// println!("foo: {}", signals.foo); -/// println!("bar: {}", signals.bar); -/// } -/// -/// ``` -#[derive(Debug)] -pub struct ReadSignals(pub T); - -impl OptionalFromRequest for ReadSignals -where - Bytes: FromRequest, -{ - type Rejection = Response; - - async fn from_request(req: Request, state: &S) -> Result, Self::Rejection> { - if req.headers().get(DATASTAR_REQ_HEADER_STR).is_none() { - return Ok(None); - } - Ok(Some( - >::from_request(req, state).await?, - )) - } -} - -impl FromRequest for ReadSignals -where - Bytes: FromRequest, -{ - type Rejection = Response; - - async fn from_request(req: Request, state: &S) -> Result { - let json = match *req.method() { - http::Method::GET => { - let query = Query::::from_request(req, state) - .await - .map_err(IntoResponse::into_response)?; - - let signals = query.0.datastar.as_str().ok_or( - (http::StatusCode::BAD_REQUEST, "Failed to parse JSON str").into_response(), - )?; - - serde_json::from_str(signals).map_err( - #[cfg_attr(not(feature = "tracing"), expect(unused_variables))] - |err| { - #[cfg(feature = "tracing")] - tracing::debug!(%err, "failed to parse JSON value"); - - ( - http::StatusCode::BAD_REQUEST, - "Failed to parse JSON value from query", - ) - .into_response() - }, - ) - } - _ => { - let Json(json) = as FromRequest>::from_request(req, state) - .await - .map_err( - #[cfg_attr(not(feature = "tracing"), expect(unused_variables))] - |err| { - #[cfg(feature = "tracing")] - tracing::debug!(%err, "failed to parse JSON value from payload"); - - ( - http::StatusCode::BAD_REQUEST, - "Failed to parse JSON value from payload", - ) - .into_response() - }, - )?; - Ok(json) - } - }?; - Ok(Self(json)) - } -} diff --git a/sdk/rust/src/consts.rs b/sdk/rust/src/consts.rs deleted file mode 100644 index ad194b052..000000000 --- a/sdk/rust/src/consts.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Datastar constants -#![allow(rustdoc::invalid_html_tags)] - -// This is auto-generated by Datastar. DO NOT EDIT. - -#[expect(unused)] -pub(crate) const DATASTAR_KEY: &str = "datastar"; -#[allow(unused)] -pub(crate) const DATASTAR_REQ_HEADER_STR: &str = "datastar-request"; -#[expect(unused)] -pub(crate) const VERSION: &str = "1.0.0-RC.1"; - -// #region Defaults - -// #region Default durations - -/// The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. -pub const DEFAULT_SSE_RETRY_DURATION: u64 = 1000; - -// #endregion - -// #region Default strings - -// #endregion - -// #region Datalines - -pub(crate) const SELECTOR_DATALINE_LITERAL: &str = "selector"; -pub(crate) const MODE_DATALINE_LITERAL: &str = "mode"; -pub(crate) const ELEMENTS_DATALINE_LITERAL: &str = "elements"; -pub(crate) const USE_VIEW_TRANSITION_DATALINE_LITERAL: &str = "useViewTransition"; -pub(crate) const SIGNALS_DATALINE_LITERAL: &str = "signals"; -pub(crate) const ONLY_IF_MISSING_DATALINE_LITERAL: &str = "onlyIfMissing"; - -// #endregion - -// #region Default booleans - -/// Should elements be patched using the ViewTransition API? -pub(crate) const DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS: bool = false; -/// Should a given set of signals patch if they are missing? -pub(crate) const DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING: bool = false; - -// #endregion - -// #region Enums - -/// The mode in which an element is patched into the DOM. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ElementPatchMode { - /// Morphs the element into the existing element. - #[default] - Outer, - /// Replaces the inner HTML of the existing element. - Inner, - /// Removes the existing element. - Remove, - /// Replaces the existing element with the new element. - Replace, - /// Prepends the element inside to the existing element. - Prepend, - /// Appends the element inside the existing element. - Append, - /// Inserts the element before the existing element. - Before, - /// Inserts the element after the existing element. - After, -} - -impl ElementPatchMode { - /// Returns the [`ElementPatchMode`] as a string. - pub(crate) const fn as_str(&self) -> &str { - match self { - Self::Outer => "outer", - Self::Inner => "inner", - Self::Remove => "remove", - Self::Replace => "replace", - Self::Prepend => "prepend", - Self::Append => "append", - Self::Before => "before", - Self::After => "after", - } - } -} -/// The type protocol on top of SSE which allows for core pushed based communication between the server and the client. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum EventType { - /// An event for patching HTML elements into the DOM. - PatchElements, - /// An event for patching signals. - PatchSignals, -} - -impl EventType { - /// Returns the [`EventType`] as a string. - pub(crate) const fn as_str(&self) -> &str { - match self { - Self::PatchElements => "datastar-patch-elements", - Self::PatchSignals => "datastar-patch-signals", - } - } -} -// #endregion - -// #endregion diff --git a/sdk/rust/src/execute_script.rs b/sdk/rust/src/execute_script.rs deleted file mode 100644 index 2e0e4d4b6..000000000 --- a/sdk/rust/src/execute_script.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! [`ExecuteScript`] executes JavaScript in the browser. -//! -//! This is sugar for `PatchElements` specifically for executing scripts. - -use { - crate::{ - DatastarEvent, - consts::{self, ElementPatchMode}, - }, - core::time::Duration, -}; - -/// [`ExecuteScript`] executes JavaScript in the browser -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExecuteScript { - /// `id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see - pub id: Option, - /// `retry` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see - pub retry: Duration, - /// `script` is a string that represents the JavaScript to be executed by the browser. - pub script: String, - /// Whether to remove the script after execution, if not provided the Datastar client side will default to `true`. - pub auto_remove: Option, - /// A list of attributes to add to the script element, if not provided the Datastar client side will default to `type="module"`. - /// Each item in the array ***must*** be properly formatted. - pub attributes: Vec, -} - -impl ExecuteScript { - /// Creates a new [`ExecuteScript`] event with the given script. - pub fn new(script: impl Into) -> Self { - Self { - id: None, - retry: Duration::from_millis(consts::DEFAULT_SSE_RETRY_DURATION), - script: script.into(), - auto_remove: Default::default(), - attributes: Default::default(), - } - } - - /// Sets the `id` of the [`ExecuteScript`] event. - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - /// Sets the `retry` of the [`ExecuteScript`] event. - pub fn retry(mut self, retry: Duration) -> Self { - self.retry = retry; - self - } - - /// Sets the `script` of the [`ExecuteScript`] event. - pub fn auto_remove(mut self, auto_remove: bool) -> Self { - self.auto_remove = Some(auto_remove); - self - } - - /// Sets the `attribute` of the [`ExecuteScript`] event. - pub fn attributes(mut self, attributes: impl IntoIterator>) -> Self { - self.attributes = attributes.into_iter().map(Into::into).collect(); - self - } - - /// Converts this [`ExecuteScript`] into a [`DatastarEvent`]. - #[inline] - pub fn into_datastar_event(mut self) -> DatastarEvent { - let id = self.id.take(); - self.convert_to_datastar_event_inner(id) - } - - /// Copy this [`ExecuteScript`] as a [`DatastarEvent`]. - #[inline] - pub fn as_datastar_event(&self) -> DatastarEvent { - self.convert_to_datastar_event_inner(self.id.clone()) - } - - fn convert_to_datastar_event_inner(&self, id: Option) -> DatastarEvent { - let mut data: Vec = Vec::new(); - - data.push(format!("{} body", consts::SELECTOR_DATALINE_LITERAL)); - - data.push(format!( - "{} {}", - consts::MODE_DATALINE_LITERAL, - ElementPatchMode::Append.as_str(), - )); - - let mut s = format!("{} '); - s.push_str(scripts_lines.next().unwrap_or_default()); - data.push(s); - - for line in scripts_lines { - data.push(format!("{} {}", consts::ELEMENTS_DATALINE_LITERAL, line)); - } - - data.last_mut().unwrap().push_str(""); - - DatastarEvent { - event: consts::EventType::PatchElements, - id, - retry: self.retry, - data, - } - } -} - -impl From<&ExecuteScript> for DatastarEvent { - #[inline] - fn from(val: &ExecuteScript) -> Self { - val.as_datastar_event() - } -} - -impl From for DatastarEvent { - #[inline] - fn from(val: ExecuteScript) -> Self { - val.into_datastar_event() - } -} diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs deleted file mode 100644 index 3570dc028..000000000 --- a/sdk/rust/src/lib.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Datastar is a Rust implementation of the [Datastar](https://data-star.dev) SDK specification. - -#![forbid(missing_docs)] -#![forbid(missing_debug_implementations)] - -#[cfg(feature = "axum")] -pub mod axum; -#[cfg(feature = "rocket")] -pub mod rocket; - -pub mod execute_script; -pub mod patch_elements; -pub mod patch_signals; - -#[doc = include_str!("../README.md")] -#[cfg(doctest)] -#[expect(unused)] -struct ReadmeDoctests; - -pub mod consts; - -/// The prelude for the `datastar` crate -pub mod prelude { - pub use crate::{ - DatastarEvent, consts::ElementPatchMode, execute_script::ExecuteScript, - patch_elements::PatchElements, patch_signals::PatchSignals, - }; -} - -use core::{fmt::Display, time::Duration}; - -/// [`DatastarEvent`] is a struct that represents a generic Datastar event. -/// All Datastar events implement `Into`. -#[derive(Debug)] -pub struct DatastarEvent { - /// `event` is the type of event. - pub event: consts::EventType, - /// `id` is can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see - pub id: Option, - /// `retry` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see - pub retry: Duration, - /// `data` is the data that is sent with the event. - pub data: Vec, -} - -impl Display for DatastarEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "event: {}", self.event.as_str())?; - - if let Some(id) = &self.id { - write!(f, "\nid: {id}")?; - } - - let millis = self.retry.as_millis(); - if millis != consts::DEFAULT_SSE_RETRY_DURATION as u128 { - write!(f, "\nretry: {millis}")?; - } - - for line in &self.data { - write!(f, "\ndata: {line}")?; - } - - write!(f, "\n\n")?; - - Ok(()) - } -} diff --git a/sdk/rust/src/patch_elements.rs b/sdk/rust/src/patch_elements.rs deleted file mode 100644 index c56b959a2..000000000 --- a/sdk/rust/src/patch_elements.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! [`PatchElements`] patches HTML elements into the DOM. - -use { - crate::{ - DatastarEvent, - consts::{self, ElementPatchMode}, - }, - core::time::Duration, -}; - -/// [`PatchElements`] patches HTML elements into the DOM. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct PatchElements { - /// `id` is can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see - pub id: Option, - /// `retry` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see - pub retry: Duration, - /// The HTML elements to patched into the DOM. - /// - /// In case of [`ElementPatchMode::Remove`] this attribute will be `None`. - pub elements: Option, - /// The CSS selector to use to patch the elements. - /// If not provided, Datastar will default to using the id attribute of the elements. - pub selector: Option, - /// The mode to use when patching the element into the DOM. - /// If not provided the Datastar client side will default to [`ElementPatchMode::Outer`]. - pub mode: ElementPatchMode, - /// Whether to use view transitions, if not provided the Datastar client side will default to `false`. - pub use_view_transition: bool, -} - -impl PatchElements { - /// Creates a new [`PatchElements`] event with the given elements. - pub fn new(elements: impl Into) -> Self { - Self { - id: None, - retry: Duration::from_millis(consts::DEFAULT_SSE_RETRY_DURATION), - elements: Some(elements.into()), - selector: None, - mode: ElementPatchMode::default(), - use_view_transition: consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS, - } - } - - /// Creates a new [`PatchElements`] to delete the elements for the given selector. - pub fn new_remove(selector: impl Into) -> Self { - Self { - id: None, - retry: Duration::from_millis(consts::DEFAULT_SSE_RETRY_DURATION), - elements: None, - selector: Some(selector.into()), - mode: ElementPatchMode::Remove, - use_view_transition: consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS, - } - } - - /// Sets the `id` of the [`PatchElements`] event. - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - /// Sets the `retry` of the [`PatchElements`] event. - pub fn retry(mut self, retry: Duration) -> Self { - self.retry = retry; - self - } - - /// Sets the `selector` of the [`PatchElements`] event. - pub fn selector(mut self, selector: impl Into) -> Self { - self.selector = Some(selector.into()); - self - } - - /// Sets the `mode` of the [`PatchElements`] event. - pub fn mode(mut self, mode: ElementPatchMode) -> Self { - self.mode = mode; - self - } - - /// Sets the `use_view_transition` of the [`PatchElements`] event. - pub fn use_view_transition(mut self, use_view_transition: bool) -> Self { - self.use_view_transition = use_view_transition; - self - } - - /// Converts this [`PatchElements`] into a [`DatastarEvent`]. - #[inline] - pub fn into_datastar_event(mut self) -> DatastarEvent { - let id = self.id.take(); - self.convert_to_datastar_event_inner(id) - } - - /// Copy this [`PatchElements`] as a [`DatastarEvent`]. - #[inline] - pub fn as_datastar_event(&self) -> DatastarEvent { - self.convert_to_datastar_event_inner(self.id.clone()) - } - - fn convert_to_datastar_event_inner(&self, id: Option) -> DatastarEvent { - let mut data: Vec = Vec::new(); - - if let Some(selector) = &self.selector { - data.push(format!( - "{} {}", - consts::SELECTOR_DATALINE_LITERAL, - selector - )); - } - - if self.mode != ElementPatchMode::default() { - data.push(format!( - "{} {}", - consts::MODE_DATALINE_LITERAL, - self.mode.as_str() - )); - } - - if self.use_view_transition != consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS { - data.push(format!( - "{} {}", - consts::USE_VIEW_TRANSITION_DATALINE_LITERAL, - self.use_view_transition - )); - } - - if let Some(ref elements) = self.elements { - for line in elements.lines() { - data.push(format!("{} {}", consts::ELEMENTS_DATALINE_LITERAL, line)); - } - } - - DatastarEvent { - event: consts::EventType::PatchElements, - id, - retry: self.retry, - data, - } - } -} - -impl From<&PatchElements> for DatastarEvent { - #[inline] - fn from(val: &PatchElements) -> Self { - val.as_datastar_event() - } -} - -impl From for DatastarEvent { - #[inline] - fn from(val: PatchElements) -> Self { - val.into_datastar_event() - } -} diff --git a/sdk/rust/src/patch_signals.rs b/sdk/rust/src/patch_signals.rs deleted file mode 100644 index 0d7c6124b..000000000 --- a/sdk/rust/src/patch_signals.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! [`PatchSignals`] patches signals into the signal store. - -use { - crate::{DatastarEvent, consts}, - core::time::Duration, -}; - -/// [`PatchSignals`] patches signals into the signal store. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct PatchSignals { - /// `id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see - pub id: Option, - /// `retry` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see - pub retry: Duration, - /// `signals` is a JavaScript object or JSON string that will be sent to the browser to update signals in the signals. - /// The data ***must*** evaluate to a valid JavaScript. It will be converted to signals by the Datastar client side. - pub signals: String, - /// Whether to patch the signal only if it does not already exist. - /// If not provided, the Datastar client side will default to false, which will cause the data to be patched into the signals. - pub only_if_missing: bool, -} - -impl PatchSignals { - /// Creates a new [`PatchSignals`] event with the given signals. - pub fn new(signals: impl Into) -> Self { - Self { - id: None, - retry: Duration::from_millis(consts::DEFAULT_SSE_RETRY_DURATION), - signals: signals.into(), - only_if_missing: consts::DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING, - } - } - - /// Sets the `id` of the [`PatchSignals`] event. - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - /// Sets the `retry` of the [`PatchSignals`] event. - pub fn retry(mut self, retry: Duration) -> Self { - self.retry = retry; - self - } - - /// Sets the `only_if_missing` of the [`PatchSignals`] event. - pub fn only_if_missing(mut self, only_if_missing: bool) -> Self { - self.only_if_missing = only_if_missing; - self - } - - /// Converts this [`PatchSignals`] into a [`DatastarEvent`]. - #[inline] - pub fn into_datastar_event(mut self) -> DatastarEvent { - let id = self.id.take(); - self.convert_to_datastar_event_inner(id) - } - - /// Copy this [`PatchSignals`] as a [`DatastarEvent`]. - #[inline] - pub fn as_datastar_event(&self) -> DatastarEvent { - self.convert_to_datastar_event_inner(self.id.clone()) - } - - fn convert_to_datastar_event_inner(&self, id: Option) -> DatastarEvent { - let mut data: Vec = Vec::new(); - - if self.only_if_missing != consts::DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING { - data.push(format!( - "{} {}", - consts::ONLY_IF_MISSING_DATALINE_LITERAL, - self.only_if_missing - )); - } - - for line in self.signals.lines() { - data.push(format!("{} {line}", consts::SIGNALS_DATALINE_LITERAL)); - } - - DatastarEvent { - event: consts::EventType::PatchSignals, - id, - retry: self.retry, - data, - } - } -} - -impl From<&PatchSignals> for DatastarEvent { - #[inline] - fn from(val: &PatchSignals) -> Self { - val.as_datastar_event() - } -} - -impl From for DatastarEvent { - #[inline] - fn from(val: PatchSignals) -> Self { - val.into_datastar_event() - } -} diff --git a/sdk/rust/src/rocket.rs b/sdk/rust/src/rocket.rs deleted file mode 100644 index 392856fe4..000000000 --- a/sdk/rust/src/rocket.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Rocket integration for Datastar. - -use { - crate::{ - DatastarEvent, - prelude::{PatchElements, PatchSignals}, - }, - rocket::response::stream::Event, - std::fmt::Write, -}; - -impl PatchElements { - /// Write this [`PatchElements`] into a Rocket SSE [`Event`]. - pub fn write_as_rocket_sse_event(&self) -> Event { - self.as_datastar_event().write_as_rocket_sse_event() - } -} - -impl PatchSignals { - /// Write this [`PatchSignals`] into a Rocket SSE [`Event`]. - pub fn write_as_rocket_sse_event(&self) -> Event { - self.as_datastar_event().write_as_rocket_sse_event() - } -} - -impl DatastarEvent { - /// Turn this [`DatastarEvent`] into a Rocket SSE [`Event`]. - pub fn write_as_rocket_sse_event(&self) -> Event { - let mut data = String::with_capacity( - self.data.iter().map(|s| s.len()).sum::() + self.data.len() - 1, - ); - - let mut sep = ""; - for line in self.data.iter() { - // Assumption: std::fmt::write does not fail ever for [`String`]. - let _ = write!(&mut data, "{sep}{line}"); - sep = "\n"; - } - - let event = Event::data(data) - .event(self.event.as_str().to_owned()) - .with_retry(self.retry); - - match self.id.as_deref() { - Some(id) => event.id(id.to_owned()), - None => event, - } - } -} diff --git a/sdk/test/.gitignore b/sdk/test/.gitignore deleted file mode 100644 index 42d8cb515..000000000 --- a/sdk/test/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -**/**/norm*.txt -**/**/testOutput.txt diff --git a/sdk/test/README.md b/sdk/test/README.md deleted file mode 100644 index c891f55b7..000000000 --- a/sdk/test/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Datastar SDK testing suite - -This test suite uses curl(1), cat(1), sh(1), and awk(1) to test that a server respects the SDK spec found in `../README.md`. - -It expects a server to expose a `/test` endpoint that accepts all HTTP methods. The server should then use ReadSignals -to extract the `events` array. It must then loop through the array of events and use `event.type` to decide which server sent event to use. If the output of the server differs from the expected output, then an error will be printed to the terminal. - -## Usage - -``` -$ ./test-all.sh $server_address -Running tests with argument: $server_address -Processing GET cases... -Processing POST cases... -``` - -If nothing else is output then all tests passed! - -Results of the test can be found in `./get_cases/$case_name/testOutput.txt` (or `post_cases` depending on the test). - -## Adding new cases - -To add a new test case, simply add a folder named after the test in either `./get-cases` or `./post-cases`. - -That folder must contain an `input.json` file and an `output.txt` file. - -The `input.json` file must contain valid json of the following shape: - -``` -{"events": - [ - { "type": "executeScript", - "script": "console.log('hello');", - "eventId": 1, - "retryDuration": 2000, - "attributes": { - "type": "text/javascript", - "blocking": false - }, - "autoRemove": false - } - ] -} -``` - -The `output.txt` file must contain valid a `text/eventstream` like such: - -``` -event: datastar-patch-elements -id: 1 -retry: 2000 -elements: ; -``` - -### Special case for multiline signals - -For the event type `patchSignals` the `input.json` contains the `signals` as JSON-object which should be converted to a single signals line in the `output.txt`. - -If you want to output multi-line signals, then the input must contain `signals-raw` as String with `\n` in them instead. This is due to the fact that Json parsers would otherwise interpret the input file without the line breaks. - -So the impementation of the server has to interpret `signals-raw` as String first, and if not present `signals` as JSON-object. diff --git a/sdk/test/compare-sse.sh b/sdk/test/compare-sse.sh deleted file mode 100755 index 726d6e9fb..000000000 --- a/sdk/test/compare-sse.sh +++ /dev/null @@ -1,265 +0,0 @@ -#!/bin/sh - -# Usage: compare-sse.sh expected.txt actual.txt -# Compares two SSE files allowing fields to be in any order -# but preserving order within same prefix types - -# Function for comparing SSE outputs with error display -compare_sse_with_output() { - expected="$1" - actual="$2" - - # Call the main comparison logic - compare_sse "$expected" "$actual" || { - echo "Difference between expected and actual output:" - echo "" - diff -u "$expected" "$actual" || true - return 1 - } - return 0 -} - -# Main comparison function -compare_sse() { - if [ $# -ne 2 ]; then - echo "Usage: $0 expected.txt actual.txt" >&2 - exit 1 - fi - - expected="$1" - actual="$2" - - if [ ! -f "$expected" ]; then - echo "Expected file not found: $expected" >&2 - exit 1 - fi - - if [ ! -f "$actual" ]; then - echo "Actual file not found: $actual" >&2 - exit 1 - fi - - # Use awk to parse and compare SSE events -awk ' -BEGIN { - RS = "\n\n\n" # Events separated by double newlines - FS = "\n" # Fields separated by single newlines - event_count_1 = 0 - event_count_2 = 0 -} - -# Process first file (expected) -NR == FNR { - if (NF > 0) { - event_count_1++ - delete data_fields - delete other_fields - data_count = 0 - - # Parse fields and separate data fields from others - for (i = 1; i <= NF; i++) { - if ($i != "") { - # Extract prefix (everything before first colon) - prefix = $i - sub(/:.*/, "", prefix) - - if (prefix == "data") { - # Extract subgroup (first word after "data: ") - subgroup = $i - sub(/^data: /, "", subgroup) - sub(/ .*$/, "", subgroup) - - # Store data fields with subgroup for sorting - data_count++ - data_fields[data_count] = $i - data_subgroups[data_count] = subgroup - } else { - # Store non-data fields by prefix - if (!(prefix in other_fields)) { - other_fields[prefix] = "" - } - if (other_fields[prefix] != "") { - other_fields[prefix] = other_fields[prefix] "\n" - } - other_fields[prefix] = other_fields[prefix] $i - } - } - } - - # Store event data - for (prefix in other_fields) { - events_1[event_count_1, prefix] = other_fields[prefix] - } - - # Sort and store data fields - if (data_count > 0) { - # Create sorting indices based on subgroup, preserving order within subgroups - for (i = 1; i <= data_count; i++) { - sort_indices[i] = i - } - - # Sort indices by subgroup name - for (i = 1; i <= data_count; i++) { - for (j = i + 1; j <= data_count; j++) { - if (data_subgroups[sort_indices[i]] > data_subgroups[sort_indices[j]]) { - temp = sort_indices[i] - sort_indices[i] = sort_indices[j] - sort_indices[j] = temp - } - } - } - - # Join data fields in sorted order - sorted_data = "" - for (i = 1; i <= data_count; i++) { - if (sorted_data != "") { - sorted_data = sorted_data "\n" - } - sorted_data = sorted_data data_fields[sort_indices[i]] - } - events_1[event_count_1, "data"] = sorted_data - } - } - next -} - -# Process second file (actual) -{ - if (NF > 0) { - event_count_2++ - delete data_fields - delete other_fields - data_count = 0 - - # Parse fields and separate data fields from others - for (i = 1; i <= NF; i++) { - if ($i != "") { - # Extract prefix (everything before first colon) - prefix = $i - sub(/:.*/, "", prefix) - - if (prefix == "data") { - # Extract subgroup (first word after "data: ") - subgroup = $i - sub(/^data: /, "", subgroup) - sub(/ .*$/, "", subgroup) - - # Store data fields with subgroup for sorting - data_count++ - data_fields[data_count] = $i - data_subgroups[data_count] = subgroup - } else { - # Store non-data fields by prefix - if (!(prefix in other_fields)) { - other_fields[prefix] = "" - } - if (other_fields[prefix] != "") { - other_fields[prefix] = other_fields[prefix] "\n" - } - other_fields[prefix] = other_fields[prefix] $i - } - } - } - - # Store event data - for (prefix in other_fields) { - events_2[event_count_2, prefix] = other_fields[prefix] - } - - # Sort and store data fields - if (data_count > 0) { - # Create sorting indices based on subgroup, preserving order within subgroups - for (i = 1; i <= data_count; i++) { - sort_indices[i] = i - } - - # Sort indices by subgroup name - for (i = 1; i <= data_count; i++) { - for (j = i + 1; j <= data_count; j++) { - if (data_subgroups[sort_indices[i]] > data_subgroups[sort_indices[j]]) { - temp = sort_indices[i] - sort_indices[i] = sort_indices[j] - sort_indices[j] = temp - } - } - } - - # Join data fields in sorted order - sorted_data = "" - for (i = 1; i <= data_count; i++) { - if (sorted_data != "") { - sorted_data = sorted_data "\n" - } - sorted_data = sorted_data data_fields[sort_indices[i]] - } - events_2[event_count_2, "data"] = sorted_data - } - } -} - -END { - # Compare event counts - if (event_count_1 != event_count_2) { - print "Event count mismatch: expected " event_count_1 ", got " event_count_2 > "/dev/stderr" - exit 1 - } - - # Compare each event - for (e = 1; e <= event_count_1; e++) { - # Collect all prefixes from both events - delete all_prefixes - for (key in events_1) { - split(key, parts, SUBSEP) - if (parts[1] == e) { - all_prefixes[parts[2]] = 1 - } - } - for (key in events_2) { - split(key, parts, SUBSEP) - if (parts[1] == e) { - all_prefixes[parts[2]] = 1 - } - } - - # Compare fields for each prefix - for (prefix in all_prefixes) { - key1 = e SUBSEP prefix - key2 = e SUBSEP prefix - - # Check if prefix exists in both - if (!(key1 in events_1) && !(key2 in events_2)) { - continue - } - - if (!(key1 in events_1)) { - print "Event " e ": missing prefix \"" prefix "\" in expected" > "/dev/stderr" - exit 1 - } - - if (!(key2 in events_2)) { - print "Event " e ": missing prefix \"" prefix "\" in actual" > "/dev/stderr" - exit 1 - } - - # Compare field content - if (events_1[key1] != events_2[key2]) { - print "Event " e ": mismatch in \"" prefix "\" fields" > "/dev/stderr" - print "Expected:" > "/dev/stderr" - print events_1[key1] > "/dev/stderr" - print "Actual:" > "/dev/stderr" - print events_2[key2] > "/dev/stderr" - exit 1 - } - } - } - - # All matches - exit 0 -} -' "$expected" "$actual" -} - -# If script is called directly (not sourced), run the comparison -if [ "${0##*/}" = "compare-sse.sh" ]; then - compare_sse "$@" -fi \ No newline at end of file diff --git a/sdk/test/test-all.sh b/sdk/test/test-all.sh deleted file mode 100755 index 04962f204..000000000 --- a/sdk/test/test-all.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh - -# Check if an argument is provided -if [ -z "$1" ]; then - echo "Usage: $0 " - exit 1 -fi - -echo "Running tests with argument: $1" - -# Run tests for GET cases -if [ -d "./get-cases" ]; then - echo "Processing GET cases..." - for case in ./get-cases/*; do - if [ -d "$case" ]; then - ./test-get.sh "$case" "$1" - fi - done - echo "Finished processing GET cases" -else - echo "Directory './get-cases' not found!" -fi - -# Run tests for POST cases -if [ -d "./post-cases" ]; then - echo "Processing POST cases..." - for case in ./post-cases/*; do - if [ -d "$case" ]; then - ./test-post.sh "$case" "$1" - fi - done - echo "Finished processing POST cases" -else - echo "Directory './post-cases' not found!" -fi diff --git a/sdk/test/test-get.sh b/sdk/test/test-get.sh deleted file mode 100755 index 15f7e95e4..000000000 --- a/sdk/test/test-get.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -. ./compare-sse.sh - -[ ! -f "$1/input.json" ] && echo "case $1 does not have input.json" && return 1 -[ ! -f "$1/output.txt" ] && echo "case $1 does not have output.txt" && return 1 - -input=$(cat "$1/input.json") - -[ -f "$1/testOutput.txt" ] && rm "$1/testOutput.txt" - -curl -sN --get -H "Accept: text/event-stream" -H "datastar-request: true" --data-urlencode "datastar=$input" "$2/test" -o "$1/testOutput.txt" - -[ ! -f "$1/testOutput.txt" ] && echo "case $1 failed: your server did not return anything" && return 1 - -compare_sse_with_output "$1/output.txt" "$1/testOutput.txt" || { exit 1; } - -exit 0 diff --git a/sdk/test/test-post.sh b/sdk/test/test-post.sh deleted file mode 100755 index 977e8e800..000000000 --- a/sdk/test/test-post.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -. ./compare-sse.sh - -[ ! -f "$1/input.json" ] && echo "case $1 does not have input.json" && return 1 -[ ! -f "$1/output.txt" ] && echo "case $1 does not have output.txt" && return 1 - -[ -f "$1/testOutput.txt" ] && rm "$1/testOutput.txt" - -input=$(cat "$1/input.json") - -curl -sN -H "Accept: text/event-stream" -H "datastar-request: true" -H "Content-Type: application/json" --data "$input" "$2/test" -o "$1/testOutput.txt" - -[ ! -f "$1/testOutput.txt" ] && echo "case $1 failed: your server did not return anything" && return 1 - -compare_sse_with_output "$1/output.txt" "$1/testOutput.txt" || { exit 1; } - -exit 0 diff --git a/sdk/tests/.dockerignore b/sdk/tests/.dockerignore new file mode 100644 index 000000000..4d8df6d5e --- /dev/null +++ b/sdk/tests/.dockerignore @@ -0,0 +1,23 @@ +# Git files +.git +.gitignore + +# Test output +testdata/ +**/**/testOutput.txt +**/**/norm*.txt + +# Go test cache +*.test + +# IDE files +.vscode/ +.idea/ + +# Documentation +*.md + +# Docker files (don't copy into context) +Dockerfile +docker-compose.yml +.dockerignore \ No newline at end of file diff --git a/sdk/tests/.gitignore b/sdk/tests/.gitignore new file mode 100644 index 000000000..d975dd2d3 --- /dev/null +++ b/sdk/tests/.gitignore @@ -0,0 +1,16 @@ +# Test output files +**/**/norm*.txt +**/**/testOutput.txt + +# Test output directory created on failures +testdata/ + +# Go test binary +*.test + +# Go build output +test-runner +compare-sse + +# sdk tests +.docker-build-timestamp \ No newline at end of file diff --git a/sdk/tests/Dockerfile b/sdk/tests/Dockerfile new file mode 100644 index 000000000..d16d81ef7 --- /dev/null +++ b/sdk/tests/Dockerfile @@ -0,0 +1,27 @@ +# Build stage +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -o datastar-sdk-tests ./cmd/datastar-sdk-tests + +# Final stage +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy the binary from builder +COPY --from=builder /app/datastar-sdk-tests . + +# Run the binary +ENTRYPOINT ["./datastar-sdk-tests"] \ No newline at end of file diff --git a/sdk/tests/README.md b/sdk/tests/README.md new file mode 100644 index 000000000..6211e3c35 --- /dev/null +++ b/sdk/tests/README.md @@ -0,0 +1,149 @@ +# Datastar SDK Test Suite + +A comprehensive test suite for validating Datastar SDK implementations across different languages. + +## Installation + +### As a Go Library + +```bash +go get github.com/starfederation/datastar/sdk/tests +``` + +## Usage + +### Running Tests + +```bash +# Run all tests against default server (localhost:7331) +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest + +# Run with custom server +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -server http://localhost:8080 + +# Run only GET tests +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -type get + +# Run only POST tests +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -type post + +# Verbose output +go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest -v +``` + +### Using Docker + +```bash +# Run tests using Docker container +docker run --rm --network host datastar-sdk-tests:latest -server http://localhost:7331 -v +``` + +### Using go test directly + +```bash +# Clone the repository and navigate to tests directory +cd sdk/tests + +# Run all tests +go test -v + +# Run with custom server +TEST_SERVER_URL=http://localhost:8080 go test -v +``` + +## Test Structure + +The test suite includes: + +- **GET Tests** (`golden/get/`): Test cases for GET endpoints +- **POST Tests** (`golden/post/`): Test cases for POST endpoints + +Each test case contains: +- `input.json`: The request payload +- `output.txt`: The expected SSE response + +## Features + +- **HTML Normalization**: Automatically handles HTML attribute ordering differences +- **Embedded Test Data**: All test cases are embedded in the binary for portability +- **Flexible Runner**: Can be used as a CLI tool or Go library +- **Detailed Output**: Clear error messages and debug information + +## For SDK Implementers + +To validate your SDK implementation: + +1. Start your test server on port 7331 (or specify a different port) +2. Implement the `/test` endpoint that: + - For GET: reads the `datastar` query parameter + - For POST: reads the JSON body + - Returns appropriate SSE responses +3. Run `go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest` to validate your implementation + +## Test Endpoint Requirements + +The `/test` endpoint should: + +1. Use ReadSignals to extract the `events` array from the request +2. Loop through the array of events +3. Use `event.type` to decide which server sent event to generate +4. Return the appropriate SSE response + +### Input Format + +```json +{ + "events": [ + { + "type": "executeScript", + "script": "console.log('hello');", + "eventId": "event1", + "retryDuration": 2000, + "attributes": { + "type": "text/javascript", + "blocking": "false" + }, + "autoRemove": false + } + ] +} +``` + +### Output Format + +``` +event: datastar-patch-elements +id: event1 +retry: 2000 +data: mode append +data: selector body +data: elements + +``` + +## Adding New Test Cases + +To add a new test case: + +1. Create a folder in `golden/get/` or `golden/post/` named after your test +2. Add an `input.json` file with the request payload +3. Add an `output.txt` file with the expected SSE response + +### Special Cases + +#### Multiline Signals + +For `patchSignals` events, if you need multiline output: +- Use `signals-raw` as a string with `\n` characters instead of `signals` as a JSON object +- The server should check for `signals-raw` first, then fall back to `signals` + +## Test Cases + +The test suite covers: + +- Element patching (single and multiline) +- Signal patching +- Script execution +- Element/signal removal +- Various SSE formatting scenarios +- Edge cases and error conditions \ No newline at end of file diff --git a/sdk/tests/cmd/datastar-sdk-tests/main.go b/sdk/tests/cmd/datastar-sdk-tests/main.go new file mode 100644 index 000000000..dcdfbb2b9 --- /dev/null +++ b/sdk/tests/cmd/datastar-sdk-tests/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "fmt" + "os" + "testing" + + sdktests "github.com/starfederation/datastar/sdk/tests" +) + +func main() { + var ( + serverURL = flag.String("server", "http://localhost:7331", "Test server URL") + verbose = flag.Bool("v", false, "Verbose output") + testType = flag.String("type", "all", "Test type: get, post, or all") + help = flag.Bool("h", false, "Show help") + ) + flag.Parse() + + if *help { + fmt.Println("datastar-sdk-tests - Datastar SDK test suite") + fmt.Println("\nUsage:") + fmt.Println(" datastar-sdk-tests [options]") + fmt.Println("\nOptions:") + flag.PrintDefaults() + fmt.Println("\nExamples:") + fmt.Println(" datastar-sdk-tests # Run all tests") + fmt.Println(" datastar-sdk-tests -type get # Run only GET tests") + fmt.Println(" datastar-sdk-tests -server http://localhost:8080") + os.Exit(0) + } + + // Set environment variable for tests + os.Setenv("TEST_SERVER_URL", *serverURL) + + // Create a testing.M to run tests + var tests []testing.InternalTest + + switch *testType { + case "get": + tests = append(tests, testing.InternalTest{ + Name: "TestSSEGetEndpoints", + F: sdktests.TestSSEGetEndpoints, + }) + case "post": + tests = append(tests, testing.InternalTest{ + Name: "TestSSEPostEndpoints", + F: sdktests.TestSSEPostEndpoints, + }) + case "all": + tests = append(tests, + testing.InternalTest{ + Name: "TestSSEGetEndpoints", + F: sdktests.TestSSEGetEndpoints, + }, + testing.InternalTest{ + Name: "TestSSEPostEndpoints", + F: sdktests.TestSSEPostEndpoints, + }, + ) + default: + fmt.Fprintf(os.Stderr, "Unknown test type: %s\n", *testType) + os.Exit(1) + } + + // Set verbose flag if requested + if *verbose { + os.Args = append(os.Args, "-test.v") + } + + // Run tests using testing.Main + testing.Main( + func(pat, str string) (bool, error) { return true, nil }, // matchString + tests, // tests + nil, // benchmarks + nil, // examples + ) +} \ No newline at end of file diff --git a/sdk/tests/go.mod b/sdk/tests/go.mod new file mode 100644 index 000000000..ab83b19f0 --- /dev/null +++ b/sdk/tests/go.mod @@ -0,0 +1,14 @@ +module github.com/starfederation/datastar/sdk/tests + +go 1.21 + +require ( + github.com/stretchr/testify v1.8.4 + golang.org/x/net v0.19.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sdk/tests/go.sum b/sdk/tests/go.sum new file mode 100644 index 000000000..deabe9d2b --- /dev/null +++ b/sdk/tests/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdk/test/get-cases/executeScriptWithAllOptions/input.json b/sdk/tests/golden/get/executeScriptWithAllOptions/input.json similarity index 100% rename from sdk/test/get-cases/executeScriptWithAllOptions/input.json rename to sdk/tests/golden/get/executeScriptWithAllOptions/input.json diff --git a/sdk/test/get-cases/executeScriptWithAllOptions/output.txt b/sdk/tests/golden/get/executeScriptWithAllOptions/output.txt similarity index 100% rename from sdk/test/get-cases/executeScriptWithAllOptions/output.txt rename to sdk/tests/golden/get/executeScriptWithAllOptions/output.txt diff --git a/sdk/test/get-cases/executeScriptWithDefaults/input.json b/sdk/tests/golden/get/executeScriptWithDefaults/input.json similarity index 100% rename from sdk/test/get-cases/executeScriptWithDefaults/input.json rename to sdk/tests/golden/get/executeScriptWithDefaults/input.json diff --git a/sdk/test/get-cases/executeScriptWithDefaults/output.txt b/sdk/tests/golden/get/executeScriptWithDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/executeScriptWithDefaults/output.txt rename to sdk/tests/golden/get/executeScriptWithDefaults/output.txt diff --git a/sdk/test/get-cases/executeScriptWithMultilineScript/input.json b/sdk/tests/golden/get/executeScriptWithMultilineScript/input.json similarity index 100% rename from sdk/test/get-cases/executeScriptWithMultilineScript/input.json rename to sdk/tests/golden/get/executeScriptWithMultilineScript/input.json diff --git a/sdk/test/get-cases/executeScriptWithMultilineScript/output.txt b/sdk/tests/golden/get/executeScriptWithMultilineScript/output.txt similarity index 100% rename from sdk/test/get-cases/executeScriptWithMultilineScript/output.txt rename to sdk/tests/golden/get/executeScriptWithMultilineScript/output.txt diff --git a/sdk/test/get-cases/executeScriptWithoutDefaults/input.json b/sdk/tests/golden/get/executeScriptWithoutDefaults/input.json similarity index 100% rename from sdk/test/get-cases/executeScriptWithoutDefaults/input.json rename to sdk/tests/golden/get/executeScriptWithoutDefaults/input.json diff --git a/sdk/test/get-cases/executeScriptWithoutDefaults/output.txt b/sdk/tests/golden/get/executeScriptWithoutDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/executeScriptWithoutDefaults/output.txt rename to sdk/tests/golden/get/executeScriptWithoutDefaults/output.txt diff --git a/sdk/test/get-cases/patchElementsWithAllOptions/input.json b/sdk/tests/golden/get/patchElementsWithAllOptions/input.json similarity index 100% rename from sdk/test/get-cases/patchElementsWithAllOptions/input.json rename to sdk/tests/golden/get/patchElementsWithAllOptions/input.json diff --git a/sdk/test/get-cases/patchElementsWithAllOptions/output.txt b/sdk/tests/golden/get/patchElementsWithAllOptions/output.txt similarity index 100% rename from sdk/test/get-cases/patchElementsWithAllOptions/output.txt rename to sdk/tests/golden/get/patchElementsWithAllOptions/output.txt diff --git a/sdk/test/get-cases/patchElementsWithDefaults/input.json b/sdk/tests/golden/get/patchElementsWithDefaults/input.json similarity index 100% rename from sdk/test/get-cases/patchElementsWithDefaults/input.json rename to sdk/tests/golden/get/patchElementsWithDefaults/input.json diff --git a/sdk/test/get-cases/patchElementsWithDefaults/output.txt b/sdk/tests/golden/get/patchElementsWithDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/patchElementsWithDefaults/output.txt rename to sdk/tests/golden/get/patchElementsWithDefaults/output.txt diff --git a/sdk/test/get-cases/patchElementsWithMultilineElements/input.json b/sdk/tests/golden/get/patchElementsWithMultilineElements/input.json similarity index 100% rename from sdk/test/get-cases/patchElementsWithMultilineElements/input.json rename to sdk/tests/golden/get/patchElementsWithMultilineElements/input.json diff --git a/sdk/test/get-cases/patchElementsWithMultilineElements/output.txt b/sdk/tests/golden/get/patchElementsWithMultilineElements/output.txt similarity index 100% rename from sdk/test/get-cases/patchElementsWithMultilineElements/output.txt rename to sdk/tests/golden/get/patchElementsWithMultilineElements/output.txt diff --git a/sdk/test/get-cases/patchElementsWithoutDefaults/input.json b/sdk/tests/golden/get/patchElementsWithoutDefaults/input.json similarity index 100% rename from sdk/test/get-cases/patchElementsWithoutDefaults/input.json rename to sdk/tests/golden/get/patchElementsWithoutDefaults/input.json diff --git a/sdk/test/get-cases/patchElementsWithoutDefaults/output.txt b/sdk/tests/golden/get/patchElementsWithoutDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/patchElementsWithoutDefaults/output.txt rename to sdk/tests/golden/get/patchElementsWithoutDefaults/output.txt diff --git a/sdk/test/get-cases/patchSignalsWithAllOptions/input.json b/sdk/tests/golden/get/patchSignalsWithAllOptions/input.json similarity index 100% rename from sdk/test/get-cases/patchSignalsWithAllOptions/input.json rename to sdk/tests/golden/get/patchSignalsWithAllOptions/input.json diff --git a/sdk/test/get-cases/patchSignalsWithAllOptions/output.txt b/sdk/tests/golden/get/patchSignalsWithAllOptions/output.txt similarity index 100% rename from sdk/test/get-cases/patchSignalsWithAllOptions/output.txt rename to sdk/tests/golden/get/patchSignalsWithAllOptions/output.txt diff --git a/sdk/test/get-cases/patchSignalsWithDefaults/input.json b/sdk/tests/golden/get/patchSignalsWithDefaults/input.json similarity index 100% rename from sdk/test/get-cases/patchSignalsWithDefaults/input.json rename to sdk/tests/golden/get/patchSignalsWithDefaults/input.json diff --git a/sdk/test/get-cases/patchSignalsWithDefaults/output.txt b/sdk/tests/golden/get/patchSignalsWithDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/patchSignalsWithDefaults/output.txt rename to sdk/tests/golden/get/patchSignalsWithDefaults/output.txt diff --git a/sdk/test/get-cases/patchSignalsWithMultilineJson/input.json b/sdk/tests/golden/get/patchSignalsWithMultilineJson/input.json similarity index 100% rename from sdk/test/get-cases/patchSignalsWithMultilineJson/input.json rename to sdk/tests/golden/get/patchSignalsWithMultilineJson/input.json diff --git a/sdk/test/get-cases/patchSignalsWithMultilineJson/output.txt b/sdk/tests/golden/get/patchSignalsWithMultilineJson/output.txt similarity index 100% rename from sdk/test/get-cases/patchSignalsWithMultilineJson/output.txt rename to sdk/tests/golden/get/patchSignalsWithMultilineJson/output.txt diff --git a/sdk/test/get-cases/patchSignalsWithMultilineSignals/input.json b/sdk/tests/golden/get/patchSignalsWithMultilineSignals/input.json similarity index 100% rename from sdk/test/get-cases/patchSignalsWithMultilineSignals/input.json rename to sdk/tests/golden/get/patchSignalsWithMultilineSignals/input.json diff --git a/sdk/test/get-cases/patchSignalsWithMultilineSignals/output.txt b/sdk/tests/golden/get/patchSignalsWithMultilineSignals/output.txt similarity index 100% rename from sdk/test/get-cases/patchSignalsWithMultilineSignals/output.txt rename to sdk/tests/golden/get/patchSignalsWithMultilineSignals/output.txt diff --git a/sdk/test/get-cases/patchSignalsWithoutDefaults/input.json b/sdk/tests/golden/get/patchSignalsWithoutDefaults/input.json similarity index 100% rename from sdk/test/get-cases/patchSignalsWithoutDefaults/input.json rename to sdk/tests/golden/get/patchSignalsWithoutDefaults/input.json diff --git a/sdk/test/get-cases/patchSignalsWithoutDefaults/output.txt b/sdk/tests/golden/get/patchSignalsWithoutDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/patchSignalsWithoutDefaults/output.txt rename to sdk/tests/golden/get/patchSignalsWithoutDefaults/output.txt diff --git a/sdk/test/get-cases/removeElementsWithAllOptions/input.json b/sdk/tests/golden/get/removeElementsWithAllOptions/input.json similarity index 100% rename from sdk/test/get-cases/removeElementsWithAllOptions/input.json rename to sdk/tests/golden/get/removeElementsWithAllOptions/input.json diff --git a/sdk/test/get-cases/removeElementsWithAllOptions/output.txt b/sdk/tests/golden/get/removeElementsWithAllOptions/output.txt similarity index 100% rename from sdk/test/get-cases/removeElementsWithAllOptions/output.txt rename to sdk/tests/golden/get/removeElementsWithAllOptions/output.txt diff --git a/sdk/test/get-cases/removeElementsWithDefaults/input.json b/sdk/tests/golden/get/removeElementsWithDefaults/input.json similarity index 100% rename from sdk/test/get-cases/removeElementsWithDefaults/input.json rename to sdk/tests/golden/get/removeElementsWithDefaults/input.json diff --git a/sdk/test/get-cases/removeElementsWithDefaults/output.txt b/sdk/tests/golden/get/removeElementsWithDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/removeElementsWithDefaults/output.txt rename to sdk/tests/golden/get/removeElementsWithDefaults/output.txt diff --git a/sdk/test/get-cases/removeElementsWithoutDefaults/input.json b/sdk/tests/golden/get/removeElementsWithoutDefaults/input.json similarity index 100% rename from sdk/test/get-cases/removeElementsWithoutDefaults/input.json rename to sdk/tests/golden/get/removeElementsWithoutDefaults/input.json diff --git a/sdk/test/get-cases/removeElementsWithoutDefaults/output.txt b/sdk/tests/golden/get/removeElementsWithoutDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/removeElementsWithoutDefaults/output.txt rename to sdk/tests/golden/get/removeElementsWithoutDefaults/output.txt diff --git a/sdk/test/get-cases/removeSignalsWithAllOptions/input.json b/sdk/tests/golden/get/removeSignalsWithAllOptions/input.json similarity index 100% rename from sdk/test/get-cases/removeSignalsWithAllOptions/input.json rename to sdk/tests/golden/get/removeSignalsWithAllOptions/input.json diff --git a/sdk/test/get-cases/removeSignalsWithAllOptions/output.txt b/sdk/tests/golden/get/removeSignalsWithAllOptions/output.txt similarity index 100% rename from sdk/test/get-cases/removeSignalsWithAllOptions/output.txt rename to sdk/tests/golden/get/removeSignalsWithAllOptions/output.txt diff --git a/sdk/test/get-cases/removeSignalsWithDefaults/input.json b/sdk/tests/golden/get/removeSignalsWithDefaults/input.json similarity index 100% rename from sdk/test/get-cases/removeSignalsWithDefaults/input.json rename to sdk/tests/golden/get/removeSignalsWithDefaults/input.json diff --git a/sdk/test/get-cases/removeSignalsWithDefaults/output.txt b/sdk/tests/golden/get/removeSignalsWithDefaults/output.txt similarity index 100% rename from sdk/test/get-cases/removeSignalsWithDefaults/output.txt rename to sdk/tests/golden/get/removeSignalsWithDefaults/output.txt diff --git a/sdk/test/get-cases/sendTwoEvents/input.json b/sdk/tests/golden/get/sendTwoEvents/input.json similarity index 100% rename from sdk/test/get-cases/sendTwoEvents/input.json rename to sdk/tests/golden/get/sendTwoEvents/input.json diff --git a/sdk/test/get-cases/sendTwoEvents/output.txt b/sdk/tests/golden/get/sendTwoEvents/output.txt similarity index 100% rename from sdk/test/get-cases/sendTwoEvents/output.txt rename to sdk/tests/golden/get/sendTwoEvents/output.txt diff --git a/sdk/test/post-cases/readSignalsFromBody/input.json b/sdk/tests/golden/post/readSignalsFromBody/input.json similarity index 100% rename from sdk/test/post-cases/readSignalsFromBody/input.json rename to sdk/tests/golden/post/readSignalsFromBody/input.json diff --git a/sdk/test/post-cases/readSignalsFromBody/output.txt b/sdk/tests/golden/post/readSignalsFromBody/output.txt similarity index 100% rename from sdk/test/post-cases/readSignalsFromBody/output.txt rename to sdk/tests/golden/post/readSignalsFromBody/output.txt diff --git a/sdk/tests/runner.go b/sdk/tests/runner.go new file mode 100644 index 000000000..b41ce78b9 --- /dev/null +++ b/sdk/tests/runner.go @@ -0,0 +1,140 @@ +package sdktests + +import ( + "bytes" + "embed" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "time" +) + +// TestRunner provides methods to run SDK tests programmatically +type TestRunner struct { + ServerURL string + Client *http.Client +} + +// NewTestRunner creates a new test runner +func NewTestRunner(serverURL string) *TestRunner { + if serverURL == "" { + serverURL = "http://localhost:7331" + } + return &TestRunner{ + ServerURL: serverURL, + Client: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// RunGetTest runs a single GET test case +func (tr *TestRunner) RunGetTest(inputData []byte) ([]byte, error) { + u, err := url.Parse(tr.ServerURL + "/test") + if err != nil { + return nil, err + } + + q := u.Query() + q.Set("datastar", string(inputData)) + u.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("datastar-request", "true") + + return tr.makeRequest(req) +} + +// RunPostTest runs a single POST test case +func (tr *TestRunner) RunPostTest(inputData []byte) ([]byte, error) { + req, err := http.NewRequest("POST", tr.ServerURL+"/test", bytes.NewReader(inputData)) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("datastar-request", "true") + req.Header.Set("Content-Type", "application/json") + + return tr.makeRequest(req) +} + +func (tr *TestRunner) makeRequest(req *http.Request) ([]byte, error) { + resp, err := tr.Client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, body) + } + + return io.ReadAll(resp.Body) +} + +// GetTestData returns the embedded test data filesystem +func GetTestData() embed.FS { + return testData +} + +// TestCase represents a single test case +type TestCase struct { + Name string + Input []byte + Expected []byte +} + +// GetTestCases returns all test cases for a given type +func GetTestCases(testType string) ([]TestCase, error) { + var dir string + switch testType { + case "get": + dir = "golden/get" + case "post": + dir = "golden/post" + default: + return nil, fmt.Errorf("unknown test type: %s", testType) + } + + entries, err := testData.ReadDir(dir) + if err != nil { + return nil, err + } + + var cases []TestCase + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + inputPath := filepath.Join(dir, entry.Name(), "input.json") + outputPath := filepath.Join(dir, entry.Name(), "output.txt") + + input, err := testData.ReadFile(inputPath) + if err != nil { + continue + } + + expected, err := testData.ReadFile(outputPath) + if err != nil { + continue + } + + cases = append(cases, TestCase{ + Name: entry.Name(), + Input: input, + Expected: expected, + }) + } + + return cases, nil +} \ No newline at end of file diff --git a/sdk/tests/testdata.go b/sdk/tests/testdata.go new file mode 100644 index 000000000..665b714b6 --- /dev/null +++ b/sdk/tests/testdata.go @@ -0,0 +1,341 @@ +package sdktests + +import ( + "bytes" + "embed" + "fmt" + "io" + "io/fs" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" +) + +//go:embed golden +var testData embed.FS + +var serverURL = os.Getenv("TEST_SERVER_URL") + +func init() { + if serverURL == "" { + serverURL = "http://localhost:7331" + } +} + +// TestSSEGetEndpoints is an exported version of the GET endpoint tests +func TestSSEGetEndpoints(t *testing.T) { + runTestCases(t, testData, "golden/get", runGetTest) +} + +// TestSSEPostEndpoints is an exported version of the POST endpoint tests +func TestSSEPostEndpoints(t *testing.T) { + runTestCases(t, testData, "golden/post", runPostTest) +} + +func runTestCases(t *testing.T, embedFS embed.FS, casesDir string, runTest func(string, []byte) ([]byte, error)) { + entries, err := fs.ReadDir(embedFS, casesDir) + require.NoError(t, err, "Failed to read %s directory", casesDir) + + // Get unique test case names + testCases := make(map[string]bool) + for _, entry := range entries { + if entry.IsDir() { + testCases[entry.Name()] = true + } else { + // Extract test case name from file path + dir := filepath.Dir(entry.Name()) + if dir != "." && dir != casesDir { + testName := filepath.Base(dir) + testCases[testName] = true + } + } + } + + // Run each test case + for testName := range testCases { + testName := testName // capture for closure + t.Run(testName, func(t *testing.T) { + inputPath := filepath.Join(casesDir, testName, "input.json") + outputPath := filepath.Join(casesDir, testName, "output.txt") + + // Read input from embedded FS + inputData, err := embedFS.ReadFile(inputPath) + require.NoError(t, err, "Failed to read input") + + // Read expected output from embedded FS + expectedData, err := embedFS.ReadFile(outputPath) + require.NoError(t, err, "Failed to read expected output") + + // Run test + actualData, err := runTest(serverURL, inputData) + require.NoError(t, err, "Request failed") + + // Compare + err = compareSSE(t, expectedData, actualData) + if err != nil { + // Save actual output for debugging + debugDir := filepath.Join("testdata", casesDir, testName) + os.MkdirAll(debugDir, 0755) + actualPath := filepath.Join(debugDir, "testOutput.txt") + os.WriteFile(actualPath, actualData, 0644) + + t.Logf("Test case: %s", testName) + t.Logf("Actual output saved to: %s", actualPath) + } + }) + } +} + +func runGetTest(serverURL string, inputData []byte) ([]byte, error) { + u, err := url.Parse(serverURL + "/test") + if err != nil { + return nil, err + } + + q := u.Query() + q.Set("datastar", string(inputData)) + u.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("datastar-request", "true") + + return makeRequest(req) +} + +func runPostTest(serverURL string, inputData []byte) ([]byte, error) { + req, err := http.NewRequest("POST", serverURL+"/test", bytes.NewReader(inputData)) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("datastar-request", "true") + req.Header.Set("Content-Type", "application/json") + + return makeRequest(req) +} + +func makeRequest(req *http.Request) ([]byte, error) { + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, body) + } + + return io.ReadAll(resp.Body) +} + +// SSE comparison functions + +type SSEEvent struct { + Fields map[string][]string +} + +func compareSSE(t *testing.T, expected, actual []byte) error { + expectedEvents, err := parseSSE(expected) + require.NoError(t, err, "Failed to parse expected SSE") + + actualEvents, err := parseSSE(actual) + require.NoError(t, err, "Failed to parse actual SSE") + + compareEvents(t, expectedEvents, actualEvents) + return nil +} + +func parseSSE(data []byte) ([]SSEEvent, error) { + var events []SSEEvent + var currentEvent *SSEEvent + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + // Empty line marks end of event + if line == "" { + if currentEvent != nil && len(currentEvent.Fields) > 0 { + events = append(events, *currentEvent) + currentEvent = nil + } + continue + } + + // Parse field + colonIndex := strings.Index(line, ":") + if colonIndex == -1 { + continue + } + + if currentEvent == nil { + currentEvent = &SSEEvent{Fields: make(map[string][]string)} + } + + fieldName := line[:colonIndex] + fieldValue := strings.TrimSpace(line[colonIndex+1:]) + + currentEvent.Fields[fieldName] = append(currentEvent.Fields[fieldName], fieldValue) + } + + // Handle last event if file doesn't end with empty line + if currentEvent != nil && len(currentEvent.Fields) > 0 { + events = append(events, *currentEvent) + } + + return events, nil +} + +func compareEvents(t *testing.T, expected, actual []SSEEvent) { + require.Equal(t, len(expected), len(actual), "Event count mismatch") + + for i := range expected { + compareEvent(t, i+1, &expected[i], &actual[i]) + } +} + +func compareEvent(t *testing.T, eventNum int, expected, actual *SSEEvent) { + // Compare all non-data fields first + for fieldName, expectedValues := range expected.Fields { + if fieldName == "data" { + continue + } + + actualValues, ok := actual.Fields[fieldName] + assert.True(t, ok, "Event %d: missing field '%s' in actual", eventNum, fieldName) + assert.Equal(t, expectedValues, actualValues, "Event %d: mismatch in '%s' field", eventNum, fieldName) + } + + // Check for extra fields in actual + for fieldName := range actual.Fields { + if fieldName == "data" { + continue + } + _, ok := expected.Fields[fieldName] + assert.True(t, ok, "Event %d: unexpected field '%s' in actual", eventNum, fieldName) + } + + // Compare data fields with special handling + expectedData := expected.Fields["data"] + actualData := actual.Fields["data"] + + if len(expectedData) == 0 && len(actualData) == 0 { + return + } + + require.Equal(t, len(expectedData) > 0, len(actualData) > 0, "Event %d: data field presence mismatch", eventNum) + + // Parse and group data fields + expectedGroups := parseDataFields(expectedData) + actualGroups := parseDataFields(actualData) + + // Compare groups + compareDataGroups(t, eventNum, expectedGroups, actualGroups) +} + +func parseDataFields(fields []string) map[string][]string { + groups := make(map[string][]string) + + for _, field := range fields { + parts := strings.SplitN(field, " ", 2) + if len(parts) >= 1 { + subgroup := parts[0] + content := "" + if len(parts) > 1 { + content = parts[1] + } + groups[subgroup] = append(groups[subgroup], content) + } + } + + return groups +} + +func compareDataGroups(t *testing.T, eventNum int, expected, actual map[string][]string) { + // Check all expected groups exist in actual + for subgroup, expectedLines := range expected { + actualLines, ok := actual[subgroup] + assert.True(t, ok, "Event %d: missing data subgroup '%s' in actual", eventNum, subgroup) + + // Special handling for "elements" subgroup - normalize HTML + if subgroup == "elements" { + expectedHTML := strings.Join(expectedLines, "\n") + actualHTML := strings.Join(actualLines, "\n") + + normalizedExpected := normalizeHTML(expectedHTML) + normalizedActual := normalizeHTML(actualHTML) + + assert.Equal(t, normalizedExpected, normalizedActual, + "Event %d: mismatch in data 'elements' content\nExpected:\n%s\nActual:\n%s", + eventNum, expectedHTML, actualHTML) + } else { + // For non-elements, exact match + assert.Equal(t, expectedLines, actualLines, + "Event %d: mismatch in data '%s' content", eventNum, subgroup) + } + } + + // Check for extra subgroups in actual + for subgroup := range actual { + _, ok := expected[subgroup] + assert.True(t, ok, "Event %d: unexpected data subgroup '%s' in actual", eventNum, subgroup) + } +} + +func normalizeHTML(htmlStr string) string { + doc, err := html.Parse(strings.NewReader(htmlStr)) + if err != nil { + // If parsing fails, return original + return htmlStr + } + + // Normalize attributes in all element nodes + var normalize func(*html.Node) + normalize = func(n *html.Node) { + if n.Type == html.ElementNode && n.Attr != nil && len(n.Attr) > 1 { + // Sort attributes by key + sort.Slice(n.Attr, func(i, j int) bool { + return n.Attr[i].Key < n.Attr[j].Key + }) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + normalize(c) + } + } + normalize(doc) + + // Render back to string + var buf bytes.Buffer + html.Render(&buf, doc) + + // The parser adds ..., so extract just the body content + result := buf.String() + + // Extract content between and + bodyStart := strings.Index(result, "") + bodyEnd := strings.Index(result, "") + + if bodyStart != -1 && bodyEnd != -1 { + result = result[bodyStart+6 : bodyEnd] + } + + return strings.TrimSpace(result) +} \ No newline at end of file diff --git a/sdk/tests/tests_test.go b/sdk/tests/tests_test.go new file mode 100644 index 000000000..99e62c5d8 --- /dev/null +++ b/sdk/tests/tests_test.go @@ -0,0 +1,13 @@ +package sdktests + +import ( + "testing" +) + +func TestGetEndpoints(t *testing.T) { + TestSSEGetEndpoints(t) +} + +func TestPostEndpoints(t *testing.T) { + TestSSEPostEndpoints(t) +} \ No newline at end of file diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore deleted file mode 100644 index a8882a7f9..000000000 --- a/sdk/typescript/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -.cursor/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -npm \ No newline at end of file diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md deleted file mode 100644 index 4dec49351..000000000 --- a/sdk/typescript/README.md +++ /dev/null @@ -1,276 +0,0 @@ - -

- -# Datastar TypeScript SDK - -[![Version](https://img.shields.io/badge/version-1.0.0–RC.1-orange)](https://github.com/starfederation/datastar/releases) ![Static Badge](https://img.shields.io/badge/run_time-node_js-2a682d?logo=nodedotjs&labelColor=black) ![Static Badge](https://img.shields.io/badge/run_time-deno-6affaf?logo=deno&labelColor=black) ![Static Badge](https://img.shields.io/badge/run_time-bun-f672b6?logo=bun&labelColor=black) - -A TypeScript SDK for building reactive web applications with [Datastar](https://github.com/starfederation/datastar). - -Implements the [SDK spec](../README.md) and exposes an abstract ServerSentEventGenerator class that can be used to implement runtime specific classes. NodeJS and web standard runtimes are currently implemented. - -Currently it only exposes an http1 server, if you want http2 I recommend you use a reverse proxy until http2 support is added. - -## Features - -- **Multi-runtime support**: Works with Node.js, Deno, and Bun -- **TypeScript support**: Full type safety and IntelliSense - -## Quick Start - -### Installation & Import - -**Node.js:** -```bash -npm install datastar-sdk -``` -```javascript -import { ServerSentEventGenerator } from "datastar-sdk/node"; -``` - -**Deno:** -```typescript -// No installation needed, import directly from npm -import { ServerSentEventGenerator } from "npm:datastar-sdk/web"; -``` - -**Bun:** -```bash -bun add datastar-sdk -``` -```javascript -import { ServerSentEventGenerator } from "datastar-sdk/web"; -``` - -### Basic Usage - -Here's a simple example in Node showing how to read client signals and send back element patches: - -```javascript -import { ServerSentEventGenerator } from "datastar-sdk/node"; - -// Read signals from the client request -const reader = await ServerSentEventGenerator.readSignals(req); - -if (!reader.success) { - console.error('Error reading signals:', reader.error); - return; -} - -// Stream updates back to the client -ServerSentEventGenerator.stream(req, res, (stream) => { - // Patch signals - stream.patchSignals(JSON.stringify({ foo: reader.signals.foo })); - - // Patch DOM elements - stream.patchElements(`
Hello ${reader.signals.foo}
`); -}); -``` - -See examples for other runtimes below. - -## Examples - -### Runtime-Specific Examples - -| Runtime | Example Location | How to Run | Try Online | -|---------|-----------------|------------|------------| -| **Node.js** | `examples/node/node.js` | [Instructions](examples/node/README.md) | [StackBlitz](https://stackblitz.com/edit/node-datastar) | -| **Deno** | `examples/deno/deno.ts` | [Instructions](examples/deno/README.md) | [Val.town](https://www.val.town/x/eduwass/datastar-deno/code/main.tsx) | -| **Bun** | `examples/bun/bun.ts` | [Instructions](examples/bun/README.md) | [Replit](https://replit.com/@eduwass/Bun-Datastar) | - -Each example creates a simple web server demonstrating: -- Signal handling from client requests -- Element patching for DOM updates -- Real-time communication via Server-Sent Events - -### Running Examples - -1. Clone the repository -2. Navigate to the specific example directory (e.g., `examples/node/`) -3. Follow the instructions in the example's README file -4. Visit `http://localhost:3000` in your browser - -> [!NOTE] -> The `npm run serve-*` and `deno task serve-*` commands in the root directory are for SDK development and testing, not for running the user examples. - -## API Reference - -### ServerSentEventGenerator - -The main class for handling Datastar communication. - -#### Static Methods - -##### `readSignals(request)` -Reads signals from a client request. - -**Parameters:** -- `request`: HTTP request object - -**Returns:** -```typescript -{ - success: boolean; - signals?: Record; - error?: string; -} -``` - -##### `stream(request, response, callback, options?)` -Creates a Server-Sent Event stream for real-time communication. - -**Parameters:** -- `request`: HTTP request object -- `response`: HTTP response object -- `callback`: Function that receives a stream instance -- `options`: Optional configuration object - -**Options:** -```typescript -{ - onError?: (error: Error) => void; - onAbort?: () => void; - keepalive?: boolean; -} -``` - -> [!IMPORTANT] -> When `keepalive: true` is set, the stream will not be closed automatically after the callback finishes. You are responsible for calling `stream.close()` to end the stream. - -#### Stream Instance Methods - -##### `patchSignals(signals, options?)` -Patches signals into the client signal store. - -**Parameters:** -- `signals`: JSON string containing signal data to patch -- `options`: Optional configuration object with `onlyIfMissing` boolean - -**Example:** -```javascript -stream.patchSignals(JSON.stringify({ foo: "bar", count: 42 })); -``` - -##### `patchElements(elements, options?)` -Patches HTML elements into the client DOM. - -**Parameters:** -- `elements`: HTML string containing elements to patch -- `options`: Optional configuration object with `mode` and `selector` - -**Options:** -- `mode`: Patch mode - "outer", "inner", "replace", "prepend", "append", "before", "after", "remove" -- `selector`: CSS selector for targeting elements (required for some modes) -- `useViewTransition`: Whether to use View Transition API - -**Example:** -```javascript -stream.patchElements('
Updated content
'); -``` - -## Development - -### Prerequisites - -To develop or contribute to this SDK, you'll need: -- [Deno](https://deno.land/) (primary development environment) -- [Node.js](https://nodejs.org/) (for Node.js compatibility testing) -- [Bun](https://bun.sh/) (for Bun compatibility testing) - -### Building - -Build the npm package: -```bash -deno run -A build.ts -``` - -The above will pick the version from the [src/consts.ts](src/consts.ts) file. If you want to specify the version, use: -```bash -deno run -A build.ts VERSION -``` - -> [!NOTE] -> **For Developers:** The build process includes test files in the `npm/` directory for local testing, but they are excluded from the published npm package via `.npmignore`. Test files are built into `npm/esm/test/` and `npm/script/test/` directories to support the test scripts (`npm run test-node`, etc.), but these directories are not included when publishing to the npm registry. - -### Testing - -#### Automated Testing - -Run tests for all runtimes: -```bash -# Node.js -npm run test-node - -# Deno -deno task test-deno - -# Bun -bun run test-bun -``` - -#### Manual Testing - -Start a development server: -```bash -# Node.js -npm run serve-node - -# Deno -deno task serve-deno - -# Bun -bun run serve-bun -``` - -Then run the test suite manually: -```bash -cd ../test -./test-all.sh http://127.0.0.1:3000 -``` - -### Project Structure - -``` -typescript/ -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ node/ # Node.js-specific implementation -β”‚ β”œβ”€β”€ web/ # Web standards implementation (Deno/Bun) -β”‚ └── abstract/ # Abstract base classes -β”œβ”€β”€ examples/ -β”‚ β”œβ”€β”€ node/ # Node.js example -β”‚ β”œβ”€β”€ deno/ # Deno example -β”‚ └── bun/ # Bun example -└── test/ # Test suite -``` - -## Runtime Support - -### Node.js -- Import: `datastar-sdk/node` -- Requires: Node.js 18+ -- Uses Node.js-specific HTTP APIs - -### Deno -- Import: `npm:datastar-sdk/web` -- Requires: Deno 1.30+ -- Uses Web Standards APIs - -### Bun -- Import: `datastar-sdk/web` -- Requires: Bun 1.0+ -- Uses Web Standards APIs - -## Custom Implementations - -To support additional runtimes or frameworks, extend the abstract `ServerSentEventGenerator` class from `./src/abstractServerSentEventGenerator.ts`. - -You'll need to implement: -- `constructor`: Initialize runtime-specific components -- `readSignals`: Parse signals from requests -- `stream`: Create SSE streams -- `send`: Send data to clients - -The abstract class provides these public methods: -- `patchElements(elements, options?)`: Patch HTML elements -- `patchSignals(signals, options?)`: Patch signal data \ No newline at end of file diff --git a/sdk/typescript/build.ts b/sdk/typescript/build.ts deleted file mode 100644 index 96ead274e..000000000 --- a/sdk/typescript/build.ts +++ /dev/null @@ -1,68 +0,0 @@ -// ex. scripts/build_npm.ts -import { build, emptyDir } from "@deno/dnt"; -import { VERSION } from "./src/consts.ts"; - -await emptyDir("./npm"); - -await build({ - entryPoints: [ - "./src/node/serverSentEventGenerator.ts", - "./src/web/serverSentEventGenerator.ts", - "./test/node.ts", - ], - outDir: "./npm", - shims: { - // see JS docs for overview and more options - deno: true, - }, - package: { - // package.json properties - name: "@starfederation/datastar-sdk", - version: Deno.args[0] || VERSION, - description: "Cross-runtime Javascript SDK for Datastar", - license: "MIT", - repository: { - type: "git", - url: "git+https://github.com/starfederation/datastar.git", - }, - bugs: { - url: "https://github.com/starfederation/datastar/issues", - }, - exports: { - "./abstractServerSentEventGenerator": { - "types": "./esm/abstractServerSentEventGenerator.d.ts", - "import": "./esm/abstractServerSentEventGenerator.js", - "require": "./script/abstractServerSentEventGenerator.js", - }, - "./consts": { - "types": "./esm/consts.d.ts", - "import": "./esm/consts.js", - "require": "./script/consts.js", - }, - "./types": { - "types": "./esm/types.d.ts", - }, - "./node": { - "types": "./esm/node/serverSentEventGenerator.d.ts", - "import": "./esm/node/serverSentEventGenerator.js", - "require": "./script/node/serverSentEventGenerator.js", - }, - "./web": { - "types": "./esm/web/serverSentEventGenerator.d.ts", - "import": "./esm/web/serverSentEventGenerator.js", - "require": "./script/web/serverSentEventGenerator.js", - }, - ".": { - "types": "./esm/node/serverSentEventGenerator.d.ts", - "import": "./esm/node/serverSentEventGenerator.js", - "require": "./script/node/serverSentEventGenerator.js", - }, - }, - }, - postBuild() { - // steps to run after building and before running the tests - //Deno.copyFileSync("LICENSE", "npm/LICENSE"); - Deno.copyFileSync("README.md", "npm/README.md"); - }, - rootTestDir: "./src", -}); \ No newline at end of file diff --git a/sdk/typescript/bun.lockb b/sdk/typescript/bun.lockb deleted file mode 100755 index 367d1244d..000000000 Binary files a/sdk/typescript/bun.lockb and /dev/null differ diff --git a/sdk/typescript/deno.json b/sdk/typescript/deno.json deleted file mode 100644 index ff8950980..000000000 --- a/sdk/typescript/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "imports": { - "@deno/dnt": "jsr:@deno/dnt@^0.41.3" - }, - "tasks": { - "check": "deno lint && deno check src/node/node.ts && deno check src/web/deno.ts && bun run --check src/web/bun.ts", - "build": "deno run -A build.ts", - "serve-deno": "deno run -A src/web/deno.ts", - "test-deno": "./test/test-deno.sh" - }, - "nodeModulesDir": "auto" -} diff --git a/sdk/typescript/deno.lock b/sdk/typescript/deno.lock deleted file mode 100644 index 1088d31e5..000000000 --- a/sdk/typescript/deno.lock +++ /dev/null @@ -1,178 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@david/code-block-writer@^13.0.2": "13.0.3", - "jsr:@deno/cache-dir@~0.10.3": "0.10.3", - "jsr:@deno/dnt@~0.41.3": "0.41.3", - "jsr:@deno/graph@~0.73.1": "0.73.1", - "jsr:@std/assert@0.223": "0.223.0", - "jsr:@std/assert@0.226": "0.226.0", - "jsr:@std/bytes@0.223": "0.223.0", - "jsr:@std/fmt@0.223": "0.223.0", - "jsr:@std/fmt@1": "1.0.4", - "jsr:@std/fs@0.223": "0.223.0", - "jsr:@std/fs@1": "1.0.9", - "jsr:@std/fs@~0.229.3": "0.229.3", - "jsr:@std/io@0.223": "0.223.0", - "jsr:@std/path@0.223": "0.223.0", - "jsr:@std/path@1": "1.0.8", - "jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1", - "jsr:@std/path@^1.0.8": "1.0.8", - "jsr:@std/path@~0.225.2": "0.225.2", - "jsr:@ts-morph/bootstrap@0.24": "0.24.0", - "jsr:@ts-morph/common@0.24": "0.24.0", - "npm:@types/node@^22.10.2": "22.10.5", - "npm:datastar-sdk@*": "1.0.0-beta.11", - "npm:deepmerge-ts@^7.1.4": "7.1.4", - "npm:type-fest@^4.32.0": "4.32.0", - "npm:typescript@~5.6.2": "5.6.3" - }, - "jsr": { - "@david/code-block-writer@13.0.3": { - "integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f" - }, - "@deno/cache-dir@0.10.3": { - "integrity": "eb022f84ecc49c91d9d98131c6e6b118ff63a29e343624d058646b9d50404776", - "dependencies": [ - "jsr:@deno/graph", - "jsr:@std/fmt@0.223", - "jsr:@std/fs@0.223", - "jsr:@std/io", - "jsr:@std/path@0.223" - ] - }, - "@deno/dnt@0.41.3": { - "integrity": "b2ef2c8a5111eef86cb5bfcae103d6a2938e8e649e2461634a7befb7fc59d6d2", - "dependencies": [ - "jsr:@david/code-block-writer", - "jsr:@deno/cache-dir", - "jsr:@std/fmt@1", - "jsr:@std/fs@1", - "jsr:@std/path@1", - "jsr:@ts-morph/bootstrap" - ] - }, - "@deno/graph@0.73.1": { - "integrity": "cd69639d2709d479037d5ce191a422eabe8d71bb68b0098344f6b07411c84d41" - }, - "@std/assert@0.223.0": { - "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" - }, - "@std/assert@0.226.0": { - "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3" - }, - "@std/bytes@0.223.0": { - "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" - }, - "@std/fmt@0.223.0": { - "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208" - }, - "@std/fmt@1.0.4": { - "integrity": "e14fe5bedee26f80877e6705a97a79c7eed599e81bb1669127ef9e8bc1e29a74" - }, - "@std/fs@0.223.0": { - "integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c" - }, - "@std/fs@0.229.3": { - "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb", - "dependencies": [ - "jsr:@std/path@1.0.0-rc.1" - ] - }, - "@std/fs@1.0.9": { - "integrity": "3eef7e3ed3d317b29432c7dcb3b20122820dbc574263f721cb0248ad91bad890", - "dependencies": [ - "jsr:@std/path@^1.0.8" - ] - }, - "@std/io@0.223.0": { - "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", - "dependencies": [ - "jsr:@std/assert@0.223", - "jsr:@std/bytes" - ] - }, - "@std/path@0.223.0": { - "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989", - "dependencies": [ - "jsr:@std/assert@0.223" - ] - }, - "@std/path@0.225.2": { - "integrity": "0f2db41d36b50ef048dcb0399aac720a5348638dd3cb5bf80685bf2a745aa506", - "dependencies": [ - "jsr:@std/assert@0.226" - ] - }, - "@std/path@1.0.0-rc.1": { - "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" - }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" - }, - "@ts-morph/bootstrap@0.24.0": { - "integrity": "a826a2ef7fa8a7c3f1042df2c034d20744d94da2ee32bf29275bcd4dffd3c060", - "dependencies": [ - "jsr:@ts-morph/common" - ] - }, - "@ts-morph/common@0.24.0": { - "integrity": "12b625b8e562446ba658cdbe9ad77774b4bd96b992ae8bd34c60dbf24d06c1f3", - "dependencies": [ - "jsr:@std/fs@~0.229.3", - "jsr:@std/path@~0.225.2" - ] - } - }, - "npm": { - "@types/node@22.10.5": { - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", - "dependencies": [ - "undici-types" - ] - }, - "datastar-sdk@1.0.0-beta.11": { - "integrity": "sha512-rIhUd4Myz5Cvf3DI6d8Hgk9J3jFnuqkHyRl5sdWMlXKnNJesP4cthXl8iwA9/KcEKVWIn3fjCu852DVc6D4npQ==", - "dependencies": [ - "deepmerge-ts", - "type-fest" - ] - }, - "deepmerge-ts@7.1.4": { - "integrity": "sha512-fxqo6nHGQ9zOVgI4KXqtWXJR/yCLtC7aXIVq+6jc8tHPFUxlFmuUcm2kC4vztQ+LJxQ3gER/XAWearGYQ8niGA==" - }, - "type-fest@4.32.0": { - "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==" - }, - "typescript@5.6.3": { - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "bin": true - }, - "undici-types@6.20.0": { - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - } - }, - "remote": { - "https://deno.land/std@0.140.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", - "https://deno.land/std@0.140.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", - "https://deno.land/std@0.140.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", - "https://deno.land/std@0.140.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", - "https://deno.land/std@0.140.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", - "https://deno.land/std@0.140.0/async/mod.ts": "6e42e275b44367361a81842dd1a789c55ab206d7c8a877d7163ab5c460625be6", - "https://deno.land/std@0.140.0/async/mux_async_iterator.ts": "f4d1d259b0c694d381770ddaaa4b799a94843eba80c17f4a2ec2949168e52d1e", - "https://deno.land/std@0.140.0/async/pool.ts": "97b0dd27c69544e374df857a40902e74e39532f226005543eabacb551e277082", - "https://deno.land/std@0.140.0/async/tee.ts": "1341feb1f5b1a96f8628d0f8fc07d8c43d3813423f18a63bf1b4785568d21b1f", - "https://deno.land/std@0.140.0/http/server.ts": "3da75405704bebcf212a55966a68a489f7e094ba52b5d38f181fe0ef8461a55d" - }, - "workspace": { - "dependencies": [ - "jsr:@deno/dnt@~0.41.3" - ], - "packageJson": { - "dependencies": [ - "npm:@types/node@^22.10.2", - "npm:typescript@~5.6.2" - ] - } - } -} diff --git a/sdk/typescript/examples/bun/.gitignore b/sdk/typescript/examples/bun/.gitignore deleted file mode 100644 index a14702c40..000000000 --- a/sdk/typescript/examples/bun/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/sdk/typescript/examples/bun/README.md b/sdk/typescript/examples/bun/README.md deleted file mode 100644 index ec28140ec..000000000 --- a/sdk/typescript/examples/bun/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Bun example - -Run: -```bash -bun install -bun start -``` diff --git a/sdk/typescript/examples/bun/bun.lock b/sdk/typescript/examples/bun/bun.lock deleted file mode 100644 index 30d1a1614..000000000 --- a/sdk/typescript/examples/bun/bun.lock +++ /dev/null @@ -1,34 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "bun", - "dependencies": { - "datastar-sdk": "^1.0.0-beta.11", - }, - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="], - - "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="], - - "bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="], - - "datastar-sdk": ["datastar-sdk@1.0.0-beta.11", "", { "dependencies": { "deepmerge-ts": "*", "type-fest": "*" } }, "sha512-rIhUd4Myz5Cvf3DI6d8Hgk9J3jFnuqkHyRl5sdWMlXKnNJesP4cthXl8iwA9/KcEKVWIn3fjCu852DVc6D4npQ=="], - - "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - } -} diff --git a/sdk/typescript/examples/bun/bun.ts b/sdk/typescript/examples/bun/bun.ts deleted file mode 100644 index e3044789e..000000000 --- a/sdk/typescript/examples/bun/bun.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ServerSentEventGenerator } from "datastar-sdk/web"; - -const server = Bun.serve({ - port: 3000, - routes: { - "/": () => { - return new Response( - `
Hello
`, - { - headers: { "Content-Type": "text/html" }, - }, - ); - }, - "/merge": async (req: Request) => { - const reader = await ServerSentEventGenerator.readSignals(req); - - if (!reader.success) { - console.error("Error while reading signals", reader.error); - - return new Response(`Error while reading signals`, { - headers: { "Content-Type": "text/html" }, - }); - } - - if (!("foo" in reader.signals)) { - console.error("The foo signal is not present"); - - return new Response("The foo signal is not present", { - headers: { "Content-Type": "text/html" }, - }); - } - - return ServerSentEventGenerator.stream((stream) => { - stream.patchElements( - `
Hello ${reader.signals.foo}
`, - ); - }); - }, - }, - async fetch(req: Request) { - return new Response(`Path not found: ${req.url}`, { - headers: { "Content-Type": "text/html" }, - }); - }, -}); - -console.log("Server is running on http://localhost:" + server.port); \ No newline at end of file diff --git a/sdk/typescript/examples/bun/package.json b/sdk/typescript/examples/bun/package.json deleted file mode 100644 index 4c009dfcc..000000000 --- a/sdk/typescript/examples/bun/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "bun", - "module": "index.ts", - "type": "module", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "datastar-sdk": "^1.0.0-RC.1" - }, - "scripts": { - "start": "bun bun.ts" - } -} diff --git a/sdk/typescript/examples/bun/tsconfig.json b/sdk/typescript/examples/bun/tsconfig.json deleted file mode 100644 index bfa0fead5..000000000 --- a/sdk/typescript/examples/bun/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/sdk/typescript/examples/deno/README.md b/sdk/typescript/examples/deno/README.md deleted file mode 100644 index 70013b196..000000000 --- a/sdk/typescript/examples/deno/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Deno example - -Run: -```shell -$ deno run -A deno.ts -``` \ No newline at end of file diff --git a/sdk/typescript/examples/deno/deno.ts b/sdk/typescript/examples/deno/deno.ts deleted file mode 100644 index 8bcb7839a..000000000 --- a/sdk/typescript/examples/deno/deno.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; -import { ServerSentEventGenerator } from "npm:datastar-sdk/web"; - -serve(async (req: Request) => { - const url = new URL(req.url); - - if (url.pathname === "/") { - return new Response( - `
Hello
`, - { - headers: { "Content-Type": "text/html" }, - }, - ); - } else if (url.pathname.includes("/merge")) { - const reader = await ServerSentEventGenerator.readSignals(req); - - if (!reader.success) { - console.error("Error while reading signals", reader.error); - - return new Response(`Error while reading signals`, { - headers: { "Content-Type": "text/html" }, - }); - } - - if (!("foo" in reader.signals)) { - console.error("The foo signal is not present"); - - return new Response("The foo signal is not present", { - headers: { "Content-Type": "text/html" }, - }); - } - - return ServerSentEventGenerator.stream((stream) => { - stream.patchElements( - `
Hello ${reader.signals.foo}
`, - ); - }); - } - - return new Response(`Path not found: ${req.url}`, { - headers: { "Content-Type": "text/html" }, - }); -}); diff --git a/sdk/typescript/examples/node/README.md b/sdk/typescript/examples/node/README.md deleted file mode 100644 index 9728b6a97..000000000 --- a/sdk/typescript/examples/node/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Node example - -Run: -```shell -$ npm install -$ npm start -``` \ No newline at end of file diff --git a/sdk/typescript/examples/node/node.js b/sdk/typescript/examples/node/node.js deleted file mode 100644 index 081da5bc3..000000000 --- a/sdk/typescript/examples/node/node.js +++ /dev/null @@ -1,42 +0,0 @@ -import { createServer } from "node:http"; -import { ServerSentEventGenerator } from "datastar-sdk/node"; - -const hostname = "127.0.0.1"; -const port = 3000; - -const server = createServer(async (req, res) => { - if (req.url === "/") { - const headers = new Headers({ "Content-Type": "text/html" }); - res.setHeaders(headers); - res.end( - `
Hello
`, - ); - } else if (req.url?.includes("/merge")) { - const reader = await ServerSentEventGenerator.readSignals(req); - - if (!reader.success) { - console.error("Error while reading signals", reader.error); - res.end(`Error while reading signals`); - return; - } - - if (!("foo" in reader.signals)) { - console.error("The foo signal is not present"); - - res.end("The foo signal is not present"); - return; - } - - ServerSentEventGenerator.stream(req, res, (stream) => { - stream.patchElements( - `
Hello ${reader.signals.foo}
`, - ); - }); - } else { - res.end("Path not found"); - } -}); - -server.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); -}); diff --git a/sdk/typescript/examples/node/package-lock.json b/sdk/typescript/examples/node/package-lock.json deleted file mode 100644 index d3a1660d5..000000000 --- a/sdk/typescript/examples/node/package-lock.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "node", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "node", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "datastar-sdk": "^1.0.0-beta.11" - } - }, - "node_modules/datastar-sdk": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/datastar-sdk/-/datastar-sdk-1.0.0-beta.11.tgz", - "integrity": "sha512-rIhUd4Myz5Cvf3DI6d8Hgk9J3jFnuqkHyRl5sdWMlXKnNJesP4cthXl8iwA9/KcEKVWIn3fjCu852DVc6D4npQ==", - "license": "MIT", - "dependencies": { - "deepmerge-ts": "*", - "type-fest": "*" - } - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/sdk/typescript/examples/node/package.json b/sdk/typescript/examples/node/package.json deleted file mode 100644 index 776003d68..000000000 --- a/sdk/typescript/examples/node/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "node", - "version": "1.0.0", - "main": "node.js", - "scripts": { - "start": "node node.js" - }, - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "datastar-sdk": "^1.0.0-RC.1" - } -} diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json deleted file mode 100644 index 1ec34c091..000000000 --- a/sdk/typescript/package-lock.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "name": "@starfederation/datastar-sdk", - "version": "1.0.0-RC.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@starfederation/datastar-sdk", - "version": "1.0.0-RC.1", - "license": "MIT", - "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "~5.6.2" - } - }, - "node_modules/.deno/@types+bun@1.2.16": { - "extraneous": true - }, - "node_modules/.deno/@types+node@22.10.5": { - "extraneous": true - }, - "node_modules/.deno/bun-types@1.2.16": { - "extraneous": true - }, - "node_modules/.deno/datastar-sdk@1.0.0-beta.11": { - "extraneous": true - }, - "node_modules/.deno/deepmerge-ts@7.1.4/node_modules/deepmerge-ts": { - "version": "7.1.4", - "extraneous": true, - "license": "BSD-3-Clause", - "devDependencies": { - "@commitlint/cli": "19.5.0", - "@commitlint/config-conventional": "19.5.0", - "@cspell/dict-cryptocurrencies": "5.0.3", - "@eslint/compat": "1.2.2", - "@rebeccastevens/eslint-config": "3.3.3", - "@rollup/plugin-replace": "6.0.1", - "@rollup/plugin-typescript": "12.1.1", - "@sebbo2002/semantic-release-jsr": "2.0.1", - "@semantic-release/changelog": "6.0.3", - "@semantic-release/commit-analyzer": "13.0.0", - "@semantic-release/git": "10.0.1", - "@semantic-release/github": "11.0.0", - "@semantic-release/npm": "12.0.1", - "@semantic-release/release-notes-generator": "14.0.1", - "@stylistic/eslint-plugin": "2.9.0", - "@types/lodash": "4.17.13", - "@types/node": "20.17.5", - "@typescript-eslint/eslint-plugin": "8.8.1", - "@typescript-eslint/parser": "8.8.1", - "@vitest/coverage-v8": "2.1.2", - "@vitest/eslint-plugin": "1.1.7", - "commitizen": "4.3.1", - "cspell": "8.15.0", - "cz-conventional-changelog": "3.3.0", - "eslint": "9.12.0", - "eslint-config-prettier": "9.1.0", - "eslint-flat-config-utils": "0.4.0", - "eslint-import-resolver-typescript": "3.6.3", - "eslint-merge-processors": "0.1.0", - "eslint-plugin-eslint-comments": "3.2.0", - "eslint-plugin-format": "0.1.2", - "eslint-plugin-functional": "7.1.0", - "eslint-plugin-import-x": "4.4.0", - "eslint-plugin-jsdoc": "50.4.3", - "eslint-plugin-jsonc": "2.16.0", - "eslint-plugin-markdown": "5.1.0", - "eslint-plugin-n": "17.12.0", - "eslint-plugin-no-only-tests": "3.3.0", - "eslint-plugin-optimize-regex": "1.2.1", - "eslint-plugin-prettier": "5.2.1", - "eslint-plugin-promise": "7.1.0", - "eslint-plugin-regexp": "2.6.0", - "eslint-plugin-sonarjs": "2.0.4", - "eslint-plugin-unicorn": "56.0.0", - "eslint-plugin-yml": "1.15.0", - "husky": "9.1.6", - "jsonc-eslint-parser": "2.4.0", - "knip": "5.36.1", - "lint-staged": "15.2.10", - "lodash": "4.17.21", - "markdownlint-cli2": "0.14.0", - "prettier": "3.3.3", - "prettier-plugin-packagejson": "2.5.3", - "rimraf": "6.0.1", - "rollup": "4.24.3", - "rollup-plugin-deassert": "1.3.0", - "rollup-plugin-dts-bundle-generator": "1.4.0", - "semantic-release": "24.1.2", - "tsc-files": "1.1.4", - "tsd": "0.31.2", - "typescript": "5.6.3", - "vite-tsconfig-paths": "5.0.1", - "vitest": "2.1.2", - "yaml-eslint-parser": "1.2.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/.deno/type-fest@4.32.0/node_modules/type-fest": { - "version": "4.32.0", - "extraneous": true, - "license": "(MIT OR CC0-1.0)", - "devDependencies": { - "expect-type": "^1.1.0", - "npm-run-all2": "^7.0.1", - "tsd": "^0.31.2", - "typescript": "~5.7.2", - "xo": "^0.59.3" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/.deno/typescript@5.6.3": { - "dev": true - }, - "node_modules/.deno/typescript@5.6.3/node_modules/typescript": { - "version": "5.6.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/.deno/undici-types@6.20.0/node_modules/undici-types": { - "version": "6.20.0", - "extraneous": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.15.32", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/typescript": { - "resolved": "node_modules/.deno/typescript@5.6.3/node_modules/typescript", - "link": true - }, - "node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - } - } -} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json deleted file mode 100644 index 9ecbe1e4a..000000000 --- a/sdk/typescript/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "@starfederation/datastar-sdk", - "version": "1.0.0-RC.1", - "description": "TypeScript SDK for Datastar", - "scripts": { - "check": "deno lint && deno check src/node/node.ts && deno check src/web/deno.ts && bun run --check src/web/bun.ts", - "build": "deno run -A build.ts", - "serve-deno": "deno run -A test/deno.ts", - "serve-bun": "bun run test/bun.ts", - "serve-node": "deno run -A build.ts && node npm/esm/test/node.js", - "test-node": "./test/test-node.sh", - "test-deno": "./test/test-deno.sh", - "test-bun": "./test/test-bun.sh" - }, - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/starfederation/datastar.git" - }, - "author": "Patrick Marchand", - "license": "MIT", - "bugs": { - "url": "https://github.com/starfederation/datastar/issues" - }, - "homepage": "https://github.com/starfederation/datastar#readme", - "files": [ - "npm/script/src/", - "npm/script/package.json", - "npm/esm/src/", - "npm/esm/package.json", - "npm/package.json", - "npm/README.md", - "README.md" - ], - "dependencies": {}, - "devDependencies": { - "typescript": "~5.6.2", - "@types/node": "^22.10.2" - } -} diff --git a/sdk/typescript/pnpm-lock.yaml b/sdk/typescript/pnpm-lock.yaml deleted file mode 100644 index c3198d111..000000000 --- a/sdk/typescript/pnpm-lock.yaml +++ /dev/null @@ -1,65 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@types/node": - specifier: ^22.10.2 - version: 22.10.2 - deepmerge-ts: - specifier: ^7.1.4 - version: 7.1.4 - type-fest: - specifier: ^4.32.0 - version: 4.32.0 - devDependencies: - typescript: - specifier: ~5.6.2 - version: 5.6.3 - -packages: - "@types/node@22.10.2": - resolution: { - integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==, - } - - deepmerge-ts@7.1.4: - resolution: { - integrity: sha512-fxqo6nHGQ9zOVgI4KXqtWXJR/yCLtC7aXIVq+6jc8tHPFUxlFmuUcm2kC4vztQ+LJxQ3gER/XAWearGYQ8niGA==, - } - engines: { node: ">=16.0.0" } - - type-fest@4.32.0: - resolution: { - integrity: sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==, - } - engines: { node: ">=16" } - - typescript@5.6.3: - resolution: { - integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==, - } - engines: { node: ">=14.17" } - hasBin: true - - undici-types@6.20.0: - resolution: { - integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==, - } - -snapshots: - "@types/node@22.10.2": - dependencies: - undici-types: 6.20.0 - - deepmerge-ts@7.1.4: {} - - type-fest@4.32.0: {} - - typescript@5.6.3: {} - - undici-types@6.20.0: {} diff --git a/sdk/typescript/src/abstractServerSentEventGenerator.ts b/sdk/typescript/src/abstractServerSentEventGenerator.ts deleted file mode 100644 index bbb4bf33c..000000000 --- a/sdk/typescript/src/abstractServerSentEventGenerator.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { - DatastarEventOptions, - DefaultMapping, - EventType, - PatchElementsOptions, - PatchSignalsOptions, - Jsonifiable, - ElementPatchMode, -} from "./types.ts"; - -import { - DatastarDatalineElements, - DatastarDatalinePatchMode, - DatastarDatalineSelector, - DatastarDatalineSignals, - DefaultSseRetryDurationMs, - ElementPatchModes, -} from "./consts.ts"; - -/** - * Abstract ServerSentEventGenerator class, responsible for initializing and handling - * server-sent events (SSE) as well as reading signals sent by the client. - * - * The concrete implementation must override the send and constructor methods as well - * as implement readSignals and stream static methods. - */ -export abstract class ServerSentEventGenerator { - protected constructor() {} - - /** - * Validates that the provided mode is a valid ElementPatchMode. - * @param mode - The mode to validate - * @throws {Error} If the mode is invalid - */ - private validateElementPatchMode(mode: string): asserts mode is ElementPatchMode { - if (!ElementPatchModes.includes(mode as ElementPatchMode)) { - throw new Error(`Invalid ElementPatchMode: "${mode}". Valid modes are: ${ElementPatchModes.join(', ')}`); - } - } - - - /** - * Validates required parameters are not empty or undefined. - * @param value - The value to validate - * @param paramName - The parameter name for error messages - * @throws {Error} If the value is empty or undefined - */ - private validateRequired(value: string | undefined, paramName: string): asserts value is string { - if (!value || value.trim() === '') { - throw new Error(`${paramName} is required and cannot be empty`); - } - } - - /** - * Sends a server-sent event (SSE) to the client. - * - * Runtimes should override this method by calling the parent function - * with `super.send(event, dataLines, options)`. That will return all the - * datalines as an array of strings that should be streamed to the client. - * - * @param eventType - The type of the event. - * @param dataLines - Lines of data to send. - * @param [sendOptions] - Additional options for sending events. - */ - protected send( - event: EventType, - dataLines: string[], - options: DatastarEventOptions, - ): string[] { - const { eventId, retryDuration } = options || {}; - - const typeLine = [`event: ${event}\n`]; - const idLine = eventId ? [`id: ${eventId}\n`] : []; - const retryLine = !retryDuration || retryDuration === 1000 ? [] : [ - `retry: ${retryDuration ?? DefaultSseRetryDurationMs}\n`, - ]; - - return typeLine.concat( - idLine, - retryLine, - dataLines.map((data) => { - return `data: ${data}\n`; - }), - ["\n"], - ); - } - - private eachNewlineIsADataLine(prefix: string, data: string) { - return data.split("\n").map((line) => { - return `${prefix} ${line}`; - }); - } - - private eachOptionIsADataLine( - options: Record, - ): string[] { - return Object.keys(options).filter((key) => { - return !this.hasDefaultValue(key, options[key as keyof typeof options]); - }).flatMap((key) => { - return this.eachNewlineIsADataLine( - key, - options[key as keyof typeof options]!.toString(), - ); - }); - } - - private hasDefaultValue(key: string, val: unknown): boolean { - if (key in DefaultMapping) { - return val === (DefaultMapping as Record)[key]; - } - - return false; - } - - /** - * Patches HTML elements into the DOM. - * - * @param elements - HTML elements that will be patched. - * @param [options] - Additional options for patching. - * @throws {Error} If validation fails - */ - public patchElements( - elements: string, - options?: PatchElementsOptions, - ): ReturnType { - const { eventId, retryDuration, ...renderOptions } = options || - {} as Partial; - - // Validate patch mode if provided - const patchMode = (renderOptions as Record)[DatastarDatalinePatchMode] as string; - if (patchMode) { - this.validateElementPatchMode(patchMode); - } - - // Check if we're in remove mode with a selector - const selector = (renderOptions as Record)[DatastarDatalineSelector] as string; - const isRemoveWithSelector = patchMode === 'remove' && selector; - - // Validate required parameters - elements only required when not removing with selector - if (!isRemoveWithSelector) { - this.validateRequired(elements, 'elements'); - } - - // Per spec: If no selector specified, elements must have IDs (this validation would be complex - // and is better handled client-side, but we ensure elements is not empty) - if (!selector && patchMode === 'remove') { - // For remove mode, elements parameter may be omitted when selector is supplied - // but since we have no selector, we need elements with IDs - if (!elements || elements.trim() === '') { - throw new Error('For remove mode without selector, elements parameter with IDs is required'); - } - } - - // Build data lines - skip elements data line if empty in remove mode with selector - const dataLines = this.eachOptionIsADataLine(renderOptions); - if (!isRemoveWithSelector || elements.trim() !== '') { - dataLines.push(...this.eachNewlineIsADataLine(DatastarDatalineElements, elements)); - } - - return this.send("datastar-patch-elements", dataLines, { - eventId, - retryDuration, - }); - } - - /** - * Patches signals into the signal store. - * - * @param signals - JSON string containing signal data to patch. - * @param [options] - Additional options for patching. - * @throws {Error} If validation fails - */ - public patchSignals( - signals: string, - options?: PatchSignalsOptions, - ): ReturnType { - // Validate required parameters - this.validateRequired(signals, 'signals'); - - - const { eventId, retryDuration, ...eventOptions } = options || - {} as Partial; - - const dataLines = this.eachOptionIsADataLine(eventOptions) - .concat(this.eachNewlineIsADataLine(DatastarDatalineSignals, signals)); - - return this.send("datastar-patch-signals", dataLines, { - eventId, - retryDuration, - }); - } -} diff --git a/sdk/typescript/src/consts.ts b/sdk/typescript/src/consts.ts deleted file mode 100644 index 784416821..000000000 --- a/sdk/typescript/src/consts.ts +++ /dev/null @@ -1,73 +0,0 @@ -// This is auto-generated by Datastar. DO NOT EDIT. - -export const DATASTAR = "datastar" as const; -export const DATASTAR_REQUEST = "Datastar-Request"; -export const VERSION = "1.0.0-RC.12"; - -// #region Defaults - -// #region Default durations - -// The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. -export const DefaultSseRetryDurationMs = 1000; - -// #endregion - - -// #region Default booleans - -// Should elements be patched using the ViewTransition API? -export const DefaultElementsUseViewTransitions = false; - -// Should a given set of signals patch if they are missing? -export const DefaultPatchSignalsOnlyIfMissing = false; - -// #endregion - -// #region Datalines - -export const DatastarDatalineSelector = "selector" -export const DatastarDatalinePatchMode = "mode" -export const DatastarDatalineElements = "elements" -export const DatastarDatalineUseViewTransition = "useViewTransition" -export const DatastarDatalineSignals = "signals" -export const DatastarDatalineOnlyIfMissing = "onlyIfMissing" -export const DatastarDatalinePaths = "paths" -// #endregion - - -// #region Enums - -// The mode in which an element is patched into the DOM. -export const ElementPatchModes = [ -// Morph entire element, preserving state - "outer", -// Morph inner HTML only, preserving state - "inner", -// Replace entire element, reset state - "replace", -// Insert at beginning inside target - "prepend", -// Insert at end inside target - "append", -// Insert before target element - "before", -// Insert after target element - "after", -// Remove target element from DOM - "remove", -] as const; - -// Default value for ElementPatchMode -export const DefaultElementPatchMode = "outer"; - -// The type protocol on top of SSE which allows for core pushed based communication between the server and the client. -export const EventTypes = [ -// An event for patching HTML elements into the DOM. - "datastar-patch-elements", -// An event for patching signals. - "datastar-patch-signals", -] as const; -// #endregion - -// #endregion \ No newline at end of file diff --git a/sdk/typescript/src/node/serverSentEventGenerator.ts b/sdk/typescript/src/node/serverSentEventGenerator.ts deleted file mode 100644 index 13f43f6ce..000000000 --- a/sdk/typescript/src/node/serverSentEventGenerator.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { DatastarEventOptions, EventType, sseHeaders, StreamOptions, Jsonifiable } from "../types.ts"; - -import { ServerSentEventGenerator as AbstractSSEGenerator } from "../abstractServerSentEventGenerator.ts"; - -import { IncomingMessage, ServerResponse } from "node:http"; -import process from "node:process"; - -function isRecord(obj: unknown): obj is Record { - return typeof obj === "object" && obj !== null; -} - -/** - * ServerSentEventGenerator class, responsible for initializing and handling - * server-sent events (SSE) as well as reading signals sent by the client. - * Cannot be instantiated directly, you must use the stream static method. - */ -export class ServerSentEventGenerator extends AbstractSSEGenerator { - protected req: IncomingMessage; - protected res: ServerResponse; - - protected constructor(req: IncomingMessage, res: ServerResponse) { - super(); - this.req = req; - this.res = res; - - this.res.writeHead(200, sseHeaders); - // Per spec: "Should flush response immediately to prevent timeouts" - this.res.flushHeaders(); - } - - /** - * Initializes the server-sent event generator and executes the onStart callback. - * - * @param req - The NodeJS request object. - * @param res - The NodeJS response object. - * @param onStart - A function that will be passed the initialized ServerSentEventGenerator class as it's first parameter. - * @param options? - An object that can contain onError and onCancel callbacks as well as a keepalive boolean. - * The onAbort callback will be called whenever the request is aborted - * - * The onError callback will be called whenever an error is met. If provided, the onAbort callback will also be executed. - * If an onError callback is not provided, then the stream will be ended and the error will be thrown up. - * - * The stream is always closed after the onStart callback ends. - * If onStart is non blocking, but you still need the stream to stay open after it is called, - * then the keepalive option will maintain it open until the request is aborted by the client. - */ - static async stream( - req: IncomingMessage, - res: ServerResponse, - onStart: (stream: ServerSentEventGenerator) => Promise | void, - options?: StreamOptions, - ): Promise { - const generator = new ServerSentEventGenerator(req, res); - - req.on("close", async () => { - const onAbort = options?.onAbort ? options.onAbort() : null; - if (onAbort instanceof Promise) await onAbort; - - res.end(); - }); - - try { - const stream = onStart(generator); - if (stream instanceof Promise) await stream; - if (!options?.keepalive) { - res.end(); - } - } catch (error: unknown) { - const onAbort = options?.onAbort ? options.onAbort() : null; - if (onAbort instanceof Promise) await onAbort; - - if (options?.onError) { - const onError = options.onError(error); - if (onError instanceof Promise) await onError; - res.end(); - } else { - res.end(); - throw error; - } - } - } - - protected override send( - event: EventType, - dataLines: string[], - options: DatastarEventOptions, - ): string[] { - const eventLines = super.send(event, dataLines, options); - - // Join all lines and write as a single chunk to avoid extra newlines - const eventText = eventLines.join(''); - this.res.write(eventText); - - return eventLines; - } - - /** - * Reads client sent signals based on HTTP methods - * - * @params request - The NodeJS Request object. - * - * @returns An object containing a success boolean and either the client's signals or an error message. - */ - static async readSignals(request: IncomingMessage): Promise< - | { success: true; signals: Record } - | { success: false; error: string } - > { - if (request.method === "GET") { - const url = new URL( - `http://${process.env.HOST ?? "localhost"}${request.url}`, - ); - const params = url.searchParams; - - try { - if (params.has("datastar")) { - const signals = JSON.parse(params.get("datastar")!); - if (isRecord(signals)) { - return { success: true, signals }; - } else throw new Error("Datastar param is not a record"); - } else throw new Error("No datastar object in request"); - } catch (e: unknown) { - if (isRecord(e) && "message" in e && typeof e.message === "string") { - return { success: false, error: e.message }; - } else {return { - success: false, - error: "unknown error when parsing request", - };} - } - } - const body = await new Promise((resolve, _) => { - let chunks = ""; - request.on("data", (chunk) => { - chunks += chunk; - }); - request.on("end", () => { - resolve(chunks); - }); - }); - - try { - if (typeof body !== "string") throw Error("body was not a string"); - const parsedBody = JSON.parse(body); - - if (isRecord(parsedBody)) { - return { success: true, signals: parsedBody }; - } else { - throw new Error("Parsed JSON body is not of type record"); - } - } catch (e: unknown) { - if (isRecord(e) && "message" in e && typeof e.message === "string") { - return { success: false, error: e.message }; - } else { - return { - success: false, - error: "unknown error when parsing request", - }; - } - } - } -} diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/src/types.ts deleted file mode 100644 index 20e6263a8..000000000 --- a/sdk/typescript/src/types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - DatastarDatalineElements, - DatastarDatalinePatchMode, - DatastarDatalineOnlyIfMissing, - DatastarDatalineSelector, - DatastarDatalineSignals, - DatastarDatalineUseViewTransition, - DefaultElementPatchMode, - DefaultElementsUseViewTransitions, - DefaultPatchSignalsOnlyIfMissing, - EventTypes, - ElementPatchModes, -} from "./consts.ts"; - -// Simple Jsonifiable type definition to replace npm:type-fest dependency -export type Jsonifiable = - | string - | number - | boolean - | null - | undefined - | Jsonifiable[] - | { [key: string]: Jsonifiable }; - -export type ElementPatchMode = typeof ElementPatchModes[number]; -export type EventType = typeof EventTypes[number]; - -export type StreamOptions = Partial<{ - onError: (error: unknown) => Promise | void; - onAbort: (reason?: string) => Promise | void; - responseInit: Record; - keepalive: boolean; -}> - -export interface DatastarEventOptions { - eventId?: string; - retryDuration?: number; -} - -export interface ElementOptions extends DatastarEventOptions { - [DatastarDatalineUseViewTransition]?: boolean; -} - -export interface PatchElementsOptions extends ElementOptions { - [DatastarDatalinePatchMode]?: ElementPatchMode; - [DatastarDatalineSelector]?: string; -} - -export interface patchElementsEvent { - event: "datastar-patch-elements"; - options: PatchElementsOptions; - [DatastarDatalineElements]: string; -} - -export interface PatchSignalsOptions extends DatastarEventOptions { - [DatastarDatalineOnlyIfMissing]?: boolean; -} - -export interface patchSignalsEvent { - event: "datastar-patch-signals"; - options: PatchSignalsOptions; - [DatastarDatalineSignals]: Record; -} - -export const sseHeaders = { - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "Content-Type": "text/event-stream", -} as const; - -export type MultilineDatalinePrefix = - | typeof DatastarDatalineElements - | typeof DatastarDatalineSignals; - -export type DatastarEventOptionsUnion = - | PatchElementsOptions - | ElementOptions - | PatchSignalsOptions - | DatastarEventOptions; - -export type DatastarEvent = - | patchElementsEvent - | patchSignalsEvent; - -export const DefaultMapping = { - [DatastarDatalinePatchMode]: DefaultElementPatchMode, - [DatastarDatalineUseViewTransition]: DefaultElementsUseViewTransitions, - [DatastarDatalineOnlyIfMissing]: DefaultPatchSignalsOnlyIfMissing, -} as const; diff --git a/sdk/typescript/src/web/serverSentEventGenerator.ts b/sdk/typescript/src/web/serverSentEventGenerator.ts deleted file mode 100644 index 69cb679f2..000000000 --- a/sdk/typescript/src/web/serverSentEventGenerator.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { DatastarEventOptions, EventType, sseHeaders, StreamOptions, Jsonifiable } from "../types.ts"; -import { ServerSentEventGenerator as AbstractSSEGenerator } from "../abstractServerSentEventGenerator.ts"; - -function isRecord(obj: unknown): obj is Record { - return typeof obj === "object" && obj !== null; -} - -/** - * ServerSentEventGenerator class, responsible for initializing and handling - * server-sent events (SSE) as well as reading signals sent by the client. - * Cannot be instantiated directly, you must use the stream static method. - */ -export class ServerSentEventGenerator extends AbstractSSEGenerator { - protected controller: ReadableStreamDefaultController; - - protected constructor(controller: ReadableStreamDefaultController) { - super(); - this.controller = controller; - } - - /** - * Initializes the server-sent event generator and executes the onStart callback. - * - * @param onStart - A function that will be passed the initialized ServerSentEventGenerator class as it's first parameter. - * @param options? - An object that can contain options for the Response constructor onError and onCancel callbacks and a keepalive boolean. - * The onAbort callback will be called whenever the request is aborted or the stream is cancelled - * - * The onError callback will be called whenever an error is met. If provided, the onAbort callback will also be executed. - * If an onError callback is not provided, then the stream will be ended and the error will be thrown up. - * - * If responseInit is provided, then it will be passed to the Response constructor along with the default headers. - * - * The stream is always closed after the onStart callback ends. - * If onStart is non blocking, but you still need the stream to stay open after it is called, - * then the keepalive option will maintain it open until the request is aborted by the client. - * - * @returns an HTTP Response - */ - static stream( - onStart: (stream: ServerSentEventGenerator) => Promise | void, - options?: StreamOptions, - ): Response { - const readableStream = new ReadableStream({ - async start(controller) { - const generator = new ServerSentEventGenerator(controller); - - try { - const stream = onStart(generator); - if (stream instanceof Promise) await stream; - if (!options?.keepalive) { - controller.close(); - } - } catch (error) { - const errorMsg = error instanceof Error - ? error.message - : "onStart callback threw an error"; - const abortResult = options?.onAbort - ? options.onAbort(errorMsg) - : null; - - if (abortResult instanceof Promise) await abortResult; - if (options && options.onError) { - const onError = options.onError(error); - if (onError instanceof Promise) await onError; - controller.close(); - } else { - controller.close(); - throw error; - } - } - }, - async cancel(reason) { - const abortResult = options && options.onAbort - ? options.onAbort(reason) - : null; - if (abortResult instanceof Promise) await abortResult; - }, - }); - - return new Response( - readableStream, - { - ...options?.responseInit, - headers: { - ...sseHeaders, - ...(options?.responseInit?.headers as Record || {}), - }, - }, - ); - } - - protected override send( - event: EventType, - dataLines: string[], - options: DatastarEventOptions, - ): string[] { - const eventLines = super.send(event, dataLines, options); - - // Join all lines and encode as a single chunk to avoid extra newlines - const eventText = eventLines.join(''); - this.controller?.enqueue(new TextEncoder().encode(eventText)); - - return eventLines; - } - - /** - * Reads client sent signals based on HTTP methods - * - * @params request - The HTTP Request object. - * - * @returns An object containing a success boolean and either the client's signals or an error message. - */ - static async readSignals(request: Request): Promise< - | { success: true; signals: Record } - | { success: false; error: string } - > { - try { - if (request.method === "GET") { - const url = new URL(request.url); - const params = url.searchParams; - if (params.has("datastar")) { - const signals = JSON.parse(params.get("datastar")!); - - if (isRecord(signals)) { - return { success: true, signals }; - } else throw new Error("Datastar param is not a record"); - } else throw new Error("No datastar object in request"); - } - - const signals = await request.json(); - - if (isRecord(signals)) { - return { success: true, signals: signals }; - } - - throw new Error("Parsed JSON body is not of type record"); - } catch (e: unknown) { - if (isRecord(e) && "message" in e && typeof e.message === "string") { - return { success: false, error: e.message }; - } - - return { success: false, error: "unknown error when parsing request" }; - } - } -} diff --git a/sdk/typescript/test/bun.ts b/sdk/typescript/test/bun.ts deleted file mode 100644 index a4e9b150e..000000000 --- a/sdk/typescript/test/bun.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { ServerSentEventGenerator } from "../src/web/serverSentEventGenerator"; - -// This server is used for testing the Bun web standard based sdk -const server = Bun.serve({ - port: 8001, - async fetch(req) { - const url = new URL(req.url); - - if (url.pathname === "/") { - return new Response( - `
Hello
`, - { - headers: { "Content-Type": "text/html" }, - }, - ); - } else if (url.pathname.includes("/merge")) { - const reader = await ServerSentEventGenerator.readSignals(req); - - if (!reader.success) { - console.error("Error while reading signals", reader.error); - return new Response(`Error while reading signals`); - } - - if (!("foo" in reader.signals)) { - console.error("The foo signal is not present"); - return new Response("The foo signal is not present"); - } - - return ServerSentEventGenerator.stream((stream) => { - stream.patchElements( - `
Hello ${reader.signals.foo}
`, - ); - }); - } else if (url.pathname.includes("/test")) { - const reader = await ServerSentEventGenerator.readSignals(req); - if (reader.success === true) { - const events = reader.signals.events; - if (isEventArray(events)) { - return ServerSentEventGenerator.stream((stream) => { - testEvents(stream, events); - }); - } - } - } else if (url.pathname.includes("await")) { - return ServerSentEventGenerator.stream(async (stream) => { - stream.patchElements('
Merged
'); - await delay(5000); - stream.patchElements('
After 5 seconds
'); - }); - } - - return new Response(`Path not found: ${req.url}`, { - headers: { "Content-Type": "text/html" }, - }); - }, -}); - -console.log(`Bun server running at http://localhost:${server.port}/`); - -function delay(milliseconds) { - return new Promise((resolve) => { - setTimeout(resolve, milliseconds); - }); -} - -function isEventArray(events) { - return events instanceof Array && events.every((event) => { - return typeof event === "object" && event !== null && - typeof event.type === "string"; - }); -} - -function testEvents(stream, events) { - events.forEach((event) => { - const { type, ...e } = event; - - // Convert camelCase to method calls like Python SDK does - switch (type) { - case "patchElements": - handlepatchElements(stream, e); - break; - case "removeElements": - handleRemoveElements(stream, e); - break; - case "patchSignals": - handlepatchSignals(stream, e); - break; - case "removeSignals": - handleRemoveSignals(stream, e); - break; - case "executeScript": - handleExecuteScript(stream, e); - break; - // Legacy support for old event types - case "mergeFragments": - handlepatchElements(stream, { ...e, mode: e.mode || "outer" }); - break; - case "removeFragments": - handleRemoveElements(stream, e); - break; - case "mergeSignals": - handlepatchSignals(stream, e); - break; - } - }); -} - -function handlepatchElements(stream, e) { - if (e !== null && typeof e === "object") { - const { elements, mode, selector, useViewTransition, ...options } = e; - - // Build patch options - const patchOptions = { ...options }; - if (mode && mode !== "outer") patchOptions.mode = mode; - if (selector) patchOptions.selector = selector; - if (useViewTransition !== undefined) patchOptions.useViewTransition = useViewTransition; - - // For remove mode, elements might be empty which is fine - const elementsToUse = elements || ""; - stream.patchElements(elementsToUse, patchOptions); - } -} - -function handleRemoveElements(stream, e) { - if (e !== null && typeof e === "object" && "selector" in e) { - const { selector, ...options } = e; - stream.patchElements("", { ...options, mode: "remove", selector: selector }); - } -} - -function handlepatchSignals(stream, e) { - if (e !== null && typeof e === "object") { - const { signals, "signals-raw": signalsRaw, ...options } = e; - - if (signalsRaw) { - stream.patchSignals(signalsRaw, options || undefined); - } else if (signals) { - stream.patchSignals(JSON.stringify(signals), options || undefined); - } - } -} - -function handleRemoveSignals(stream, e) { - if (e !== null && typeof e === "object" && "paths" in e) { - const { paths, ...options } = e; - const pathArray = paths; - const removeSignals = {}; - pathArray.forEach(path => { - removeSignals[path] = null; - }); - stream.patchSignals(JSON.stringify(removeSignals), options || undefined); - } -} - -function handleExecuteScript(stream, e) { - if (e !== null && typeof e === "object" && "script" in e) { - const { script, autoRemove = true, attributes, ...options } = e; - let scriptElement = `${script}`; - - // Use append mode with body selector (Python SDK pattern) - stream.patchElements(scriptElement, { - mode: "append", - selector: "body", - ...options - }); - } -} \ No newline at end of file diff --git a/sdk/typescript/test/deno.ts b/sdk/typescript/test/deno.ts deleted file mode 100644 index 699507c54..000000000 --- a/sdk/typescript/test/deno.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; -import { ServerSentEventGenerator } from "../src/web/serverSentEventGenerator.ts"; -import type { Jsonifiable } from "../src/types.ts"; - -// This server is used for testing the web standard based sdk -serve(async (req: Request) => { - const url = new URL(req.url); - - if (url.pathname === "/") { - return new Response( - `
Hello
`, - { - headers: { "Content-Type": "text/html" }, - }, - ); - } else if (url.pathname.includes("/merge")) { - const reader = await ServerSentEventGenerator.readSignals(req); - - if (!reader.success) { - console.error("Error while reading signals", reader.error); - return new Response(`Error while reading signals`); - } - - if (!("foo" in reader.signals)) { - console.error("The foo signal is not present"); - return new Response("The foo signal is not present"); - } - - return ServerSentEventGenerator.stream((stream) => { - stream.patchElements( - `
Hello ${reader.signals.foo}
`, - ); - }); - } else if (url.pathname.includes("/test")) { - const reader = await ServerSentEventGenerator.readSignals(req); - if (reader.success === true) { - const events = reader.signals.events; - if (isEventArray(events)) { - return ServerSentEventGenerator.stream((stream) => { - testEvents(stream, events); - }); - } - } - } else if (url.pathname.includes("await")) { - return ServerSentEventGenerator.stream(async (stream) => { - stream.patchElements('
Merged
'); - await delay(5000); - stream.patchElements('
After 5 seconds
'); - }); - } - - return new Response(`Path not found: ${req.url}`, { - headers: { "Content-Type": "text/html" }, - }); -}); - -function delay(milliseconds: number) { - return new Promise((resolve) => { - setTimeout(resolve, milliseconds); - }); -} - -function isEventArray( - events: unknown, -): events is (Record & { type: string })[] { - return events instanceof Array && events.every((event) => { - return typeof event === "object" && event !== null && - typeof event.type === "string"; - }); -} - -function testEvents( - stream: ServerSentEventGenerator, - events: Record[], -) { - events.forEach((event) => { - const { type, ...e } = event; - - // Convert camelCase to method calls like Python SDK does - switch (type) { - case "patchElements": - handlepatchElements(stream, e); - break; - case "removeElements": - handleRemoveElements(stream, e); - break; - case "patchSignals": - handlepatchSignals(stream, e); - break; - case "removeSignals": - handleRemoveSignals(stream, e); - break; - case "executeScript": - handleExecuteScript(stream, e); - break; - // Legacy support for old event types - case "mergeFragments": - handlepatchElements(stream, { ...e, mode: e.mode || "outer" }); - break; - case "removeFragments": - handleRemoveElements(stream, e); - break; - case "mergeSignals": - handlepatchSignals(stream, e); - break; - } - }); -} - -function handlepatchElements(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object") { - const { elements, mode, selector, useViewTransition, ...options } = e; - - // Build patch options - const patchOptions: Record = { ...options }; - if (mode && mode !== "outer") patchOptions.mode = mode; - if (selector) patchOptions.selector = selector; - if (useViewTransition !== undefined) patchOptions.useViewTransition = useViewTransition; - - // For remove mode, elements might be empty which is fine - const elementsToUse = (elements as string) || ""; - stream.patchElements(elementsToUse, patchOptions); - } -} - -function handleRemoveElements(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object" && "selector" in e) { - const { selector, ...options } = e; - stream.patchElements("", { ...options, mode: "remove", selector: selector as string }); - } -} - -function handlepatchSignals(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object") { - const { signals, "signals-raw": signalsRaw, ...options } = e; - - if (signalsRaw) { - stream.patchSignals(signalsRaw as string, options || undefined); - } else if (signals) { - stream.patchSignals(JSON.stringify(signals), options || undefined); - } - } -} - -function handleRemoveSignals(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object" && "paths" in e) { - const { paths, ...options } = e; - const pathArray = paths as string[]; - const removeSignals: Record = {}; - pathArray.forEach(path => { - removeSignals[path] = null; - }); - stream.patchSignals(JSON.stringify(removeSignals), options || undefined); - } -} - -function handleExecuteScript(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object" && "script" in e) { - const { script, autoRemove = true, attributes, ...options } = e; - let scriptElement = `${script}`; - - // Use append mode with body selector (Python SDK pattern) - stream.patchElements(scriptElement, { - mode: "append", - selector: "body", - ...options - }); - } -} diff --git a/sdk/typescript/test/node.ts b/sdk/typescript/test/node.ts deleted file mode 100644 index 94eb338f3..000000000 --- a/sdk/typescript/test/node.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { createServer } from "node:http"; -import { ServerSentEventGenerator } from "../src/node/serverSentEventGenerator.ts"; -import type { Jsonifiable } from "../src/types.ts"; - -const hostname = "127.0.0.1"; -const port = 3000; - -// This server is used for testing the node sdk -const server = createServer(async (req, res) => { - if (req.url === "/") { - res.setHeader("Content-Type", "text/html"); - res.end( - `
Hello
`, - ); - } else if (req.url?.includes("/merge")) { - const reader = await ServerSentEventGenerator.readSignals(req); - - if (!reader.success) { - console.error("Error while reading signals", reader.error); - res.end(`Error while reading signals`); - return; - } - - if (!("foo" in reader.signals)) { - console.error("The foo signal is not present"); - res.end("The foo signal is not present"); - return; - } - - ServerSentEventGenerator.stream(req, res, (stream) => { - stream.patchElements( - `
Hello ${reader.signals.foo}
`, - ); - }); - } else if (req.url?.includes("/test")) { - const reader = await ServerSentEventGenerator.readSignals(req); - if (reader.success) { - const events = reader.signals.events; - if (isEventArray(events)) { - ServerSentEventGenerator.stream(req, res, (stream) => { - testEvents(stream, events); - }); - } - } else { - res.end(reader.error); - } - } else if (req.url?.includes("/await")) { - ServerSentEventGenerator.stream(req, res, async (stream) => { - stream.patchElements('
Merged
'); - await delay(5000); - stream.patchElements('
After 10 seconds
'); - }); - } else { - res.end("Path not found"); - } -}); - -server.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); -}); - -function delay(milliseconds: number) { - return new Promise((resolve) => { - setTimeout(resolve, milliseconds); - }); -} - -function isEventArray( - events: unknown, -): events is (Record & { type: string })[] { - return events instanceof Array && events.every((event) => { - return typeof event === "object" && event !== null && - typeof event.type === "string"; - }); -} - -function testEvents( - stream: ServerSentEventGenerator, - events: Record[], -) { - events.forEach((event) => { - const { type, ...e } = event; - - // Convert camelCase to method calls like Python SDK does - switch (type) { - case "patchElements": - handlepatchElements(stream, e); - break; - case "removeElements": - handleRemoveElements(stream, e); - break; - case "patchSignals": - handlepatchSignals(stream, e); - break; - case "removeSignals": - handleRemoveSignals(stream, e); - break; - case "executeScript": - handleExecuteScript(stream, e); - break; - // Legacy support for old event types - case "mergeFragments": - handlepatchElements(stream, { ...e, mode: e.mode || "outer" }); - break; - case "removeFragments": - handleRemoveElements(stream, e); - break; - case "mergeSignals": - handlepatchSignals(stream, e); - break; - } - }); -} - -function handlepatchElements(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object") { - const { elements, mode, selector, useViewTransition, ...options } = e; - - // Build patch options - const patchOptions: Record = { ...options }; - if (mode && mode !== "outer") patchOptions.mode = mode; - if (selector) patchOptions.selector = selector; - if (useViewTransition !== undefined) patchOptions.useViewTransition = useViewTransition; - - // For remove mode, elements might be empty which is fine - const elementsToUse = (elements as string) || ""; - stream.patchElements(elementsToUse, patchOptions); - } -} - -function handleRemoveElements(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object" && "selector" in e) { - const { selector, ...options } = e; - stream.patchElements("", { ...options, mode: "remove", selector: selector as string }); - } -} - -function handlepatchSignals(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object") { - const { signals, "signals-raw": signalsRaw, ...options } = e; - - if (signalsRaw) { - stream.patchSignals(signalsRaw as string, options || undefined); - } else if (signals) { - stream.patchSignals(JSON.stringify(signals), options || undefined); - } - } -} - -function handleRemoveSignals(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object" && "paths" in e) { - const { paths, ...options } = e; - const pathArray = paths as string[]; - const removeSignals: Record = {}; - pathArray.forEach(path => { - removeSignals[path] = null; - }); - stream.patchSignals(JSON.stringify(removeSignals), options || undefined); - } -} - -function handleExecuteScript(stream: ServerSentEventGenerator, e: Record) { - if (e !== null && typeof e === "object" && "script" in e) { - const { script, autoRemove = true, attributes, ...options } = e; - let scriptElement = `${script}`; - - // Use append mode with body selector (Python SDK pattern) - stream.patchElements(scriptElement, { - mode: "append", - selector: "body", - ...options - }); - } -} diff --git a/sdk/typescript/test/test-bun.sh b/sdk/typescript/test/test-bun.sh deleted file mode 100755 index 0162ba197..000000000 --- a/sdk/typescript/test/test-bun.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash - -# Bun SDK Test Script -# Starts the Bun test server, runs tests, and cleans up - -set -e # Exit on any error - -# Change to the typescript SDK directory -cd "$(dirname "$0")/.." - -echo "Starting Bun test server from built package..." -bun run test/bun.ts & -SERVER_PID=$! - -# Function to cleanup server on exit -cleanup() { - if [ ! -z "$SERVER_PID" ]; then - echo "Stopping test server..." - kill $SERVER_PID 2>/dev/null || true - wait $SERVER_PID 2>/dev/null || true - fi -} - -# Setup cleanup trap -trap cleanup EXIT - -echo "Waiting for server to start..." -sleep 5 - -# Wait for server to be ready -max_attempts=15 -attempt=1 -while [ $attempt -le $max_attempts ]; do - if curl -s http://localhost:8001/ > /dev/null 2>&1; then - echo "Server ready! Running tests..." - break - fi - - if [ $attempt -eq $max_attempts ]; then - echo "Error: Server failed to start after $max_attempts attempts" - exit 1 - fi - - echo "Server not ready, waiting... (attempt $attempt/$max_attempts)" - sleep 2 - attempt=$((attempt + 1)) -done - -# Store current directory -ORIGINAL_DIR=$(pwd) - -# Run the test suite and track failures -cd ../test - -echo "Running test suite manually to properly catch failures..." -FAILED_TESTS=0 -TOTAL_TESTS=0 - -# Process GET cases -if [ -d "./get-cases" ]; then - echo "Processing GET cases..." - for case in ./get-cases/*; do - if [ -d "$case" ]; then - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - echo "Running test: $(basename "$case")" - if ! ./test-get.sh "$case" "http://localhost:8001"; then - echo " ❌ FAILED: $(basename "$case")" - FAILED_TESTS=$((FAILED_TESTS + 1)) - else - echo " βœ… PASSED: $(basename "$case")" - fi - fi - done - echo "Finished processing GET cases" -fi - -# Process POST cases -if [ -d "./post-cases" ]; then - echo "Processing POST cases..." - for case in ./post-cases/*; do - if [ -d "$case" ]; then - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - echo "Running test: $(basename "$case")" - if ! ./test-post.sh "$case" "http://localhost:8001"; then - echo " ❌ FAILED: $(basename "$case")" - FAILED_TESTS=$((FAILED_TESTS + 1)) - else - echo " βœ… PASSED: $(basename "$case")" - fi - fi - done - echo "Finished processing POST cases" -fi - -# Return to original directory -cd "$ORIGINAL_DIR" - -# Report results -echo "" -echo "==============================" -echo "Test Results Summary:" -echo " Total tests: $TOTAL_TESTS" -echo " Passed: $((TOTAL_TESTS - FAILED_TESTS))" -echo " Failed: $FAILED_TESTS" -echo "==============================" - -if [ $FAILED_TESTS -eq 0 ]; then - echo "πŸŽ‰ All tests passed!" - exit 0 -else - echo "πŸ’₯ $FAILED_TESTS test(s) failed!" - exit 1 -fi \ No newline at end of file diff --git a/sdk/typescript/test/test-deno.sh b/sdk/typescript/test/test-deno.sh deleted file mode 100755 index 70ff5a371..000000000 --- a/sdk/typescript/test/test-deno.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash - -# Deno SDK Test Script -# Starts the Deno test server, runs tests, and cleans up - -set -e # Exit on any error - -# Change to the typescript SDK directory -cd "$(dirname "$0")/.." - -echo "Starting Deno test server..." -deno run --allow-net test/deno.ts & -SERVER_PID=$! - -# Function to cleanup server on exit -cleanup() { - if [ ! -z "$SERVER_PID" ]; then - echo "Stopping test server..." - kill $SERVER_PID 2>/dev/null || true - wait $SERVER_PID 2>/dev/null || true - fi -} - -# Setup cleanup trap -trap cleanup EXIT - -echo "Waiting for server to start..." -sleep 5 - -# Wait for server to be ready -max_attempts=15 -attempt=1 -while [ $attempt -le $max_attempts ]; do - if curl -s http://localhost:8000/ > /dev/null 2>&1; then - echo "Server ready! Running tests..." - break - fi - - if [ $attempt -eq $max_attempts ]; then - echo "Error: Server failed to start after $max_attempts attempts" - exit 1 - fi - - echo "Server not ready, waiting... (attempt $attempt/$max_attempts)" - sleep 2 - attempt=$((attempt + 1)) -done - -# Store current directory -ORIGINAL_DIR=$(pwd) - -# Run the test suite and track failures -cd ../test - -echo "Running test suite manually to properly catch failures..." -FAILED_TESTS=0 -TOTAL_TESTS=0 - -# Process GET cases -if [ -d "./get-cases" ]; then - echo "Processing GET cases..." - for case in ./get-cases/*; do - if [ -d "$case" ]; then - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - echo "Running test: $(basename "$case")" - if ! ./test-get.sh "$case" "http://localhost:8000"; then - echo " ❌ FAILED: $(basename "$case")" - FAILED_TESTS=$((FAILED_TESTS + 1)) - else - echo " βœ… PASSED: $(basename "$case")" - fi - fi - done - echo "Finished processing GET cases" -fi - -# Process POST cases -if [ -d "./post-cases" ]; then - echo "Processing POST cases..." - for case in ./post-cases/*; do - if [ -d "$case" ]; then - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - echo "Running test: $(basename "$case")" - if ! ./test-post.sh "$case" "http://localhost:8000"; then - echo " ❌ FAILED: $(basename "$case")" - FAILED_TESTS=$((FAILED_TESTS + 1)) - else - echo " βœ… PASSED: $(basename "$case")" - fi - fi - done - echo "Finished processing POST cases" -fi - -# Return to original directory -cd "$ORIGINAL_DIR" - -# Report results -echo "" -echo "==============================" -echo "Test Results Summary:" -echo " Total tests: $TOTAL_TESTS" -echo " Passed: $((TOTAL_TESTS - FAILED_TESTS))" -echo " Failed: $FAILED_TESTS" -echo "==============================" - -if [ $FAILED_TESTS -eq 0 ]; then - echo "πŸŽ‰ All tests passed!" - exit 0 -else - echo "πŸ’₯ $FAILED_TESTS test(s) failed!" - exit 1 -fi \ No newline at end of file diff --git a/sdk/typescript/test/test-node.sh b/sdk/typescript/test/test-node.sh deleted file mode 100755 index 883749fd6..000000000 --- a/sdk/typescript/test/test-node.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash - -# Node.js SDK Test Script -# Builds the project, starts the test server, runs tests, and cleans up - -set -e # Exit on any error - -# Change to the typescript SDK directory -cd "$(dirname "$0")/.." - -echo "Building Node.js SDK..." -deno run -A build.ts - -echo "Starting Node.js test server..." -node npm/esm/test/node.js & -SERVER_PID=$! - -# Function to cleanup server on exit -cleanup() { - if [ ! -z "$SERVER_PID" ]; then - echo "Stopping test server..." - kill $SERVER_PID 2>/dev/null || true - wait $SERVER_PID 2>/dev/null || true - fi -} - -# Setup cleanup trap -trap cleanup EXIT - -echo "Waiting for server to start..." -sleep 5 - -# Wait for server to be ready -max_attempts=15 -attempt=1 -while [ $attempt -le $max_attempts ]; do - if curl -s http://127.0.0.1:3000/ > /dev/null 2>&1; then - echo "Server ready! Running tests..." - break - fi - - if [ $attempt -eq $max_attempts ]; then - echo "Error: Server failed to start after $max_attempts attempts" - exit 1 - fi - - echo "Server not ready, waiting... (attempt $attempt/$max_attempts)" - sleep 2 - attempt=$((attempt + 1)) -done - -# Store current directory -ORIGINAL_DIR=$(pwd) - -# Run the test suite and track failures -cd ../test - -echo "Running test suite manually to properly catch failures..." -FAILED_TESTS=0 -TOTAL_TESTS=0 - -# Process GET cases -if [ -d "./get-cases" ]; then - echo "Processing GET cases..." - for case in ./get-cases/*; do - if [ -d "$case" ]; then - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - echo "Running test: $(basename "$case")" - if ! ./test-get.sh "$case" "http://127.0.0.1:3000"; then - echo " ❌ FAILED: $(basename "$case")" - FAILED_TESTS=$((FAILED_TESTS + 1)) - else - echo " βœ… PASSED: $(basename "$case")" - fi - fi - done - echo "Finished processing GET cases" -fi - -# Process POST cases -if [ -d "./post-cases" ]; then - echo "Processing POST cases..." - for case in ./post-cases/*; do - if [ -d "$case" ]; then - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - echo "Running test: $(basename "$case")" - if ! ./test-post.sh "$case" "http://127.0.0.1:3000"; then - echo " ❌ FAILED: $(basename "$case")" - FAILED_TESTS=$((FAILED_TESTS + 1)) - else - echo " βœ… PASSED: $(basename "$case")" - fi - fi - done - echo "Finished processing POST cases" -fi - -# Return to original directory -cd "$ORIGINAL_DIR" - -# Report results -echo "" -echo "==============================" -echo "Test Results Summary:" -echo " Total tests: $TOTAL_TESTS" -echo " Passed: $((TOTAL_TESTS - FAILED_TESTS))" -echo " Failed: $FAILED_TESTS" -echo "==============================" - -if [ $FAILED_TESTS -eq 0 ]; then - echo "πŸŽ‰ All tests passed!" - exit 0 -else - echo "πŸ’₯ $FAILED_TESTS test(s) failed!" - exit 1 -fi \ No newline at end of file diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json deleted file mode 100644 index 459ee9626..000000000 --- a/sdk/typescript/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2022", - "resolveJsonModule": true, - "moduleDetection": "force", - "isolatedModules": true, - "strict": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "module": "commonjs", - "outDir": "dist", - "sourceMap": true, - "declaration": true, - "lib": [ - "es2022" - ] - }, - "include": [ - "src/**/*.ts" - ], -} \ No newline at end of file diff --git a/sdk/zig/.github/PULL_REQUEST_TEMPLATE.md b/sdk/zig/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 07aa49b63..000000000 --- a/sdk/zig/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,4 +0,0 @@ -**Please do not submit any pull requests here – they will be closed!** - -This repository is a read-only subset of the Datastar monorepo. Please open your pull request there instead: -https://github.com/starfederation/datastar diff --git a/sdk/zig/.gitignore b/sdk/zig/.gitignore deleted file mode 100644 index 7c046b645..000000000 --- a/sdk/zig/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.zig-cache -zig-out \ No newline at end of file diff --git a/sdk/zig/README.md b/sdk/zig/README.md deleted file mode 100644 index 94a53df59..000000000 --- a/sdk/zig/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Datastar Zig SDK - -The Datastar SDK in Zig, with support for http.zig and tokamak. - -## Installation - -Install with `zig fetch --save git+https://github.com/starfederation/datastar-zig` and add datastar as a dependency. - -```zig -const datastar = b.dependency("datastar", .{ - .target = target, - .optimize = optimize, - .framework = .httpz, // or .tokamak -}).module("datastar"); - -exe.root_module.addImport("datastar", datastar); -``` - -## Usage -```zig -const datastar = @import("datastar").httpz; - -// Creates a new `ServerSentEventGenerator`. -var sse = try datastar.ServerSentEventGenerator.init(res); - -// Merges HTML fragments into the DOM. -try sse.mergeFragments("
What do you put in a toaster?
", .{}); - -// Merges signals into the signals. -try sse.mergeSignals(.{ .response = "", .answer = "bread" }, .{}); -``` - -Full examples at https://github.com/starfederation/datastar/tree/main/examples/zig \ No newline at end of file diff --git a/sdk/zig/build.zig b/sdk/zig/build.zig deleted file mode 100644 index 48b5afad2..000000000 --- a/sdk/zig/build.zig +++ /dev/null @@ -1,67 +0,0 @@ -const std = @import("std"); - -pub const Framework = enum { - httpz, - tokamak, - all, -}; - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - const dep_opts = .{ .target = target, .optimize = optimize }; - - const framework = b.option( - Framework, - "framework", - "Decide which framework to use", - ) orelse .all; - - const options = b.addOptions(); - options.addOption(bool, "http1", true); - options.addOption(Framework, "framework", framework); - - const httpz = b.dependency("httpz", dep_opts).module("httpz"); - const tokamak = b.dependency("tokamak", dep_opts).module("tokamak"); - - const imports: []const std.Build.Module.Import = switch (framework) { - .httpz => &.{.{ .name = "httpz", .module = httpz }}, - .tokamak => &.{.{ .name = "tokamak", .module = tokamak }}, - .all => &.{ - .{ .name = "httpz", .module = httpz }, - .{ .name = "tokamak", .module = tokamak }, - }, - }; - - const datastar = b.addModule( - "datastar", - .{ - .root_source_file = b.path("src/root.zig"), - .imports = imports, - }, - ); - - datastar.addOptions("config", options); - - const tests = b.addTest(.{ - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = optimize, - .test_runner = .{ - .path = b.path("test_runner.zig"), - .mode = .simple, - }, - }); - - tests.root_module.addOptions("config", options); - - for (imports) |import| { - tests.root_module.addImport(import.name, import.module); - } - - const run_test = b.addRunArtifact(tests); - run_test.has_side_effects = true; - - const test_step = b.step("test", "Run tests"); - test_step.dependOn(&run_test.step); -} diff --git a/sdk/zig/build.zig.zon b/sdk/zig/build.zig.zon deleted file mode 100644 index 749bc2448..000000000 --- a/sdk/zig/build.zig.zon +++ /dev/null @@ -1,20 +0,0 @@ -.{ - .name = .datastar, - .fingerprint = 0xc03ca25b47dce68f, - .version = "1.0.0-beta.5", - .dependencies = .{ - .httpz = .{ - .url = "git+https://github.com/karlseguin/http.zig?ref=master#56258131ef4505543fef5484451867c13c5ff322", - .hash = "httpz-0.0.0-PNVzrJSuBgDFvO7mtd2qDzaq8_hXIu1BqFuL1jwAV8Ac", - }, - .tokamak = .{ - .url = "git+https://github.com/cztomsik/tokamak#c9491b312eabbd5683bc236950fd2455c7b07752", - .hash = "tokamak-2.0.0-FbnSeSbfAQAIiCUmj1RRpAio0RAHWvRoMFozHcnbeeMb", - }, - }, - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - }, -} diff --git a/sdk/zig/src/ServerSentEventGenerator.zig b/sdk/zig/src/ServerSentEventGenerator.zig deleted file mode 100644 index 9cb91b806..000000000 --- a/sdk/zig/src/ServerSentEventGenerator.zig +++ /dev/null @@ -1,339 +0,0 @@ -const std = @import("std"); -const consts = @import("consts.zig"); -const ArrayList = std.ArrayListUnmanaged; - -const default_execute_script_attributes: []const []const u8 = &[_][]const u8{consts.default_execute_script_attributes}; - -allocator: std.mem.Allocator, -writer: std.net.Stream.Writer, - -pub const ExecuteScriptOptions = struct { - /// `event_id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id - event_id: ?[]const u8 = null, - /// `retry_duration` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry - retry_duration: u32 = consts.default_sse_retry_duration, - /// A list of attributes to add to the script element. - /// Each item in the array ***must*** be a string in the format `key value`. - attributes: []const []const u8 = default_execute_script_attributes, - /// Whether to remove the script after execution. - auto_remove: bool = consts.default_execute_script_auto_remove, -}; - -pub const MergeFragmentsOptions = struct { - /// `event_id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id - event_id: ?[]const u8 = null, - /// `retry_duration` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry - retry_duration: u32 = consts.default_sse_retry_duration, - /// The CSS selector to use to insert the fragments. - selector: ?[]const u8 = null, - /// The mode to use when merging the fragment into the DOM. - merge_mode: consts.FragmentMergeMode = consts.default_fragment_merge_mode, - /// Whether to use view transitions. - use_view_transition: bool = consts.default_fragments_use_view_transitions, -}; - -pub const MergeSignalsOptions = struct { - /// `event_id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id - event_id: ?[]const u8 = null, - /// `retry_duration` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry - retry_duration: u32 = consts.default_sse_retry_duration, - /// Whether to merge the signal only if it does not already exist. - only_if_missing: bool = consts.default_merge_signals_only_if_missing, -}; - -pub const RemoveFragmentsOptions = struct { - /// `event_id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id - event_id: ?[]const u8 = null, - /// `retry_duration` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry - retry_duration: u32 = consts.default_sse_retry_duration, - /// Whether to use view transitions. - use_view_transition: bool = consts.default_fragments_use_view_transitions, -}; - -pub const RemoveSignalsOptions = struct { - /// `event_id` can be used by the backend to replay events. - /// This is part of the SSE spec and is used to tell the browser how to handle the event. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id - event_id: ?[]const u8 = null, - /// `retry_duration` is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. - /// For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry - retry_duration: u32 = consts.default_sse_retry_duration, -}; - -fn send( - self: *@This(), - event: consts.EventType, - data: []const u8, - options: struct { - event_id: ?[]const u8 = null, - retry_duration: u32 = consts.default_sse_retry_duration, - }, -) !void { - try self.writer.print("event: {}\n", .{event}); - - if (options.event_id) |id| { - try self.writer.print("id: {s}\n", .{id}); - } - - if (options.retry_duration != consts.default_sse_retry_duration) { - try self.writer.print("retry: {d}\n", .{options.retry_duration}); - } - - var iter = std.mem.splitScalar(u8, data, '\n'); - while (iter.next()) |line| { - if (line.len == 0) continue; - try self.writer.print("data: {s}\n", .{line}); - } - - try self.writer.writeAll("\n\n"); -} - -/// `executeScript` executes JavaScript in the browser -/// -/// See the [Datastar documentation](https://data-star.dev/reference/sse_events#datastar-execute-script) for more information. -pub fn executeScript( - self: *@This(), - /// `script` is a string that represents the JavaScript to be executed by the browser. - script: []const u8, - options: ExecuteScriptOptions, -) !void { - var data = ArrayList(u8).empty; - errdefer data.deinit(self.allocator); - const writer = data.writer(self.allocator); - - if (options.attributes.len != 1 or !std.mem.eql( - u8, - default_execute_script_attributes[0], - options.attributes[0], - )) { - for (options.attributes) |attribute| { - try writer.print( - consts.attributes_dataline_literal ++ " {s}\n", - .{ - attribute, - }, - ); - } - } - - if (options.auto_remove != consts.default_execute_script_auto_remove) { - try writer.print( - consts.auto_remove_dataline_literal ++ " {}\n", - .{ - options.auto_remove, - }, - ); - } - - var iter = std.mem.splitScalar(u8, script, '\n'); - while (iter.next()) |elem| { - try writer.print( - consts.script_dataline_literal ++ " {s}\n", - .{ - elem, - }, - ); - } - - try self.send( - .execute_script, - try data.toOwnedSlice(self.allocator), - .{ - .event_id = options.event_id, - .retry_duration = options.retry_duration, - }, - ); -} - -/// `mergeFragments` merges one or more fragments into the DOM. By default, -/// Datastar merges fragments using Idiomorph, which matches top level elements based on their ID. -/// -/// See the [Datastar documentation](https://data-star.dev/reference/sse_events#datastar-merge-fragments) for more information. -pub fn mergeFragments( - self: *@This(), - /// The HTML fragments to merge into the DOM. - fragments: []const u8, - options: MergeFragmentsOptions, -) !void { - var data = ArrayList(u8).empty; - errdefer data.deinit(self.allocator); - const writer = data.writer(self.allocator); - - if (options.selector) |selector| { - try writer.print( - consts.selector_dataline_literal ++ " {s}\n", - .{ - selector, - }, - ); - } - - if (options.merge_mode != consts.default_fragment_merge_mode) { - try writer.print( - consts.merge_mode_dataline_literal ++ " {}\n", - .{ - options.merge_mode, - }, - ); - } - - if (options.use_view_transition != consts.default_fragments_use_view_transitions) { - try writer.print( - consts.use_view_transition_dataline_literal ++ " {}\n", - .{ - options.use_view_transition, - }, - ); - } - - var iter = std.mem.splitScalar(u8, fragments, '\n'); - while (iter.next()) |elem| { - try writer.print( - consts.fragments_dataline_literal ++ " {s}\n", - .{ - elem, - }, - ); - } - - try self.send( - .merge_fragments, - try data.toOwnedSlice(self.allocator), - .{ - .event_id = options.event_id, - .retry_duration = options.retry_duration, - }, - ); -} - -/// `mergeSignals` sends one or more signals to the browser to be merged into the signals. -/// This function takes in `anytype` as the signals to merge, which can be any type that can be serialized to JSON. -/// -/// See the [Datastar documentation](https://data-star.dev/reference/sse_events#datastar-merge-signals) for more information. -pub fn mergeSignals( - self: *@This(), - signals: anytype, - options: MergeSignalsOptions, -) !void { - var data = ArrayList(u8).empty; - errdefer data.deinit(self.allocator); - const writer = data.writer(self.allocator); - - if (options.only_if_missing != consts.default_merge_signals_only_if_missing) { - try writer.print( - consts.only_if_missing_dataline_literal ++ " {}\n", - .{ - options.only_if_missing, - }, - ); - } - - try writer.writeAll(consts.signals_dataline_literal ++ " "); - try std.json.stringify(signals, .{}, writer); - try writer.writeByte('\n'); - - try self.send( - .merge_signals, - try data.toOwnedSlice(self.allocator), - .{ - .event_id = options.event_id, - .retry_duration = options.retry_duration, - }, - ); -} - -/// `removeFragments` sends a selector to the browser to remove HTML fragments from the DOM. -/// -/// See the [Datastar documentation](https://data-star.dev/reference/sse_events#datastar-remove-fragments) for more information. -pub fn removeFragments( - self: *@This(), - selector: []const u8, - options: RemoveFragmentsOptions, -) !void { - var data = ArrayList(u8).empty; - errdefer data.deinit(self.allocator); - const writer = data.writer(self.allocator); - - if (options.use_view_transition != consts.default_fragments_use_view_transitions) { - try writer.print( - consts.use_view_transition_dataline_literal ++ " {}\n", - .{ - options.use_view_transition, - }, - ); - } - - try writer.print( - consts.selector_dataline_literal ++ " {s}\n", - .{ - selector, - }, - ); - - try self.send( - .remove_fragments, - try data.toOwnedSlice(self.allocator), - .{ - .event_id = options.event_id, - .retry_duration = options.retry_duration, - }, - ); -} - -/// `removeSignals` sends signals to the browser to be removed from the signals. -/// -/// See the [Datastar documentation](https://data-star.dev/reference/sse_events#datastar-remove-signals) for more information. -pub fn removeSignals( - self: *@This(), - paths: []const []const u8, - options: RemoveSignalsOptions, -) !void { - var data = ArrayList(u8).empty; - errdefer data.deinit(self.allocator); - const writer = data.writer(self.allocator); - - for (paths) |path| { - try writer.print( - consts.paths_dataline_literal ++ " {s}\n", - .{ - path, - }, - ); - } - - try self.send( - .remove_signals, - try data.toOwnedSlice(self.allocator), - .{ - .event_id = options.event_id, - .retry_duration = options.retry_duration, - }, - ); -} - -/// `redirect` sends an `executeScript` event to redirect the user to a new URL. -pub fn redirect( - self: *@This(), - url: []const u8, - options: ExecuteScriptOptions, -) !void { - const script = try std.fmt.allocPrint( - self.allocator, - "setTimeout(() => window.location.href = '{s}')", - .{url}, - ); - errdefer self.allocator.free(script); - try self.executeScript(script, options); -} diff --git a/sdk/zig/src/consts.zig b/sdk/zig/src/consts.zig deleted file mode 100644 index 0350991e8..000000000 --- a/sdk/zig/src/consts.zig +++ /dev/null @@ -1,117 +0,0 @@ -// This is auto-generated by Datastar. DO NOT EDIT. - -const std = @import("std"); - -pub const datastar_key = "datastar"; -pub const version = "1.0.0-RC.1"; - -// #region Defaults - -// #region Default durations - -/// The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. -pub const default_sse_retry_duration = 1000; - -// #endregion - -// #region Default strings - - -// #endregion - -// #region Datalines - -pub const selector_dataline_literal = "selector"; -pub const mode_dataline_literal = "mode"; -pub const elements_dataline_literal = "elements"; -pub const use_view_transition_dataline_literal = "useViewTransition"; -pub const signals_dataline_literal = "signals"; -pub const only_if_missing_dataline_literal = "onlyIfMissing"; - -// #endregion - -// #region Default booleans - -/// Should elements be patched using the ViewTransition API? -pub const default_elements_use_view_transitions = false; -/// Should a given set of signals patch if they are missing? -pub const default_patch_signals_only_if_missing = false; - -// #endregion - -// #region Enums - -/// The mode in which an element is patched into the DOM. -pub const ElementPatchMode = enum { - /// Morphs the element into the existing element. - outer, - /// Replaces the inner HTML of the existing element. - inner, - /// Removes the existing element. - remove, - /// Replaces the existing element with the new element. - replace, - /// Prepends the element inside to the existing element. - prepend, - /// Appends the element inside the existing element. - append, - /// Inserts the element before the existing element. - before, - /// Inserts the element after the existing element. - after, - - pub fn format( - self: @This(), - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = fmt; - _ = options; - - try writer.writeAll( - switch (self) { - .outer => "outer", - .inner => "inner", - .remove => "remove", - .replace => "replace", - .prepend => "prepend", - .append => "append", - .before => "before", - .after => "after", - }, - ); - } -}; - -pub const default_element_patch_mode = ElementPatchMode.outer; - -/// The type protocol on top of SSE which allows for core pushed based communication between the server and the client. -pub const EventType = enum { - /// An event for patching HTML elements into the DOM. - patch_elements, - /// An event for patching signals. - patch_signals, - - pub fn format( - self: @This(), - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = fmt; - _ = options; - - try writer.writeAll( - switch (self) { - .patch_elements => "datastar-patch-elements", - .patch_signals => "datastar-patch-signals", - }, - ); - } -}; - - -// #endregion - -// #endregion \ No newline at end of file diff --git a/sdk/zig/src/httpz/ServerSentEventGenerator.zig b/sdk/zig/src/httpz/ServerSentEventGenerator.zig deleted file mode 100644 index 746e12cd5..000000000 --- a/sdk/zig/src/httpz/ServerSentEventGenerator.zig +++ /dev/null @@ -1,23 +0,0 @@ -const std = @import("std"); -const config = @import("config"); -const httpz = @import("httpz"); -const ServerSentEventGenerator = @import("../ServerSentEventGenerator.zig"); - -pub fn init(res: *httpz.Response) !ServerSentEventGenerator { - res.content_type = .EVENTS; - res.header("Cache-Control", "no-cache"); - - if (config.http1) { - res.header("Connection", "keep-alive"); - } - - try res.write(); - - const conn = res.conn; - conn.handover = .close; - - return .{ - .allocator = res.arena, - .writer = conn.stream.writer(), - }; -} diff --git a/sdk/zig/src/httpz/root.zig b/sdk/zig/src/httpz/root.zig deleted file mode 100644 index 6aecfd620..000000000 --- a/sdk/zig/src/httpz/root.zig +++ /dev/null @@ -1,52 +0,0 @@ -const std = @import("std"); -const httpz = @import("httpz"); -const consts = @import("../consts.zig"); -const testing = @import("../testing.zig"); - -pub const ServerSentEventGenerator = @import("ServerSentEventGenerator.zig"); - -/// `readSignals` is a helper function that reads datastar signals from the request. -pub fn readSignals(comptime T: type, req: *httpz.Request) !T { - switch (req.method) { - .GET => { - const query = try req.query(); - const signals = query.get(consts.datastar_key) orelse return error.MissingDatastarKey; - - return std.json.parseFromSliceLeaky(T, req.arena, signals, .{}); - }, - else => { - const body = req.body() orelse return error.MissingBody; - - return std.json.parseFromSliceLeaky(T, req.arena, body, .{}); - }, - } -} - -fn sdk(req: *httpz.Request, res: *httpz.Response) !void { - var sse = try ServerSentEventGenerator.init(res); - const signals = try readSignals( - testing.Signals, - req, - ); - - try testing.sdk(&sse, signals); -} - -test sdk { - var server = try httpz.Server(void).init( - std.testing.allocator, - .{ .port = 8080 }, - {}, - ); - defer { - server.stop(); - server.deinit(); - } - - var router = try server.router(.{}); - - router.get("/test", sdk, .{}); - router.post("/test", sdk, .{}); - - try server.listen(); -} diff --git a/sdk/zig/src/root.zig b/sdk/zig/src/root.zig deleted file mode 100644 index 4d149ee2b..000000000 --- a/sdk/zig/src/root.zig +++ /dev/null @@ -1,16 +0,0 @@ -const config = @import("config"); - -pub const consts = @import("consts.zig"); -pub const ServerSentEventGenerator = @import("ServerSentEventGenerator.zig"); -pub const httpz = switch (config.framework) { - .httpz, .all => @import("httpz/root.zig"), - else => undefined, -}; -pub const tk = switch (config.framework) { - .tokamak, .all => @import("tokamak/root.zig"), - else => undefined, -}; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/sdk/zig/src/testing.zig b/sdk/zig/src/testing.zig deleted file mode 100644 index bf7996f31..000000000 --- a/sdk/zig/src/testing.zig +++ /dev/null @@ -1,177 +0,0 @@ -const std = @import("std"); -const consts = @import("consts.zig"); -const ServerSentEventGenerator = @import("ServerSentEventGenerator.zig"); - -pub const Signals = struct { - events: []const std.json.Value, -}; - -const ExecuteScript = struct { - script: []const u8, - eventId: ?[]const u8 = null, - retryDuration: ?u32 = null, - attributes: ?std.json.Value = null, - autoRemove: ?bool = null, -}; - -const MergeFragments = struct { - fragments: []const u8, - eventId: ?[]const u8 = null, - retryDuration: ?u32 = null, - selector: ?[]const u8 = null, - mergeMode: ?consts.FragmentMergeMode = null, - useViewTransition: ?bool = null, -}; - -const MergeSignals = struct { - signals: std.json.Value, - eventId: ?[]const u8 = null, - retryDuration: ?u32 = null, - onlyIfMissing: ?bool = null, -}; - -const RemoveFragments = struct { - selector: []const u8, - eventId: ?[]const u8 = null, - retryDuration: ?u32 = null, - useViewTransition: ?bool = null, -}; - -const RemoveSignals = struct { - paths: []const []const u8, - eventId: ?[]const u8 = null, - retryDuration: ?u32 = null, -}; - -pub fn sdk(sse: *ServerSentEventGenerator, signals: Signals) !void { - for (signals.events) |event| { - const event_type = event.object.get("type").?.string; - - if (std.mem.eql(u8, event_type, "executeScript")) { - const ev = try std.json.parseFromValueLeaky( - ExecuteScript, - sse.allocator, - event, - .{ .ignore_unknown_fields = true }, - ); - - const attrs = blk: { - if (ev.attributes) |attrs| { - var result = std.ArrayList([]const u8).init(sse.allocator); - - var iter = attrs.object.iterator(); - while (iter.next()) |entry| { - var value = try std.json.stringifyAlloc( - sse.allocator, - entry.value_ptr.*, - .{}, - ); - - switch (entry.value_ptr.*) { - .string => { - value = value[1 .. value.len - 1]; - }, - else => {}, - } - - const string = try std.fmt.allocPrint( - sse.allocator, - "{s} {s}", - .{ - entry.key_ptr.*, - value, - }, - ); - - try result.append(string); - } - - break :blk try result.toOwnedSlice(); - } else { - break :blk &[_][]const u8{consts.default_execute_script_attributes}; - } - }; - - try sse.executeScript( - ev.script, - .{ - .event_id = ev.eventId, - .retry_duration = ev.retryDuration orelse consts.default_sse_retry_duration, - .attributes = attrs, - .auto_remove = ev.autoRemove orelse true, - }, - ); - } else if (std.mem.eql(u8, event_type, "mergeFragments")) { - const ev = try std.json.parseFromValueLeaky( - MergeFragments, - sse.allocator, - event, - .{ .ignore_unknown_fields = true }, - ); - - try sse.mergeFragments( - ev.fragments, - .{ - .event_id = ev.eventId, - .retry_duration = ev.retryDuration orelse consts.default_sse_retry_duration, - .selector = ev.selector, - .merge_mode = ev.mergeMode orelse consts.default_fragment_merge_mode, - .use_view_transition = ev.useViewTransition orelse consts.default_fragments_use_view_transitions, - }, - ); - } else if (std.mem.eql(u8, event_type, "mergeSignals")) { - const ev = try std.json.parseFromValueLeaky( - MergeSignals, - sse.allocator, - event, - .{ .ignore_unknown_fields = true }, - ); - - const json = try std.json.stringifyAlloc( - sse.allocator, - ev.signals, - .{}, - ); - - try sse.mergeSignals( - json, - .{ - .event_id = ev.eventId, - .retry_duration = ev.retryDuration orelse consts.default_sse_retry_duration, - .only_if_missing = ev.onlyIfMissing orelse consts.default_merge_signals_only_if_missing, - }, - ); - } else if (std.mem.eql(u8, event_type, "removeFragments")) { - const ev = try std.json.parseFromValueLeaky( - RemoveFragments, - sse.allocator, - event, - .{ .ignore_unknown_fields = true }, - ); - - try sse.removeFragments( - ev.selector, - .{ - .event_id = ev.eventId, - .retry_duration = ev.retryDuration orelse consts.default_sse_retry_duration, - .use_view_transition = ev.useViewTransition orelse consts.default_fragments_use_view_transitions, - }, - ); - } else if (std.mem.eql(u8, event_type, "removeSignals")) { - const ev = try std.json.parseFromValueLeaky( - RemoveSignals, - sse.allocator, - event, - .{ .ignore_unknown_fields = true }, - ); - - try sse.removeSignals( - ev.paths, - .{ - .event_id = ev.eventId, - .retry_duration = ev.retryDuration orelse consts.default_sse_retry_duration, - }, - ); - } - } -} diff --git a/sdk/zig/src/tokamak/ServerSentEventGenerator.zig b/sdk/zig/src/tokamak/ServerSentEventGenerator.zig deleted file mode 100644 index 30ee2161c..000000000 --- a/sdk/zig/src/tokamak/ServerSentEventGenerator.zig +++ /dev/null @@ -1,23 +0,0 @@ -const std = @import("std"); -const config = @import("config"); -const tk = @import("tokamak"); -const ServerSentEventGenerator = @import("../ServerSentEventGenerator.zig"); - -pub fn init(res: *tk.Response) !ServerSentEventGenerator { - res.content_type = .EVENTS; - res.header("Cache-Control", "no-cache"); - - if (config.http1) { - res.header("Connection", "keep-alive"); - } - - try res.write(); - - const conn = res.conn; - conn.handover = .close; - - return .{ - .allocator = res.arena, - .writer = conn.stream.writer(), - }; -} diff --git a/sdk/zig/src/tokamak/root.zig b/sdk/zig/src/tokamak/root.zig deleted file mode 100644 index c7244657f..000000000 --- a/sdk/zig/src/tokamak/root.zig +++ /dev/null @@ -1,45 +0,0 @@ -const std = @import("std"); -const tk = @import("tokamak"); -const consts = @import("../consts.zig"); -const testing = @import("../testing.zig"); - -pub const ServerSentEventGenerator = @import("ServerSentEventGenerator.zig"); - -/// `readSignals` is a helper function that reads datastar signals from the request. -pub fn readSignals(comptime T: type, req: *tk.Request) !T { - switch (req.method) { - .GET => { - const query = try req.query(); - const signals = query.get(consts.datastar_key) orelse return error.MissingDatastarKey; - - return std.json.parseFromSliceLeaky(T, req.arena, signals, .{}); - }, - else => { - const body = req.body() orelse return error.MissingBody; - - return std.json.parseFromSliceLeaky(T, req.arena, body, .{}); - }, - } -} - -fn sdk(req: *tk.Request, res: *tk.Response) !void { - var sse = try ServerSentEventGenerator.init(res); - const signals = try readSignals( - testing.Signals, - req, - ); - - try testing.sdk(&sse, signals); -} - -const App = struct { - server: tk.Server, - routes: []const tk.Route = &.{ - .get("/test", sdk), - .post0("/test", sdk), - }, -}; - -test sdk { - try tk.app.run(App); -} diff --git a/sdk/zig/test_runner.zig b/sdk/zig/test_runner.zig deleted file mode 100644 index 25898fb3f..000000000 --- a/sdk/zig/test_runner.zig +++ /dev/null @@ -1,310 +0,0 @@ -// source: https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b - -// in your build.zig, you can specify a custom test runner: -// const tests = b.addTest(.{ -// .target = target, -// .optimize = optimize, -// .test_runner = b.path("test_runner.zig"), // add this line -// .root_source_file = b.path("src/main.zig"), -// }); - -const std = @import("std"); -const builtin = @import("builtin"); - -const Allocator = std.mem.Allocator; - -const BORDER = "=" ** 80; - -// use in custom panic handler -var current_test: ?[]const u8 = null; - -pub fn main() !void { - var mem: [8192]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&mem); - - const allocator = fba.allocator(); - - const env = Env.init(allocator); - defer env.deinit(allocator); - - var slowest = SlowTracker.init(allocator, 5); - defer slowest.deinit(); - - var pass: usize = 0; - var fail: usize = 0; - var skip: usize = 0; - var leak: usize = 0; - - const printer = Printer.init(); - printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line - - for (builtin.test_functions) |t| { - if (isSetup(t)) { - t.func() catch |err| { - printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err }); - return err; - }; - } - } - - for (builtin.test_functions) |t| { - if (isSetup(t) or isTeardown(t)) { - continue; - } - - var status = Status.pass; - slowest.startTiming(); - - const is_unnamed_test = isUnnamed(t); - if (env.filter) |f| { - if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) { - continue; - } - } - - const friendly_name = blk: { - const name = t.name; - var it = std.mem.splitScalar(u8, name, '.'); - while (it.next()) |value| { - if (std.mem.eql(u8, value, "test")) { - const rest = it.rest(); - break :blk if (rest.len > 0) rest else name; - } - } - break :blk name; - }; - - current_test = friendly_name; - std.testing.allocator_instance = .{}; - const result = t.func(); - current_test = null; - - const ns_taken = slowest.endTiming(friendly_name); - - if (std.testing.allocator_instance.deinit() == .leak) { - leak += 1; - printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER }); - } - - if (result) |_| { - pass += 1; - } else |err| switch (err) { - error.SkipZigTest => { - skip += 1; - status = .skip; - }, - else => { - status = .fail; - fail += 1; - printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - if (env.fail_first) { - break; - } - }, - } - - if (env.verbose) { - const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0; - printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms }); - } else { - printer.status(status, ".", .{}); - } - } - - for (builtin.test_functions) |t| { - if (isTeardown(t)) { - t.func() catch |err| { - printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err }); - return err; - }; - } - } - - const total_tests = pass + fail; - const status = if (fail == 0) Status.pass else Status.fail; - printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" }); - if (skip > 0) { - printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" }); - } - if (leak > 0) { - printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" }); - } - printer.fmt("\n", .{}); - try slowest.display(printer); - printer.fmt("\n", .{}); - std.posix.exit(if (fail == 0) 0 else 1); -} - -const Printer = struct { - out: std.fs.File.Writer, - - fn init() Printer { - return .{ - .out = std.io.getStdErr().writer(), - }; - } - - fn fmt(self: Printer, comptime format: []const u8, args: anytype) void { - std.fmt.format(self.out, format, args) catch unreachable; - } - - fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void { - const color = switch (s) { - .pass => "\x1b[32m", - .fail => "\x1b[31m", - .skip => "\x1b[33m", - else => "", - }; - const out = self.out; - out.writeAll(color) catch @panic("writeAll failed?!"); - std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!"); - self.fmt("\x1b[0m", .{}); - } -}; - -const Status = enum { - pass, - fail, - skip, - text, -}; - -const SlowTracker = struct { - const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); - max: usize, - slowest: SlowestQueue, - timer: std.time.Timer, - - fn init(allocator: Allocator, count: u32) SlowTracker { - const timer = std.time.Timer.start() catch @panic("failed to start timer"); - var slowest = SlowestQueue.init(allocator, {}); - slowest.ensureTotalCapacity(count) catch @panic("OOM"); - return .{ - .max = count, - .timer = timer, - .slowest = slowest, - }; - } - - const TestInfo = struct { - ns: u64, - name: []const u8, - }; - - fn deinit(self: SlowTracker) void { - self.slowest.deinit(); - } - - fn startTiming(self: *SlowTracker) void { - self.timer.reset(); - } - - fn endTiming(self: *SlowTracker, test_name: []const u8) u64 { - var timer = self.timer; - const ns = timer.lap(); - - var slowest = &self.slowest; - - if (slowest.count() < self.max) { - // Capacity is fixed to the # of slow tests we want to track - // If we've tracked fewer tests than this capacity, than always add - slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); - return ns; - } - - { - // Optimization to avoid shifting the dequeue for the common case - // where the test isn't one of our slowest. - const fastest_of_the_slow = slowest.peekMin() orelse unreachable; - if (fastest_of_the_slow.ns > ns) { - // the test was faster than our fastest slow test, don't add - return ns; - } - } - - // the previous fastest of our slow tests, has been pushed off. - _ = slowest.removeMin(); - slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); - return ns; - } - - fn display(self: *SlowTracker, printer: Printer) !void { - var slowest = self.slowest; - const count = slowest.count(); - printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" }); - while (slowest.removeMinOrNull()) |info| { - const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0; - printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name }); - } - } - - fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order { - _ = context; - return std.math.order(a.ns, b.ns); - } -}; - -const Env = struct { - verbose: bool, - fail_first: bool, - filter: ?[]const u8, - - fn init(allocator: Allocator) Env { - return .{ - .verbose = readEnvBool(allocator, "TEST_VERBOSE", true), - .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false), - .filter = readEnv(allocator, "TEST_FILTER"), - }; - } - - fn deinit(self: Env, allocator: Allocator) void { - if (self.filter) |f| { - allocator.free(f); - } - } - - fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 { - const v = std.process.getEnvVarOwned(allocator, key) catch |err| { - if (err == error.EnvironmentVariableNotFound) { - return null; - } - std.log.warn("failed to get env var {s} due to err {}", .{ key, err }); - return null; - }; - return v; - } - - fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool { - const value = readEnv(allocator, key) orelse return deflt; - defer allocator.free(value); - return std.ascii.eqlIgnoreCase(value, "true"); - } -}; - -pub const panic = std.debug.FullPanic(struct { - pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn { - if (current_test) |ct| { - std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER }); - } - std.debug.defaultPanic(msg, first_trace_addr); - } -}.panicFn); - -fn isUnnamed(t: std.builtin.TestFn) bool { - const marker = ".test_"; - const test_name = t.name; - const index = std.mem.indexOf(u8, test_name, marker) orelse return false; - _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false; - return true; -} - -fn isSetup(t: std.builtin.TestFn) bool { - return std.mem.endsWith(u8, t.name, "tests:beforeAll"); -} - -fn isTeardown(t: std.builtin.TestFn) bool { - return std.mem.endsWith(u8, t.name, "tests:afterAll"); -}