diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7851062b6..5a64ced96 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -43,9 +43,9 @@ jobs:
- run: cat .env >> $GITHUB_ENV || true
- run: make up
- run: make wait
+ - run: make test_short
- run: make test
timeout-minutes: 30
- - run: make test_short
- run: make test_race
- run: make test_stress
if: ${{ inputs.stress-tests }}
diff --git a/.gitignore b/.gitignore
index f85d2bf11..f7a78e709 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ docs/node_modules/
docs/public/
docs/content/src-link
docs/content/middleware
+docs/hugo_stats.json
*.out
*.log
.mod-cache
diff --git a/_examples/pubsubs/aws-sns/.validate_example.yml b/_examples/pubsubs/aws-sns/.validate_example.yml
new file mode 100644
index 000000000..3688c1f21
--- /dev/null
+++ b/_examples/pubsubs/aws-sns/.validate_example.yml
@@ -0,0 +1,4 @@
+validation_cmd: "docker compose up"
+teardown_cmd: "docker compose down"
+timeout: 120
+expected_output: "A received message: [0-9a-f\\-]+, payload: Hello, world!"
diff --git a/_examples/pubsubs/aws-sns/docker-compose.yml b/_examples/pubsubs/aws-sns/docker-compose.yml
new file mode 100644
index 000000000..fa75e2c2e
--- /dev/null
+++ b/_examples/pubsubs/aws-sns/docker-compose.yml
@@ -0,0 +1,24 @@
+services:
+ server:
+ image: golang:1.23
+ restart: unless-stopped
+ volumes:
+ - .:/app
+ - $GOPATH/pkg/mod:/go/pkg/mod
+ working_dir: /app
+ command: go run main.go
+
+ localstack:
+ image: localstack/localstack:latest
+ environment:
+ - SERVICES=sqs,sns
+ - AWS_DEFAULT_REGION=us-east-1
+ - EDGE_PORT=4566
+ ports:
+ - "4566-4597:4566-4597"
+ healthcheck:
+ test: awslocal sqs list-queues && awslocal sns list-topics
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
diff --git a/_examples/pubsubs/aws-sns/go.mod b/_examples/pubsubs/aws-sns/go.mod
new file mode 100644
index 000000000..f6245a5e0
--- /dev/null
+++ b/_examples/pubsubs/aws-sns/go.mod
@@ -0,0 +1,23 @@
+module main
+
+require (
+ github.com/ThreeDotsLabs/watermill v1.3.7
+ github.com/ThreeDotsLabs/watermill-aws v1.0.0-rc.2
+ github.com/aws/aws-sdk-go-v2 v1.30.4
+ github.com/aws/aws-sdk-go-v2/service/sns v1.31.4
+ github.com/aws/aws-sdk-go-v2/service/sqs v1.34.4
+ github.com/aws/smithy-go v1.20.4
+ github.com/samber/lo v1.47.0
+)
+
+require (
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
+ github.com/oklog/ulid v1.3.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ golang.org/x/text v0.16.0 // indirect
+)
+
+go 1.21
diff --git a/_examples/pubsubs/aws-sns/go.sum b/_examples/pubsubs/aws-sns/go.sum
new file mode 100644
index 000000000..5d1d2ffc4
--- /dev/null
+++ b/_examples/pubsubs/aws-sns/go.sum
@@ -0,0 +1,59 @@
+github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o=
+github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
+github.com/ThreeDotsLabs/watermill-aws v1.0.0-rc.2 h1:5eyROGg3sIB4eZGEkDjbjE/TXgqCVpR1yTGAWp9zxbQ=
+github.com/ThreeDotsLabs/watermill-aws v1.0.0-rc.2/go.mod h1:TTht9EHVmVF5iR5joom6VymqgkwcuxsRQfLBc5ITy28=
+github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
+github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
+github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
+github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
+github.com/aws/aws-sdk-go-v2/service/sns v1.31.4 h1:Bwb1nTBy6jrLJgSlI+jLt27rjyS1Kg030X5yWPnTecI=
+github.com/aws/aws-sdk-go-v2/service/sns v1.31.4/go.mod h1:wDacBq+NshhM8KhdysbM4wRFxVyghyj7AAI+l8+o9f0=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.34.4 h1:FXPO72iKC5YmYNEANltl763bUj8A6qT20wx8Jwvxlsw=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.34.4/go.mod h1:7idt3XszF6sE9WPS1GqZRiDJOxw4oPtlRBXodWnCGjU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
+github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
+github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
+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/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
+github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
+github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+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/_examples/pubsubs/aws-sns/main.go b/_examples/pubsubs/aws-sns/main.go
new file mode 100644
index 000000000..e66e116a7
--- /dev/null
+++ b/_examples/pubsubs/aws-sns/main.go
@@ -0,0 +1,133 @@
+// Sources for https://watermill.io/docs/getting-started/
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/url"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ amazonsns "github.com/aws/aws-sdk-go-v2/service/sns"
+ amazonsqs "github.com/aws/aws-sdk-go-v2/service/sqs"
+ transport "github.com/aws/smithy-go/endpoints"
+ "github.com/samber/lo"
+
+ "github.com/ThreeDotsLabs/watermill"
+ "github.com/ThreeDotsLabs/watermill-aws/sns"
+ "github.com/ThreeDotsLabs/watermill-aws/sqs"
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+func main() {
+ logger := watermill.NewStdLogger(false, false)
+
+ snsOpts := []func(*amazonsns.Options){
+ amazonsns.WithEndpointResolverV2(sns.OverrideEndpointResolver{
+ Endpoint: transport.Endpoint{
+ URI: *lo.Must(url.Parse("http://localstack:4566")),
+ },
+ }),
+ }
+
+ sqsOpts := []func(*amazonsqs.Options){
+ amazonsqs.WithEndpointResolverV2(sqs.OverrideEndpointResolver{
+ Endpoint: transport.Endpoint{
+ URI: *lo.Must(url.Parse("http://localstack:4566")),
+ },
+ }),
+ }
+
+ topicResolver, err := sns.NewGenerateArnTopicResolver("000000000000", "us-east-1")
+ if err != nil {
+ panic(err)
+ }
+
+ newSubscriber := func(name string) (message.Subscriber, error) {
+ subscriberConfig := sns.SubscriberConfig{
+ AWSConfig: aws.Config{
+ Credentials: aws.AnonymousCredentials{},
+ },
+ OptFns: snsOpts,
+ TopicResolver: topicResolver,
+ GenerateSqsQueueName: func(ctx context.Context, snsTopic sns.TopicArn) (string, error) {
+ topic, err := sns.ExtractTopicNameFromTopicArn(snsTopic)
+ if err != nil {
+ return "", err
+ }
+
+ return fmt.Sprintf("%v-%v", topic, name), nil
+ },
+ }
+
+ sqsSubscriberConfig := sqs.SubscriberConfig{
+ AWSConfig: aws.Config{
+ Credentials: aws.AnonymousCredentials{},
+ },
+ OptFns: sqsOpts,
+ }
+
+ return sns.NewSubscriber(subscriberConfig, sqsSubscriberConfig, logger)
+ }
+
+ subA, err := newSubscriber("subA")
+ if err != nil {
+ panic(err)
+ }
+
+ subB, err := newSubscriber("subB")
+ if err != nil {
+ panic(err)
+ }
+
+ messagesA, err := subA.Subscribe(context.Background(), "example-topic")
+ if err != nil {
+ panic(err)
+ }
+
+ messagesB, err := subB.Subscribe(context.Background(), "example-topic")
+ if err != nil {
+ panic(err)
+ }
+
+ go process("A", messagesA)
+ go process("B", messagesB)
+
+ publisherConfig := sns.PublisherConfig{
+ AWSConfig: aws.Config{
+ Credentials: aws.AnonymousCredentials{},
+ },
+ OptFns: snsOpts,
+ TopicResolver: topicResolver,
+ }
+
+ publisher, err := sns.NewPublisher(publisherConfig, logger)
+ if err != nil {
+ panic(err)
+ }
+
+ publishMessages(publisher)
+}
+
+func publishMessages(publisher message.Publisher) {
+ for {
+ msg := message.NewMessage(watermill.NewUUID(), []byte("Hello, world!"))
+
+ if err := publisher.Publish("example-topic", msg); err != nil {
+ panic(err)
+ }
+
+ time.Sleep(time.Second)
+ }
+}
+
+func process(prefix string, messages <-chan *message.Message) {
+ for msg := range messages {
+ log.Printf("%v received message: %s, payload: %s", prefix, msg.UUID, string(msg.Payload))
+
+ // we need to Acknowledge that we received and processed the message,
+ // otherwise, it will be resent over and over again.
+ msg.Ack()
+ }
+}
diff --git a/_examples/pubsubs/aws-sqs/.validate_example.yml b/_examples/pubsubs/aws-sqs/.validate_example.yml
new file mode 100644
index 000000000..b60d0b097
--- /dev/null
+++ b/_examples/pubsubs/aws-sqs/.validate_example.yml
@@ -0,0 +1,4 @@
+validation_cmd: "docker compose up"
+teardown_cmd: "docker compose down"
+timeout: 120
+expected_output: "received message: [0-9a-f\\-]+, payload: Hello, world!"
diff --git a/_examples/pubsubs/aws-sqs/docker-compose.yml b/_examples/pubsubs/aws-sqs/docker-compose.yml
new file mode 100644
index 000000000..fa75e2c2e
--- /dev/null
+++ b/_examples/pubsubs/aws-sqs/docker-compose.yml
@@ -0,0 +1,24 @@
+services:
+ server:
+ image: golang:1.23
+ restart: unless-stopped
+ volumes:
+ - .:/app
+ - $GOPATH/pkg/mod:/go/pkg/mod
+ working_dir: /app
+ command: go run main.go
+
+ localstack:
+ image: localstack/localstack:latest
+ environment:
+ - SERVICES=sqs,sns
+ - AWS_DEFAULT_REGION=us-east-1
+ - EDGE_PORT=4566
+ ports:
+ - "4566-4597:4566-4597"
+ healthcheck:
+ test: awslocal sqs list-queues && awslocal sns list-topics
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
diff --git a/_examples/pubsubs/aws-sqs/go.mod b/_examples/pubsubs/aws-sqs/go.mod
new file mode 100644
index 000000000..282fe5acf
--- /dev/null
+++ b/_examples/pubsubs/aws-sqs/go.mod
@@ -0,0 +1,22 @@
+module main
+
+require (
+ github.com/ThreeDotsLabs/watermill v1.3.7
+ github.com/ThreeDotsLabs/watermill-aws v1.0.0-rc.2
+ github.com/aws/aws-sdk-go-v2 v1.30.4
+ github.com/aws/aws-sdk-go-v2/service/sqs v1.34.4
+ github.com/aws/smithy-go v1.20.4
+ github.com/samber/lo v1.47.0
+)
+
+require (
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
+ github.com/oklog/ulid v1.3.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ golang.org/x/text v0.16.0 // indirect
+)
+
+go 1.21
diff --git a/_examples/pubsubs/aws-sqs/go.sum b/_examples/pubsubs/aws-sqs/go.sum
new file mode 100644
index 000000000..d04189996
--- /dev/null
+++ b/_examples/pubsubs/aws-sqs/go.sum
@@ -0,0 +1,57 @@
+github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o=
+github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
+github.com/ThreeDotsLabs/watermill-aws v1.0.0-rc.2 h1:5eyROGg3sIB4eZGEkDjbjE/TXgqCVpR1yTGAWp9zxbQ=
+github.com/ThreeDotsLabs/watermill-aws v1.0.0-rc.2/go.mod h1:TTht9EHVmVF5iR5joom6VymqgkwcuxsRQfLBc5ITy28=
+github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
+github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
+github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
+github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.34.4 h1:FXPO72iKC5YmYNEANltl763bUj8A6qT20wx8Jwvxlsw=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.34.4/go.mod h1:7idt3XszF6sE9WPS1GqZRiDJOxw4oPtlRBXodWnCGjU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
+github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
+github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
+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/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
+github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
+github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+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/_examples/pubsubs/aws-sqs/main.go b/_examples/pubsubs/aws-sqs/main.go
new file mode 100644
index 000000000..b3c96848b
--- /dev/null
+++ b/_examples/pubsubs/aws-sqs/main.go
@@ -0,0 +1,85 @@
+// Sources for https://watermill.io/docs/getting-started/
+package main
+
+import (
+ "context"
+ "log"
+ "net/url"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ amazonsqs "github.com/aws/aws-sdk-go-v2/service/sqs"
+ transport "github.com/aws/smithy-go/endpoints"
+ "github.com/samber/lo"
+
+ "github.com/ThreeDotsLabs/watermill"
+ "github.com/ThreeDotsLabs/watermill-aws/sqs"
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+func main() {
+ logger := watermill.NewStdLogger(false, false)
+
+ sqsOpts := []func(*amazonsqs.Options){
+ amazonsqs.WithEndpointResolverV2(sqs.OverrideEndpointResolver{
+ Endpoint: transport.Endpoint{
+ URI: *lo.Must(url.Parse("http://localstack:4566")),
+ },
+ }),
+ }
+
+ subscriberConfig := sqs.SubscriberConfig{
+ AWSConfig: aws.Config{
+ Credentials: aws.AnonymousCredentials{},
+ },
+ OptFns: sqsOpts,
+ }
+
+ subscriber, err := sqs.NewSubscriber(subscriberConfig, logger)
+ if err != nil {
+ panic(err)
+ }
+
+ messages, err := subscriber.Subscribe(context.Background(), "example-topic")
+ if err != nil {
+ panic(err)
+ }
+
+ go process(messages)
+
+ publisherConfig := sqs.PublisherConfig{
+ AWSConfig: aws.Config{
+ Credentials: aws.AnonymousCredentials{},
+ },
+ OptFns: sqsOpts,
+ }
+
+ publisher, err := sqs.NewPublisher(publisherConfig, logger)
+ if err != nil {
+ panic(err)
+ }
+
+ publishMessages(publisher)
+}
+
+func publishMessages(publisher message.Publisher) {
+ for {
+ msg := message.NewMessage(watermill.NewUUID(), []byte("Hello, world!"))
+
+ if err := publisher.Publish("example-topic", msg); err != nil {
+ panic(err)
+ }
+
+ time.Sleep(time.Second)
+ }
+}
+
+func process(messages <-chan *message.Message) {
+ for msg := range messages {
+ log.Printf("received message: %s, payload: %s", msg.UUID, string(msg.Payload))
+
+ // we need to Acknowledge that we received and processed the message,
+ // otherwise, it will be resent over and over again.
+ msg.Ack()
+ }
+}
diff --git a/_examples/real-world-examples/delayed-messages/docker-compose.yml b/_examples/real-world-examples/delayed-messages/docker-compose.yml
new file mode 100644
index 000000000..b8310f17c
--- /dev/null
+++ b/_examples/real-world-examples/delayed-messages/docker-compose.yml
@@ -0,0 +1,25 @@
+services:
+ server:
+ image: golang:1.23
+ restart: unless-stopped
+ volumes:
+ - .:/app
+ - $GOPATH/pkg/mod:/go/pkg/mod
+ working_dir: /app
+ command: go run main.go
+
+ redis:
+ image: redis:7
+ ports:
+ - 6379:6379
+ restart: unless-stopped
+
+ postgres:
+ image: postgres:15
+ restart: unless-stopped
+ ports:
+ - 5432:5432
+ environment:
+ POSTGRES_USER: watermill
+ POSTGRES_DB: watermill
+ POSTGRES_PASSWORD: "password"
diff --git a/_examples/real-world-examples/delayed-messages/go.mod b/_examples/real-world-examples/delayed-messages/go.mod
new file mode 100644
index 000000000..f16b3362b
--- /dev/null
+++ b/_examples/real-world-examples/delayed-messages/go.mod
@@ -0,0 +1,32 @@
+module delayed-messsages
+
+go 1.23.0
+
+require (
+ github.com/ThreeDotsLabs/watermill v1.4.0-rc.2
+ github.com/ThreeDotsLabs/watermill-redisstream v1.4.2
+ github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1
+ github.com/brianvoe/gofakeit/v6 v6.28.0
+ github.com/google/uuid v1.6.0
+ github.com/lib/pq v1.10.2
+ github.com/redis/go-redis/v9 v9.6.1
+)
+
+require (
+ github.com/Rican7/retry v0.3.1 // indirect
+ github.com/cenkalti/backoff/v3 v3.2.2 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
+ github.com/oklog/ulid v1.3.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sony/gobreaker v1.0.0 // indirect
+ github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+)
diff --git a/_examples/real-world-examples/delayed-messages/go.sum b/_examples/real-world-examples/delayed-messages/go.sum
new file mode 100644
index 000000000..2fdf15843
--- /dev/null
+++ b/_examples/real-world-examples/delayed-messages/go.sum
@@ -0,0 +1,142 @@
+github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=
+github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=
+github.com/ThreeDotsLabs/watermill v1.4.0-rc.2 h1:K62uIAKOkCHTXtAwY+Nj95vyLR0y25UMBsbf/FuWCeQ=
+github.com/ThreeDotsLabs/watermill v1.4.0-rc.2/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
+github.com/ThreeDotsLabs/watermill-redisstream v1.4.2 h1:FY6tsBcbhbJpKDOssU4bfybstqY0hQHwiZmVq9qyILQ=
+github.com/ThreeDotsLabs/watermill-redisstream v1.4.2/go.mod h1:69++855LyB+ckYDe60PiJLBcUrpckfDE2WwyzuVJRCk=
+github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1 h1:uYfnh1EoqXrzHu+bX/TboRyv4ou+EFcmkC1MABeQ0lI=
+github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1/go.mod h1:ttA/lhzSh0YyDkosq1Cgc7IYz6Arrba0jIWfdnON0WA=
+github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
+github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
+github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
+github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
+github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
+github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
+github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+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/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
+github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
+github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
+github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
+golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/_examples/real-world-examples/delayed-messages/main.go b/_examples/real-world-examples/delayed-messages/main.go
new file mode 100644
index 000000000..a0cd483ea
--- /dev/null
+++ b/_examples/real-world-examples/delayed-messages/main.go
@@ -0,0 +1,220 @@
+package main
+
+import (
+ "context"
+ stdSQL "database/sql"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/google/uuid"
+ _ "github.com/lib/pq"
+ "github.com/redis/go-redis/v9"
+
+ "github.com/ThreeDotsLabs/watermill"
+ "github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream"
+ "github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql"
+ "github.com/ThreeDotsLabs/watermill/components/cqrs"
+ "github.com/ThreeDotsLabs/watermill/components/delay"
+ "github.com/ThreeDotsLabs/watermill/components/forwarder"
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+func main() {
+ db, err := stdSQL.Open("postgres", "postgres://watermill:password@postgres:5432/watermill?sslmode=disable")
+ if err != nil {
+ panic(err)
+ }
+
+ logger := watermill.NewStdLogger(false, false)
+
+ redisClient := redis.NewClient(&redis.Options{Addr: "redis:6379"})
+ marshaler := cqrs.JSONMarshaler{
+ GenerateName: cqrs.StructName,
+ }
+
+ redisPublisher, err := redisstream.NewPublisher(redisstream.PublisherConfig{
+ Client: redisClient,
+ }, logger)
+ if err != nil {
+ panic(err)
+ }
+
+ var sqlPublisher message.Publisher
+ sqlPublisher, err = sql.NewDelayedPostgreSQLPublisher(db, sql.DelayedPostgreSQLPublisherConfig{
+ DelayPublisherConfig: delay.PublisherConfig{},
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ sqlPublisher = forwarder.NewPublisher(sqlPublisher, forwarder.PublisherConfig{
+ ForwarderTopic: "forwarder",
+ })
+
+ eventBus, err := cqrs.NewEventBusWithConfig(redisPublisher, cqrs.EventBusConfig{
+ GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
+ return params.EventName, nil
+ },
+ Marshaler: marshaler,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ commandBus, err := cqrs.NewCommandBusWithConfig(sqlPublisher, cqrs.CommandBusConfig{
+ GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
+ return params.CommandName, nil
+ },
+ Marshaler: marshaler,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ router := message.NewDefaultRouter(logger)
+
+ eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{
+ GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
+ return params.EventName, nil
+ },
+ SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
+ return redisstream.NewSubscriber(redisstream.SubscriberConfig{
+ Client: redisClient,
+ ConsumerGroup: params.HandlerName,
+ }, logger)
+ },
+ Marshaler: marshaler,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cqrs.CommandProcessorConfig{
+ GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
+ return params.CommandName, nil
+ },
+ SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
+ return redisstream.NewSubscriber(redisstream.SubscriberConfig{
+ Client: redisClient,
+ ConsumerGroup: params.HandlerName,
+ }, logger)
+ },
+ Marshaler: marshaler,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ err = eventProcessor.AddHandlers(
+ cqrs.NewEventHandler(
+ "OnOrderPlacedHandler",
+ func(ctx context.Context, event *OrderPlaced) error {
+ fmt.Printf("💰 Received order from %v <%v>\n", event.Customer.Name, event.Customer.Email)
+
+ cmd := SendFeedbackForm{
+ To: event.Customer.Email,
+ Name: event.Customer.Name,
+ }
+
+ // In a real world scenario, we would delay the command by a few days
+ ctx = delay.WithContext(ctx, delay.For(8*time.Second))
+
+ err := commandBus.Send(ctx, cmd)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ },
+ ),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ err = commandProcessor.AddHandlers(
+ cqrs.NewCommandHandler(
+ "OnSendFeedbackForm",
+ func(ctx context.Context, cmd *SendFeedbackForm) error {
+ fmt.Printf("📧 Sending feedback form to %v <%v>\n", cmd.Name, cmd.To)
+
+ // In a real world scenario, we would send an email to the customer here
+
+ return nil
+ },
+ ),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ sqlSubscriber, err := sql.NewDelayedPostgreSQLSubscriber(db, sql.DelayedPostgreSQLSubscriberConfig{
+ DeleteOnAck: true,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = forwarder.NewForwarder(
+ sqlSubscriber,
+ redisPublisher,
+ logger,
+ forwarder.Config{
+ ForwarderTopic: "forwarder",
+ Router: router,
+ },
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ go func() {
+ err = router.Run(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ <-router.Running()
+
+ for {
+ name := gofakeit.FirstName()
+ e := OrderPlaced{
+ OrderID: uuid.NewString(),
+ Customer: Customer{
+ Name: name,
+ Email: fmt.Sprintf("%v@%v", strings.ToLower(name), gofakeit.DomainName()),
+ },
+ }
+
+ err = eventBus.Publish(context.Background(), e)
+ if err != nil {
+ panic(err)
+ }
+
+ time.Sleep(5 * time.Second)
+ }
+}
+
+type Customer struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+}
+
+type OrderPlaced struct {
+ OrderID string `json:"order_id"`
+ Customer Customer `json:"customer"`
+}
+
+type SendFeedbackForm struct {
+ To string `json:"to"`
+ Name string `json:"name"`
+}
diff --git a/_examples/real-world-examples/delayed-requeue/docker-compose.yml b/_examples/real-world-examples/delayed-requeue/docker-compose.yml
new file mode 100644
index 000000000..b8310f17c
--- /dev/null
+++ b/_examples/real-world-examples/delayed-requeue/docker-compose.yml
@@ -0,0 +1,25 @@
+services:
+ server:
+ image: golang:1.23
+ restart: unless-stopped
+ volumes:
+ - .:/app
+ - $GOPATH/pkg/mod:/go/pkg/mod
+ working_dir: /app
+ command: go run main.go
+
+ redis:
+ image: redis:7
+ ports:
+ - 6379:6379
+ restart: unless-stopped
+
+ postgres:
+ image: postgres:15
+ restart: unless-stopped
+ ports:
+ - 5432:5432
+ environment:
+ POSTGRES_USER: watermill
+ POSTGRES_DB: watermill
+ POSTGRES_PASSWORD: "password"
diff --git a/_examples/real-world-examples/delayed-requeue/go.mod b/_examples/real-world-examples/delayed-requeue/go.mod
new file mode 100644
index 000000000..78b5e2376
--- /dev/null
+++ b/_examples/real-world-examples/delayed-requeue/go.mod
@@ -0,0 +1,33 @@
+module delayed-requeue
+
+go 1.23.0
+
+require (
+ github.com/ThreeDotsLabs/watermill v1.4.0-rc.2
+ github.com/ThreeDotsLabs/watermill-redisstream v1.4.2
+ github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1
+ github.com/brianvoe/gofakeit/v6 v6.28.0
+ github.com/lib/pq v1.10.9
+ github.com/redis/go-redis/v9 v9.7.0
+)
+
+require (
+ github.com/Rican7/retry v0.3.1 // indirect
+ github.com/cenkalti/backoff/v3 v3.2.2 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/go-sql-driver/mysql v1.8.1 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
+ github.com/oklog/ulid v1.3.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sony/gobreaker v1.0.0 // indirect
+ github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+)
diff --git a/_examples/real-world-examples/delayed-requeue/go.sum b/_examples/real-world-examples/delayed-requeue/go.sum
new file mode 100644
index 000000000..7349c7b99
--- /dev/null
+++ b/_examples/real-world-examples/delayed-requeue/go.sum
@@ -0,0 +1,144 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=
+github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=
+github.com/ThreeDotsLabs/watermill v1.4.0-rc.2 h1:K62uIAKOkCHTXtAwY+Nj95vyLR0y25UMBsbf/FuWCeQ=
+github.com/ThreeDotsLabs/watermill v1.4.0-rc.2/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
+github.com/ThreeDotsLabs/watermill-redisstream v1.4.2 h1:FY6tsBcbhbJpKDOssU4bfybstqY0hQHwiZmVq9qyILQ=
+github.com/ThreeDotsLabs/watermill-redisstream v1.4.2/go.mod h1:69++855LyB+ckYDe60PiJLBcUrpckfDE2WwyzuVJRCk=
+github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1 h1:uYfnh1EoqXrzHu+bX/TboRyv4ou+EFcmkC1MABeQ0lI=
+github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1/go.mod h1:ttA/lhzSh0YyDkosq1Cgc7IYz6Arrba0jIWfdnON0WA=
+github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
+github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
+github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
+github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
+github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
+github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
+github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
+github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
+github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
+github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
+golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/_examples/real-world-examples/delayed-requeue/main.go b/_examples/real-world-examples/delayed-requeue/main.go
new file mode 100644
index 000000000..7aa0a06fd
--- /dev/null
+++ b/_examples/real-world-examples/delayed-requeue/main.go
@@ -0,0 +1,197 @@
+package main
+
+import (
+ "context"
+ stdSQL "database/sql"
+ "fmt"
+ "math/rand"
+ "time"
+
+ "github.com/ThreeDotsLabs/watermill/message/router/middleware"
+
+ "github.com/brianvoe/gofakeit/v6"
+ _ "github.com/lib/pq"
+ "github.com/redis/go-redis/v9"
+
+ "github.com/ThreeDotsLabs/watermill"
+ "github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream"
+ "github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql"
+ "github.com/ThreeDotsLabs/watermill/components/cqrs"
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+func main() {
+ db, err := stdSQL.Open("postgres", "postgres://watermill:password@postgres:5432/watermill?sslmode=disable")
+ if err != nil {
+ panic(err)
+ }
+
+ logger := watermill.NewStdLogger(false, false)
+
+ redisClient := redis.NewClient(&redis.Options{Addr: "redis:6379"})
+
+ redisPublisher, err := redisstream.NewPublisher(redisstream.PublisherConfig{
+ Client: redisClient,
+ }, logger)
+ if err != nil {
+ panic(err)
+ }
+
+ delayedRequeuer, err := sql.NewPostgreSQLDelayedRequeuer(sql.DelayedRequeuerConfig{
+ DB: db,
+ Publisher: redisPublisher,
+ DelayOnError: &middleware.DelayOnError{
+ InitialInterval: 10 * time.Second,
+ MaxInterval: 3 * time.Minute,
+ Multiplier: 2,
+ },
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ marshaler := cqrs.JSONMarshaler{
+ GenerateName: cqrs.StructName,
+ }
+
+ eventBus, err := cqrs.NewEventBusWithConfig(redisPublisher, cqrs.EventBusConfig{
+ GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
+ return params.EventName, nil
+ },
+ Marshaler: marshaler,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ router := message.NewDefaultRouter(logger)
+ router.AddMiddleware(delayedRequeuer.Middleware()...)
+
+ eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{
+ GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
+ return params.EventName, nil
+ },
+ SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
+ return redisstream.NewSubscriber(redisstream.SubscriberConfig{
+ Client: redisClient,
+ ConsumerGroup: params.HandlerName,
+ }, logger)
+ },
+ Marshaler: marshaler,
+ Logger: logger,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ err = eventProcessor.AddHandlers(
+ cqrs.NewEventHandler(
+ "OnOrderPlacedHandler",
+ func(ctx context.Context, event *OrderPlaced) error {
+ if event.OrderID == "" {
+ fmt.Println("ERROR: Received order placed without order_id")
+ return fmt.Errorf("empty order_id")
+ }
+
+ fmt.Println("Received order placed:", event.OrderID)
+
+ return nil
+ },
+ ),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ go func() {
+ err = delayedRequeuer.Run(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ go func() {
+ err = router.Run(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ <-router.Running()
+
+ i := 0
+
+ for {
+ e := newFakeOrderPlaced()
+
+ i++
+
+ if i == 10 {
+ e.OrderID = ""
+ i = 0
+ }
+
+ err = eventBus.Publish(context.Background(), e)
+ if err != nil {
+ panic(err)
+ }
+
+ time.Sleep(1 * time.Second)
+ }
+}
+
+func newFakeOrderPlaced() OrderPlaced {
+ var products []Product
+
+ for i := 0; i < rand.Intn(5)+1; i++ {
+ products = append(products, Product{
+ ID: watermill.NewShortUUID(),
+ Name: gofakeit.ProductName(),
+ })
+ }
+
+ return OrderPlaced{
+ OrderID: watermill.NewUUID(),
+ Customer: Customer{
+ ID: watermill.NewULID(),
+ Name: gofakeit.Name(),
+ Email: gofakeit.Email(),
+ Phone: gofakeit.Phone(),
+ },
+ Address: Address{
+ Street: gofakeit.Street(),
+ City: gofakeit.City(),
+ Zip: gofakeit.Zip(),
+ Country: gofakeit.Country(),
+ },
+ Products: products,
+ }
+}
+
+type OrderPlaced struct {
+ OrderID string `json:"order_id"`
+ Customer Customer `json:"customer"`
+ Address Address `json:"address"`
+ Products []Product `json:"products"`
+}
+
+type Customer struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Phone string `json:"phone"`
+}
+
+type Address struct {
+ Street string `json:"street"`
+ City string `json:"city"`
+ Zip string `json:"zip"`
+ Country string `json:"country"`
+}
+
+type Product struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
diff --git a/codecov.yml b/codecov.yml
index 1a05fc5a2..4181db8d3 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,2 +1,12 @@
ignore:
- - "pubsub/tests" # test helpers used to test Pub/Subs
+ - "pubsub/tests" # test helpers used to test Pub/Subs
+
+comment: no # do not comment PR with the result
+
+coverage:
+ project:
+ default:
+ target: auto
+ threshold: 5%
+ status:
+ patch: false # do not run coverage on patch nor changes
diff --git a/components/delay/delay.go b/components/delay/delay.go
new file mode 100644
index 000000000..255419af1
--- /dev/null
+++ b/components/delay/delay.go
@@ -0,0 +1,68 @@
+package delay
+
+import (
+ "context"
+ "time"
+
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+// Delay represents a message's delay.
+// It can be either a delay until a specific time or a delay for a specific duration.
+// The zero value of Delay is a zero delay.
+//
+// IMPORTANT: Delay doesn't work with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.
+// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/
+type Delay struct {
+ time time.Time
+ duration time.Duration
+}
+
+func (d Delay) IsZero() bool {
+ return d.time.IsZero()
+}
+
+// Until returns a delay of the given time.
+func Until(delayedUntil time.Time) Delay {
+ return Delay{
+ time: delayedUntil,
+ duration: delayedUntil.Sub(time.Now().UTC()),
+ }
+}
+
+// For returns a delay of now plus the given duration.
+func For(delayedFor time.Duration) Delay {
+ return Delay{
+ time: time.Now().UTC().Add(delayedFor),
+ duration: delayedFor,
+ }
+}
+
+type contextKey string
+
+var (
+ delayContextKey = contextKey("delay")
+)
+
+// WithContext returns a new context with the given delay.
+// If used together with a publisher wrapped with NewPublisher, the delay will be applied to the message.
+//
+// IMPORTANT: Delay doesn't work with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.
+// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/
+func WithContext(ctx context.Context, delay Delay) context.Context {
+ return context.WithValue(ctx, delayContextKey, delay)
+}
+
+const (
+ DelayedUntilKey = "_watermill_delayed_until"
+ DelayedForKey = "_watermill_delayed_for"
+)
+
+// Message sets the delay metadata on the message.
+//
+// IMPORTANT: Delay doesn't work with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.
+// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/
+func Message(msg *message.Message, delay Delay) {
+ msg.Metadata.Set(DelayedUntilKey, delay.time.Format(time.RFC3339))
+ msg.Metadata.Set(DelayedForKey, delay.duration.String())
+}
diff --git a/components/delay/publisher.go b/components/delay/publisher.go
new file mode 100644
index 000000000..7318ecfb0
--- /dev/null
+++ b/components/delay/publisher.go
@@ -0,0 +1,83 @@
+package delay
+
+import (
+ "errors"
+
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+type DefaultDelayGeneratorParams struct {
+ Topic string
+ Message *message.Message
+}
+
+// PublisherConfig is a configuration for the delay publisher.
+type PublisherConfig struct {
+ // DefaultDelayGenerator is a function that generates the default delay for a message.
+ // If the message doesn't have the delay metadata set, the default delay will be applied.
+ DefaultDelayGenerator func(params DefaultDelayGeneratorParams) (Delay, error)
+
+ // AllowNoDelay allows publishing messages without a delay set.
+ // By default, the publisher returns an error when a message is published without a delay and no default delay generator is provided.
+ AllowNoDelay bool
+}
+
+// NewPublisher wraps a publisher with a delay mechanism.
+// A message can be published with delay metadata set in the context by using the WithContext function.
+// If the message doesn't have the delay metadata set, the default delay will be applied, if provided.
+func NewPublisher(pub message.Publisher, config PublisherConfig) (message.Publisher, error) {
+ return &publisher{
+ pub: pub,
+ config: config,
+ }, nil
+}
+
+type publisher struct {
+ pub message.Publisher
+ config PublisherConfig
+}
+
+func (p *publisher) Publish(topic string, messages ...*message.Message) error {
+ for i := range messages {
+ err := p.applyDelay(topic, messages[i])
+ if err != nil {
+ return err
+ }
+ }
+ return p.pub.Publish(topic, messages...)
+}
+
+func (p *publisher) Close() error {
+ return p.pub.Close()
+}
+
+func (p *publisher) applyDelay(topic string, msg *message.Message) error {
+ if msg.Metadata.Get(DelayedForKey) != "" {
+ return nil
+ }
+
+ if msg.Context().Value(delayContextKey) != nil {
+ delay := msg.Context().Value(delayContextKey).(Delay)
+ Message(msg, delay)
+ return nil
+ }
+
+ if p.config.DefaultDelayGenerator != nil {
+ delay, err := p.config.DefaultDelayGenerator(DefaultDelayGeneratorParams{
+ Topic: topic,
+ Message: msg,
+ })
+ if err != nil {
+ return err
+ }
+ Message(msg, delay)
+
+ return nil
+ }
+
+ if !p.config.AllowNoDelay {
+ return errors.New("message doesn't have a delay set")
+ }
+
+ return nil
+}
diff --git a/components/delay/publisher_test.go b/components/delay/publisher_test.go
new file mode 100644
index 000000000..bd6ba3e58
--- /dev/null
+++ b/components/delay/publisher_test.go
@@ -0,0 +1,178 @@
+package delay_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ThreeDotsLabs/watermill/components/delay"
+ "github.com/ThreeDotsLabs/watermill/message"
+ "github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
+)
+
+func TestPublisher(t *testing.T) {
+ pubSub := gochannel.NewGoChannel(gochannel.Config{}, nil)
+
+ messages, err := pubSub.Subscribe(context.Background(), "test")
+ require.NoError(t, err)
+
+ pub, err := delay.NewPublisher(pubSub, delay.PublisherConfig{})
+ require.NoError(t, err)
+
+ pubAllowNoDelay, err := delay.NewPublisher(pubSub, delay.PublisherConfig{
+ AllowNoDelay: true,
+ })
+ require.NoError(t, err)
+
+ defaultDelayPub, err := delay.NewPublisher(pubSub, delay.PublisherConfig{
+ DefaultDelayGenerator: func(params delay.DefaultDelayGeneratorParams) (delay.Delay, error) {
+ return delay.For(1 * time.Second), nil
+ },
+ })
+ require.NoError(t, err)
+
+ testCases := []struct {
+ name string
+ publisher message.Publisher
+ messageConstructor func(id string) *message.Message
+ expectedError bool
+ expectedDelay time.Duration
+ }{
+ {
+ name: "no delay",
+ publisher: pub,
+ messageConstructor: func(id string) *message.Message {
+ return message.NewMessage(id, nil)
+ },
+ expectedError: true,
+ expectedDelay: 0,
+ },
+ {
+ name: "no delay but allowed",
+ publisher: pubAllowNoDelay,
+ messageConstructor: func(id string) *message.Message {
+ return message.NewMessage(id, nil)
+ },
+ expectedDelay: 0,
+ },
+ {
+ name: "default delay",
+ publisher: defaultDelayPub,
+ messageConstructor: func(id string) *message.Message {
+ return message.NewMessage(id, nil)
+ },
+ expectedDelay: 1 * time.Second,
+ },
+ {
+ name: "delay from metadata",
+ publisher: pub,
+ messageConstructor: func(id string) *message.Message {
+ msg := message.NewMessage(id, nil)
+ delay.Message(msg, delay.For(2*time.Second))
+ return msg
+ },
+ expectedDelay: 2 * time.Second,
+ },
+ {
+ name: "default delay override with metadata",
+ publisher: defaultDelayPub,
+ messageConstructor: func(id string) *message.Message {
+ msg := message.NewMessage(id, nil)
+ delay.Message(msg, delay.For(2*time.Second))
+ return msg
+ },
+ expectedDelay: 2 * time.Second,
+ },
+ {
+ name: "delay from context",
+ publisher: pub,
+ messageConstructor: func(id string) *message.Message {
+ msg := message.NewMessage(id, nil)
+ ctx := delay.WithContext(context.Background(), delay.For(3*time.Second))
+ msg.SetContext(ctx)
+ return msg
+ },
+ expectedDelay: 3 * time.Second,
+ },
+ {
+ name: "default delay override with context",
+ publisher: defaultDelayPub,
+ messageConstructor: func(id string) *message.Message {
+ msg := message.NewMessage(id, nil)
+ ctx := delay.WithContext(context.Background(), delay.For(3*time.Second))
+ msg.SetContext(ctx)
+ return msg
+ },
+ expectedDelay: 3 * time.Second,
+ },
+ {
+ name: "delay with until",
+ publisher: pub,
+ messageConstructor: func(id string) *message.Message {
+ msg := message.NewMessage(id, nil)
+ delay.Message(msg, delay.Until(time.Now().UTC().Add(4*time.Second)))
+ return msg
+ },
+ expectedDelay: 4 * time.Second,
+ },
+ {
+ name: "both metadata and context set",
+ publisher: defaultDelayPub,
+ messageConstructor: func(id string) *message.Message {
+ msg := message.NewMessage(id, nil)
+ delay.Message(msg, delay.For(5*time.Second))
+ ctx := delay.WithContext(context.Background(), delay.For(6*time.Second))
+ msg.SetContext(ctx)
+ return msg
+ },
+ expectedDelay: 5 * time.Second,
+ },
+ }
+
+ for i, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ id := fmt.Sprint(i)
+
+ msg := testCase.messageConstructor(id)
+ err = testCase.publisher.Publish("test", msg)
+
+ if testCase.expectedError {
+ require.Error(t, err)
+ return
+ }
+
+ require.NoError(t, err)
+ assertMessage(t, messages, id, testCase.expectedDelay)
+ })
+ }
+}
+
+func assertMessage(t *testing.T, messages <-chan *message.Message, expectedID string, expectedDelay time.Duration) {
+ t.Helper()
+ select {
+ case msg := <-messages:
+ assert.Equal(t, expectedID, msg.UUID)
+
+ if expectedDelay == 0 {
+ assert.Empty(t, msg.Metadata.Get(delay.DelayedUntilKey))
+ assert.Empty(t, msg.Metadata.Get(delay.DelayedForKey))
+ } else {
+ delayedFor, err := time.ParseDuration(msg.Metadata.Get(delay.DelayedForKey))
+ require.NoError(t, err)
+ assert.Equal(t, expectedDelay, delayedFor.Round(time.Second))
+
+ delayedUntil, err := time.Parse(time.RFC3339, msg.Metadata.Get(delay.DelayedUntilKey))
+ require.NoError(t, err)
+
+ assert.WithinDuration(t, time.Now().UTC().Add(expectedDelay), delayedUntil, 1*time.Second)
+ }
+
+ msg.Ack()
+ case <-time.After(100 * time.Millisecond):
+ require.Fail(t, "timeout")
+ }
+}
diff --git a/components/requeuer/requeuer.go b/components/requeuer/requeuer.go
new file mode 100644
index 000000000..623b43fe0
--- /dev/null
+++ b/components/requeuer/requeuer.go
@@ -0,0 +1,158 @@
+package requeuer
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/ThreeDotsLabs/watermill"
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+const RetriesKey = "_watermill_requeuer_retries"
+
+// Requeuer is a component that moves messages from one topic to another.
+// It can be used to requeue messages that failed to process.
+type Requeuer struct {
+ config Config
+}
+
+// GeneratePublishTopicParams are the parameters passed to the GeneratePublishTopic function.
+type GeneratePublishTopicParams struct {
+ Message *message.Message
+}
+
+// Config is the configuration for the Requeuer.
+type Config struct {
+ // Subscriber is the subscriber to consume messages from. Required.
+ Subscriber message.Subscriber
+
+ // SubscribeTopic is the topic related to the Subscriber to consume messages from. Required.
+ SubscribeTopic string
+
+ // Publisher is the publisher to publish requeued messages to. Required.
+ Publisher message.Publisher
+
+ // GeneratePublishTopic is the topic related to the Publisher to publish the requeued message to.
+ // For example, it could be a constant, or taken from the message's metadata.
+ // Required.
+ GeneratePublishTopic func(params GeneratePublishTopicParams) (string, error)
+
+ // Delay is the duration to wait before requeueing the message. Optional.
+ // The default is no delay.
+ //
+ // This can be useful to avoid requeueing messages too quickly, for example, to avoid
+ // requeueing a message that failed to process due to a temporary issue.
+ //
+ // Avoid setting this to a very high value, as it will block the message processing.
+ Delay time.Duration
+
+ // Router is the custom router to run the requeue handler on. Optional.
+ Router *message.Router
+}
+
+func (c *Config) setDefaults(logger watermill.LoggerAdapter) error {
+ if c.Router == nil {
+ router, err := message.NewRouter(message.RouterConfig{}, logger)
+ if err != nil {
+ return fmt.Errorf("could not create router: %w", err)
+ }
+
+ c.Router = router
+ }
+
+ return nil
+}
+
+func (c *Config) validate() error {
+ if c.Subscriber == nil {
+ return errors.New("subscriber is required")
+ }
+
+ if c.SubscribeTopic == "" {
+ return errors.New("subscribe topic is required")
+ }
+
+ if c.Publisher == nil {
+ return errors.New("publisher is required")
+ }
+
+ if c.GeneratePublishTopic == nil {
+ return errors.New("generate publish topic is required")
+ }
+
+ return nil
+}
+
+// NewRequeuer creates a new Requeuer with the provided Config.
+// It's not started automatically. You need to call Run on the returned Requeuer.
+func NewRequeuer(
+ config Config,
+ logger watermill.LoggerAdapter,
+) (*Requeuer, error) {
+ if logger == nil {
+ logger = watermill.NewStdLogger(false, false)
+ }
+
+ err := config.setDefaults(logger)
+ if err != nil {
+ return nil, err
+ }
+
+ err = config.validate()
+ if err != nil {
+ return nil, fmt.Errorf("invalid config: %w", err)
+ }
+
+ r := &Requeuer{
+ config: config,
+ }
+
+ config.Router.AddNoPublisherHandler(
+ "requeuer",
+ config.SubscribeTopic,
+ config.Subscriber,
+ r.handler,
+ )
+
+ return r, nil
+}
+
+func (r *Requeuer) handler(msg *message.Message) error {
+ if r.config.Delay > 0 {
+ select {
+ case <-msg.Context().Done():
+ return msg.Context().Err()
+ case <-time.After(r.config.Delay):
+ }
+ }
+
+ topic, err := r.config.GeneratePublishTopic(GeneratePublishTopicParams{Message: msg})
+ if err != nil {
+ return err
+ }
+
+ retriesStr := msg.Metadata.Get(RetriesKey)
+ retries, err := strconv.Atoi(retriesStr)
+ if err != nil {
+ retries = 0
+ }
+
+ retries++
+
+ msg.Metadata.Set(RetriesKey, strconv.Itoa(retries))
+
+ err = r.config.Publisher.Publish(topic, msg)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Run runs the Requeuer.
+func (r *Requeuer) Run(ctx context.Context) error {
+ return r.config.Router.Run(ctx)
+}
diff --git a/components/requeuer/requeuer_test.go b/components/requeuer/requeuer_test.go
new file mode 100644
index 000000000..bf48c8d8c
--- /dev/null
+++ b/components/requeuer/requeuer_test.go
@@ -0,0 +1,102 @@
+package requeuer_test
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ThreeDotsLabs/watermill"
+ "github.com/ThreeDotsLabs/watermill/components/requeuer"
+ "github.com/ThreeDotsLabs/watermill/message"
+ "github.com/ThreeDotsLabs/watermill/message/router/middleware"
+ "github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
+)
+
+func TestRequeue(t *testing.T) {
+ logger := watermill.NewStdLogger(false, false)
+
+ pubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)
+
+ requeue, err := requeuer.NewRequeuer(requeuer.Config{
+ Subscriber: pubSub,
+ SubscribeTopic: "requeue",
+ Publisher: pubSub,
+ GeneratePublishTopic: func(params requeuer.GeneratePublishTopicParams) (string, error) {
+ return "test", nil
+ },
+ Delay: time.Millisecond * 200,
+ }, logger)
+ require.NoError(t, err)
+
+ go func() {
+ err := requeue.Run(context.Background())
+ require.NoError(t, err)
+ }()
+
+ router, err := message.NewRouter(message.RouterConfig{}, logger)
+ require.NoError(t, err)
+
+ pq, err := middleware.PoisonQueue(pubSub, "requeue")
+ require.NoError(t, err)
+
+ router.AddMiddleware(pq)
+
+ receivedMessages := make(chan int, 10)
+
+ counter := 0
+
+ router.AddNoPublisherHandler(
+ "test",
+ "test",
+ pubSub,
+ func(msg *message.Message) error {
+ i, err := strconv.Atoi(string(msg.Payload))
+ if err != nil {
+ return err
+ }
+
+ counter++
+
+ if counter < 10 && i%2 == 0 {
+ return errors.New("error")
+ }
+
+ receivedMessages <- i
+
+ return nil
+ },
+ )
+
+ go func() {
+ err := router.Run(context.Background())
+ require.NoError(t, err)
+ }()
+
+ time.Sleep(time.Second)
+
+ for i := 0; i < 10; i++ {
+ msg := message.NewMessage(watermill.NewUUID(), []byte(fmt.Sprint(i)))
+ err := pubSub.Publish("test", msg)
+ require.NoError(t, err)
+ }
+
+ var received []int
+
+ timeout := false
+ for !timeout {
+ select {
+ case i := <-receivedMessages:
+ received = append(received, i)
+ case <-time.After(5 * time.Second):
+ timeout = true
+ break
+ }
+ }
+
+ require.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, received)
+}
diff --git a/docs/build.sh b/docs/build.sh
index 2ea03e0fa..16125dfee 100755
--- a/docs/build.sh
+++ b/docs/build.sh
@@ -46,6 +46,11 @@ else
"components/cqrs/cqrs.go"
"components/cqrs/marshaler.go"
+ "components/delay/delay.go"
+ "components/delay/publisher.go"
+
+ "components/requeuer/requeuer.go"
+
"components/metrics/builder.go"
"components/metrics/http.go"
@@ -74,6 +79,7 @@ cloneOrPull "https://github.com/ThreeDotsLabs/watermill-sql.git" content/src-lin
cloneOrPull "https://github.com/ThreeDotsLabs/watermill-firestore.git" content/src-link/watermill-firestore
cloneOrPull "https://github.com/ThreeDotsLabs/watermill-bolt.git" content/src-link/watermill-bolt
cloneOrPull "https://github.com/ThreeDotsLabs/watermill-redisstream.git" content/src-link/watermill-redisstream
+cloneOrPull "https://github.com/ThreeDotsLabs/watermill-aws.git" content/src-link/watermill-aws
find content/src-link -name '*.md' -delete
find content/src-link -name '*.html' -delete
diff --git a/docs/content/advanced/delayed-messages.md b/docs/content/advanced/delayed-messages.md
new file mode 100644
index 000000000..f5d6ff729
--- /dev/null
+++ b/docs/content/advanced/delayed-messages.md
@@ -0,0 +1,43 @@
++++
+title = "Delayed Messages"
+description = "Receive messages with a delay"
+weight = -40
+draft = false
+bref = "Receive messages with a delay"
++++
+
+Delaying events or commands is a common use case in many applications.
+For example, you may want to send the user a reminder after a few days of signing up.
+It's not a complex logic to implement, but you can leverage messages to use it out of the box.
+
+## Delay Metadata
+
+Watermill's [`delay`](https://github.com/ThreeDotsLabs/watermill/tree/master/components/delay) package allows you to
+*add delay metadata* to messages.
+
+{{< callout "danger" >}}
+**The delay metadata does nothing by itself. You need to use a Pub/Sub implementation that supports it to make it work.**
+
+See below for supported Pub/Subs.
+{{< /callout >}}
+
+There are two APIs you can use. If you work with raw messages, use `delay.Message`:
+
+```go
+msg := message.NewMessage(watermill.NewUUID(), []byte("hello"))
+delay.Message(msg, delay.For(time.Second * 10))
+```
+
+If you use the CQRS component, use `delay.WithContext` instead (since you can't access the message directly):
+
+{{% load-snippet-partial file="src-link/_examples/real-world-examples/delayed-messages/main.go" first_line_contains="cmd := SendFeedbackForm" last_line_contains="return err" padding_after="1" %}}
+
+You can also use `delay.Until` instead of `delay.For` to specify `time.Time` instead of `time.Duration`.
+
+## Supported Pub/Subs
+
+* [PostgreSQL](/pubsubs/sql/)
+
+## Full Example
+
+See the [full example](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/real-world-examples/delayed-messages) in the Watermill repository.
diff --git a/docs/content/advanced/requeuing-after-error.md b/docs/content/advanced/requeuing-after-error.md
new file mode 100644
index 000000000..f137dae81
--- /dev/null
+++ b/docs/content/advanced/requeuing-after-error.md
@@ -0,0 +1,55 @@
++++
+title = "Requeuing After Error"
+description = "How to requeue a message after it fails to process"
+weight = -20
+draft = false
+bref = "How to requeue a message after it fails to process"
++++
+
+When a message fails to process (a nack is sent), it usually blocks other messages on the same topic (within the same consumer group or partition).
+
+Depending on your setup, it may be useful to requeue the failed message back to the tail of the queue.
+
+Consider this if:
+* You don't care about the order of messages.
+* Your system isn't resilient to blocked messages.
+
+## Requeuer
+
+The `Requeuer` component is a wrapper on the `Router` that moves messages from one topic to another.
+
+{{% load-snippet-partial file="src-link/components/requeuer/requeuer.go" first_line_contains="type Config" last_line_contains="}" %}}
+
+A trivial usage can look like this. It requeues messages from one topic to the same topic after a delay.
+
+{{< callout "danger" >}}
+Using the delay this way is not recommended, as it blocks the entire requeue process for the given time.
+{{< /callout >}}
+
+```go
+req, err := requeuer.NewRequeuer(requeuer.Config{
+ Subscriber: sub,
+ SubscribeTopic: "topic",
+ Publisher: pub,
+ GeneratePublishTopic: func(params requeuer.GeneratePublishTopicParams) (string, error) {
+ return "topic", nil
+ },
+ Delay: time.Millisecond * 200,
+}, logger)
+if err != nil {
+ return err
+}
+
+err := req.Run(context.Background())
+if err != nil {
+ return err
+}
+```
+
+A better way to use the `Requeuer` is to combine it with the `Poison` middleware.
+The middleware moves messages to a separate "poison" topic.
+Then, the requeuer moves them back to the original topic based on the metadata.
+
+You combine this with a Pub/Sub that supports delayed messages.
+See the [full example based on PostgreSQL](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/real-world-examples/delayed-requeue/main.go).
+
diff --git a/docs/content/docs/getting-started.md b/docs/content/docs/getting-started.md
index 4c0765937..37e1cc13e 100644
--- a/docs/content/docs/getting-started.md
+++ b/docs/content/docs/getting-started.md
@@ -198,6 +198,42 @@ A more detailed explanation of how it is working (and how to add live code reloa
{{% load-snippet-partial file="src-link/_examples/pubsubs/sql/main.go" first_line_contains="func process" %}}
{{< /tab >}}
+{{< tab "AWS SQS" "aws-sqs" >}}
+
+
+Running in Docker
+
+{{% load-snippet file="src-link/_examples/pubsubs/aws-sqs/docker-compose.yml" type="yaml" %}}
+
+The source should go to `main.go`.
+
+To run, execute `docker-compose up`.
+
+A more detailed explanation of how it is working (and how to add live code reload) can be found in [*Go Docker dev environment* article](https://threedots.tech/post/go-docker-dev-environment-with-go-modules-and-live-code-reloading/).
+
+
+{{% load-snippet-partial file="src-link/_examples/pubsubs/aws-sqs/main.go" first_line_contains="package main" last_line_contains="process(messages)" %}}
+{{% load-snippet-partial file="src-link/_examples/pubsubs/aws-sqs/main.go" first_line_contains="func process" %}}
+{{< /tab >}}
+
+{{< tab "AWS SNS" "aws-sns" >}}
+
+
+Running in Docker
+
+{{% load-snippet file="src-link/_examples/pubsubs/aws-sns/docker-compose.yml" type="yaml" %}}
+
+The source should go to `main.go`.
+
+To run, execute `docker-compose up`.
+
+A more detailed explanation of how it is working (and how to add live code reload) can be found in [*Go Docker dev environment* article](https://threedots.tech/post/go-docker-dev-environment-with-go-modules-and-live-code-reloading/).
+
+
+{{% load-snippet-partial file="src-link/_examples/pubsubs/aws-sns/main.go" first_line_contains="package main" last_line_contains="go process(" padding_after="1" %}}
+{{% load-snippet-partial file="src-link/_examples/pubsubs/aws-sns/main.go" first_line_contains="func process" %}}
+{{< /tab >}}
+
{{< /tabs >}}
### Creating Messages
@@ -222,7 +258,6 @@ if err != nil {
}
```
-
{{< tabs "publishing" >}}
{{< tab "Go Channel" "go-channel" >}}
@@ -249,6 +284,14 @@ if err != nil {
{{% load-snippet-partial file="src-link/_examples/pubsubs/sql/main.go" first_line_contains="message.NewMessage" last_line_contains="publisher.Publish" padding_after="2" %}}
{{< /tab >}}
+{{< tab "AWS SQS" "aws-sqs" >}}
+{{% load-snippet-partial file="src-link/_examples/pubsubs/aws-sqs/main.go" first_line_contains="message.NewMessage" last_line_contains="publisher.Publish" padding_after="2" %}}
+{{< /tab >}}
+
+{{< tab "AWS SNS" "aws-sns" >}}
+{{% load-snippet-partial file="src-link/_examples/pubsubs/aws-sns/main.go" first_line_contains="message.NewMessage" last_line_contains="publisher.Publish" padding_after="2" %}}
+{{< /tab >}}
+
{{< /tabs >}}
## Router
@@ -304,6 +347,9 @@ The complete example's source can be found at [/_examples/basic/3-router/main.go
To see Watermill's logs, pass any logger that implements the [LoggerAdapter](https://github.com/ThreeDotsLabs/watermill/blob/master/log.go).
For experimental development, you can use `NewStdLogger`.
+Watermill provides ready-to-use `slog` adapter. You can create it with [`watermill.NewSlogLogger`](https://github.com/ThreeDotsLabs/watermill/blob/master/slog.go).
+You can also map Watermill's log levels to `slog` levels with [`watermill.NewSlogLoggerWithLevelMapping`](https://github.com/ThreeDotsLabs/watermill/blob/master/slog.go).
+
## What's next?
For more details, see [documentation topics]({{< ref "/docs" >}}).
diff --git a/docs/content/pubsubs/amazonsqs.md b/docs/content/pubsubs/amazonsqs.md
deleted file mode 100644
index ce370f195..000000000
--- a/docs/content/pubsubs/amazonsqs.md
+++ /dev/null
@@ -1,9 +0,0 @@
-+++
-title = "Amazon SNS/SQS (alpha)"
-description = "Work in Progress"
-date = 2021-07-29T15:30:00+02:00
-bref = "Work in Progress"
-weight = 10
-+++
-
-There's an initial implementation looking for contributors and testers: https://github.com/ThreeDotsLabs/watermill-amazonsqs
diff --git a/docs/content/pubsubs/aws.md b/docs/content/pubsubs/aws.md
new file mode 100644
index 000000000..fd90edcd8
--- /dev/null
+++ b/docs/content/pubsubs/aws.md
@@ -0,0 +1,250 @@
++++
+title = "Amazon AWS SNS/SQS"
+description = "AWS SQS and SNS are fully-managed message queuing and Pub/Sub-like services that make it easy to decouple and scale microservices, distributed systems, and serverless applications."
+date = 2024-10-19T15:30:00+02:00
+bref = "AWS SQS and SNS are fully-managed message queuing and Pub/Sub-like services that make it easy to decouple and scale microservices, distributed systems, and serverless applications."
+weight = 10
++++
+
+AWS SQS and SNS are fully-managed message queuing and Pub/Sub-like services that make it easy to decouple
+and scale microservices, distributed systems, and serverless applications.
+
+Watermill provides a simple way to use AWS SQS and SNS with Go.
+It handles all the AWS SDK internals and provides a simple API to publish and subscribe messages.
+
+Official Documentation:
+- [SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html)
+- [SNS](https://docs.aws.amazon.com/sns/latest/dg/welcome.html)
+
+You can find a fully functional example with AWS SNS in the Watermill examples:
+- [SNS](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/aws-sns)
+- [SQS](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/aws-sqs)
+
+## Installation
+
+```bash
+go get github.com/ThreeDotsLabs/watermill-aws
+```
+
+## SQS vs SNS
+
+While both SQS and SNS are messaging services provided by AWS, they serve different purposes and are best suited for different scenarios in your Watermill applications.
+
+### How SNS is connected with SQS
+
+To use SNS as a Pub/Sub (to have multiple subscribers receiving the same message), you need to create an SNS topic and subscribe to SQS queues.
+When a message is published to the SNS topic, it will be delivered to all subscribed SQS queues.
+We implemented this logic in the `watermill-aws` package out of the box.
+
+When you subscribe to an SNS topic, Watermill AWS creates an SQS queue and subscribes to it.
+
+[![](https://mermaid.ink/img/pako:eNptkU1uwyAQRq-CWLWScwEW2TjbVnboru4Cw9hB5ccZoFIV5e7FxVRKUjaM5j0-jZgLlV4BZTTAOYGTcNBiRmEHR_JZBEYt9SJcJB0RgfBXTro0Gh1OgI_OijfrzS9a_mP0xchXnyABeXcfj1ZbU3gag0Q9AhaxqN1uv8-U1VGIhRDEDKHgjFahzwIHpyot0Hi_lKqtUueNIZPH-5ie77LSMnKEmNDd5rQFdehlblf2FJ7vwg9gIMLt2zV5w0ew_usPkwm9Jef1Y4qZx6cNtYBWaJW3dFnbA40nsDBQlksl8HOgg7tmT6To-beTlEVM0NC0KBHrRimbhAm5C0pHjy9l7b_bbyj6NJ824_oDU0CtBA?type=png)](https://mermaid.live/edit#pako:eNptkU1uwyAQRq-CWLWScwEW2TjbVnboru4Cw9hB5ccZoFIV5e7FxVRKUjaM5j0-jZgLlV4BZTTAOYGTcNBiRmEHR_JZBEYt9SJcJB0RgfBXTro0Gh1OgI_OijfrzS9a_mP0xchXnyABeXcfj1ZbU3gag0Q9AhaxqN1uv8-U1VGIhRDEDKHgjFahzwIHpyot0Hi_lKqtUueNIZPH-5ie77LSMnKEmNDd5rQFdehlblf2FJ7vwg9gIMLt2zV5w0ew_usPkwm9Jef1Y4qZx6cNtYBWaJW3dFnbA40nsDBQlksl8HOgg7tmT6To-beTlEVM0NC0KBHrRimbhAm5C0pHjy9l7b_bbyj6NJ824_oDU0CtBA)
+
+We can say, that a single SQS queue acts as a consumer group or subscription in other Pub/Sub implementations.
+
+The mechanism is detailed in [AWS documentation](https://docs.aws.amazon.com/sns/latest/dg/subscribe-sqs-queue-to-sns-topic.html).
+
+### How to choose between SQS and SNS
+
+#### SQS (Simple Queue Service)
+
+- Use when you need a simple message queue with a single consumer.
+- Great for task queues or background job processing.
+- Supports exactly-once processing (with FIFO queues) and guaranteed order (mostly).
+
+Example use case: Processing user uploads in the background.
+
+[![](https://mermaid.ink/img/pako:eNplkT1uwzAMRq8icGoB5wIasjhrASdevdASHQvVj0NJAYogd69c2wmaaCL4HilI3w1U0AQSIl0yeUUHg2dG13lRzoScjDIT-iQagVG0x1Y0ubcmjsTvzoxX65gp07tRb7zNfVRs-nnNojW7_b4QuV0gHMWIZ4oLtiFMS1U_xGCtGAK_mIXtilJLcaKU2W_4OV1Qw0GV9sY-4ufL8gNZSvR_dt684hO5cH1gMXBw4vJ8M3kNFThih0aX773N7Q7SSI46kKXUyN8ddP5ePMwptD9egUycqYI8aUxbFCAHtLF0SZsU-GvJ6y-2Cjjk87ga91_dnpaW?type=png)](https://mermaid.live/edit#pako:eNplkT1uwzAMRq8icGoB5wIasjhrASdevdASHQvVj0NJAYogd69c2wmaaCL4HilI3w1U0AQSIl0yeUUHg2dG13lRzoScjDIT-iQagVG0x1Y0ubcmjsTvzoxX65gp07tRb7zNfVRs-nnNojW7_b4QuV0gHMWIZ4oLtiFMS1U_xGCtGAK_mIXtilJLcaKU2W_4OV1Qw0GV9sY-4ufL8gNZSvR_dt684hO5cH1gMXBw4vJ8M3kNFThih0aX773N7Q7SSI46kKXUyN8ddP5ePMwptD9egUycqYI8aUxbFCAHtLF0SZsU-GvJ6y-2Cjjk87ga91_dnpaW)
+
+#### SNS (Simple Notification Service)
+
+- Use when you need to broadcast messages to multiple subscribers.
+- Perfect for implementing pub/sub patterns.
+- Useful for event-driven architectures.
+- Supports multiple types of subscribers (SQS, Lambda, HTTP/S, email, SMS, etc.).
+
+Example use case: Notifying multiple services about a new user registration.
+
+Our SNS implementation in Watermill automatically creates and manages SQS queues for each subscriber, simplifying the process of using SNS with multiple SQS queues.
+
+Remember, you can use both in the same application where appropriate. For instance, you might use SNS to broadcast events and SQS to process specific tasks triggered by those events.
+
+To learn how SNS and SQS work together, see the [How SNS is connected with SQS](#how-sns-is-connected-with-sqs) section.
+
+## SQS
+
+### Characteristics
+
+| Feature | Implements | Note |
+|---------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| ConsumerGroups | no | it's a queue, for consumer groups-like functionality use [SNS](#sns) |
+| ExactlyOnceDelivery | no | [yes](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html) |
+| GuaranteedOrder | yes\* | from [AWS Docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html): _"(...) due to the highly distributed architecture, more than one copy of a message might be delivered, and messages may occasionally arrive out of order. Despite this, standard queues make a best-effort attempt to maintain the order in which messages are sent."_ |
+| Persistent | yes | |
+
+### Required permissions
+
+- `"sqs:ReceiveMessage"`
+- `"sqs:DeleteMessage"`
+- `"sqs:GetQueueUrl"`
+- `"sqs:CreateQueue"`
+- `"sqs:GetQueueAttributes"`
+- `"sqs:SendMessage"`
+- `"sqs:ChangeMessageVisibility"`
+
+[todo - verify]
+
+### SQS Configuration
+
+{{% load-snippet-partial file="src-link/watermill-aws/sqs/config.go" first_line_contains="type SubscriberConfig struct " last_line_contains="type GenerateCreateQueueInputFunc" %}}
+
+### Resolving Queue URL
+
+In the Watermill model, we are normalizing the AWS queue url to `topic` used in the `Publish` and `Subscribe` methods.
+
+To give you flexibility of what you want to use as a topic in Watermill, you can customize resolving the queue URL.
+
+{{% load-snippet-partial file="src-link/watermill-aws/sqs/url_resolver.go" first_line_contains="// QueueUrlResolver" last_line_contains="GenerateQueueUrlResolver" %}}
+
+You can implement your own `QueueUrlResolver` or use one of the provided resolvers.
+
+By default, `GetQueueUrlByNameUrlResolver` resolver is used:
+
+{{% load-snippet-partial file="src-link/watermill-aws/sqs/url_resolver.go" first_line_contains="// GetQueueUrlByNameUrlResolver " last_line_contains="NewGetQueueUrlByNameUrlResolver" %}}
+
+There are two more resolvers available:
+
+{{% load-snippet-partial file="src-link/watermill-aws/sqs/url_resolver.go" first_line_contains="// GenerateQueueUrlResolver" last_line_contains="}" %}}
+
+{{% load-snippet-partial file="src-link/watermill-aws/sqs/url_resolver.go" first_line_contains="// TransparentUrlResolver" last_line_contains="}" %}}
+
+### Using with SQS emulator
+
+You may want to use [`goaws`](https://github.com/Admiral-Piett/goaws) or [`localstack`](https://hub.docker.com/r/localstack/localstack) for local development or testing.
+
+You can override the endpoint using the `OptFns` option in the `SubscriberConfig` or `PublisherConfig`.
+
+```go
+package main
+
+import (
+ amazonsqs "github.com/aws/aws-sdk-go-v2/service/sqs"
+ "github.com/ThreeDotsLabs/watermill-amazonsqs/sqs"
+)
+
+func main() {
+ // ...
+
+ sqsOpts := []func(*amazonsqs.Options){
+ amazonsqs.WithEndpointResolverV2(sqs.OverrideEndpointResolver{
+ Endpoint: transport.Endpoint{
+ URI: *lo.Must(url.Parse("http://localstack:4566")),
+ },
+ }),
+ }
+
+ sqsConfig := sqs.SubscriberConfig{
+ AWSConfig: cfg,
+ OptFns: sqsOpts,
+ }
+
+ sub, err := sqs.NewSubscriber(sqsConfig, logger)
+ if err != nil {
+ panic(fmt.Errorf("unable to create new subscriber: %w", err))
+ }
+
+ // ...
+}
+
+```
+
+## SNS
+
+### Characteristics
+
+| Feature | Implements | Note |
+|---------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| ConsumerGroups | yes | yes |
+| ExactlyOnceDelivery | no | [yes](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html) |
+| GuaranteedOrder | yes\* | from [AWS Docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html): _"(...) due to the highly distributed architecture, more than one copy of a message might be delivered, and messages may occasionally arrive out of order. Despite this, standard queues make a best-effort attempt to maintain the order in which messages are sent."_ |
+| Persistent | yes | |
+
+
+### Required permissions
+
+- `sns:Subscribe`
+- `sns:ConfirmSubscription`
+- `sns:Receive`
+- `sns:Unsubscribe`
+
+and all permissions required for SQS:
+
+- `sqs:ReceiveMessage`
+- `sqs:DeleteMessage`
+- `sqs:GetQueueUrl`
+- `sqs:CreateQueue`
+- `sqs:GetQueueAttributes`
+- `sqs:SendMessage`
+- `sqs:ChangeMessageVisibility`
+- `sqs:SetQueueAttributes`
+
+Additionally, if `sns.SubscriberConfig.DoNotSetQueueAccessPolicy` is not enabled, you should have the following:
+
+- `sqs:SetQueueAttributes`
+
+### SNS Configuration
+
+{{% load-snippet-partial file="src-link/watermill-aws/sns/config.go" first_line_contains="type SubscriberConfig struct " last_line_contains="type GenerateSqsQueueNameFn" %}}
+
+Additionally, because SNS Subscriber uses SQS ques as "subscriptions", you need to pass [SQS configuration](#sqs-configuration) as well.
+
+### Resolving Queue URL
+
+In the Watermill model, we normalise AWS Topic ARN to the `topic` used in the `Publish` and `Subscribe` methods.
+
+{{% load-snippet-partial file="src-link/watermill-aws/sns/topic.go" first_line_contains="// TopicResolver" last_line_contains="}" %}}
+
+We are providing two out-of-the-box resolvers:
+
+{{% load-snippet-partial file="src-link/watermill-aws/sns/topic.go" first_line_contains="// TransparentTopicResolver" last_line_contains="}" %}}
+
+{{% load-snippet-partial file="src-link/watermill-aws/sns/topic.go" first_line_contains="// GenerateArnTopicResolver" last_line_contains="}" %}}
+
+### Using with SNS emulator
+
+You may want to use [`goaws`](https://github.com/Admiral-Piett/goaws) or [`localstack`](https://hub.docker.com/r/localstack/localstack) for local development or testing.
+
+You can override the endpoint using the `OptFns` option in the `SubscriberConfig` or `PublisherConfig`.
+
+```go
+package main
+
+import (
+ amazonsns "github.com/aws/aws-sdk-go-v2/service/sns"
+ "github.com/ThreeDotsLabs/watermill-amazonsns/sns"
+)
+
+func main() {
+ // ...
+
+ snsOpts := []func(*amazonsns.Options){
+ amazonsns.WithEndpointResolverV2(sns.OverrideEndpointResolver{
+ Endpoint: transport.Endpoint{
+ URI: *lo.Must(url.Parse("http://localstack:4566")),
+ },
+ }),
+ }
+
+ snsConfig := sns.SubscriberConfig{
+ AWSConfig: cfg,
+ OptFns: snsOpts,
+ }
+
+ sub, err := sns.NewSubscriber(snsConfig, sqsConfig, logger)
+ if err != nil {
+ panic(fmt.Errorf("unable to create new subscriber: %w", err))
+ }
+
+ // ...
+}
+```
diff --git a/docs/content/pubsubs/sql.md b/docs/content/pubsubs/sql.md
index 5413c9436..49df08b1e 100644
--- a/docs/content/pubsubs/sql.md
+++ b/docs/content/pubsubs/sql.md
@@ -30,12 +30,12 @@ go get github.com/ThreeDotsLabs/watermill-sql/v3
### Characteristics
-| Feature | Implements | Note |
-|---------------------|------------|-------------------------------------------|
-| ConsumerGroups | yes | See `ConsumerGroup` in `SubscriberConfig` |
-| ExactlyOnceDelivery | yes* | Just for MySQL implementation |
-| GuaranteedOrder | yes | |
-| Persistent | yes | |
+| Feature | Implements | Note |
+|---------------------|------------|-------------------------------------------------------------------------------|
+| ConsumerGroups | yes | See `ConsumerGroup` in `SubscriberConfig` (not supported by the queue schema) |
+| ExactlyOnceDelivery | yes* | Just for MySQL implementation |
+| GuaranteedOrder | yes | |
+| Persistent | yes | |
### Schema
@@ -83,7 +83,7 @@ constructor. You have to create one publisher for each transaction.
Example:
{{% load-snippet-partial file="src-link/_examples/real-world-examples/transactional-events/main.go" first_line_contains="func simulateEvents" last_line_contains="return pub.Publish(" padding_after="3" %}}
-### Subscribing
+## Subscribing
To create a subscriber, you need to pass not only proper schema adapter, but also an offsets adapter.
@@ -97,9 +97,28 @@ Example:
{{% load-snippet-partial file="src-link/watermill-sql/pkg/sql/subscriber.go" first_line_contains="func (s *Subscriber) Subscribe" last_line_contains="func (s *Subscriber) Subscribe" %}}
-### Offsets Adapter
+## Offsets Adapter
The logic for storing offsets of messages is provided by the `OffsetsAdapter`. If your schema uses auto-incremented integer as the row ID,
it should work out of the box with default offset adapters.
{{% load-snippet-partial file="src-link/watermill-sql/pkg/sql/offsets_adapter.go" first_line_contains="type OffsetsAdapter" %}}
+
+## Queue
+
+Instead of the default Pub/Sub schema, you can use the *queue* schema and offsets adapters.
+
+It's a simpler schema that doesn't support consumer groups.
+However, it has other advantages.
+
+It lets you specify a custom `WHERE` clause for getting the messages.
+You can use it to filter messages by some condition in the payload or in the metadata.
+
+Additionally, you can choose to delete messages from the table after they are acknowledged.
+Thanks to this, the table doesn't grow in size with time.
+
+Currently, this schema is supported only for PostgreSQL.
+
+{{% load-snippet-partial file="src-link/watermill-sql/pkg/sql/queue_schema_adapter_postgresql.go" first_line_contains="// PostgreSQLQueueSchema" last_line_contains="}" %}}
+
+{{% load-snippet-partial file="src-link/watermill-sql/pkg/sql/queue_offsets_adapter_postgresql.go" first_line_contains="// PostgreSQLQueueOffsetsAdapter" last_line_contains="}" %}}
diff --git a/docs/hugo_stats.json b/docs/hugo_stats.json
deleted file mode 100644
index 391e6403f..000000000
--- a/docs/hugo_stats.json
+++ /dev/null
@@ -1,453 +0,0 @@
-{
- "htmlElements": {
- "tags": [
- "a",
- "article",
- "aside",
- "base",
- "blockquote",
- "body",
- "br",
- "button",
- "circle",
- "code",
- "details",
- "div",
- "em",
- "figcaption",
- "figure",
- "footer",
- "form",
- "g",
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "head",
- "header",
- "html",
- "iframe",
- "img",
- "input",
- "kbd",
- "label",
- "li",
- "line",
- "link",
- "main",
- "meta",
- "nav",
- "noscript",
- "ol",
- "p",
- "path",
- "pre",
- "script",
- "section",
- "small",
- "span",
- "strong",
- "style",
- "summary",
- "svg",
- "table",
- "tbody",
- "td",
- "template",
- "th",
- "thead",
- "time",
- "title",
- "tr",
- "ul"
- ],
- "classes": [
- "DocSearch-Label",
- "active",
- "advanced",
- "anchor",
- "bi",
- "bi-airplane-fill",
- "bi-globe-americas",
- "bi-puzzle-fill",
- "bi-rocket",
- "bi-shield-fill-check",
- "btn",
- "btn-close",
- "btn-cta",
- "btn-lg",
- "btn-link",
- "btn-outline-primary",
- "btn-primary",
- "card",
- "card-body",
- "card-list",
- "categories",
- "chroma",
- "col-lg-10",
- "col-lg-11",
- "col-lg-12",
- "col-lg-5",
- "col-lg-8",
- "col-lg-9",
- "col-md-12",
- "col-xl-3",
- "col-xl-4",
- "col-xl-8",
- "col-xl-9",
- "container",
- "container-fluid",
- "container-lg",
- "content",
- "contributors",
- "created-date",
- "d-flex",
- "d-lg-block",
- "d-lg-none",
- "d-md-block",
- "d-md-none",
- "d-none",
- "d-xl-block",
- "d-xl-none",
- "development",
- "docs",
- "docs-content",
- "docs-links",
- "docs-sidebar",
- "docs-toc",
- "doks-sidebar",
- "edit-page",
- "error404",
- "expressive-code",
- "fade",
- "feather",
- "feather-edit-2",
- "flex-column",
- "flex-grow-1",
- "flex-lg-row",
- "flex-md-row",
- "flex-row",
- "flex-sm-row",
- "flex-xl-nowrap",
- "footer",
- "form-control",
- "form-control-lg",
- "frame",
- "fs-5",
- "h-auto",
- "h4",
- "h5",
- "header",
- "highlight",
- "home",
- "icon",
- "icon-tabler",
- "icon-tabler-arrow-left",
- "icon-tabler-arrow-right",
- "icon-tabler-brand-github",
- "icon-tabler-dots-vertical",
- "icon-tabler-external-link",
- "icon-tabler-menu",
- "icon-tabler-moon",
- "icon-tabler-search",
- "icon-tabler-sun",
- "icon-tabler-x",
- "is-terminal",
- "justify-content-between",
- "justify-content-center",
- "justify-content-end",
- "lead",
- "list",
- "list-inline",
- "list-inline-item",
- "list-nested",
- "list-unstyled",
- "list-view",
- "m-2",
- "m-3",
- "mb-0",
- "mb-1",
- "mb-4",
- "mb-5",
- "me-2",
- "me-auto",
- "me-lg-1",
- "me-lg-3",
- "message",
- "modal",
- "modal-body",
- "modal-content",
- "modal-dialog",
- "modal-dialog-scrollable",
- "modal-footer",
- "modal-fullscreen-md-down",
- "modal-header",
- "modal-title",
- "ms-1",
- "ms-2",
- "ms-3",
- "ms-auto",
- "ms-lg-2",
- "mt-3",
- "mt-5",
- "mt-n3",
- "mx-2",
- "mx-auto",
- "mx-xl-auto",
- "my-3",
- "nav",
- "nav-item",
- "nav-link",
- "nav-tabs",
- "navbar",
- "navbar-brand",
- "navbar-expand-lg",
- "navbar-nav",
- "not-content",
- "offcanvas",
- "offcanvas-body",
- "offcanvas-end",
- "offcanvas-header",
- "offcanvas-start",
- "offcanvas-title",
- "only-dark",
- "only-light",
- "order-3",
- "order-lg-4",
- "p-0",
- "p-2",
- "page-footer-meta",
- "page-links",
- "page-nav",
- "pb-3",
- "pubsubs",
- "px-0",
- "query-no-results",
- "rounded-pill",
- "row",
- "search-form",
- "search-input",
- "search-loading",
- "search-no-recent",
- "search-no-results",
- "search-result",
- "search-results",
- "search-text",
- "section",
- "section-features",
- "section-md",
- "section-nav",
- "show",
- "single",
- "smaller",
- "social-link",
- "src-link",
- "status",
- "sticky-top",
- "stretched-link",
- "submitted",
- "support",
- "tab-content",
- "tab-pane",
- "tags",
- "taxonomy",
- "text-body-secondary",
- "text-center",
- "text-decoration-none",
- "text-end",
- "text-lg-end",
- "text-lg-start",
- "text-muted",
- "text-reset",
- "title",
- "title-submitted",
- "toc-mobile",
- "visually-hidden",
- "w-100",
- "wrap"
- ],
- "ids": [
- "TableOfContents",
- "ack",
- "acknack-mechanism",
- "adding-handler-after-the-router-has-started",
- "amqp-consumer-groups",
- "amqp-topologybuilder",
- "async-publish",
- "at-least-once-delivery",
- "available-middleware",
- "building-a-read-model-with-the-event-handler",
- "building-blocks",
- "built-in-implementations",
- "buttonColorMode",
- "characteristics",
- "circuit-breaker",
- "close",
- "close-1",
- "closing-the-router",
- "code-standards",
- "command",
- "command-and-event-marshaler",
- "command-bus",
- "command-handler",
- "command-handler-1",
- "command-processor",
- "community-support",
- "configuration",
- "configuring",
- "connecting",
- "context",
- "controlling-fanin-component",
- "core-nats",
- "correlation",
- "cqrs",
- "creating-messages",
- "custom-http-status-codes",
- "customization",
- "debugging-pubsub-tests",
- "deduplicator",
- "doks-docs-nav",
- "duplicator",
- "ensuring-that-the-router-is-running",
- "event",
- "event-bus",
- "event-group-processor",
- "event-handler",
- "event-handler-1",
- "event-handler-groups",
- "event-processor",
- "example",
- "example-application",
- "example-domain",
- "examples",
- "execution-models",
- "existing-issues",
- "exported-metrics",
- "exposing-the-metrics-endpoint",
- "extending-schema",
- "fanin-component",
- "fanout-component",
- "forwarder-component",
- "generic-handlers",
- "grafana-dashboard",
- "grep-is-your-friend",
- "h-rh-i-0",
- "handler",
- "handlers",
- "how-can-i-help",
- "i-have-a-deadlock",
- "ignore-errors",
- "implementing-custom-pubsub",
- "importing-the-dashboard",
- "install",
- "installation",
- "instant-ack",
- "introduction",
- "local-development",
- "logging",
- "marshaler",
- "marshalingunmarshaling",
- "middleware",
- "nack",
- "nav-tab",
- "nav-tabContent",
- "new-ideas",
- "new-pubsub-implementations",
- "no-publisher-handler",
- "observability",
- "offcanvasNavMain",
- "offcanvasNavMainLabel",
- "offcanvasNavSection",
- "offcanvasNavSectionLabel",
- "offsets-adapter",
- "one-minute-background",
- "other",
- "partitioning",
- "passing-custom-sarama-config",
- "passing-redisuniversalclient",
- "plugin",
- "poison",
- "producing-messages",
- "professional-support",
- "publisher",
- "publisher--subscriber",
- "publisher-configuration",
- "publishing",
- "publishing-an-event-first-storing-data-next",
- "publishing-messages",
- "publishing-messages-in-transactions-and-why-we-should-care",
- "publishing-multiple-messages",
- "pubsubs",
- "query",
- "randomfail",
- "receiving-acknack",
- "recoverer",
- "retry",
- "router",
- "router-configuration",
- "running",
- "running-a-single-test",
- "running-the-router",
- "schema",
- "search-form",
- "searchModal",
- "searchModalLabel",
- "searchResults",
- "searchToggleDesktop",
- "searchToggleMobile",
- "sending-a-command",
- "sending-ack",
- "socialMenu",
- "stopping-running-handler",
- "storing-data-and-publishing-an-event-in-one-transaction",
- "storing-data-first-publishing-an-event-next",
- "subscriber",
- "subscriber-configuration",
- "subscribing",
- "subscribing-for-messages",
- "subscription-name",
- "support",
- "tabs-getting-started-0",
- "tabs-getting-started-0-tab",
- "tabs-getting-started-1",
- "tabs-getting-started-1-tab",
- "tabs-getting-started-2",
- "tabs-getting-started-2-tab",
- "tabs-getting-started-3",
- "tabs-getting-started-3-tab",
- "tabs-getting-started-4",
- "tabs-getting-started-4-tab",
- "tabs-getting-started-5",
- "tabs-getting-started-5-tab",
- "tabs-publishing-0",
- "tabs-publishing-0-tab",
- "tabs-publishing-1",
- "tabs-publishing-1-tab",
- "tabs-publishing-2",
- "tabs-publishing-2-tab",
- "tabs-publishing-3",
- "tabs-publishing-3-tab",
- "tabs-publishing-4",
- "tabs-publishing-4-tab",
- "tabs-publishing-5",
- "tabs-publishing-5-tab",
- "testing",
- "the-pubsub-interface",
- "throttle",
- "timeout",
- "tls-config",
- "toc",
- "todo-list",
- "topic",
- "transactions",
- "universal-tests",
- "usage",
- "what-is-watermill",
- "whats-next",
- "why-use-watermill",
- "wiring-it-up",
- "wrapping-publishers-subscribers-and-handlers"
- ]
- }
-}
diff --git a/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.content b/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.content
index 733dbbb37..8ec6aa497 100644
--- a/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.content
+++ b/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.content
@@ -1 +1 @@
-:root[data-bs-theme="light"],[data-bs-theme="light"] ::backdrop{--sl-color-white: hsl(224, 10%, 10%);--sl-color-gray-1: hsl(224, 14%, 16%);--sl-color-gray-2: hsl(224, 10%, 23%);--sl-color-gray-3: hsl(224, 7%, 36%);--sl-color-gray-4: hsl(224, 6%, 56%);--sl-color-gray-5: hsl(224, 6%, 77%);--sl-color-gray-6: hsl(224, 20%, 94%);--sl-color-gray-7: hsl(224, 19%, 97%);--sl-color-black: hsl(0, 0%, 100%)}:root,::backdrop{--sl-color-white: hsl(0, 0%, 100%);--sl-color-gray-1: hsl(224, 20%, 94%);--sl-color-gray-2: hsl(224, 6%, 77%);--sl-color-gray-3: hsl(224, 6%, 56%);--sl-color-gray-4: hsl(224, 7%, 36%);--sl-color-gray-5: hsl(224, 10%, 23%);--sl-color-gray-6: hsl(224, 14%, 16%);--sl-color-black: hsl(224, 10%, 10%);--sl-hue-orange: 41;--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);--sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);--sl-hue-green: 101;--sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);--sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);--sl-hue-blue: 234;--sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);--sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);--sl-hue-purple: 281;--sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);--sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);--sl-hue-red: 339;--sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);--sl-color-accent-low: hsl(224, 54%, 20%);--sl-color-accent: hsl(224, 100%, 60%);--sl-color-accent-high: hsl(224, 100%, 85%);--sl-color-text: var(--sl-color-gray-2);--sl-color-text-accent: var(--sl-color-accent-high);--sl-color-text-invert: var(--sl-color-accent-low);--sl-color-bg: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-6);--sl-color-bg-sidebar: var(--sl-color-gray-6);--sl-color-bg-inline-code: var(--sl-color-gray-5);--sl-color-hairline-light: var(--sl-color-gray-5);--sl-color-hairline: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-black);--sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12), 0px 2px 1px hsla(0, 0%, 0%, 0.24);--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08), 0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12), 0px 1px 1px hsla(0, 0%, 0%, 0.15);--sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03), 0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33), 0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);--sl-text-xs: 0.8125rem;--sl-text-sm: 0.875rem;--sl-text-base: 1rem;--sl-text-lg: 1.125rem;--sl-text-xl: 1.25rem;--sl-text-2xl: 1.5rem;--sl-text-3xl: 1.8125rem;--sl-text-4xl: 2.1875rem;--sl-text-5xl: 2.625rem;--sl-text-6xl: 4rem;--sl-text-body: var(--sl-text-base);--sl-text-body-sm: var(--sl-text-xs);--sl-text-code: var(--sl-text-sm);--sl-text-code-sm: var(--sl-text-xs);--sl-text-h1: var(--sl-text-4xl);--sl-text-h2: var(--sl-text-3xl);--sl-text-h3: var(--sl-text-2xl);--sl-text-h4: var(--sl-text-xl);--sl-text-h5: var(--sl-text-lg);--sl-line-height: 1.8;--sl-line-height-headings: 1.2;--sl-font-system: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--__sl-font: var(--sl-font, ""), var(--sl-font-system);--__sl-font-mono: var(--sl-font-mono, ""), var(--sl-font-system-mono);--sl-nav-height: 3.5rem;--sl-nav-pad-x: 1rem;--sl-nav-pad-y: 0.75rem;--sl-mobile-toc-height: 3rem;--sl-sidebar-width: 18.75rem;--sl-sidebar-pad-x: 1rem;--sl-content-width: 45rem;--sl-content-pad-x: 1rem;--sl-menu-button-size: 2rem;--sl-nav-gap: var(--sl-content-pad-x);--sl-outline-offset-inside: -0.1875rem;--sl-z-index-toc: 4;--sl-z-index-menu: 5;--sl-z-index-navbar: 10;--sl-z-index-skiplink: 20}:root{--purple-hsl: 255, 60%, 60%;--overlay-blurple: hsla(var(--purple-hsl), 0.2)}:root{--ec-brdRad: 0px;--ec-brdWd: 1px;--ec-brdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-codeFontFml: var(--__sl-font-mono);--ec-codeFontSize: var(--sl-text-code);--ec-codeFontWg: 400;--ec-codeLineHt: var(--sl-line-height);--ec-codePadBlk: 0.75rem;--ec-codePadInl: 1rem;--ec-codeBg: #011627;--ec-codeFg: #d6deeb;--ec-codeSelBg: #1d3b53;--ec-uiFontFml: var(--__sl-font);--ec-uiFontSize: 0.9rem;--ec-uiFontWg: 400;--ec-uiLineHt: 1.65;--ec-uiPadBlk: 0.25rem;--ec-uiPadInl: 1rem;--ec-uiSelBg: #234d708c;--ec-uiSelFg: #ffffff;--ec-focusBrd: #122d42;--ec-sbThumbCol: #ffffff17;--ec-sbThumbHoverCol: #ffffff49;--ec-tm-lineMarkerAccentMarg: 0rem;--ec-tm-lineMarkerAccentWd: 0.15rem;--ec-tm-lineDiffIndMargLeft: 0.25rem;--ec-tm-inlMarkerBrdWd: 1.5px;--ec-tm-inlMarkerBrdRad: 0.2rem;--ec-tm-inlMarkerPad: 0.15rem;--ec-tm-insDiffIndContent: "+";--ec-tm-delDiffIndContent: "-";--ec-tm-markBg: #ffffff17;--ec-tm-markBrdCol: #ffffff40;--ec-tm-insBg: #1e571599;--ec-tm-insBrdCol: #487f3bd0;--ec-tm-insDiffIndCol: #79b169d0;--ec-tm-delBg: #862d2799;--ec-tm-delBrdCol: #b4554bd0;--ec-tm-delDiffIndCol: #ed8779d0;--ec-frm-shdCol: #011627;--ec-frm-frameBoxShdCssVal: none;--ec-frm-edActTabBg: var(--sl-color-gray-6);--ec-frm-edActTabFg: var(--sl-color-text);--ec-frm-edActTabBrdCol: transparent;--ec-frm-edActTabIndHt: 1px;--ec-frm-edActTabIndTopCol: var(--sl-color-accent-high);--ec-frm-edActTabIndBtmCol: transparent;--ec-frm-edTabsMargInlStart: 0;--ec-frm-edTabsMargBlkStart: 0;--ec-frm-edTabBrdRad: 0px;--ec-frm-edTabBarBg: var(--sl-color-black);--ec-frm-edTabBarBrdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edBg: var(--sl-color-gray-6);--ec-frm-trmTtbDotsFg: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmTtbDotsOpa: 0.75;--ec-frm-trmTtbBg: var(--sl-color-black);--ec-frm-trmTtbFg: var(--sl-color-text);--ec-frm-trmTtbBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmBg: var(--sl-color-gray-6);--ec-frm-inlBtnFg: var(--sl-color-text);--ec-frm-inlBtnBg: var(--sl-color-text);--ec-frm-inlBtnBgIdleOpa: 0;--ec-frm-inlBtnBgHoverOrFocusOpa: 0.2;--ec-frm-inlBtnBgActOpa: 0.3;--ec-frm-inlBtnBrd: var(--sl-color-text);--ec-frm-inlBtnBrdOpa: 0.4;--ec-frm-tooltipSuccessBg: #158744;--ec-frm-tooltipSuccessFg: white}:root,[data-bs-theme="light"]{--bs-blue: #3347ff;--bs-indigo: #6610f2;--bs-purple: #bd53ee;--bs-pink: #d63384;--bs-red: #ee5389;--bs-orange: #fd7e14;--bs-yellow: #eebd53;--bs-green: #84ee53;--bs-teal: #20c997;--bs-cyan: #0dcaf0;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-primary: #4f46e5;--bs-secondary: #6c757d;--bs-success: #84ee53;--bs-info: #3347ff;--bs-warning: #eebd53;--bs-danger: #ee5389;--bs-light: #f8f9fa;--bs-dark: #212529;--bs-primary-rgb: 79,70,229;--bs-secondary-rgb: 108,117,125;--bs-success-rgb: 132.2821,238.017,83.283;--bs-info-rgb: 51,71.4,255;--bs-warning-rgb: 238.017,189.0179,83.283;--bs-danger-rgb: 238.017,83.283,137.4399;--bs-light-rgb: 248,249,250;--bs-dark-rgb: 33,37,41;--bs-primary-text-emphasis: #201c5c;--bs-secondary-text-emphasis: #2b2f32;--bs-success-text-emphasis: #355f21;--bs-info-text-emphasis: #141d66;--bs-warning-text-emphasis: #5f4c21;--bs-danger-text-emphasis: #5f2137;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #dcdafa;--bs-secondary-bg-subtle: #e2e3e5;--bs-success-bg-subtle: #e6fcdd;--bs-info-bg-subtle: #d6daff;--bs-warning-bg-subtle: #fcf2dd;--bs-danger-bg-subtle: #fcdde7;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #b9b5f5;--bs-secondary-border-subtle: #c4c8cb;--bs-success-border-subtle: #cef8ba;--bs-info-border-subtle: #adb6ff;--bs-warning-border-subtle: #f8e5ba;--bs-danger-border-subtle: #f8bad0;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255,255,255;--bs-black-rgb: 0,0,0;--bs-font-sans-serif: "Heebo", "sans-serif", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255,255,255,0.15), rgba(255,255,255,0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #1d2d35;--bs-body-color-rgb: 29,45,53;--bs-body-bg: #fff;--bs-body-bg-rgb: 255,255,255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0,0,0;--bs-secondary-color: rgba(29,45,53,0.75);--bs-secondary-color-rgb: 29,45,53;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233,236,239;--bs-tertiary-color: rgba(29,45,53,0.5);--bs-tertiary-color-rgb: 29,45,53;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248,249,250;--bs-heading-color: inherit;--bs-link-color: #4f46e5;--bs-link-color-rgb: 79,70,229;--bs-link-decoration: none;--bs-link-hover-color: #3f38b7;--bs-link-hover-color-rgb: 63,56,183;--bs-link-hover-decoration: underline;--bs-code-color: #d63384;--bs-highlight-color: #1d2d35;--bs-highlight-bg: #fcf2dd;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0,0,0,0.175);--bs-border-radius: .375rem;--bs-border-radius-sm: .25rem;--bs-border-radius-lg: .5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0,0,0,0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0,0,0,0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0,0,0,0.075);--bs-focus-ring-width: .25rem;--bs-focus-ring-opacity: .25;--bs-focus-ring-color: rgba(79,70,229,0.25);--bs-form-valid-color: #84ee53;--bs-form-valid-border-color: #84ee53;--bs-form-invalid-color: #ee5389;--bs-form-invalid-border-color: #ee5389}[data-bs-theme="dark"]{color-scheme:dark;--bs-body-color: #c1c3c8;--bs-body-color-rgb: 192.831,194.7078,199.869;--bs-body-bg: #17181c;--bs-body-bg-rgb: 22.95,24.31,28.05;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255,255,255;--bs-secondary-color: rgba(193,195,200,0.75);--bs-secondary-color-rgb: 192.831,194.7078,199.869;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52,58,64;--bs-tertiary-color: rgba(193,195,200,0.5);--bs-tertiary-color-rgb: 192.831,194.7078,199.869;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43,48,53;--bs-primary-text-emphasis: #9590ef;--bs-secondary-text-emphasis: #a7acb1;--bs-success-text-emphasis: #b5f598;--bs-info-text-emphasis: #8591ff;--bs-warning-text-emphasis: #f5d798;--bs-danger-text-emphasis: #f598b8;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #100e2e;--bs-secondary-bg-subtle: #161719;--bs-success-bg-subtle: #1a3011;--bs-info-bg-subtle: #0a0e33;--bs-warning-bg-subtle: #302611;--bs-danger-bg-subtle: #30111b;--bs-light-bg-subtle: #23262f;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #2f2a89;--bs-secondary-border-subtle: #41464b;--bs-success-border-subtle: #4f8f32;--bs-info-border-subtle: #1f2b99;--bs-warning-border-subtle: #8f7132;--bs-danger-border-subtle: #8f3252;--bs-light-border-subtle: #353841;--bs-dark-border-subtle: #343a40;--bs-heading-color: #fff;--bs-link-color: #b3c7ff;--bs-link-hover-color: #c2d2ff;--bs-link-color-rgb: 178.5,198.9,255;--bs-link-hover-color-rgb: 194,210,255;--bs-code-color: #e685b5;--bs-highlight-color: #c1c3c8;--bs-highlight-bg: #5f4c21;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255,255,255,0.15);--bs-form-valid-color: #b5f598;--bs-form-valid-border-color: #b5f598;--bs-form-invalid-color: #f598b8;--bs-form-invalid-border-color: #f598b8}*,*::before,*::after{box-sizing:border-box}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:700;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width: 1200px){h1,.h1{font-size:2.5rem}}h2,.h2{font-size:calc(1.325rem + .9vw)}@media (min-width: 1200px){h2,.h2{font-size:2rem}}h3,.h3{font-size:calc(1.3rem + .6vw)}@media (min-width: 1200px){h3,.h3{font-size:1.75rem}}h4,.h4{font-size:calc(1.275rem + .3vw)}@media (min-width: 1200px){h4,.h4{font-size:1.5rem}}h5,.h5{font-size:1.25rem}p{margin-top:0;margin-bottom:1rem}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}strong{font-weight:bolder}small,.small{font-size:.875em}mark,.mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:none}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb);text-decoration:underline}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button{text-transform:none}[list]:not([type="date"]):not([type="datetime-local"]):not([type="month"]):not([type="week"]):not([type="time"])::-webkit-calendar-picker-indicator{display:none !important}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}iframe{border:0}summary{display:list-item;cursor:pointer}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:400}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.img-fluid{max-width:100%;height:auto}.figure{display:inline-block}.container,.container-fluid,.container-lg{--bs-gutter-x: 3rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container-lg,.container{max-width:960px}}@media (min-width: 1200px){.container-lg,.container{max-width:1240px}}@media (min-width: 1400px){.container-lg,.container{max-width:1820px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.row{--bs-gutter-x: 3rem;--bs-gutter-y: 0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}@media (min-width: 768px){.col-md-12{flex:0 0 auto;width:75%}}@media (min-width: 992px){.col-lg-5{flex:0 0 auto;width:31.25%}.col-lg-8{flex:0 0 auto;width:50%}.col-lg-9{flex:0 0 auto;width:56.25%}.col-lg-10{flex:0 0 auto;width:62.5%}.col-lg-11{flex:0 0 auto;width:68.75%}.col-lg-12{flex:0 0 auto;width:75%}}@media (min-width: 1200px){.col-xl-3{flex:0 0 auto;width:18.75%}.col-xl-4{flex:0 0 auto;width:25%}.col-xl-8{flex:0 0 auto;width:50%}.col-xl-9{flex:0 0 auto;width:56.25%}}.sticky-top{position:sticky;top:0;z-index:1020}.visually-hidden{width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.table,table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: var(--bs-emphasis-color);--bs-table-bg: var(--bs-body-bg);--bs-table-border-color: var(--bs-border-color);--bs-table-accent-bg: rgba(0,0,0,0);--bs-table-striped-color: var(--bs-emphasis-color);--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color: var(--bs-emphasis-color);--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color: var(--bs-emphasis-color);--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*,table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody,table>tbody{vertical-align:inherit}.table>thead,table>thead{vertical-align:bottom}[data-bs-theme="dark"] table{--bs-table-color: #fff;--bs-table-bg: #212529;--bs-table-border-color: #4d5154;--bs-table-striped-bg: #2c3034;--bs-table-striped-color: #fff;--bs-table-active-bg: #373b3e;--bs-table-active-color: #fff;--bs-table-hover-bg: #323539;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type="file"]{overflow:hidden}.form-control[type="file"]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#a7a3f2;outline:0;box-shadow:none}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}li input[type="checkbox"]{--bs-form-check-bg: var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-color-adjust:exact}li input[type="checkbox"]{border-radius:.25em}li input[type="radio"][type="checkbox"]{border-radius:50%}li input[type="checkbox"]:active{filter:brightness(90%)}li input[type="checkbox"]:focus{border-color:#a7a3f2;outline:0;box-shadow:0 0 0 .25rem rgba(79,70,229,0.25)}li input[type="checkbox"]:checked{background-color:#4f46e5;border-color:#4f46e5}li input:checked[type="checkbox"]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}li input[type="checkbox"]:checked[type="radio"]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}li input[type="checkbox"]:indeterminate{background-color:#4f46e5;border-color:#4f46e5;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}li input[type="checkbox"]:disabled{pointer-events:none;filter:none;opacity:.5}.btn{--bs-btn-padding-x: .75rem;--bs-btn-padding-y: .375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: var(--bs-body-color);--bs-btn-bg: transparent;--bs-btn-border-width: var(--bs-border-width);--bs-btn-border-color: transparent;--bs-btn-border-radius: var(--bs-border-radius);--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);--bs-btn-disabled-opacity: .65;--bs-btn-focus-box-shadow: 0 0 0 0 rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);text-decoration:none;background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #4f46e5;--bs-btn-border-color: #4f46e5;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #433cc3;--bs-btn-hover-border-color: #3f38b7;--bs-btn-focus-shadow-rgb: 105,98,233;--bs-btn-active-color: #fff;--bs-btn-active-bg: #3f38b7;--bs-btn-active-border-color: #3b35ac;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #4f46e5;--bs-btn-disabled-border-color: #4f46e5}.btn-outline-primary{--bs-btn-color: #4f46e5;--bs-btn-border-color: #4f46e5;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #4f46e5;--bs-btn-hover-border-color: #4f46e5;--bs-btn-focus-shadow-rgb: 79,70,229;--bs-btn-active-color: #fff;--bs-btn-active-bg: #4f46e5;--bs-btn-active-border-color: #4f46e5;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #4f46e5;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #4f46e5;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: var(--bs-link-color);--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 105,98,233;text-decoration:none}.btn-link:hover,.btn-link:focus-visible{text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg{--bs-btn-padding-y: .5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius: var(--bs-border-radius-lg)}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-link-color);--bs-nav-link-hover-color: var(--bs-link-hover-color);--bs-nav-link-disabled-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);background:none;border:0;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color);text-decoration:none}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(79,70,229,0.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: var(--bs-border-width);--bs-nav-tabs-border-color: var(--bs-border-color);--bs-nav-tabs-border-radius: var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color: var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg: var(--bs-body-bg);--bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: .5rem;--bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y: .3125rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.25rem;--bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x: .5rem;--bs-navbar-toggler-padding-y: .25rem;--bs-navbar-toggler-padding-x: .75rem;--bs-navbar-toggler-font-size: 1.25rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2829,45,53,0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius: var(--bs-border-radius);--bs-navbar-toggler-focus-width: 0;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color);text-decoration:none}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}@media (min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:transparent !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar[data-bs-theme="dark"]{--bs-navbar-color: #c1c3c8;--bs-navbar-hover-color: #b3c7ff;--bs-navbar-disabled-color: rgba(255,255,255,0.25);--bs-navbar-active-color: #b3c7ff;--bs-navbar-brand-color: #b3c7ff;--bs-navbar-brand-hover-color: #b3c7ff;--bs-navbar-toggler-border-color: rgba(255,255,255,0.1);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23c1c3c8' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: .5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: var(--bs-border-width);--bs-card-border-color: #e9ecef;--bs-card-border-radius: var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y: .5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: var(--bs-body-bg);--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 1.5rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);text-decoration:none;background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: .5;--bs-btn-close-hover-opacity: .75;--bs-btn-close-focus-shadow: 0 0 0 .25rem rgba(79,70,229,0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: .25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}[data-bs-theme="dark"] .btn-close{filter:var(--bs-btn-close-white-filter)}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: .5rem;--bs-modal-color: ;--bs-modal-bg: var(--bs-body-bg);--bs-modal-border-color: var(--bs-border-color-translucent);--bs-modal-border-width: var(--bs-border-width);--bs-modal-border-radius: var(--bs-border-radius-lg);--bs-modal-box-shadow: var(--bs-box-shadow-sm);--bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: var(--bs-border-color);--bs-modal-header-border-width: var(--bs-border-width);--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: .5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: var(--bs-border-color);--bs-modal-footer-border-width: var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform 0.3s ease-out;transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: .5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}}@media (max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header,.modal-fullscreen-md-down .modal-footer{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.offcanvas{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 332px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: var(--bs-body-color);--bs-offcanvas-bg: var(--bs-body-bg);--bs-offcanvas-border-width: var(--bs-border-width);--bs-offcanvas-border-color: var(--bs-border-color-translucent);--bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);--bs-offcanvas-transition: transform .3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}@keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.d-flex{display:flex !important}.d-none{display:none !important}.w-100{width:100% !important}.h-auto{height:auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-grow-1{flex-grow:1 !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.order-3{order:3 !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mt-3{margin-top:1rem !important}.mt-5{margin-top:3rem !important}.me-2{margin-right:.5rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-auto{margin-left:auto !important}.mt-n3{margin-top:-1rem !important}.p-0{padding:0 !important}.p-2{padding:.5rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.pb-3{padding-bottom:1rem !important}.fs-5{font-size:1.25rem !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}@media (min-width: 576px){.flex-sm-row{flex-direction:row !important}}@media (min-width: 768px){.d-md-block{display:block !important}.d-md-none{display:none !important}.flex-md-row{flex-direction:row !important}}@media (min-width: 992px){.d-lg-block{display:block !important}.d-lg-none{display:none !important}.flex-lg-row{flex-direction:row !important}.order-lg-4{order:4 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-3{margin-right:1rem !important}.ms-lg-2{margin-left:.5rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}}@media (min-width: 1200px){.d-xl-block{display:block !important}.d-xl-none{display:none !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}}@font-face{font-family:Jost;font-style:normal;font-weight:400;font-display:swap;src:local("Jost Regular Regular"),local("Jost-Regular"),local("Jost* Book"),local("Jost-Book"),url("fonts/vendor/jost/jost-v4-latin-regular.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-regular.woff") format("woff")}@font-face{font-family:Jost;font-style:normal;font-weight:500;font-display:swap;src:local("Jost Regular Medium"),local("JostRoman-Medium"),local("Jost* Medium"),local("Jost-Medium"),url("fonts/vendor/jost/jost-v4-latin-500.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-500.woff") format("woff")}@font-face{font-family:Jost;font-style:normal;font-weight:700;font-display:swap;src:local("Jost Regular Bold"),local("JostRoman-Bold"),local("Jost* Bold"),local("Jost-Bold"),url("fonts/vendor/jost/jost-v4-latin-700.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-700.woff") format("woff")}@font-face{font-family:Jost;font-style:italic;font-weight:400;font-display:swap;src:local("Jost Italic Italic"),local("Jost-Italic"),local("Jost* BookItalic"),local("Jost-BookItalic"),url("fonts/vendor/jost/jost-v4-latin-italic.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-italic.woff") format("woff")}@font-face{font-family:Jost;font-style:italic;font-weight:500;font-display:swap;src:local("Jost Italic Medium Italic"),local("JostItalic-Medium"),local("Jost* Medium Italic"),local("Jost-MediumItalic"),url("fonts/vendor/jost/jost-v4-latin-500italic.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-500italic.woff") format("woff")}@font-face{font-family:Jost;font-style:italic;font-weight:700;font-display:swap;src:local("Jost Italic Bold Italic"),local("JostItalic-Bold"),local("Jost* Bold Italic"),local("Jost-BoldItalic"),url("fonts/vendor/jost/jost-v4-latin-700italic.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-700italic.woff") format("woff")}html[data-bs-theme="dark"] .icon-tabler-sun{display:block}html[data-bs-theme="dark"] .icon-tabler-moon{display:none}html[data-bs-theme="light"] .icon-tabler-sun{display:none}html[data-bs-theme="light"] .icon-tabler-moon{display:block}.contributors .content,.error404 .content,.docs.list .content,.categories.list .content,.tags.list .content,.list.section .content{padding-top:1rem;padding-bottom:3rem}.content img{max-width:100%}h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:2rem;margin-bottom:1rem}@media (min-width: 768px){body{font-size:1.125rem}h1,h2,h3,h4,h5,.h1,.h2,.h3,.h4,.h5{margin-bottom:1.125rem}}.home h1,.home .h1{font-size:calc(1.875rem + 1.5vw);margin-top:-1rem}a:hover,a:focus{text-decoration:underline}a.btn:hover,a.btn:focus{text-decoration:none}.section{padding-top:5rem;padding-bottom:5rem}body.section{padding-top:0;padding-bottom:0}.section-md{padding-top:3rem;padding-bottom:3rem}.docs-sidebar{order:2}@media (min-width: 992px){.docs-sidebar{order:0;border-right:1px solid #e9ecef}@supports (position: sticky){.docs-sidebar{position:sticky;top:4.25rem;z-index:1000;height:calc(100vh - 4.25rem)}}}@media (min-width: 1200px){.docs-sidebar{flex:0 1 320px}}.docs-links{padding-bottom:5rem}@media (min-width: 992px){@supports (position: sticky){.docs-links{max-height:calc(100vh - 4rem);overflow-y:scroll}}}@media (min-width: 992px){.docs-links{display:block;width:auto;margin-right:-1.5rem;padding-bottom:4rem}}.docs-toc{order:2}@supports (position: sticky){.docs-toc{position:sticky;top:4.25rem;height:calc(100vh - 4.25rem);overflow-y:auto}}.docs-content{padding-bottom:3rem;order:1}.navbar a:hover,.navbar a:focus{text-decoration:none}#TableOfContents ul,#toc ul{padding-left:0;list-style:none}#toc a.active{color:#4f46e5;font-weight:500}.section-features{padding-top:2rem}.modal-backdrop{background-color:#fff}.modal-backdrop.show{opacity:0.7}@media (min-width: 768px){.modal-backdrop.show{opacity:0}}li input[type="checkbox"]{margin:0.25rem;border:1px solid #ced4da}li input[type="checkbox"]:disabled{pointer-events:none;filter:none;opacity:1}li input[type="checkbox"]:checked{background-color:#5d2f86;border-color:#5d2f86}[data-bs-theme="dark"] li input[type="checkbox"]{border:1px solid #6c757d}[data-bs-theme="dark"] li input[type="checkbox"]:checked{background-color:#b3c7ff;border-color:#b3c7ff;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%231d2d35' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.highlight>.chroma{border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}.bg{background-color:var(--sl-color-gray-7)}.chroma{background-color:var(--sl-color-gray-7)}.chroma .err{color:inherit}.chroma .lnlinks{outline:none;text-decoration:none;color:inherit}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0}.chroma .hl{background-color:#0000001a}.chroma .hl{border-inline-start:0.15rem solid #00000055;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}.chroma .hl .ln{margin-left:-0.15rem}.chroma .lnt{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .ln{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .line{display:flex}.chroma .k{color:#000000;font-weight:bold}.chroma .kc{color:#000000;font-weight:bold}.chroma .kd{color:#000000;font-weight:bold}.chroma .kn{color:#000000;font-weight:bold}.chroma .kp{color:#000000;font-weight:bold}.chroma .kr{color:#000000;font-weight:bold}.chroma .kt{color:#445588;font-weight:bold}.chroma .na{color:#008080}.chroma .nb{color:#0086b3}.chroma .bp{color:#999999}.chroma .nc{color:#445588;font-weight:bold}.chroma .no{color:#008080}.chroma .nd{color:#3c5d5d;font-weight:bold}.chroma .ni{color:#800080}.chroma .ne{color:#990000;font-weight:bold}.chroma .nf{color:#990000;font-weight:bold}.chroma .nl{color:#990000;font-weight:bold}.chroma .nn{color:#555555}.chroma .nt{color:#000080}.chroma .nv{color:#008080}.chroma .vc{color:#008080}.chroma .vg{color:#008080}.chroma .vi{color:#008080}.chroma .s{color:#dd1144}.chroma .sa{color:#dd1144}.chroma .sb{color:#dd1144}.chroma .sc{color:#dd1144}.chroma .dl{color:#dd1144}.chroma .sd{color:#dd1144}.chroma .s2{color:#dd1144}.chroma .se{color:#dd1144}.chroma .sh{color:#dd1144}.chroma .si{color:#dd1144}.chroma .sx{color:#dd1144}.chroma .sr{color:#009926}.chroma .s1{color:#dd1144}.chroma .ss{color:#990073}.chroma .m{color:#009999}.chroma .mb{color:#009999}.chroma .mf{color:#009999}.chroma .mh{color:#009999}.chroma .mi{color:#009999}.chroma .il{color:#009999}.chroma .mo{color:#009999}.chroma .o{color:#000000;font-weight:bold}.chroma .ow{color:#000000;font-weight:bold}.chroma .c{color:#999988;font-style:italic}.chroma .ch{color:#999988;font-style:italic}.chroma .cm{color:#999988;font-style:italic}.chroma .c1{color:#999988;font-style:italic}.chroma .cs{color:#999999;font-weight:bold;font-style:italic}.chroma .cp{color:#999999;font-weight:bold;font-style:italic}.chroma .cpf{color:#999999;font-weight:bold;font-style:italic}.chroma .gd{color:#000000;background-color:#ffdddd}.chroma .ge{color:inherit;font-style:italic}.chroma .gr{color:#aa0000}.chroma .gh{color:#999999}.chroma .gi{color:#000000;background-color:#ddffdd}.chroma .go{color:#888888}.chroma .gp{color:#555555}.chroma .gs{font-weight:bold}.chroma .gu{color:#aaaaaa}.chroma .gt{color:#aa0000}.chroma .gl{text-decoration:underline}.chroma .w{color:#bbbbbb}[data-bs-theme="dark"] .highlight>.chroma{border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}[data-bs-theme="dark"] .bg{color:#c9d1d9;background-color:var(--sl-color-gray-6)}[data-bs-theme="dark"] .chroma{color:#c9d1d9;background-color:var(--sl-color-gray-6)}[data-bs-theme="dark"] .chroma .err{color:inherit}[data-bs-theme="dark"] .chroma .lnlinks{outline:none;text-decoration:none;color:inherit}[data-bs-theme="dark"] .chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}[data-bs-theme="dark"] .chroma .lntable{border-spacing:0;padding:0;margin:0;border:0}[data-bs-theme="dark"] .chroma .hl{background-color:#ffffff17}[data-bs-theme="dark"] .chroma .hl{border-inline-start:0.15rem solid #ffffff40;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}[data-bs-theme="dark"] .chroma .hl .ln{margin-left:-0.15rem}[data-bs-theme="dark"] .chroma .lnt{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#64686c}[data-bs-theme="dark"] .chroma .ln{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681}[data-bs-theme="dark"] .chroma .line{display:flex}[data-bs-theme="dark"] .chroma .k{color:#ff7b72}[data-bs-theme="dark"] .chroma .kc{color:#79c0ff}[data-bs-theme="dark"] .chroma .kd{color:#ff7b72}[data-bs-theme="dark"] .chroma .kn{color:#ff7b72}[data-bs-theme="dark"] .chroma .kp{color:#79c0ff}[data-bs-theme="dark"] .chroma .kr{color:#ff7b72}[data-bs-theme="dark"] .chroma .kt{color:#ff7b72}[data-bs-theme="dark"] .chroma .na{color:#d2a8ff}[data-bs-theme="dark"] .chroma .nc{color:#f0883e;font-weight:bold}[data-bs-theme="dark"] .chroma .no{color:#79c0ff;font-weight:bold}[data-bs-theme="dark"] .chroma .nd{color:#d2a8ff;font-weight:bold}[data-bs-theme="dark"] .chroma .ni{color:#ffa657}[data-bs-theme="dark"] .chroma .ne{color:#f0883e;font-weight:bold}[data-bs-theme="dark"] .chroma .nf{color:#d2a8ff;font-weight:bold}[data-bs-theme="dark"] .chroma .nl{color:#79c0ff;font-weight:bold}[data-bs-theme="dark"] .chroma .nn{color:#ff7b72}[data-bs-theme="dark"] .chroma .py{color:#79c0ff}[data-bs-theme="dark"] .chroma .nt{color:#7ee787}[data-bs-theme="dark"] .chroma .nv{color:#79c0ff}[data-bs-theme="dark"] .chroma .l{color:#a5d6ff}[data-bs-theme="dark"] .chroma .ld{color:#79c0ff}[data-bs-theme="dark"] .chroma .s{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sa{color:#79c0ff}[data-bs-theme="dark"] .chroma .sb{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sc{color:#a5d6ff}[data-bs-theme="dark"] .chroma .dl{color:#79c0ff}[data-bs-theme="dark"] .chroma .sd{color:#a5d6ff}[data-bs-theme="dark"] .chroma .s2{color:#a5d6ff}[data-bs-theme="dark"] .chroma .se{color:#79c0ff}[data-bs-theme="dark"] .chroma .sh{color:#79c0ff}[data-bs-theme="dark"] .chroma .si{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sx{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sr{color:#79c0ff}[data-bs-theme="dark"] .chroma .s1{color:#a5d6ff}[data-bs-theme="dark"] .chroma .ss{color:#a5d6ff}[data-bs-theme="dark"] .chroma .m{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mb{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mf{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mh{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mi{color:#a5d6ff}[data-bs-theme="dark"] .chroma .il{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mo{color:#a5d6ff}[data-bs-theme="dark"] .chroma .o{color:inherit;font-weight:bold}[data-bs-theme="dark"] .chroma .ow{color:#ff7b72;font-weight:bold}[data-bs-theme="dark"] .chroma .c{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .ch{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .cm{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .c1{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .cs{color:#8b949e;font-weight:bold;font-style:italic}[data-bs-theme="dark"] .chroma .cp{color:#8b949e;font-weight:bold;font-style:italic}[data-bs-theme="dark"] .chroma .cpf{color:#8b949e;font-weight:bold;font-style:italic}[data-bs-theme="dark"] .chroma .gd{color:#ffa198;background-color:#490202}[data-bs-theme="dark"] .chroma .ge{font-style:italic}[data-bs-theme="dark"] .chroma .gr{color:#ffa198}[data-bs-theme="dark"] .chroma .gh{color:#79c0ff;font-weight:bold}[data-bs-theme="dark"] .chroma .gi{color:#56d364;background-color:#0f5323}[data-bs-theme="dark"] .chroma .go{color:#8b949e}[data-bs-theme="dark"] .chroma .gp{color:#8b949e}[data-bs-theme="dark"] .chroma .gs{font-weight:bold}[data-bs-theme="dark"] .chroma .gu{color:#79c0ff}[data-bs-theme="dark"] .chroma .gt{color:#ff7b72}[data-bs-theme="dark"] .chroma .gl{text-decoration:underline}[data-bs-theme="dark"] .chroma .w{color:#6e7681}[data-bs-theme="dark"] h1,[data-bs-theme="dark"] .h1,[data-bs-theme="dark"] h2,[data-bs-theme="dark"] .h2,[data-bs-theme="dark"] h3,[data-bs-theme="dark"] .h3,[data-bs-theme="dark"] h4,[data-bs-theme="dark"] .h4{color:#fff}[data-bs-theme="dark"] body{background:#17181c;color:#c1c3c8}[data-bs-theme="dark"] a{color:#b3c7ff}[data-bs-theme="dark"] .btn-primary{--bs-btn-color: #000;--bs-btn-bg: #b3c7ff;--bs-btn-border-color: #b3c7ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #becfff;--bs-btn-hover-border-color: #bacdff;--bs-btn-focus-shadow-rgb: 152,169,217;--bs-btn-active-color: #000;--bs-btn-active-bg: #c2d2ff;--bs-btn-active-border-color: #bacdff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #b3c7ff;--bs-btn-disabled-border-color: #b3c7ff;color:#17181c}[data-bs-theme="dark"] .btn-outline-primary{--bs-btn-color: #b3c7ff;--bs-btn-border-color: #b3c7ff;--bs-btn-hover-color: #b3c7ff;--bs-btn-hover-bg: #b3c7ff;--bs-btn-hover-border-color: #b3c7ff;--bs-btn-focus-shadow-rgb: 178.5,198.9,255;--bs-btn-active-color: #000;--bs-btn-active-bg: #b3c7ff;--bs-btn-active-border-color: #b3c7ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #b3c7ff;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #b3c7ff;--bs-gradient: none;color:#b3c7ff}[data-bs-theme="dark"] .btn-outline-primary:hover{color:#17181c}[data-bs-theme="dark"] .navbar{background-color:rgba(23,24,28,0.95);border-bottom:1px solid #23262f}[data-bs-theme="dark"] body.home .navbar{border-bottom:0}[data-bs-theme="dark"] .offcanvas-header{border-bottom:1px solid #343a40}[data-bs-theme="dark"] .offcanvas .nav-link{color:#c1c3c8}[data-bs-theme="dark"] .offcanvas .nav-link:hover,[data-bs-theme="dark"] .offcanvas .nav-link:focus{color:#b3c7ff}[data-bs-theme="dark"] .offcanvas .nav-link.active{color:#b3c7ff}[data-bs-theme="dark"] .page-links a{color:#c1c3c8}[data-bs-theme="dark"] .page-links a:hover{text-decoration:none;color:#b3c7ff}[data-bs-theme="dark"] .navbar .btn-link{color:#c1c3c8}[data-bs-theme="dark"] .content .btn-link{color:#b3c7ff}[data-bs-theme="dark"] .content .btn-link:hover{color:#b3c7ff}[data-bs-theme="dark"] .navbar .btn-link:hover{color:#b3c7ff}[data-bs-theme="dark"] .navbar .btn-link:active{color:#b3c7ff}[data-bs-theme="dark"] .form-control{color:#dee2e6}[data-bs-theme="dark"] .form-control::-moz-placeholder{color:#ced4da;opacity:1}[data-bs-theme="dark"] .form-control::placeholder{color:#ced4da;opacity:1}@media (min-width: 992px){[data-bs-theme="dark"] .docs-sidebar{order:0;border-right:1px solid #23262f}}[data-bs-theme="dark"] blockquote{border-left:3px solid #23262f}[data-bs-theme="dark"] .footer{border-top:1px solid #23262f}[data-bs-theme="dark"] .docs-links,[data-bs-theme="dark"] .docs-toc{scrollbar-width:thin;scrollbar-color:#17181c #17181c}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar{width:5px}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar-track,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar-track{background:#17181c}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar-thumb,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar-thumb{background:#17181c}[data-bs-theme="dark"] .docs-links:hover,[data-bs-theme="dark"] .docs-toc:hover{scrollbar-width:thin;scrollbar-color:#23262f #17181c}[data-bs-theme="dark"] .docs-links:hover::-webkit-scrollbar-thumb,[data-bs-theme="dark"] .docs-toc:hover::-webkit-scrollbar-thumb{background:#23262f}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar-thumb:hover,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar-thumb:hover{background:#23262f}[data-bs-theme="dark"] .docs-links h3:not(:first-child),[data-bs-theme="dark"] .docs-links .h3:not(:first-child){border-top:1px solid #23262f}[data-bs-theme="dark"] .page-links li:not(:first-child){border-top:1px dashed #23262f}[data-bs-theme="dark"] .card{background:#17181c;border:1px solid #23262f}[data-bs-theme="dark"] .text-muted{color:#adafb6 !important}[data-bs-theme="dark"] .offcanvas{background-color:#17181c}[data-bs-theme="dark"] .page-link{color:#b3c7ff;background-color:transparent;border:var(--bs-border-width) solid #23262f}[data-bs-theme="dark"] .page-link:hover{color:#17181c;background-color:#c1c3c8;border-color:#c1c3c8}[data-bs-theme="dark"] .page-link:focus{color:#17181c;background-color:#c1c3c8}[data-bs-theme="dark"] .page-item.active .page-link{color:#17181c;background-color:#b3c7ff;border-color:#b3c7ff}[data-bs-theme="dark"] .page-item.disabled .page-link{color:var(--bs-secondary-color);background-color:#23262f;border-color:#23262f}[data-bs-theme="dark"] details{border:1px solid #23262f}[data-bs-theme="dark"] summary:hover{background:#23262f}[data-bs-theme="dark"] details[open]>summary{border-bottom:1px solid #23262f}[data-bs-theme="dark"] details summary::after{content:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28222, 226, 230, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e")}[data-bs-theme="dark"] #toc a.active{color:#b3c7ff}[data-bs-theme="dark"] table th{color:#fff}[data-bs-theme="dark"] table,[data-bs-theme="dark"] [data-bs-theme="dark"] table{--bs-table-color: inherit;--bs-table-bg: $body-bg-dark;background:#17181c;border-color:#23262f}.btn-close:focus,.btn-close:active{outline:none;box-shadow:none}.navbar .btn-link{color:rgba(var(--bs-emphasis-color-rgb), 0.65);padding:0.4375rem 0}.btn-link:focus{outline:0;box-shadow:none}@media (min-width: 992px){.navbar .btn-link{padding:0.5625em 0.25rem 0.5rem 0.125rem}}.navbar .btn-link:hover{color:rgba(var(--bs-emphasis-color-rgb), 0.8)}.navbar .btn-link:active{color:rgba(var(--bs-emphasis-color-rgb), 1)}.btn-close{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-x'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");background-size:1.5rem}.offcanvas-header .btn-close{margin-right:0 !important}.clipboard{position:relative;float:right}.btn-clipboard{transition:opacity 0.25s ease-in-out;opacity:0;position:absolute;right:0.5rem;top:0.5rem;line-height:1;padding:0.3125rem 0.3125rem 0.1875rem;background-color:transparent;border-color:transparent}@media (max-width: 767.98px){.btn-clipboard{position:absolute;right:-0.5rem;top:0.5rem}}.btn-clipboard::after{width:22px;height:22px;display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;background-color:#495057}.btn-clipboard:hover{border-color:transparent}.btn-clipboard:hover::after{width:22px;height:22px;display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;background-color:#212529}.btn-clipboard:focus,.btn-clipboard:active{border-color:transparent !important}.btn-clipboard:focus::after,.btn-clipboard:active::after{width:22px;height:22px;display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;background-color:#212529}[data-bs-theme="dark"] .btn-clipboard{background-color:transparent;border-color:transparent}[data-bs-theme="dark"] .btn-clipboard::after{background-color:#ced4da}[data-bs-theme="dark"] .btn-clipboard:hover{border-color:transparent}[data-bs-theme="dark"] .btn-clipboard:hover::after{background-color:#e9ecef}[data-bs-theme="dark"] .btn-clipboard:focus,[data-bs-theme="dark"] .btn-clipboard:active{border-color:transparent}[data-bs-theme="dark"] .btn-clipboard:focus::after,[data-bs-theme="dark"] .btn-clipboard:active::after{background-color:#e9ecef}.highlight{position:relative}@media (min-width: 768px){.highlight:hover .btn-clipboard{opacity:1}}.btn-cta{padding-left:2rem;padding-right:2rem}.expressive-code{font-family:var(--ec-uiFontFml);font-size:var(--ec-uiFontSize);line-height:var(--ec-uiLineHt);-moz-text-size-adjust:none;text-size-adjust:none;-webkit-text-size-adjust:none;margin:1.5rem 0}.expressive-code *:not(path){all:revert;box-sizing:border-box}.expressive-code pre{display:flex;margin:0;padding:0;border:var(--ec-brdWd) solid var(--ec-brdCol);border-radius:calc(var(--ec-brdRad) + var(--ec-brdWd));background:var(--ec-codeBg)}.expressive-code pre:focus-visible{outline:3px solid var(--ec-focusBrd);outline-offset:-3px}.expressive-code pre>code{all:unset;display:block;flex:1 0 100%;padding:var(--ec-codePadBlk) 0;color:var(--ec-codeFg);font-family:var(--ec-codeFontFml);font-size:var(--ec-codeFontSize);line-height:var(--ec-codeLineHt)}.expressive-code pre{overflow-x:auto}.expressive-code pre::-webkit-scrollbar,.expressive-code pre::-webkit-scrollbar-track{background-color:inherit;border-radius:calc(var(--ec-brdRad) + var(--ec-brdWd));border-top-left-radius:0;border-top-right-radius:0}.expressive-code pre::-webkit-scrollbar-thumb{background-color:var(--ec-sbThumbCol);border:4px solid transparent;background-clip:content-box;border-radius:10px}.expressive-code pre::-webkit-scrollbar-thumb:hover{background-color:var(--ec-sbThumbHoverCol)}.expressive-code .ec-line{padding-inline:var(--ec-codePadInl);padding-inline-end:calc(2rem + var(--ec-codePadInl));direction:ltr;unicode-bidi:isolate}.expressive-code .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.expressive-code .ec-line.mark{--tmLineBgCol: var(--ec-tm-markBg);--tmLineBrdCol: var(--ec-tm-markBrdCol)}.expressive-code .ec-line.ins{--tmLineBgCol: var(--ec-tm-insBg);--tmLineBrdCol: var(--ec-tm-insBrdCol)}.expressive-code .ec-line.ins::before{content:var(--ec-tm-insDiffIndContent);color:var(--ec-tm-insDiffIndCol)}.expressive-code .ec-line.del{--tmLineBgCol: var(--ec-tm-delBg);--tmLineBrdCol: var(--ec-tm-delBrdCol)}.expressive-code .ec-line.del::before{content:var(--ec-tm-delDiffIndContent);color:var(--ec-tm-delDiffIndCol)}.expressive-code .ec-line.mark,.expressive-code .ec-line.ins,.expressive-code .ec-line.del{position:relative;background:var(--tmLineBgCol);min-width:calc(100% - var(--ec-tm-lineMarkerAccentMarg));margin-inline-start:var(--ec-tm-lineMarkerAccentMarg);border-inline-start:var(--ec-tm-lineMarkerAccentWd) solid var(--tmLineBrdCol);padding-inline-start:calc(var(--ec-codePadInl) - var(--ec-tm-lineMarkerAccentMarg) - var(--ec-tm-lineMarkerAccentWd)) !important}.expressive-code .ec-line.mark::before,.expressive-code .ec-line.ins::before,.expressive-code .ec-line.del::before{position:absolute;left:var(--ec-tm-lineDiffIndMargLeft)}.expressive-code .ec-line mark,.expressive-code .ec-line .mark{--tmInlineBgCol: var(--ec-tm-markBg);--tmInlineBrdCol: var(--ec-tm-markBrdCol)}.expressive-code .ec-line ins{--tmInlineBgCol: var(--ec-tm-insBg);--tmInlineBrdCol: var(--ec-tm-insBrdCol)}.expressive-code .ec-line del{--tmInlineBgCol: var(--ec-tm-delBg);--tmInlineBrdCol: var(--ec-tm-delBrdCol)}.expressive-code .ec-line mark,.expressive-code .ec-line .mark,.expressive-code .ec-line ins,.expressive-code .ec-line del{all:unset;display:inline-block;position:relative;--tmBrdL: var(--ec-tm-inlMarkerBrdWd);--tmBrdR: var(--ec-tm-inlMarkerBrdWd);--tmRadL: var(--ec-tm-inlMarkerBrdRad);--tmRadR: var(--ec-tm-inlMarkerBrdRad);margin-inline:0.025rem;padding-inline:var(--ec-tm-inlMarkerPad);border-radius:var(--tmRadL) var(--tmRadR) var(--tmRadR) var(--tmRadL);background:var(--tmInlineBgCol);background-clip:padding-box}.expressive-code .ec-line mark.open-start,.expressive-code .ec-line .open-start.mark,.expressive-code .ec-line ins.open-start,.expressive-code .ec-line del.open-start{margin-inline-start:0;padding-inline-start:0;--tmBrdL: 0px;--tmRadL: 0}.expressive-code .ec-line mark.open-end,.expressive-code .ec-line .open-end.mark,.expressive-code .ec-line ins.open-end,.expressive-code .ec-line del.open-end{margin-inline-end:0;padding-inline-end:0;--tmBrdR: 0px;--tmRadR: 0}.expressive-code .ec-line mark::before,.expressive-code .ec-line .mark::before,.expressive-code .ec-line ins::before,.expressive-code .ec-line del::before{content:"";position:absolute;pointer-events:none;display:inline-block;inset:0;border-radius:var(--tmRadL) var(--tmRadR) var(--tmRadR) var(--tmRadL);border:var(--ec-tm-inlMarkerBrdWd) solid var(--tmInlineBrdCol);border-inline-width:var(--tmBrdL) var(--tmBrdR)}.expressive-code .frame{all:unset;position:relative;display:block;--header-border-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));--tab-border-radius: calc(var(--ec-frm-edTabBrdRad) + var(--ec-brdWd));--button-spacing: 0.4rem;--code-background: var(--ec-frm-edBg);border-radius:var(--header-border-radius);box-shadow:var(--ec-frm-frameBoxShdCssVal)}.expressive-code .frame .header{display:none;z-index:1;position:relative;border-radius:var(--header-border-radius) var(--header-border-radius) 0 0}.expressive-code .frame.has-title pre,.expressive-code .frame.has-title code,.expressive-code .frame.is-terminal pre,.expressive-code .frame.is-terminal code{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.expressive-code .frame .title:empty:before{content:"\a0"}.expressive-code .frame.has-title:not(.is-terminal){--button-spacing: calc(1.9rem + 2 * (var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt)))}.expressive-code .frame.has-title:not(.is-terminal) .title{position:relative;color:var(--ec-frm-edActTabFg);background:var(--ec-frm-edActTabBg);background-clip:padding-box;margin-block-start:var(--ec-frm-edTabsMargBlkStart);padding:calc(var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt)) var(--ec-uiPadInl);border:var(--ec-brdWd) solid var(--ec-frm-edActTabBrdCol);border-radius:var(--tab-border-radius) var(--tab-border-radius) 0 0;border-bottom:none;overflow:hidden}.expressive-code .frame.has-title:not(.is-terminal) .title::after{content:"";position:absolute;pointer-events:none;inset:0;border-top:var(--ec-frm-edActTabIndHt) solid var(--ec-frm-edActTabIndTopCol);border-bottom:var(--ec-frm-edActTabIndHt) solid var(--ec-frm-edActTabIndBtmCol)}.expressive-code .frame.has-title:not(.is-terminal) .header{display:flex;background:linear-gradient(to top, var(--ec-frm-edTabBarBrdBtmCol) var(--ec-brdWd), transparent var(--ec-brdWd)),linear-gradient(var(--ec-frm-edTabBarBg), var(--ec-frm-edTabBarBg));background-repeat:no-repeat;padding-inline-start:var(--ec-frm-edTabsMargInlStart)}.expressive-code .frame.has-title:not(.is-terminal) .header::before{content:"";position:absolute;pointer-events:none;inset:0;border:var(--ec-brdWd) solid var(--ec-frm-edTabBarBrdCol);border-radius:inherit;border-bottom:none}.expressive-code .frame.is-terminal{--button-spacing: calc(1.9rem + var(--ec-brdWd) + 2 * var(--ec-uiPadBlk));--code-background: var(--ec-frm-trmBg)}.expressive-code .frame.is-terminal .header{display:flex;align-items:center;justify-content:center;padding-block:var(--ec-uiPadBlk);padding-block-end:calc(var(--ec-uiPadBlk) + var(--ec-brdWd));position:relative;font-weight:500;letter-spacing:0.025ch;color:var(--ec-frm-trmTtbFg);background:var(--ec-frm-trmTtbBg);border:var(--ec-brdWd) solid var(--ec-brdCol);border-bottom:none}.expressive-code .frame.is-terminal .header::before{content:"";position:absolute;pointer-events:none;left:var(--ec-uiPadInl);width:2.1rem;height:0.56rem;line-height:0;background-color:var(--ec-frm-trmTtbDotsFg);opacity:var(--ec-frm-trmTtbDotsOpa);-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 16' preserveAspectRatio='xMidYMid meet'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3Ccircle cx='30' cy='8' r='8'/%3E%3Ccircle cx='52' cy='8' r='8'/%3E%3C/svg%3E");-webkit-mask-repeat:no-repeat;mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 16' preserveAspectRatio='xMidYMid meet'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3Ccircle cx='30' cy='8' r='8'/%3E%3Ccircle cx='52' cy='8' r='8'/%3E%3C/svg%3E");mask-repeat:no-repeat}.expressive-code .frame.is-terminal .header::after{content:"";position:absolute;pointer-events:none;inset:0;border-bottom:var(--ec-brdWd) solid var(--ec-frm-trmTtbBrdBtmCol)}.expressive-code .frame pre{background:var(--code-background)}.expressive-code .copy{display:flex;gap:0.25rem;flex-direction:row;position:absolute;inset-block-start:calc(var(--ec-brdWd) + var(--button-spacing));inset-inline-end:calc(var(--ec-brdWd) + var(--ec-uiPadInl) / 2);direction:ltr;unicode-bidi:isolate}.expressive-code .copy button{position:relative;align-self:flex-end;margin:0;padding:0;border:none;border-radius:0.2rem;z-index:1;cursor:pointer;transition-property:opacity, background, border-color;transition-duration:0.2s;transition-timing-function:cubic-bezier(0.25, 0.46, 0.45, 0.94);width:2.5rem;height:2.5rem;background:var(--code-background);opacity:0.75}.expressive-code .copy button div{position:absolute;inset:0;border-radius:inherit;background:var(--ec-frm-inlBtnBg);opacity:var(--ec-frm-inlBtnBgIdleOpa);transition-property:inherit;transition-duration:inherit;transition-timing-function:inherit}.expressive-code .copy button::before{content:"";position:absolute;pointer-events:none;inset:0;border-radius:inherit;border:var(--ec-brdWd) solid var(--ec-frm-inlBtnBrd);opacity:var(--ec-frm-inlBtnBrdOpa)}.expressive-code .copy button::after{content:"";position:absolute;pointer-events:none;inset:0;background-color:var(--ec-frm-inlBtnFg);-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75'%3E%3Cpath d='M3 19a2 2 0 0 1-1-2V2a2 2 0 0 1 1-1h13a2 2 0 0 1 2 1'/%3E%3Crect x='6' y='5' width='16' height='18' rx='1.5' ry='1.5'/%3E%3C/svg%3E");-webkit-mask-repeat:no-repeat;mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75'%3E%3Cpath d='M3 19a2 2 0 0 1-1-2V2a2 2 0 0 1 1-1h13a2 2 0 0 1 2 1'/%3E%3Crect x='6' y='5' width='16' height='18' rx='1.5' ry='1.5'/%3E%3C/svg%3E");mask-repeat:no-repeat;margin:0.475rem;line-height:0}.expressive-code .copy button:focus::after,.expressive-code .copy button:active::after{display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='black' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='black' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;margin:0.2375rem}.expressive-code .copy button:hover,.expressive-code .copy button:focus:focus-visible{opacity:1}.expressive-code .copy button:hover div,.expressive-code .copy button:focus:focus-visible div{opacity:var(--ec-frm-inlBtnBgHoverOrFocusOpa)}.expressive-code .copy button:active{opacity:1}.expressive-code .copy button:active div{opacity:var(--ec-frm-inlBtnBgActOpa)}.expressive-code .copy .feedback{--tooltip-arrow-size: 0.35rem;--tooltip-bg: var(--ec-frm-tooltipSuccessBg);color:var(--ec-frm-tooltipSuccessFg);pointer-events:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;position:relative;align-self:center;background-color:var(--tooltip-bg);z-index:99;padding:0.125rem 0.75rem;border-radius:0.2rem;margin-inline-end:var(--tooltip-arrow-size);opacity:0;transition-property:opacity, transform;transition-duration:0.2s;transition-timing-function:ease-in-out;transform:translate3d(0, 0.25rem, 0)}.expressive-code .copy .feedback::after{content:"";position:absolute;pointer-events:none;top:calc(50% - var(--tooltip-arrow-size));inset-inline-end:calc(-2 * (var(--tooltip-arrow-size) - 0.5px));border:var(--tooltip-arrow-size) solid transparent;border-inline-start-color:var(--tooltip-bg)}.expressive-code .copy .feedback.show{opacity:1;transform:translate3d(0, 0, 0)}@media (hover: hover){.expressive-code .copy button{opacity:0;width:2rem;height:2rem}.expressive-code .frame:hover .copy button:not(:hover),.expressive-code .frame:focus-within :focus-visible~.copy button:not(:hover),.expressive-code .frame .copy .feedback.show~button:not(:hover){opacity:0.75}}:root{--ec-brdRad: 0px;--ec-brdWd: 1px;--ec-brdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-codeFontFml: var(--__sl-font-mono);--ec-codeFontSize: var(--sl-text-code);--ec-codeFontWg: 400;--ec-codeLineHt: var(--sl-line-height);--ec-codePadBlk: 0;--ec-codePadInl: 1rem;--ec-codeBg: #011627;--ec-codeFg: #d6deeb;--ec-codeSelBg: #1d3b53;--ec-uiFontFml: var(--__sl-font);--ec-uiFontSize: 0.9rem;--ec-uiFontWg: 400;--ec-uiLineHt: 1.65;--ec-uiPadBlk: 0.25rem;--ec-uiPadInl: 1rem;--ec-uiSelBg: #234d708c;--ec-uiSelFg: #ffffff;--ec-focusBrd: #122d42;--ec-sbThumbCol: #ffffff17;--ec-sbThumbHoverCol: #ffffff49;--ec-tm-lineMarkerAccentMarg: 0rem;--ec-tm-lineMarkerAccentWd: 0.15rem;--ec-tm-lineDiffIndMargLeft: 0.25rem;--ec-tm-inlMarkerBrdWd: 1.5px;--ec-tm-inlMarkerBrdRad: 0.2rem;--ec-tm-inlMarkerPad: 0.15rem;--ec-tm-insDiffIndContent: "+";--ec-tm-delDiffIndContent: "-";--ec-tm-markBg: #ffffff17;--ec-tm-markBrdCol: #ffffff40;--ec-tm-insBg: #1e571599;--ec-tm-insBrdCol: #487f3bd0;--ec-tm-insDiffIndCol: #79b169d0;--ec-tm-delBg: #862d2799;--ec-tm-delBrdCol: #b4554bd0;--ec-tm-delDiffIndCol: #ed8779d0;--ec-frm-shdCol: #011627;--ec-frm-frameBoxShdCssVal: none;--ec-frm-edActTabBg: var(--sl-color-gray-6);--ec-frm-edActTabFg: var(--sl-color-text);--ec-frm-edActTabBrdCol: transparent;--ec-frm-edActTabIndHt: 1px;--ec-frm-edActTabIndTopCol: var(--sl-color-accent-high);--ec-frm-edActTabIndBtmCol: transparent;--ec-frm-edTabsMargInlStart: 0;--ec-frm-edTabsMargBlkStart: 0;--ec-frm-edTabBrdRad: 0px;--ec-frm-edTabBarBg: var(--sl-color-black);--ec-frm-edTabBarBrdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edBg: var(--sl-color-gray-6);--ec-frm-trmTtbDotsFg: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmTtbDotsOpa: 0.75;--ec-frm-trmTtbBg: var(--sl-color-black);--ec-frm-trmTtbFg: var(--sl-color-text);--ec-frm-trmTtbBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmBg: var(--sl-color-gray-6);--ec-frm-inlBtnFg: var(--sl-color-text);--ec-frm-inlBtnBg: var(--sl-color-text);--ec-frm-inlBtnBgIdleOpa: 0;--ec-frm-inlBtnBgHoverOrFocusOpa: 0.2;--ec-frm-inlBtnBgActOpa: 0.3;--ec-frm-inlBtnBrd: var(--sl-color-text);--ec-frm-inlBtnBrdOpa: 0.4;--ec-frm-tooltipSuccessBg: #158744;--ec-frm-tooltipSuccessFg: white}.expressive-code .ec-line span[style^="--"]:not([class]){color:var(0, inherit);font-style:var(0fs, inherit);font-weight:var(0fw, inherit);-webkit-text-decoration:var(0td, inherit);text-decoration:var(0td, inherit)}@media (prefers-color-scheme: light){:root:not([data-bs-theme="dark"]){--ec-codeBg: #fbfbfb;--ec-codeFg: #403f53;--ec-codeSelBg: #e0e0e0;--ec-uiSelBg: #d3e8f8;--ec-uiSelFg: #403f53;--ec-focusBrd: #93a1a1;--ec-sbThumbCol: #0000001a;--ec-sbThumbHoverCol: #0000005c;--ec-tm-markBg: #0000001a;--ec-tm-markBrdCol: #00000055;--ec-tm-insBg: #8ec77d99;--ec-tm-insDiffIndCol: #336a28d0;--ec-tm-delBg: #ff9c8e99;--ec-tm-delDiffIndCol: #9d4138d0;--ec-frm-shdCol: #d9d9d9;--ec-frm-edActTabBg: var(--sl-color-gray-7);--ec-frm-edActTabIndTopCol: #5d2f86;--ec-frm-edTabBarBg: var(--sl-color-gray-6);--ec-frm-edBg: var(--sl-color-gray-7);--ec-frm-trmTtbBg: var(--sl-color-gray-6);--ec-frm-trmBg: var(--sl-color-gray-7);--ec-frm-tooltipSuccessBg: #078662}:root:not([data-bs-theme="dark"]) .expressive-code .ec-line span[style^="--"]:not([class]){color:var(1, inherit);font-style:var(1fs, inherit);font-weight:var(1fw, inherit);-webkit-text-decoration:var(1td, inherit);text-decoration:var(1td, inherit)}}:root[data-bs-theme="light"] .expressive-code,.expressive-code[data-bs-theme="light"]{--ec-codeBg: #fbfbfb;--ec-codeFg: #403f53;--ec-codeSelBg: #e0e0e0;--ec-uiSelBg: #d3e8f8;--ec-uiSelFg: #403f53;--ec-focusBrd: #93a1a1;--ec-sbThumbCol: #0000001a;--ec-sbThumbHoverCol: #0000005c;--ec-tm-markBg: #0000001a;--ec-tm-markBrdCol: #00000055;--ec-tm-insBg: #8ec77d99;--ec-tm-insDiffIndCol: #336a28d0;--ec-tm-delBg: #ff9c8e99;--ec-tm-delDiffIndCol: #9d4138d0;--ec-frm-shdCol: #d9d9d9;--ec-frm-edActTabBg: var(--sl-color-gray-7);--ec-frm-edActTabIndTopCol: #5d2f86;--ec-frm-edTabBarBg: var(--sl-color-gray-6);--ec-frm-edBg: var(--sl-color-gray-7);--ec-frm-trmTtbBg: var(--sl-color-gray-6);--ec-frm-trmBg: var(--sl-color-gray-7);--ec-frm-tooltipSuccessBg: #078662}:root[data-bs-theme="light"] .expressive-code .ec-line span[style^="--"]:not([class]),.expressive-code[data-bs-theme="light"] .ec-line span[style^="--"]:not([class]){color:var(1, inherit);font-style:var(1fs, inherit);font-weight:var(1fw, inherit);-webkit-text-decoration:var(1td, inherit);text-decoration:var(1td, inherit)}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:.875rem}code:not(:where(.not-content *)){background-color:var(--sl-color-gray-6);margin-block:-0.125rem;padding:0.125rem 0.375rem;color:inherit}[data-bs-theme="dark"] code:not(:where(.not-content *)){background-color:var(--sl-color-gray-5)}.math-block{display:block;margin:2rem 0;overflow-x:auto}.math-inline{display:inline}[data-bs-theme="dark"] .math-inline img,[data-bs-theme="dark"] .math-block img{filter:invert(1)}img.diagram{height:auto;width:100%;margin:1rem 0 2rem}img.diagram-kroki-mermaid{background:#fff}.highlight>pre{padding:0.875rem 1rem}.highlight div{padding:0}.highlight>.chroma{overflow-x:auto;border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}.chroma .ln{padding:0 0.5rem 0 0}.chroma .hl{border-inline-start:0.15rem solid #0005;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}.chroma .hl .ln{margin-left:-0.15rem}.highlight .chroma .lntable .lnt,.highlight .chroma .lntable .hl{display:flex}.chroma .lntd:first-child{padding:0}.chroma .lntd:first-child .lnt{padding-left:1rem}.chroma .lntd:nth-child(2){padding:0}.highlight .chroma .lntable .lntd+.lntd{width:100%}[data-bs-theme="dark"] .chroma .ln{padding:0 0.5em 0 0}.chroma .lntd pre{padding:1rem 0;margin-bottom:0}.highlight>.chroma::-webkit-scrollbar,.highlight>.chroma::-webkit-scrollbar-track{background-color:inherit;border-radius:1px;border-top-left-radius:0;border-top-right-radius:0}.highlight>.chroma::-webkit-scrollbar-thumb{background-color:#dddee0;border:4px solid transparent;background-clip:content-box;border-radius:10px}.highlight>.chroma::-webkit-scrollbar-thumb:hover{background-color:#9d9e9f}[data-bs-theme="dark"] .highlight>.chroma::-webkit-scrollbar-thumb{background-color:#ffffff17}[data-bs-theme="dark"] .highlight>.chroma::-webkit-scrollbar-thumb:hover{background-color:#ffffff49}[data-bs-theme="dark"] .highlight>.chroma{border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}[data-bs-theme="dark"] .chroma .hl{border-inline-start:0.15rem solid #ffffff40;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}[data-bs-theme="dark"] .chroma .hl .ln{margin-left:-0.15rem}blockquote{margin-bottom:1rem;font-size:1.25rem;border-left:3px solid #dee2e6;padding-left:1rem}details{display:block;position:relative;border:1px solid #e9ecef;border-radius:0.25rem;padding:0.5rem 1rem 0;margin:0.5rem 0}summary{list-style:none;display:inline-block;width:calc(100% + 2rem);margin:-0.5rem -1rem 0;padding:0.5rem 1rem}summary::-webkit-details-marker{display:none}summary:hover{background:#f8f9fa}details summary::after{display:inline-block;content:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%2829, 45, 53, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");transition:transform 0.35s ease;transform-origin:center center;position:absolute;right:1rem}details[open]>summary::after{transform:rotate(90deg)}details[open]{padding:0.5rem 1rem}details[open]>summary{border-bottom:1px solid #dee2e6;margin-bottom:0.5rem}details h2,details .h2,details h3,details .h3,details h4,details .h4{margin:1rem 0 0.5rem}details p:last-child{margin-bottom:0}details ul,details ol{margin-bottom:0}details pre{margin:0 0 1rem}.search-form label{font-weight:normal}img{max-width:100%;height:auto}img[data-sizes="auto"]{display:block}img{font-size:0}figcaption{font-size:1rem;margin-top:0.5rem;font-style:italic}.blur-up{filter:blur(5px);transition:filter 400ms}.blur-up.lazyloaded{filter:unset}.search-form .form-control:focus{border:2px solid #4f46e5}[data-bs-theme="dark"] .search-form .form-control:focus{border:2px solid #b3c7ff}[data-bs-theme="dark"] .search-form .btn-link{color:#b3c7ff}.search-form .btn-link,.modal-body p.message,.modal-footer{font-size:.875rem}.modal-body::-webkit-scrollbar{width:0.25rem}.modal-body::-webkit-scrollbar-track{background-color:#f1f1f1}.modal-body::-webkit-scrollbar-thumb{background-color:#c1c1c1}[data-bs-theme="dark"] .modal-body::-webkit-scrollbar-track{background-color:#424242}[data-bs-theme="dark"] .modal-body::-webkit-scrollbar-thumb{background-color:#686868}@media (min-width: 768px){#searchModal .modal-dialog{max-height:40rem}}.search-result h2,.search-result .h2{margin-top:0}.search-result a:focus{outline:0 none}.search-result .content{margin-top:0.5rem;padding-top:0 !important;padding-bottom:0 !important}.search-result .card .content p{margin-bottom:0}.search-result .card .content a{position:relative;z-index:1}.search-result:hover .card,.search-result.selected .card{background-color:#4f46e5;color:#fff}.search-result:hover .card .content a,.search-result.selected .card .content a{color:#fff;text-decoration:underline}[data-bs-theme="dark"] .search-result:hover .card,[data-bs-theme="dark"] .search-result.selected .card{background-color:#b3c7ff;color:#23262f}[data-bs-theme="dark"] .search-result:hover .card .content a,[data-bs-theme="dark"] .search-result.selected .card .content a{color:#23262f;text-decoration:underline}[data-bs-theme="dark"] .search-result:hover .card h2,[data-bs-theme="dark"] .search-result:hover .card .h2,[data-bs-theme="dark"] .search-result.selected .card h2,[data-bs-theme="dark"] .search-result.selected .card .h2{color:#17181c}.search-result .submitted{font-size:.875rem;margin-top:0.5rem}.section-nav{padding-top:2rem}.section-nav details{border:0;padding:0;margin:0.5rem 0}.section-nav details[open]{padding:0}.section-nav summary{width:100%;padding:0;margin:0;font-weight:700}.section-nav summary:hover{background:none}.section-nav details[open]>summary{border-bottom:0;margin-bottom:0}.section-nav ul.list-nested details{padding-left:1rem;margin-top:0.5rem}.section-nav ul.list-nested li{margin:0}.section-nav a{display:block;margin:0.5rem 0;color:#1d2d35;font-size:1rem;text-decoration:none}.section-nav a:hover,.section-nav a:active{color:#4f46e5}.section-nav li.active a{color:#4f46e5;font-weight:500}.section-nav ul.list-nested li a{padding-left:1rem}.section-nav ul.list-nested{border-left:1px solid #e9ecef}[data-bs-theme="dark"] .section-nav ul.list-nested{border-left:1px solid #23262f}[data-bs-theme="dark"] .section-nav a{color:#c1c3c8}[data-bs-theme="dark"] .section-nav a:hover,[data-bs-theme="dark"] .section-nav a:active{color:var(--sl-color-text-accent)}[data-bs-theme="dark"] .section-nav li.active a{color:var(--sl-color-text-accent);font-weight:500}[data-bs-theme="dark"] .section-nav summary{color:#fff}table{margin:3rem 0}.nav-tabs{border-bottom:0.0625rem solid #d8dee4;margin-bottom:1rem}.nav-tabs .nav-link{margin-bottom:-0.0625rem !important;background:none;border:0;border-top-left-radius:0;border-top-right-radius:0;color:inherit}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:transparent;color:var(--bs-emphasis-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{background-color:transparent;border-color:transparent;border-bottom:0.125rem solid #4f46e5}[data-bs-theme="dark"] .nav-tabs{border-bottom:0.0625rem solid #343a40}[data-bs-theme="dark"] .nav-tabs .nav-link.active,[data-bs-theme="dark"] .nav-tabs .nav-item.show .nav-link{border-bottom:0.125rem solid #b3c7ff}.footer{border-top:1px solid #e9ecef;padding-top:1.125rem;padding-bottom:1.125rem}.footer ul{margin-bottom:0}.footer li{font-size:.875rem;margin-bottom:0}.footer .list-inline-item:not(:last-child){margin-right:1rem}@media (max-width: 991.98px){.footer .col-lg-8{margin-top:0.25rem;margin-bottom:0.25rem}}@media (min-width: 768px){.footer li{font-size:1rem}}.navbar-brand{font-weight:700}.navbar-brand svg{margin-right:0.25rem}[data-bs-theme="dark"] .navbar-brand{color:inherit}.navbar{z-index:1000;background-color:rgba(255,255,255,0.95);border-bottom:1px solid #e9ecef}@media (min-width: 992px){.navbar{z-index:1025}}@media (min-width: 768px){.navbar-brand{font-size:1.375rem}}.nav-item{margin-left:0}@media (max-width: 991.98px){.navbar-nav .nav-link{font-weight:400}}@media (min-width: 768px){.nav-item{margin-left:0.5rem}}@media (max-width: 575.98px){.navbar .offcanvas.offcanvas-start,.navbar .offcanvas.offcanvas-end{width:80vw}}.offcanvas-header{border-bottom:1px solid #dee2e6;padding-top:1.0625rem;padding-bottom:0.8125rem}h5.offcanvas-title,.offcanvas-title.h5{margin:0;color:inherit}.offcanvas .nav-link{color:#1d2d35}.offcanvas .nav-link:hover,.offcanvas .nav-link:focus{color:#4f46e5}.offcanvas .nav-link.active{color:#4f46e5}.home .navbar{border-bottom:0}@media (min-width: 992px){.navbar-brand{margin-right:0.75rem !important}}.social-link{padding-right:0.375rem;padding-left:0.375rem}@media (max-width: 991.98px){#buttonColorMode{margin:0.5rem 0}#socialMenu{margin:0.5rem 0 0.5rem -0.25rem}.navbar-nav{margin-top:1rem}.nav-item .nav-link{font-weight:400;font-size:1.125rem}}.modal-backdrop,.offcanvas-backdrop{visibility:hidden;background:rgba(23,24,28,0.5);opacity:0}[data-bs-theme="dark"] .modal-backdrop,[data-bs-theme="dark"] .offcanvas-backdrop{visibility:hidden;background:rgba(23,24,28,0.5);opacity:0}.modal-backdrop.show,.offcanvas-backdrop.show{visibility:visible;opacity:1;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.showing,.hiding{transition:none;display:none}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg{padding-right:0.75rem}.docs-content>h2[id]::before,.docs-content>[id].h2::before,.docs-content>h3[id]::before,.docs-content>[id].h3::before,.docs-content>h4[id]::before,.docs-content>[id].h4::before{display:block;height:6rem;margin-top:-6rem;content:""}.docs-content ul,.docs-content ol{margin-bottom:1rem}.anchor{visibility:hidden;margin-left:0.375rem}.edit-page a{color:var(--sl-color-gray-3)}h1:hover a,.h1:hover a,h2:hover a,.h2:hover a,h3:hover a,.h3:hover a,h4:hover a,.h4:hover a{visibility:visible;text-decoration:none}.card-list{margin-top:2.25rem}.page-footer-meta{margin-top:2rem;margin-bottom:2rem}.edit-page{font-size:.875rem;margin-top:0.25rem;margin-bottom:0.25rem}@media (min-width: 768px){.edit-page{font-size:1rem;margin-top:0.75rem;margin-bottom:0.25rem}}.edit-page a:hover{color:var(--sl-color-gray-4);text-decoration:none}[data-bs-theme="dark"] .edit-page a:hover{color:var(--sl-color-gray-2)}.edit-page svg{margin-right:0.25rem;margin-bottom:0.25rem}p.meta{margin-top:0.5rem;font-size:1rem}.toc-mobile{margin-top:2rem;margin-bottom:2rem}.page-link:hover{text-decoration:none}ul li{margin:0.25rem 0}.page-nav .card .icon-tabler-arrow-left{margin-right:0.75rem}.page-nav .card .icon-tabler-arrow-right{margin-left:0.75rem}.page-nav .card:hover{border:1px solid #d9d9d9}[data-bs-theme="dark"] .page-nav .card{border:1px solid #353841}[data-bs-theme="dark"] .page-nav .card:hover{border:1px solid #888c96}.home .card,.contributors.list .card,.categories.list .card,.tags.list .card{margin-top:2rem;margin-bottom:2rem;transition:transform 0.3s}.home .content .card:hover,.contributors.list .content .card:hover,.categories.list .content .card:hover,.tags.list .content .card:hover{transform:scale(1.025)}.home .content .card-body,.contributors.list .content .card-body,.categories.list .content .card-body,.tags.list .content .card-body{padding:0 2rem 1rem}.page-item:first-child,.page-item:last-child,.page-item.disabled{display:none}.page-item a{margin-left:0.5rem;margin-right:0.5rem;padding-left:0.875rem;padding-right:0.875rem}.docs-links,.docs-toc{scrollbar-width:thin;scrollbar-color:#fff #fff}.docs-links::-webkit-scrollbar,.docs-toc::-webkit-scrollbar{width:5px}.docs-links::-webkit-scrollbar-track,.docs-toc::-webkit-scrollbar-track{background:#fff}.docs-links::-webkit-scrollbar-thumb,.docs-toc::-webkit-scrollbar-thumb{background:#fff}.docs-links:hover,.docs-toc:hover{scrollbar-width:thin;scrollbar-color:#e9ecef #fff}.docs-links:hover::-webkit-scrollbar-thumb,.docs-toc:hover::-webkit-scrollbar-thumb{background:#e9ecef}.docs-links::-webkit-scrollbar-thumb:hover,.docs-toc::-webkit-scrollbar-thumb:hover{background:#e9ecef}.docs-links h3,.docs-links .h3,.page-links h3,.page-links .h3{font-size:1.125rem;margin:1.25rem 0 0.5rem;padding:1.5rem 0 0}@media (min-width: 992px){.docs-links h3,.docs-links .h3,.page-links h3,.page-links .h3{margin:1.125rem 1.5rem 0.75rem 0;padding:1.375rem 0 0}}.docs-links h3:not(:first-child),.docs-links .h3:not(:first-child){border-top:1px solid #e9ecef}.page-links li{margin-top:0.375rem;padding-top:0.375rem}.page-links li ul li{border-top:none;padding-left:1rem;margin-top:0.125rem;padding-top:0.125rem}.page-links li:not(:first-child){border-top:1px dashed #e9ecef}.page-links a{color:#1d2d35;display:block;padding:0.125rem 0;font-size:.9375rem;text-decoration:none}.page-links a:hover,.page-links a.active{text-decoration:none;color:#4f46e5}.nav-link.active{font-weight:500}*{-webkit-font-smoothing:antialiased}h1,.h1,h2,.h2,h3,.h3,h4,.h4,h5,.h5,.navbar-brand{font-family:Quicksand, sans-serif;font-weight:700}[data-bs-theme="dark"] .only-light{display:none}[data-bs-theme="light"] .only-dark{display:none}
+:root[data-bs-theme="light"],[data-bs-theme="light"] ::backdrop{--sl-color-white: hsl(224, 10%, 10%);--sl-color-gray-1: hsl(224, 14%, 16%);--sl-color-gray-2: hsl(224, 10%, 23%);--sl-color-gray-3: hsl(224, 7%, 36%);--sl-color-gray-4: hsl(224, 6%, 56%);--sl-color-gray-5: hsl(224, 6%, 77%);--sl-color-gray-6: hsl(224, 20%, 94%);--sl-color-gray-7: hsl(224, 19%, 97%);--sl-color-black: hsl(0, 0%, 100%)}:root,::backdrop{--sl-color-white: hsl(0, 0%, 100%);--sl-color-gray-1: hsl(224, 20%, 94%);--sl-color-gray-2: hsl(224, 6%, 77%);--sl-color-gray-3: hsl(224, 6%, 56%);--sl-color-gray-4: hsl(224, 7%, 36%);--sl-color-gray-5: hsl(224, 10%, 23%);--sl-color-gray-6: hsl(224, 14%, 16%);--sl-color-black: hsl(224, 10%, 10%);--sl-hue-orange: 41;--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);--sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);--sl-hue-green: 101;--sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);--sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);--sl-hue-blue: 234;--sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);--sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);--sl-hue-purple: 281;--sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);--sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);--sl-hue-red: 339;--sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);--sl-color-accent-low: hsl(224, 54%, 20%);--sl-color-accent: hsl(224, 100%, 60%);--sl-color-accent-high: hsl(224, 100%, 85%);--sl-color-text: var(--sl-color-gray-2);--sl-color-text-accent: var(--sl-color-accent-high);--sl-color-text-invert: var(--sl-color-accent-low);--sl-color-bg: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-6);--sl-color-bg-sidebar: var(--sl-color-gray-6);--sl-color-bg-inline-code: var(--sl-color-gray-5);--sl-color-hairline-light: var(--sl-color-gray-5);--sl-color-hairline: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-black);--sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12), 0px 2px 1px hsla(0, 0%, 0%, 0.24);--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08), 0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12), 0px 1px 1px hsla(0, 0%, 0%, 0.15);--sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03), 0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33), 0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);--sl-text-xs: 0.8125rem;--sl-text-sm: 0.875rem;--sl-text-base: 1rem;--sl-text-lg: 1.125rem;--sl-text-xl: 1.25rem;--sl-text-2xl: 1.5rem;--sl-text-3xl: 1.8125rem;--sl-text-4xl: 2.1875rem;--sl-text-5xl: 2.625rem;--sl-text-6xl: 4rem;--sl-text-body: var(--sl-text-base);--sl-text-body-sm: var(--sl-text-xs);--sl-text-code: var(--sl-text-sm);--sl-text-code-sm: var(--sl-text-xs);--sl-text-h1: var(--sl-text-4xl);--sl-text-h2: var(--sl-text-3xl);--sl-text-h3: var(--sl-text-2xl);--sl-text-h4: var(--sl-text-xl);--sl-text-h5: var(--sl-text-lg);--sl-line-height: 1.8;--sl-line-height-headings: 1.2;--sl-font-system: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--__sl-font: var(--sl-font, ""), var(--sl-font-system);--__sl-font-mono: var(--sl-font-mono, ""), var(--sl-font-system-mono);--sl-nav-height: 3.5rem;--sl-nav-pad-x: 1rem;--sl-nav-pad-y: 0.75rem;--sl-mobile-toc-height: 3rem;--sl-sidebar-width: 18.75rem;--sl-sidebar-pad-x: 1rem;--sl-content-width: 45rem;--sl-content-pad-x: 1rem;--sl-menu-button-size: 2rem;--sl-nav-gap: var(--sl-content-pad-x);--sl-outline-offset-inside: -0.1875rem;--sl-z-index-toc: 4;--sl-z-index-menu: 5;--sl-z-index-navbar: 10;--sl-z-index-skiplink: 20}:root{--purple-hsl: 255, 60%, 60%;--overlay-blurple: hsla(var(--purple-hsl), 0.2)}:root{--ec-brdRad: 0px;--ec-brdWd: 1px;--ec-brdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-codeFontFml: var(--__sl-font-mono);--ec-codeFontSize: var(--sl-text-code);--ec-codeFontWg: 400;--ec-codeLineHt: var(--sl-line-height);--ec-codePadBlk: 0.75rem;--ec-codePadInl: 1rem;--ec-codeBg: #011627;--ec-codeFg: #d6deeb;--ec-codeSelBg: #1d3b53;--ec-uiFontFml: var(--__sl-font);--ec-uiFontSize: 0.9rem;--ec-uiFontWg: 400;--ec-uiLineHt: 1.65;--ec-uiPadBlk: 0.25rem;--ec-uiPadInl: 1rem;--ec-uiSelBg: #234d708c;--ec-uiSelFg: #ffffff;--ec-focusBrd: #122d42;--ec-sbThumbCol: #ffffff17;--ec-sbThumbHoverCol: #ffffff49;--ec-tm-lineMarkerAccentMarg: 0rem;--ec-tm-lineMarkerAccentWd: 0.15rem;--ec-tm-lineDiffIndMargLeft: 0.25rem;--ec-tm-inlMarkerBrdWd: 1.5px;--ec-tm-inlMarkerBrdRad: 0.2rem;--ec-tm-inlMarkerPad: 0.15rem;--ec-tm-insDiffIndContent: "+";--ec-tm-delDiffIndContent: "-";--ec-tm-markBg: #ffffff17;--ec-tm-markBrdCol: #ffffff40;--ec-tm-insBg: #1e571599;--ec-tm-insBrdCol: #487f3bd0;--ec-tm-insDiffIndCol: #79b169d0;--ec-tm-delBg: #862d2799;--ec-tm-delBrdCol: #b4554bd0;--ec-tm-delDiffIndCol: #ed8779d0;--ec-frm-shdCol: #011627;--ec-frm-frameBoxShdCssVal: none;--ec-frm-edActTabBg: var(--sl-color-gray-6);--ec-frm-edActTabFg: var(--sl-color-text);--ec-frm-edActTabBrdCol: transparent;--ec-frm-edActTabIndHt: 1px;--ec-frm-edActTabIndTopCol: var(--sl-color-accent-high);--ec-frm-edActTabIndBtmCol: transparent;--ec-frm-edTabsMargInlStart: 0;--ec-frm-edTabsMargBlkStart: 0;--ec-frm-edTabBrdRad: 0px;--ec-frm-edTabBarBg: var(--sl-color-black);--ec-frm-edTabBarBrdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edBg: var(--sl-color-gray-6);--ec-frm-trmTtbDotsFg: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmTtbDotsOpa: 0.75;--ec-frm-trmTtbBg: var(--sl-color-black);--ec-frm-trmTtbFg: var(--sl-color-text);--ec-frm-trmTtbBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmBg: var(--sl-color-gray-6);--ec-frm-inlBtnFg: var(--sl-color-text);--ec-frm-inlBtnBg: var(--sl-color-text);--ec-frm-inlBtnBgIdleOpa: 0;--ec-frm-inlBtnBgHoverOrFocusOpa: 0.2;--ec-frm-inlBtnBgActOpa: 0.3;--ec-frm-inlBtnBrd: var(--sl-color-text);--ec-frm-inlBtnBrdOpa: 0.4;--ec-frm-tooltipSuccessBg: #158744;--ec-frm-tooltipSuccessFg: white}:root,[data-bs-theme="light"]{--bs-blue: #3347ff;--bs-indigo: #6610f2;--bs-purple: #bd53ee;--bs-pink: #d63384;--bs-red: #ee5389;--bs-orange: #fd7e14;--bs-yellow: #eebd53;--bs-green: #84ee53;--bs-teal: #20c997;--bs-cyan: #0dcaf0;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-primary: #4f46e5;--bs-secondary: #6c757d;--bs-success: #84ee53;--bs-info: #3347ff;--bs-warning: #eebd53;--bs-danger: #ee5389;--bs-light: #f8f9fa;--bs-dark: #212529;--bs-primary-rgb: 79,70,229;--bs-secondary-rgb: 108,117,125;--bs-success-rgb: 132.2821,238.017,83.283;--bs-info-rgb: 51,71.4,255;--bs-warning-rgb: 238.017,189.0179,83.283;--bs-danger-rgb: 238.017,83.283,137.4399;--bs-light-rgb: 248,249,250;--bs-dark-rgb: 33,37,41;--bs-primary-text-emphasis: #201c5c;--bs-secondary-text-emphasis: #2b2f32;--bs-success-text-emphasis: #355f21;--bs-info-text-emphasis: #141d66;--bs-warning-text-emphasis: #5f4c21;--bs-danger-text-emphasis: #5f2137;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #dcdafa;--bs-secondary-bg-subtle: #e2e3e5;--bs-success-bg-subtle: #e6fcdd;--bs-info-bg-subtle: #d6daff;--bs-warning-bg-subtle: #fcf2dd;--bs-danger-bg-subtle: #fcdde7;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #b9b5f5;--bs-secondary-border-subtle: #c4c8cb;--bs-success-border-subtle: #cef8ba;--bs-info-border-subtle: #adb6ff;--bs-warning-border-subtle: #f8e5ba;--bs-danger-border-subtle: #f8bad0;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255,255,255;--bs-black-rgb: 0,0,0;--bs-font-sans-serif: "Heebo", "sans-serif", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255,255,255,0.15), rgba(255,255,255,0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #1d2d35;--bs-body-color-rgb: 29,45,53;--bs-body-bg: #fff;--bs-body-bg-rgb: 255,255,255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0,0,0;--bs-secondary-color: rgba(29,45,53,0.75);--bs-secondary-color-rgb: 29,45,53;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233,236,239;--bs-tertiary-color: rgba(29,45,53,0.5);--bs-tertiary-color-rgb: 29,45,53;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248,249,250;--bs-heading-color: inherit;--bs-link-color: #4f46e5;--bs-link-color-rgb: 79,70,229;--bs-link-decoration: none;--bs-link-hover-color: #3f38b7;--bs-link-hover-color-rgb: 63,56,183;--bs-link-hover-decoration: underline;--bs-code-color: #d63384;--bs-highlight-color: #1d2d35;--bs-highlight-bg: #fcf2dd;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0,0,0,0.175);--bs-border-radius: .375rem;--bs-border-radius-sm: .25rem;--bs-border-radius-lg: .5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0,0,0,0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0,0,0,0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0,0,0,0.075);--bs-focus-ring-width: .25rem;--bs-focus-ring-opacity: .25;--bs-focus-ring-color: rgba(79,70,229,0.25);--bs-form-valid-color: #84ee53;--bs-form-valid-border-color: #84ee53;--bs-form-invalid-color: #ee5389;--bs-form-invalid-border-color: #ee5389}[data-bs-theme="dark"]{color-scheme:dark;--bs-body-color: #c1c3c8;--bs-body-color-rgb: 192.831,194.7078,199.869;--bs-body-bg: #17181c;--bs-body-bg-rgb: 22.95,24.31,28.05;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255,255,255;--bs-secondary-color: rgba(193,195,200,0.75);--bs-secondary-color-rgb: 192.831,194.7078,199.869;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52,58,64;--bs-tertiary-color: rgba(193,195,200,0.5);--bs-tertiary-color-rgb: 192.831,194.7078,199.869;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43,48,53;--bs-primary-text-emphasis: #9590ef;--bs-secondary-text-emphasis: #a7acb1;--bs-success-text-emphasis: #b5f598;--bs-info-text-emphasis: #8591ff;--bs-warning-text-emphasis: #f5d798;--bs-danger-text-emphasis: #f598b8;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #100e2e;--bs-secondary-bg-subtle: #161719;--bs-success-bg-subtle: #1a3011;--bs-info-bg-subtle: #0a0e33;--bs-warning-bg-subtle: #302611;--bs-danger-bg-subtle: #30111b;--bs-light-bg-subtle: #23262f;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #2f2a89;--bs-secondary-border-subtle: #41464b;--bs-success-border-subtle: #4f8f32;--bs-info-border-subtle: #1f2b99;--bs-warning-border-subtle: #8f7132;--bs-danger-border-subtle: #8f3252;--bs-light-border-subtle: #353841;--bs-dark-border-subtle: #343a40;--bs-heading-color: #fff;--bs-link-color: #b3c7ff;--bs-link-hover-color: #c2d2ff;--bs-link-color-rgb: 178.5,198.9,255;--bs-link-hover-color-rgb: 194,210,255;--bs-code-color: #e685b5;--bs-highlight-color: #c1c3c8;--bs-highlight-bg: #5f4c21;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255,255,255,0.15);--bs-form-valid-color: #b5f598;--bs-form-valid-border-color: #b5f598;--bs-form-invalid-color: #f598b8;--bs-form-invalid-border-color: #f598b8}*,*::before,*::after{box-sizing:border-box}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:700;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width: 1200px){h1,.h1{font-size:2.5rem}}h2,.h2{font-size:calc(1.325rem + .9vw)}@media (min-width: 1200px){h2,.h2{font-size:2rem}}h3,.h3{font-size:calc(1.3rem + .6vw)}@media (min-width: 1200px){h3,.h3{font-size:1.75rem}}h4,.h4{font-size:calc(1.275rem + .3vw)}@media (min-width: 1200px){h4,.h4{font-size:1.5rem}}h5,.h5{font-size:1.25rem}p{margin-top:0;margin-bottom:1rem}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}strong{font-weight:bolder}small,.small{font-size:.875em}mark,.mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:none}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb);text-decoration:underline}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button{text-transform:none}[list]:not([type="date"]):not([type="datetime-local"]):not([type="month"]):not([type="week"]):not([type="time"])::-webkit-calendar-picker-indicator{display:none !important}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}iframe{border:0}summary{display:list-item;cursor:pointer}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:400}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.img-fluid{max-width:100%;height:auto}.figure{display:inline-block}.container,.container-fluid,.container-lg{--bs-gutter-x: 3rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container-lg,.container{max-width:960px}}@media (min-width: 1200px){.container-lg,.container{max-width:1240px}}@media (min-width: 1400px){.container-lg,.container{max-width:1820px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.row{--bs-gutter-x: 3rem;--bs-gutter-y: 0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}@media (min-width: 768px){.col-md-12{flex:0 0 auto;width:75%}}@media (min-width: 992px){.col-lg-5{flex:0 0 auto;width:31.25%}.col-lg-8{flex:0 0 auto;width:50%}.col-lg-9{flex:0 0 auto;width:56.25%}.col-lg-10{flex:0 0 auto;width:62.5%}.col-lg-11{flex:0 0 auto;width:68.75%}.col-lg-12{flex:0 0 auto;width:75%}}@media (min-width: 1200px){.col-xl-3{flex:0 0 auto;width:18.75%}.col-xl-4{flex:0 0 auto;width:25%}.col-xl-8{flex:0 0 auto;width:50%}.col-xl-9{flex:0 0 auto;width:56.25%}}.sticky-top{position:sticky;top:0;z-index:1020}.visually-hidden{width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.table,table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: var(--bs-emphasis-color);--bs-table-bg: var(--bs-body-bg);--bs-table-border-color: var(--bs-border-color);--bs-table-accent-bg: rgba(0,0,0,0);--bs-table-striped-color: var(--bs-emphasis-color);--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color: var(--bs-emphasis-color);--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color: var(--bs-emphasis-color);--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*,table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody,table>tbody{vertical-align:inherit}.table>thead,table>thead{vertical-align:bottom}[data-bs-theme="dark"] table{--bs-table-color: #fff;--bs-table-bg: #212529;--bs-table-border-color: #4d5154;--bs-table-striped-bg: #2c3034;--bs-table-striped-color: #fff;--bs-table-active-bg: #373b3e;--bs-table-active-color: #fff;--bs-table-hover-bg: #323539;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type="file"]{overflow:hidden}.form-control[type="file"]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#a7a3f2;outline:0;box-shadow:none}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}li input[type="checkbox"]{--bs-form-check-bg: var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-color-adjust:exact}li input[type="checkbox"]{border-radius:.25em}li input[type="radio"][type="checkbox"]{border-radius:50%}li input[type="checkbox"]:active{filter:brightness(90%)}li input[type="checkbox"]:focus{border-color:#a7a3f2;outline:0;box-shadow:0 0 0 .25rem rgba(79,70,229,0.25)}li input[type="checkbox"]:checked{background-color:#4f46e5;border-color:#4f46e5}li input:checked[type="checkbox"]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}li input[type="checkbox"]:checked[type="radio"]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}li input[type="checkbox"]:indeterminate{background-color:#4f46e5;border-color:#4f46e5;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}li input[type="checkbox"]:disabled{pointer-events:none;filter:none;opacity:.5}.btn{--bs-btn-padding-x: .75rem;--bs-btn-padding-y: .375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: var(--bs-body-color);--bs-btn-bg: transparent;--bs-btn-border-width: var(--bs-border-width);--bs-btn-border-color: transparent;--bs-btn-border-radius: var(--bs-border-radius);--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);--bs-btn-disabled-opacity: .65;--bs-btn-focus-box-shadow: 0 0 0 0 rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);text-decoration:none;background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #4f46e5;--bs-btn-border-color: #4f46e5;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #433cc3;--bs-btn-hover-border-color: #3f38b7;--bs-btn-focus-shadow-rgb: 105,98,233;--bs-btn-active-color: #fff;--bs-btn-active-bg: #3f38b7;--bs-btn-active-border-color: #3b35ac;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #4f46e5;--bs-btn-disabled-border-color: #4f46e5}.btn-outline-primary{--bs-btn-color: #4f46e5;--bs-btn-border-color: #4f46e5;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #4f46e5;--bs-btn-hover-border-color: #4f46e5;--bs-btn-focus-shadow-rgb: 79,70,229;--bs-btn-active-color: #fff;--bs-btn-active-bg: #4f46e5;--bs-btn-active-border-color: #4f46e5;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #4f46e5;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #4f46e5;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: var(--bs-link-color);--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 105,98,233;text-decoration:none}.btn-link:hover,.btn-link:focus-visible{text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg{--bs-btn-padding-y: .5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius: var(--bs-border-radius-lg)}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-link-color);--bs-nav-link-hover-color: var(--bs-link-hover-color);--bs-nav-link-disabled-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);background:none;border:0;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color);text-decoration:none}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(79,70,229,0.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: var(--bs-border-width);--bs-nav-tabs-border-color: var(--bs-border-color);--bs-nav-tabs-border-radius: var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color: var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg: var(--bs-body-bg);--bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: .5rem;--bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y: .3125rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.25rem;--bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x: .5rem;--bs-navbar-toggler-padding-y: .25rem;--bs-navbar-toggler-padding-x: .75rem;--bs-navbar-toggler-font-size: 1.25rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2829,45,53,0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius: var(--bs-border-radius);--bs-navbar-toggler-focus-width: 0;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color);text-decoration:none}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}@media (min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:transparent !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar[data-bs-theme="dark"]{--bs-navbar-color: #c1c3c8;--bs-navbar-hover-color: #b3c7ff;--bs-navbar-disabled-color: rgba(255,255,255,0.25);--bs-navbar-active-color: #b3c7ff;--bs-navbar-brand-color: #b3c7ff;--bs-navbar-brand-hover-color: #b3c7ff;--bs-navbar-toggler-border-color: rgba(255,255,255,0.1);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23c1c3c8' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: .5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: var(--bs-border-width);--bs-card-border-color: #e9ecef;--bs-card-border-radius: var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y: .5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: var(--bs-body-bg);--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 1.5rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);text-decoration:none;background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: .5;--bs-btn-close-hover-opacity: .75;--bs-btn-close-focus-shadow: 0 0 0 .25rem rgba(79,70,229,0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: .25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}[data-bs-theme="dark"] .btn-close{filter:var(--bs-btn-close-white-filter)}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: .5rem;--bs-modal-color: ;--bs-modal-bg: var(--bs-body-bg);--bs-modal-border-color: var(--bs-border-color-translucent);--bs-modal-border-width: var(--bs-border-width);--bs-modal-border-radius: var(--bs-border-radius-lg);--bs-modal-box-shadow: var(--bs-box-shadow-sm);--bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: var(--bs-border-color);--bs-modal-header-border-width: var(--bs-border-width);--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: .5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: var(--bs-border-color);--bs-modal-footer-border-width: var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform 0.3s ease-out;transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: .5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}}@media (max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header,.modal-fullscreen-md-down .modal-footer{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.offcanvas{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 332px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: var(--bs-body-color);--bs-offcanvas-bg: var(--bs-body-bg);--bs-offcanvas-border-width: var(--bs-border-width);--bs-offcanvas-border-color: var(--bs-border-color-translucent);--bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);--bs-offcanvas-transition: transform .3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}@keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.d-flex{display:flex !important}.d-none{display:none !important}.w-100{width:100% !important}.h-auto{height:auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-grow-1{flex-grow:1 !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.order-3{order:3 !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.me-2{margin-right:.5rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-auto{margin-left:auto !important}.mt-n3{margin-top:-1rem !important}.p-0{padding:0 !important}.p-2{padding:.5rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.pt-4{padding-top:1.5rem !important}.pe-4{padding-right:1.5rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.ps-3{padding-left:1rem !important}.fs-5{font-size:1.25rem !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}@media (min-width: 576px){.flex-sm-row{flex-direction:row !important}}@media (min-width: 768px){.d-md-block{display:block !important}.d-md-none{display:none !important}.flex-md-row{flex-direction:row !important}}@media (min-width: 992px){.d-lg-block{display:block !important}.d-lg-none{display:none !important}.flex-lg-row{flex-direction:row !important}.order-lg-4{order:4 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-3{margin-right:1rem !important}.ms-lg-2{margin-left:.5rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}}@media (min-width: 1200px){.d-xl-block{display:block !important}.d-xl-none{display:none !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}}@font-face{font-family:Jost;font-style:normal;font-weight:400;font-display:swap;src:local("Jost Regular Regular"),local("Jost-Regular"),local("Jost* Book"),local("Jost-Book"),url("fonts/vendor/jost/jost-v4-latin-regular.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-regular.woff") format("woff")}@font-face{font-family:Jost;font-style:normal;font-weight:500;font-display:swap;src:local("Jost Regular Medium"),local("JostRoman-Medium"),local("Jost* Medium"),local("Jost-Medium"),url("fonts/vendor/jost/jost-v4-latin-500.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-500.woff") format("woff")}@font-face{font-family:Jost;font-style:normal;font-weight:700;font-display:swap;src:local("Jost Regular Bold"),local("JostRoman-Bold"),local("Jost* Bold"),local("Jost-Bold"),url("fonts/vendor/jost/jost-v4-latin-700.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-700.woff") format("woff")}@font-face{font-family:Jost;font-style:italic;font-weight:400;font-display:swap;src:local("Jost Italic Italic"),local("Jost-Italic"),local("Jost* BookItalic"),local("Jost-BookItalic"),url("fonts/vendor/jost/jost-v4-latin-italic.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-italic.woff") format("woff")}@font-face{font-family:Jost;font-style:italic;font-weight:500;font-display:swap;src:local("Jost Italic Medium Italic"),local("JostItalic-Medium"),local("Jost* Medium Italic"),local("Jost-MediumItalic"),url("fonts/vendor/jost/jost-v4-latin-500italic.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-500italic.woff") format("woff")}@font-face{font-family:Jost;font-style:italic;font-weight:700;font-display:swap;src:local("Jost Italic Bold Italic"),local("JostItalic-Bold"),local("Jost* Bold Italic"),local("Jost-BoldItalic"),url("fonts/vendor/jost/jost-v4-latin-700italic.woff2") format("woff2"),url("fonts/vendor/jost/jost-v4-latin-700italic.woff") format("woff")}html[data-bs-theme="dark"] .icon-tabler-sun{display:block}html[data-bs-theme="dark"] .icon-tabler-moon{display:none}html[data-bs-theme="light"] .icon-tabler-sun{display:none}html[data-bs-theme="light"] .icon-tabler-moon{display:block}.contributors .content,.error404 .content,.docs.list .content,.categories.list .content,.tags.list .content,.list.section .content{padding-top:1rem;padding-bottom:3rem}.content img{max-width:100%}h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:2rem;margin-bottom:1rem}@media (min-width: 768px){body{font-size:1.125rem}h1,h2,h3,h4,h5,.h1,.h2,.h3,.h4,.h5{margin-bottom:1.125rem}}.home h1,.home .h1{font-size:calc(1.875rem + 1.5vw);margin-top:-1rem}a:hover,a:focus{text-decoration:underline}a.btn:hover,a.btn:focus{text-decoration:none}.section{padding-top:5rem;padding-bottom:5rem}body.section{padding-top:0;padding-bottom:0}.section-md{padding-top:3rem;padding-bottom:3rem}.docs-sidebar{order:2}@media (min-width: 992px){.docs-sidebar{order:0;border-right:1px solid #e9ecef}@supports (position: sticky){.docs-sidebar{position:sticky;top:4.25rem;z-index:1000;height:calc(100vh - 4.25rem)}}}@media (min-width: 1200px){.docs-sidebar{flex:0 1 320px}}.docs-links{padding-bottom:5rem}@media (min-width: 992px){@supports (position: sticky){.docs-links{max-height:calc(100vh - 4rem);overflow-y:scroll}}}@media (min-width: 992px){.docs-links{display:block;width:auto;margin-right:-1.5rem;padding-bottom:4rem}}.docs-toc{order:2}@supports (position: sticky){.docs-toc{position:sticky;top:4.25rem;height:calc(100vh - 4.25rem);overflow-y:auto}}.docs-content{padding-bottom:3rem;order:1}.navbar a:hover,.navbar a:focus{text-decoration:none}#TableOfContents ul,#toc ul{padding-left:0;list-style:none}#toc a.active{color:#4f46e5;font-weight:500}.section-features{padding-top:2rem}.modal-backdrop{background-color:#fff}.modal-backdrop.show{opacity:0.7}@media (min-width: 768px){.modal-backdrop.show{opacity:0}}li input[type="checkbox"]{margin:0.25rem;border:1px solid #ced4da}li input[type="checkbox"]:disabled{pointer-events:none;filter:none;opacity:1}li input[type="checkbox"]:checked{background-color:#5d2f86;border-color:#5d2f86}[data-bs-theme="dark"] li input[type="checkbox"]{border:1px solid #6c757d}[data-bs-theme="dark"] li input[type="checkbox"]:checked{background-color:#b3c7ff;border-color:#b3c7ff;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%231d2d35' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.highlight>.chroma{border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}.bg{background-color:var(--sl-color-gray-7)}.chroma{background-color:var(--sl-color-gray-7)}.chroma .err{color:inherit}.chroma .lnlinks{outline:none;text-decoration:none;color:inherit}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0}.chroma .hl{background-color:#0000001a}.chroma .hl{border-inline-start:0.15rem solid #00000055;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}.chroma .hl .ln{margin-left:-0.15rem}.chroma .lnt{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .ln{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .line{display:flex}.chroma .k{color:#000000;font-weight:bold}.chroma .kc{color:#000000;font-weight:bold}.chroma .kd{color:#000000;font-weight:bold}.chroma .kn{color:#000000;font-weight:bold}.chroma .kp{color:#000000;font-weight:bold}.chroma .kr{color:#000000;font-weight:bold}.chroma .kt{color:#445588;font-weight:bold}.chroma .na{color:#008080}.chroma .nb{color:#0086b3}.chroma .bp{color:#999999}.chroma .nc{color:#445588;font-weight:bold}.chroma .no{color:#008080}.chroma .nd{color:#3c5d5d;font-weight:bold}.chroma .ni{color:#800080}.chroma .ne{color:#990000;font-weight:bold}.chroma .nf{color:#990000;font-weight:bold}.chroma .nl{color:#990000;font-weight:bold}.chroma .nn{color:#555555}.chroma .nt{color:#000080}.chroma .nv{color:#008080}.chroma .vc{color:#008080}.chroma .vg{color:#008080}.chroma .vi{color:#008080}.chroma .s{color:#dd1144}.chroma .sa{color:#dd1144}.chroma .sb{color:#dd1144}.chroma .sc{color:#dd1144}.chroma .dl{color:#dd1144}.chroma .sd{color:#dd1144}.chroma .s2{color:#dd1144}.chroma .se{color:#dd1144}.chroma .sh{color:#dd1144}.chroma .si{color:#dd1144}.chroma .sx{color:#dd1144}.chroma .sr{color:#009926}.chroma .s1{color:#dd1144}.chroma .ss{color:#990073}.chroma .m{color:#009999}.chroma .mb{color:#009999}.chroma .mf{color:#009999}.chroma .mh{color:#009999}.chroma .mi{color:#009999}.chroma .il{color:#009999}.chroma .mo{color:#009999}.chroma .o{color:#000000;font-weight:bold}.chroma .ow{color:#000000;font-weight:bold}.chroma .c{color:#999988;font-style:italic}.chroma .ch{color:#999988;font-style:italic}.chroma .cm{color:#999988;font-style:italic}.chroma .c1{color:#999988;font-style:italic}.chroma .cs{color:#999999;font-weight:bold;font-style:italic}.chroma .cp{color:#999999;font-weight:bold;font-style:italic}.chroma .cpf{color:#999999;font-weight:bold;font-style:italic}.chroma .gd{color:#000000;background-color:#ffdddd}.chroma .ge{color:inherit;font-style:italic}.chroma .gr{color:#aa0000}.chroma .gh{color:#999999}.chroma .gi{color:#000000;background-color:#ddffdd}.chroma .go{color:#888888}.chroma .gp{color:#555555}.chroma .gs{font-weight:bold}.chroma .gu{color:#aaaaaa}.chroma .gt{color:#aa0000}.chroma .gl{text-decoration:underline}.chroma .w{color:#bbbbbb}[data-bs-theme="dark"] .highlight>.chroma{border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}[data-bs-theme="dark"] .bg{color:#c9d1d9;background-color:var(--sl-color-gray-6)}[data-bs-theme="dark"] .chroma{color:#c9d1d9;background-color:var(--sl-color-gray-6)}[data-bs-theme="dark"] .chroma .err{color:inherit}[data-bs-theme="dark"] .chroma .lnlinks{outline:none;text-decoration:none;color:inherit}[data-bs-theme="dark"] .chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}[data-bs-theme="dark"] .chroma .lntable{border-spacing:0;padding:0;margin:0;border:0}[data-bs-theme="dark"] .chroma .hl{background-color:#ffffff17}[data-bs-theme="dark"] .chroma .hl{border-inline-start:0.15rem solid #ffffff40;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}[data-bs-theme="dark"] .chroma .hl .ln{margin-left:-0.15rem}[data-bs-theme="dark"] .chroma .lnt{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#64686c}[data-bs-theme="dark"] .chroma .ln{white-space:pre;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681}[data-bs-theme="dark"] .chroma .line{display:flex}[data-bs-theme="dark"] .chroma .k{color:#ff7b72}[data-bs-theme="dark"] .chroma .kc{color:#79c0ff}[data-bs-theme="dark"] .chroma .kd{color:#ff7b72}[data-bs-theme="dark"] .chroma .kn{color:#ff7b72}[data-bs-theme="dark"] .chroma .kp{color:#79c0ff}[data-bs-theme="dark"] .chroma .kr{color:#ff7b72}[data-bs-theme="dark"] .chroma .kt{color:#ff7b72}[data-bs-theme="dark"] .chroma .na{color:#d2a8ff}[data-bs-theme="dark"] .chroma .nc{color:#f0883e;font-weight:bold}[data-bs-theme="dark"] .chroma .no{color:#79c0ff;font-weight:bold}[data-bs-theme="dark"] .chroma .nd{color:#d2a8ff;font-weight:bold}[data-bs-theme="dark"] .chroma .ni{color:#ffa657}[data-bs-theme="dark"] .chroma .ne{color:#f0883e;font-weight:bold}[data-bs-theme="dark"] .chroma .nf{color:#d2a8ff;font-weight:bold}[data-bs-theme="dark"] .chroma .nl{color:#79c0ff;font-weight:bold}[data-bs-theme="dark"] .chroma .nn{color:#ff7b72}[data-bs-theme="dark"] .chroma .py{color:#79c0ff}[data-bs-theme="dark"] .chroma .nt{color:#7ee787}[data-bs-theme="dark"] .chroma .nv{color:#79c0ff}[data-bs-theme="dark"] .chroma .l{color:#a5d6ff}[data-bs-theme="dark"] .chroma .ld{color:#79c0ff}[data-bs-theme="dark"] .chroma .s{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sa{color:#79c0ff}[data-bs-theme="dark"] .chroma .sb{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sc{color:#a5d6ff}[data-bs-theme="dark"] .chroma .dl{color:#79c0ff}[data-bs-theme="dark"] .chroma .sd{color:#a5d6ff}[data-bs-theme="dark"] .chroma .s2{color:#a5d6ff}[data-bs-theme="dark"] .chroma .se{color:#79c0ff}[data-bs-theme="dark"] .chroma .sh{color:#79c0ff}[data-bs-theme="dark"] .chroma .si{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sx{color:#a5d6ff}[data-bs-theme="dark"] .chroma .sr{color:#79c0ff}[data-bs-theme="dark"] .chroma .s1{color:#a5d6ff}[data-bs-theme="dark"] .chroma .ss{color:#a5d6ff}[data-bs-theme="dark"] .chroma .m{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mb{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mf{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mh{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mi{color:#a5d6ff}[data-bs-theme="dark"] .chroma .il{color:#a5d6ff}[data-bs-theme="dark"] .chroma .mo{color:#a5d6ff}[data-bs-theme="dark"] .chroma .o{color:inherit;font-weight:bold}[data-bs-theme="dark"] .chroma .ow{color:#ff7b72;font-weight:bold}[data-bs-theme="dark"] .chroma .c{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .ch{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .cm{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .c1{color:#8b949e;font-style:italic}[data-bs-theme="dark"] .chroma .cs{color:#8b949e;font-weight:bold;font-style:italic}[data-bs-theme="dark"] .chroma .cp{color:#8b949e;font-weight:bold;font-style:italic}[data-bs-theme="dark"] .chroma .cpf{color:#8b949e;font-weight:bold;font-style:italic}[data-bs-theme="dark"] .chroma .gd{color:#ffa198;background-color:#490202}[data-bs-theme="dark"] .chroma .ge{font-style:italic}[data-bs-theme="dark"] .chroma .gr{color:#ffa198}[data-bs-theme="dark"] .chroma .gh{color:#79c0ff;font-weight:bold}[data-bs-theme="dark"] .chroma .gi{color:#56d364;background-color:#0f5323}[data-bs-theme="dark"] .chroma .go{color:#8b949e}[data-bs-theme="dark"] .chroma .gp{color:#8b949e}[data-bs-theme="dark"] .chroma .gs{font-weight:bold}[data-bs-theme="dark"] .chroma .gu{color:#79c0ff}[data-bs-theme="dark"] .chroma .gt{color:#ff7b72}[data-bs-theme="dark"] .chroma .gl{text-decoration:underline}[data-bs-theme="dark"] .chroma .w{color:#6e7681}[data-bs-theme="dark"] h1,[data-bs-theme="dark"] .h1,[data-bs-theme="dark"] h2,[data-bs-theme="dark"] .h2,[data-bs-theme="dark"] h3,[data-bs-theme="dark"] .h3,[data-bs-theme="dark"] h4,[data-bs-theme="dark"] .h4{color:#fff}[data-bs-theme="dark"] body{background:#17181c;color:#c1c3c8}[data-bs-theme="dark"] a{color:#b3c7ff}[data-bs-theme="dark"] .callout a{color:inherit}[data-bs-theme="dark"] .btn-primary{--bs-btn-color: #000;--bs-btn-bg: #b3c7ff;--bs-btn-border-color: #b3c7ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #becfff;--bs-btn-hover-border-color: #bacdff;--bs-btn-focus-shadow-rgb: 152,169,217;--bs-btn-active-color: #000;--bs-btn-active-bg: #c2d2ff;--bs-btn-active-border-color: #bacdff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #b3c7ff;--bs-btn-disabled-border-color: #b3c7ff;color:#17181c}[data-bs-theme="dark"] .btn-outline-primary{--bs-btn-color: #b3c7ff;--bs-btn-border-color: #b3c7ff;--bs-btn-hover-color: #b3c7ff;--bs-btn-hover-bg: #b3c7ff;--bs-btn-hover-border-color: #b3c7ff;--bs-btn-focus-shadow-rgb: 178.5,198.9,255;--bs-btn-active-color: #000;--bs-btn-active-bg: #b3c7ff;--bs-btn-active-border-color: #b3c7ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color: #b3c7ff;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #b3c7ff;--bs-gradient: none;color:#b3c7ff}[data-bs-theme="dark"] .btn-outline-primary:hover{color:#17181c}[data-bs-theme="dark"] .navbar{background-color:rgba(23,24,28,0.95);border-bottom:1px solid #23262f}[data-bs-theme="dark"] body.home .navbar{border-bottom:0}[data-bs-theme="dark"] .offcanvas-header{border-bottom:1px solid #343a40}[data-bs-theme="dark"] .offcanvas .nav-link{color:#c1c3c8}[data-bs-theme="dark"] .offcanvas .nav-link:hover,[data-bs-theme="dark"] .offcanvas .nav-link:focus{color:#b3c7ff}[data-bs-theme="dark"] .offcanvas .nav-link.active{color:#b3c7ff}[data-bs-theme="dark"] .page-links a{color:#c1c3c8}[data-bs-theme="dark"] .page-links a:hover{text-decoration:none;color:#b3c7ff}[data-bs-theme="dark"] .navbar .btn-link{color:#c1c3c8}[data-bs-theme="dark"] .content .btn-link{color:#b3c7ff}[data-bs-theme="dark"] .content .btn-link:hover{color:#b3c7ff}[data-bs-theme="dark"] .navbar .btn-link:hover{color:#b3c7ff}[data-bs-theme="dark"] .navbar .btn-link:active{color:#b3c7ff}[data-bs-theme="dark"] .form-control{color:#dee2e6}[data-bs-theme="dark"] .form-control::-moz-placeholder{color:#ced4da;opacity:1}[data-bs-theme="dark"] .form-control::placeholder{color:#ced4da;opacity:1}@media (min-width: 992px){[data-bs-theme="dark"] .docs-sidebar{order:0;border-right:1px solid #23262f}}[data-bs-theme="dark"] blockquote{border-left:3px solid #23262f}[data-bs-theme="dark"] .footer{border-top:1px solid #23262f}[data-bs-theme="dark"] .docs-links,[data-bs-theme="dark"] .docs-toc{scrollbar-width:thin;scrollbar-color:#17181c #17181c}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar{width:5px}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar-track,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar-track{background:#17181c}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar-thumb,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar-thumb{background:#17181c}[data-bs-theme="dark"] .docs-links:hover,[data-bs-theme="dark"] .docs-toc:hover{scrollbar-width:thin;scrollbar-color:#23262f #17181c}[data-bs-theme="dark"] .docs-links:hover::-webkit-scrollbar-thumb,[data-bs-theme="dark"] .docs-toc:hover::-webkit-scrollbar-thumb{background:#23262f}[data-bs-theme="dark"] .docs-links::-webkit-scrollbar-thumb:hover,[data-bs-theme="dark"] .docs-toc::-webkit-scrollbar-thumb:hover{background:#23262f}[data-bs-theme="dark"] .docs-links h3:not(:first-child),[data-bs-theme="dark"] .docs-links .h3:not(:first-child){border-top:1px solid #23262f}[data-bs-theme="dark"] .page-links li:not(:first-child){border-top:1px dashed #23262f}[data-bs-theme="dark"] .card{background:#17181c;border:1px solid #23262f}[data-bs-theme="dark"] .text-muted{color:#adafb6 !important}[data-bs-theme="dark"] .offcanvas{background-color:#17181c}[data-bs-theme="dark"] .page-link{color:#b3c7ff;background-color:transparent;border:var(--bs-border-width) solid #23262f}[data-bs-theme="dark"] .page-link:hover{color:#17181c;background-color:#c1c3c8;border-color:#c1c3c8}[data-bs-theme="dark"] .page-link:focus{color:#17181c;background-color:#c1c3c8}[data-bs-theme="dark"] .page-item.active .page-link{color:#17181c;background-color:#b3c7ff;border-color:#b3c7ff}[data-bs-theme="dark"] .page-item.disabled .page-link{color:var(--bs-secondary-color);background-color:#23262f;border-color:#23262f}[data-bs-theme="dark"] details{border:1px solid #23262f}[data-bs-theme="dark"] summary:hover{background:#23262f}[data-bs-theme="dark"] details[open]>summary{border-bottom:1px solid #23262f}[data-bs-theme="dark"] details summary::after{content:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28222, 226, 230, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e")}[data-bs-theme="dark"] #toc a.active{color:#b3c7ff}[data-bs-theme="dark"] table th{color:#fff}[data-bs-theme="dark"] table,[data-bs-theme="dark"] [data-bs-theme="dark"] table{--bs-table-color: inherit;--bs-table-bg: $body-bg-dark;background:#17181c;border-color:#23262f}.btn-close:focus,.btn-close:active{outline:none;box-shadow:none}.navbar .btn-link{color:rgba(var(--bs-emphasis-color-rgb), 0.65);padding:0.4375rem 0}.btn-link:focus{outline:0;box-shadow:none}@media (min-width: 992px){.navbar .btn-link{padding:0.5625em 0.25rem 0.5rem 0.125rem}}.navbar .btn-link:hover{color:rgba(var(--bs-emphasis-color-rgb), 0.8)}.navbar .btn-link:active{color:rgba(var(--bs-emphasis-color-rgb), 1)}.btn-close{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-x'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");background-size:1.5rem}.offcanvas-header .btn-close{margin-right:0 !important}.clipboard{position:relative;float:right}.btn-clipboard{transition:opacity 0.25s ease-in-out;opacity:0;position:absolute;right:0.5rem;top:0.5rem;line-height:1;padding:0.3125rem 0.3125rem 0.1875rem;background-color:transparent;border-color:transparent}@media (max-width: 767.98px){.btn-clipboard{position:absolute;right:-0.5rem;top:0.5rem}}.btn-clipboard::after{width:22px;height:22px;display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;background-color:#495057}.btn-clipboard:hover{border-color:transparent}.btn-clipboard:hover::after{width:22px;height:22px;display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-copy' width='22' height='22' viewBox='0 0 24 24' stroke-width='1' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z'%3E%3C/path%3E%3Cpath d='M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;background-color:#212529}.btn-clipboard:focus,.btn-clipboard:active{border-color:transparent !important}.btn-clipboard:focus::after,.btn-clipboard:active::after{width:22px;height:22px;display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;background-color:#212529}[data-bs-theme="dark"] .btn-clipboard{background-color:transparent;border-color:transparent}[data-bs-theme="dark"] .btn-clipboard::after{background-color:#ced4da}[data-bs-theme="dark"] .btn-clipboard:hover{border-color:transparent}[data-bs-theme="dark"] .btn-clipboard:hover::after{background-color:#e9ecef}[data-bs-theme="dark"] .btn-clipboard:focus,[data-bs-theme="dark"] .btn-clipboard:active{border-color:transparent}[data-bs-theme="dark"] .btn-clipboard:focus::after,[data-bs-theme="dark"] .btn-clipboard:active::after{background-color:#e9ecef}.highlight{position:relative}@media (min-width: 768px){.highlight:hover .btn-clipboard{opacity:1}}.btn-cta{padding-left:2rem;padding-right:2rem}.callout{--bs-link-color-rgb: var(--callout-link);--bs-code-color: var(--callout-code-color);color:var(--callout-color, inherit);background-color:var(--callout-bg, var(--bs-gray-100));border-left:0.25rem solid var(--callout-border, var(--bs-gray-300));border-radius:0}.callout a{text-decoration:underline}.callout .highlight{background-color:rgba(0,0,0,0.05)}.callout-content{min-width:0}.callout.callout-danger{border-color:var(--sl-color-red);background-color:var(--sl-color-red-high)}.callout.callout-danger .callout-body a{color:var(--sl-color-red-low)}.callout.callout-danger .callout-body,.callout.callout-danger .callout-body a:hover,.callout.callout-danger .callout-body a:active{color:var(--sl-color-white)}[data-bs-theme="dark"] .callout{color:var(--sl-color-gray-1)}[data-bs-theme="dark"] .callout.callout-danger{border-color:var(--sl-color-red);background-color:var(--sl-color-red-low)}[data-bs-theme="dark"] .callout.callout-danger .callout-body a{color:var(--sl-color-red-high)}[data-bs-theme="dark"] .callout.callout-danger .callout-body,[data-bs-theme="dark"] .callout.callout-danger .callout-body a:hover,[data-bs-theme="dark"] .callout.callout-danger .callout-body a:active{color:var(--sl-color-white)}[data-bs-theme="dark"] .callout.callout-danger code:not(:where(.not-content *)){color:var(--ec-codeFg)}.expressive-code{font-family:var(--ec-uiFontFml);font-size:var(--ec-uiFontSize);line-height:var(--ec-uiLineHt);-moz-text-size-adjust:none;text-size-adjust:none;-webkit-text-size-adjust:none;margin:1.5rem 0}.expressive-code *:not(path){all:revert;box-sizing:border-box}.expressive-code pre{display:flex;margin:0;padding:0;border:var(--ec-brdWd) solid var(--ec-brdCol);border-radius:calc(var(--ec-brdRad) + var(--ec-brdWd));background:var(--ec-codeBg)}.expressive-code pre:focus-visible{outline:3px solid var(--ec-focusBrd);outline-offset:-3px}.expressive-code pre>code{all:unset;display:block;flex:1 0 100%;padding:var(--ec-codePadBlk) 0;color:var(--ec-codeFg);font-family:var(--ec-codeFontFml);font-size:var(--ec-codeFontSize);line-height:var(--ec-codeLineHt)}.expressive-code pre{overflow-x:auto}.expressive-code pre::-webkit-scrollbar,.expressive-code pre::-webkit-scrollbar-track{background-color:inherit;border-radius:calc(var(--ec-brdRad) + var(--ec-brdWd));border-top-left-radius:0;border-top-right-radius:0}.expressive-code pre::-webkit-scrollbar-thumb{background-color:var(--ec-sbThumbCol);border:4px solid transparent;background-clip:content-box;border-radius:10px}.expressive-code pre::-webkit-scrollbar-thumb:hover{background-color:var(--ec-sbThumbHoverCol)}.expressive-code .ec-line{padding-inline:var(--ec-codePadInl);padding-inline-end:calc(2rem + var(--ec-codePadInl));direction:ltr;unicode-bidi:isolate}.expressive-code .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.expressive-code .ec-line.mark{--tmLineBgCol: var(--ec-tm-markBg);--tmLineBrdCol: var(--ec-tm-markBrdCol)}.expressive-code .ec-line.ins{--tmLineBgCol: var(--ec-tm-insBg);--tmLineBrdCol: var(--ec-tm-insBrdCol)}.expressive-code .ec-line.ins::before{content:var(--ec-tm-insDiffIndContent);color:var(--ec-tm-insDiffIndCol)}.expressive-code .ec-line.del{--tmLineBgCol: var(--ec-tm-delBg);--tmLineBrdCol: var(--ec-tm-delBrdCol)}.expressive-code .ec-line.del::before{content:var(--ec-tm-delDiffIndContent);color:var(--ec-tm-delDiffIndCol)}.expressive-code .ec-line.mark,.expressive-code .ec-line.ins,.expressive-code .ec-line.del{position:relative;background:var(--tmLineBgCol);min-width:calc(100% - var(--ec-tm-lineMarkerAccentMarg));margin-inline-start:var(--ec-tm-lineMarkerAccentMarg);border-inline-start:var(--ec-tm-lineMarkerAccentWd) solid var(--tmLineBrdCol);padding-inline-start:calc(var(--ec-codePadInl) - var(--ec-tm-lineMarkerAccentMarg) - var(--ec-tm-lineMarkerAccentWd)) !important}.expressive-code .ec-line.mark::before,.expressive-code .ec-line.ins::before,.expressive-code .ec-line.del::before{position:absolute;left:var(--ec-tm-lineDiffIndMargLeft)}.expressive-code .ec-line mark,.expressive-code .ec-line .mark{--tmInlineBgCol: var(--ec-tm-markBg);--tmInlineBrdCol: var(--ec-tm-markBrdCol)}.expressive-code .ec-line ins{--tmInlineBgCol: var(--ec-tm-insBg);--tmInlineBrdCol: var(--ec-tm-insBrdCol)}.expressive-code .ec-line del{--tmInlineBgCol: var(--ec-tm-delBg);--tmInlineBrdCol: var(--ec-tm-delBrdCol)}.expressive-code .ec-line mark,.expressive-code .ec-line .mark,.expressive-code .ec-line ins,.expressive-code .ec-line del{all:unset;display:inline-block;position:relative;--tmBrdL: var(--ec-tm-inlMarkerBrdWd);--tmBrdR: var(--ec-tm-inlMarkerBrdWd);--tmRadL: var(--ec-tm-inlMarkerBrdRad);--tmRadR: var(--ec-tm-inlMarkerBrdRad);margin-inline:0.025rem;padding-inline:var(--ec-tm-inlMarkerPad);border-radius:var(--tmRadL) var(--tmRadR) var(--tmRadR) var(--tmRadL);background:var(--tmInlineBgCol);background-clip:padding-box}.expressive-code .ec-line mark.open-start,.expressive-code .ec-line .open-start.mark,.expressive-code .ec-line ins.open-start,.expressive-code .ec-line del.open-start{margin-inline-start:0;padding-inline-start:0;--tmBrdL: 0px;--tmRadL: 0}.expressive-code .ec-line mark.open-end,.expressive-code .ec-line .open-end.mark,.expressive-code .ec-line ins.open-end,.expressive-code .ec-line del.open-end{margin-inline-end:0;padding-inline-end:0;--tmBrdR: 0px;--tmRadR: 0}.expressive-code .ec-line mark::before,.expressive-code .ec-line .mark::before,.expressive-code .ec-line ins::before,.expressive-code .ec-line del::before{content:"";position:absolute;pointer-events:none;display:inline-block;inset:0;border-radius:var(--tmRadL) var(--tmRadR) var(--tmRadR) var(--tmRadL);border:var(--ec-tm-inlMarkerBrdWd) solid var(--tmInlineBrdCol);border-inline-width:var(--tmBrdL) var(--tmBrdR)}.expressive-code .frame{all:unset;position:relative;display:block;--header-border-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));--tab-border-radius: calc(var(--ec-frm-edTabBrdRad) + var(--ec-brdWd));--button-spacing: 0.4rem;--code-background: var(--ec-frm-edBg);border-radius:var(--header-border-radius);box-shadow:var(--ec-frm-frameBoxShdCssVal)}.expressive-code .frame .header{display:none;z-index:1;position:relative;border-radius:var(--header-border-radius) var(--header-border-radius) 0 0}.expressive-code .frame.has-title pre,.expressive-code .frame.has-title code,.expressive-code .frame.is-terminal pre,.expressive-code .frame.is-terminal code{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.expressive-code .frame .title:empty:before{content:"\a0"}.expressive-code .frame.has-title:not(.is-terminal){--button-spacing: calc(1.9rem + 2 * (var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt)))}.expressive-code .frame.has-title:not(.is-terminal) .title{position:relative;color:var(--ec-frm-edActTabFg);background:var(--ec-frm-edActTabBg);background-clip:padding-box;margin-block-start:var(--ec-frm-edTabsMargBlkStart);padding:calc(var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt)) var(--ec-uiPadInl);border:var(--ec-brdWd) solid var(--ec-frm-edActTabBrdCol);border-radius:var(--tab-border-radius) var(--tab-border-radius) 0 0;border-bottom:none;overflow:hidden}.expressive-code .frame.has-title:not(.is-terminal) .title::after{content:"";position:absolute;pointer-events:none;inset:0;border-top:var(--ec-frm-edActTabIndHt) solid var(--ec-frm-edActTabIndTopCol);border-bottom:var(--ec-frm-edActTabIndHt) solid var(--ec-frm-edActTabIndBtmCol)}.expressive-code .frame.has-title:not(.is-terminal) .header{display:flex;background:linear-gradient(to top, var(--ec-frm-edTabBarBrdBtmCol) var(--ec-brdWd), transparent var(--ec-brdWd)),linear-gradient(var(--ec-frm-edTabBarBg), var(--ec-frm-edTabBarBg));background-repeat:no-repeat;padding-inline-start:var(--ec-frm-edTabsMargInlStart)}.expressive-code .frame.has-title:not(.is-terminal) .header::before{content:"";position:absolute;pointer-events:none;inset:0;border:var(--ec-brdWd) solid var(--ec-frm-edTabBarBrdCol);border-radius:inherit;border-bottom:none}.expressive-code .frame.is-terminal{--button-spacing: calc(1.9rem + var(--ec-brdWd) + 2 * var(--ec-uiPadBlk));--code-background: var(--ec-frm-trmBg)}.expressive-code .frame.is-terminal .header{display:flex;align-items:center;justify-content:center;padding-block:var(--ec-uiPadBlk);padding-block-end:calc(var(--ec-uiPadBlk) + var(--ec-brdWd));position:relative;font-weight:500;letter-spacing:0.025ch;color:var(--ec-frm-trmTtbFg);background:var(--ec-frm-trmTtbBg);border:var(--ec-brdWd) solid var(--ec-brdCol);border-bottom:none}.expressive-code .frame.is-terminal .header::before{content:"";position:absolute;pointer-events:none;left:var(--ec-uiPadInl);width:2.1rem;height:0.56rem;line-height:0;background-color:var(--ec-frm-trmTtbDotsFg);opacity:var(--ec-frm-trmTtbDotsOpa);-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 16' preserveAspectRatio='xMidYMid meet'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3Ccircle cx='30' cy='8' r='8'/%3E%3Ccircle cx='52' cy='8' r='8'/%3E%3C/svg%3E");-webkit-mask-repeat:no-repeat;mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 16' preserveAspectRatio='xMidYMid meet'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3Ccircle cx='30' cy='8' r='8'/%3E%3Ccircle cx='52' cy='8' r='8'/%3E%3C/svg%3E");mask-repeat:no-repeat}.expressive-code .frame.is-terminal .header::after{content:"";position:absolute;pointer-events:none;inset:0;border-bottom:var(--ec-brdWd) solid var(--ec-frm-trmTtbBrdBtmCol)}.expressive-code .frame pre{background:var(--code-background)}.expressive-code .copy{display:flex;gap:0.25rem;flex-direction:row;position:absolute;inset-block-start:calc(var(--ec-brdWd) + var(--button-spacing));inset-inline-end:calc(var(--ec-brdWd) + var(--ec-uiPadInl) / 2);direction:ltr;unicode-bidi:isolate}.expressive-code .copy button{position:relative;align-self:flex-end;margin:0;padding:0;border:none;border-radius:0.2rem;z-index:1;cursor:pointer;transition-property:opacity, background, border-color;transition-duration:0.2s;transition-timing-function:cubic-bezier(0.25, 0.46, 0.45, 0.94);width:2.5rem;height:2.5rem;background:var(--code-background);opacity:0.75}.expressive-code .copy button div{position:absolute;inset:0;border-radius:inherit;background:var(--ec-frm-inlBtnBg);opacity:var(--ec-frm-inlBtnBgIdleOpa);transition-property:inherit;transition-duration:inherit;transition-timing-function:inherit}.expressive-code .copy button::before{content:"";position:absolute;pointer-events:none;inset:0;border-radius:inherit;border:var(--ec-brdWd) solid var(--ec-frm-inlBtnBrd);opacity:var(--ec-frm-inlBtnBrdOpa)}.expressive-code .copy button::after{content:"";position:absolute;pointer-events:none;inset:0;background-color:var(--ec-frm-inlBtnFg);-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75'%3E%3Cpath d='M3 19a2 2 0 0 1-1-2V2a2 2 0 0 1 1-1h13a2 2 0 0 1 2 1'/%3E%3Crect x='6' y='5' width='16' height='18' rx='1.5' ry='1.5'/%3E%3C/svg%3E");-webkit-mask-repeat:no-repeat;mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75'%3E%3Cpath d='M3 19a2 2 0 0 1-1-2V2a2 2 0 0 1 1-1h13a2 2 0 0 1 2 1'/%3E%3Crect x='6' y='5' width='16' height='18' rx='1.5' ry='1.5'/%3E%3C/svg%3E");mask-repeat:no-repeat;margin:0.475rem;line-height:0}.expressive-code .copy button:focus::after,.expressive-code .copy button:active::after{display:inline-block;content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='black' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' stroke-width='1.25' stroke='black' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'%3E%3C/path%3E%3Cpath d='M5 12l5 5l10 -10'%3E%3C/path%3E%3C/svg%3E") no-repeat 50% 50%;-webkit-mask-size:cover;mask-size:cover;margin:0.2375rem}.expressive-code .copy button:hover,.expressive-code .copy button:focus:focus-visible{opacity:1}.expressive-code .copy button:hover div,.expressive-code .copy button:focus:focus-visible div{opacity:var(--ec-frm-inlBtnBgHoverOrFocusOpa)}.expressive-code .copy button:active{opacity:1}.expressive-code .copy button:active div{opacity:var(--ec-frm-inlBtnBgActOpa)}.expressive-code .copy .feedback{--tooltip-arrow-size: 0.35rem;--tooltip-bg: var(--ec-frm-tooltipSuccessBg);color:var(--ec-frm-tooltipSuccessFg);pointer-events:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;position:relative;align-self:center;background-color:var(--tooltip-bg);z-index:99;padding:0.125rem 0.75rem;border-radius:0.2rem;margin-inline-end:var(--tooltip-arrow-size);opacity:0;transition-property:opacity, transform;transition-duration:0.2s;transition-timing-function:ease-in-out;transform:translate3d(0, 0.25rem, 0)}.expressive-code .copy .feedback::after{content:"";position:absolute;pointer-events:none;top:calc(50% - var(--tooltip-arrow-size));inset-inline-end:calc(-2 * (var(--tooltip-arrow-size) - 0.5px));border:var(--tooltip-arrow-size) solid transparent;border-inline-start-color:var(--tooltip-bg)}.expressive-code .copy .feedback.show{opacity:1;transform:translate3d(0, 0, 0)}@media (hover: hover){.expressive-code .copy button{opacity:0;width:2rem;height:2rem}.expressive-code .frame:hover .copy button:not(:hover),.expressive-code .frame:focus-within :focus-visible~.copy button:not(:hover),.expressive-code .frame .copy .feedback.show~button:not(:hover){opacity:0.75}}:root{--ec-brdRad: 0px;--ec-brdWd: 1px;--ec-brdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-codeFontFml: var(--__sl-font-mono);--ec-codeFontSize: var(--sl-text-code);--ec-codeFontWg: 400;--ec-codeLineHt: var(--sl-line-height);--ec-codePadBlk: 0;--ec-codePadInl: 1rem;--ec-codeBg: #011627;--ec-codeFg: #d6deeb;--ec-codeSelBg: #1d3b53;--ec-uiFontFml: var(--__sl-font);--ec-uiFontSize: 0.9rem;--ec-uiFontWg: 400;--ec-uiLineHt: 1.65;--ec-uiPadBlk: 0.25rem;--ec-uiPadInl: 1rem;--ec-uiSelBg: #234d708c;--ec-uiSelFg: #ffffff;--ec-focusBrd: #122d42;--ec-sbThumbCol: #ffffff17;--ec-sbThumbHoverCol: #ffffff49;--ec-tm-lineMarkerAccentMarg: 0rem;--ec-tm-lineMarkerAccentWd: 0.15rem;--ec-tm-lineDiffIndMargLeft: 0.25rem;--ec-tm-inlMarkerBrdWd: 1.5px;--ec-tm-inlMarkerBrdRad: 0.2rem;--ec-tm-inlMarkerPad: 0.15rem;--ec-tm-insDiffIndContent: "+";--ec-tm-delDiffIndContent: "-";--ec-tm-markBg: #ffffff17;--ec-tm-markBrdCol: #ffffff40;--ec-tm-insBg: #1e571599;--ec-tm-insBrdCol: #487f3bd0;--ec-tm-insDiffIndCol: #79b169d0;--ec-tm-delBg: #862d2799;--ec-tm-delBrdCol: #b4554bd0;--ec-tm-delDiffIndCol: #ed8779d0;--ec-frm-shdCol: #011627;--ec-frm-frameBoxShdCssVal: none;--ec-frm-edActTabBg: var(--sl-color-gray-6);--ec-frm-edActTabFg: var(--sl-color-text);--ec-frm-edActTabBrdCol: transparent;--ec-frm-edActTabIndHt: 1px;--ec-frm-edActTabIndTopCol: var(--sl-color-accent-high);--ec-frm-edActTabIndBtmCol: transparent;--ec-frm-edTabsMargInlStart: 0;--ec-frm-edTabsMargBlkStart: 0;--ec-frm-edTabBrdRad: 0px;--ec-frm-edTabBarBg: var(--sl-color-black);--ec-frm-edTabBarBrdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-edBg: var(--sl-color-gray-6);--ec-frm-trmTtbDotsFg: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmTtbDotsOpa: 0.75;--ec-frm-trmTtbBg: var(--sl-color-black);--ec-frm-trmTtbFg: var(--sl-color-text);--ec-frm-trmTtbBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);--ec-frm-trmBg: var(--sl-color-gray-6);--ec-frm-inlBtnFg: var(--sl-color-text);--ec-frm-inlBtnBg: var(--sl-color-text);--ec-frm-inlBtnBgIdleOpa: 0;--ec-frm-inlBtnBgHoverOrFocusOpa: 0.2;--ec-frm-inlBtnBgActOpa: 0.3;--ec-frm-inlBtnBrd: var(--sl-color-text);--ec-frm-inlBtnBrdOpa: 0.4;--ec-frm-tooltipSuccessBg: #158744;--ec-frm-tooltipSuccessFg: white}.expressive-code .ec-line span[style^="--"]:not([class]){color:var(0, inherit);font-style:var(0fs, inherit);font-weight:var(0fw, inherit);-webkit-text-decoration:var(0td, inherit);text-decoration:var(0td, inherit)}@media (prefers-color-scheme: light){:root:not([data-bs-theme="dark"]){--ec-codeBg: #fbfbfb;--ec-codeFg: #403f53;--ec-codeSelBg: #e0e0e0;--ec-uiSelBg: #d3e8f8;--ec-uiSelFg: #403f53;--ec-focusBrd: #93a1a1;--ec-sbThumbCol: #0000001a;--ec-sbThumbHoverCol: #0000005c;--ec-tm-markBg: #0000001a;--ec-tm-markBrdCol: #00000055;--ec-tm-insBg: #8ec77d99;--ec-tm-insDiffIndCol: #336a28d0;--ec-tm-delBg: #ff9c8e99;--ec-tm-delDiffIndCol: #9d4138d0;--ec-frm-shdCol: #d9d9d9;--ec-frm-edActTabBg: var(--sl-color-gray-7);--ec-frm-edActTabIndTopCol: #5d2f86;--ec-frm-edTabBarBg: var(--sl-color-gray-6);--ec-frm-edBg: var(--sl-color-gray-7);--ec-frm-trmTtbBg: var(--sl-color-gray-6);--ec-frm-trmBg: var(--sl-color-gray-7);--ec-frm-tooltipSuccessBg: #078662}:root:not([data-bs-theme="dark"]) .expressive-code .ec-line span[style^="--"]:not([class]){color:var(1, inherit);font-style:var(1fs, inherit);font-weight:var(1fw, inherit);-webkit-text-decoration:var(1td, inherit);text-decoration:var(1td, inherit)}}:root[data-bs-theme="light"] .expressive-code,.expressive-code[data-bs-theme="light"]{--ec-codeBg: #fbfbfb;--ec-codeFg: #403f53;--ec-codeSelBg: #e0e0e0;--ec-uiSelBg: #d3e8f8;--ec-uiSelFg: #403f53;--ec-focusBrd: #93a1a1;--ec-sbThumbCol: #0000001a;--ec-sbThumbHoverCol: #0000005c;--ec-tm-markBg: #0000001a;--ec-tm-markBrdCol: #00000055;--ec-tm-insBg: #8ec77d99;--ec-tm-insDiffIndCol: #336a28d0;--ec-tm-delBg: #ff9c8e99;--ec-tm-delDiffIndCol: #9d4138d0;--ec-frm-shdCol: #d9d9d9;--ec-frm-edActTabBg: var(--sl-color-gray-7);--ec-frm-edActTabIndTopCol: #5d2f86;--ec-frm-edTabBarBg: var(--sl-color-gray-6);--ec-frm-edBg: var(--sl-color-gray-7);--ec-frm-trmTtbBg: var(--sl-color-gray-6);--ec-frm-trmBg: var(--sl-color-gray-7);--ec-frm-tooltipSuccessBg: #078662}:root[data-bs-theme="light"] .expressive-code .ec-line span[style^="--"]:not([class]),.expressive-code[data-bs-theme="light"] .ec-line span[style^="--"]:not([class]){color:var(1, inherit);font-style:var(1fs, inherit);font-weight:var(1fw, inherit);-webkit-text-decoration:var(1td, inherit);text-decoration:var(1td, inherit)}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:.875rem}code:not(:where(.not-content *)){background-color:var(--sl-color-gray-6);margin-block:-0.125rem;padding:0.125rem 0.375rem;color:inherit}[data-bs-theme="dark"] code:not(:where(.not-content *)){background-color:var(--sl-color-gray-5)}.math-block{display:block;margin:2rem 0;overflow-x:auto}.math-inline{display:inline}[data-bs-theme="dark"] .math-inline img,[data-bs-theme="dark"] .math-block img{filter:invert(1)}img.diagram{height:auto;width:100%;margin:1rem 0 2rem}img.diagram-kroki-mermaid{background:#fff}.highlight>pre{padding:0.875rem 1rem}.highlight div{padding:0}.highlight>.chroma{overflow-x:auto;border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}.chroma .ln{padding:0 0.5rem 0 0}.chroma .hl{border-inline-start:0.15rem solid #0005;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}.chroma .hl .ln{margin-left:-0.15rem}.highlight .chroma .lntable .lnt,.highlight .chroma .lntable .hl{display:flex}.chroma .lntd:first-child{padding:0}.chroma .lntd:first-child .lnt{padding-left:1rem}.chroma .lntd:nth-child(2){padding:0}.highlight .chroma .lntable .lntd+.lntd{width:100%}[data-bs-theme="dark"] .chroma .ln{padding:0 0.5em 0 0}.chroma .lntd pre{padding:1rem 0;margin-bottom:0}.highlight>.chroma::-webkit-scrollbar,.highlight>.chroma::-webkit-scrollbar-track{background-color:inherit;border-radius:1px;border-top-left-radius:0;border-top-right-radius:0}.highlight>.chroma::-webkit-scrollbar-thumb{background-color:#dddee0;border:4px solid transparent;background-clip:content-box;border-radius:10px}.highlight>.chroma::-webkit-scrollbar-thumb:hover{background-color:#9d9e9f}[data-bs-theme="dark"] .highlight>.chroma::-webkit-scrollbar-thumb{background-color:#ffffff17}[data-bs-theme="dark"] .highlight>.chroma::-webkit-scrollbar-thumb:hover{background-color:#ffffff49}[data-bs-theme="dark"] .highlight>.chroma{border:1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%)}[data-bs-theme="dark"] .chroma .hl{border-inline-start:0.15rem solid #ffffff40;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem}[data-bs-theme="dark"] .chroma .hl .ln{margin-left:-0.15rem}blockquote{margin-bottom:1rem;font-size:1.25rem;border-left:3px solid #dee2e6;padding-left:1rem}details{display:block;position:relative;border:1px solid #e9ecef;border-radius:0.25rem;padding:0.5rem 1rem 0;margin:0.5rem 0}summary{list-style:none;display:inline-block;width:calc(100% + 2rem);margin:-0.5rem -1rem 0;padding:0.5rem 1rem}summary::-webkit-details-marker{display:none}summary:hover{background:#f8f9fa}details summary::after{display:inline-block;content:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%2829, 45, 53, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");transition:transform 0.35s ease;transform-origin:center center;position:absolute;right:1rem}details[open]>summary::after{transform:rotate(90deg)}details[open]{padding:0.5rem 1rem}details[open]>summary{border-bottom:1px solid #dee2e6;margin-bottom:0.5rem}details h2,details .h2,details h3,details .h3,details h4,details .h4{margin:1rem 0 0.5rem}details p:last-child{margin-bottom:0}details ul,details ol{margin-bottom:0}details pre{margin:0 0 1rem}.search-form label{font-weight:normal}img{max-width:100%;height:auto}img[data-sizes="auto"]{display:block}img{font-size:0}figcaption{font-size:1rem;margin-top:0.5rem;font-style:italic}.blur-up{filter:blur(5px);transition:filter 400ms}.blur-up.lazyloaded{filter:unset}.search-form .form-control:focus{border:2px solid #4f46e5}[data-bs-theme="dark"] .search-form .form-control:focus{border:2px solid #b3c7ff}[data-bs-theme="dark"] .search-form .btn-link{color:#b3c7ff}.search-form .btn-link,.modal-body p.message,.modal-footer{font-size:.875rem}.modal-body::-webkit-scrollbar{width:0.25rem}.modal-body::-webkit-scrollbar-track{background-color:#f1f1f1}.modal-body::-webkit-scrollbar-thumb{background-color:#c1c1c1}[data-bs-theme="dark"] .modal-body::-webkit-scrollbar-track{background-color:#424242}[data-bs-theme="dark"] .modal-body::-webkit-scrollbar-thumb{background-color:#686868}@media (min-width: 768px){#searchModal .modal-dialog{max-height:40rem}}.search-result h2,.search-result .h2{margin-top:0}.search-result a:focus{outline:0 none}.search-result .content{margin-top:0.5rem;padding-top:0 !important;padding-bottom:0 !important}.search-result .card .content p{margin-bottom:0}.search-result .card .content a{position:relative;z-index:1}.search-result:hover .card,.search-result.selected .card{background-color:#4f46e5;color:#fff}.search-result:hover .card .content a,.search-result.selected .card .content a{color:#fff;text-decoration:underline}[data-bs-theme="dark"] .search-result:hover .card,[data-bs-theme="dark"] .search-result.selected .card{background-color:#b3c7ff;color:#23262f}[data-bs-theme="dark"] .search-result:hover .card .content a,[data-bs-theme="dark"] .search-result.selected .card .content a{color:#23262f;text-decoration:underline}[data-bs-theme="dark"] .search-result:hover .card h2,[data-bs-theme="dark"] .search-result:hover .card .h2,[data-bs-theme="dark"] .search-result.selected .card h2,[data-bs-theme="dark"] .search-result.selected .card .h2{color:#17181c}.search-result .submitted{font-size:.875rem;margin-top:0.5rem}.section-nav{padding-top:2rem}.section-nav details{border:0;padding:0;margin:0.5rem 0}.section-nav details[open]{padding:0}.section-nav summary{width:100%;padding:0;margin:0;font-weight:700}.section-nav summary:hover{background:none}.section-nav details[open]>summary{border-bottom:0;margin-bottom:0}.section-nav ul.list-nested details{padding-left:1rem;margin-top:0.5rem}.section-nav ul.list-nested li{margin:0}.section-nav a{display:block;margin:0.5rem 0;color:#1d2d35;font-size:1rem;text-decoration:none}.section-nav a:hover,.section-nav a:active{color:#4f46e5}.section-nav li.active a{color:#4f46e5;font-weight:500}.section-nav ul.list-nested li a{padding-left:1rem}.section-nav ul.list-nested{border-left:1px solid #e9ecef}[data-bs-theme="dark"] .section-nav ul.list-nested{border-left:1px solid #23262f}[data-bs-theme="dark"] .section-nav a{color:#c1c3c8}[data-bs-theme="dark"] .section-nav a:hover,[data-bs-theme="dark"] .section-nav a:active{color:var(--sl-color-text-accent)}[data-bs-theme="dark"] .section-nav li.active a{color:var(--sl-color-text-accent);font-weight:500}[data-bs-theme="dark"] .section-nav summary{color:#fff}table{margin:3rem 0}.nav-tabs{border-bottom:0.0625rem solid #d8dee4;margin-bottom:1rem}.nav-tabs .nav-link{margin-bottom:-0.0625rem !important;background:none;border:0;border-top-left-radius:0;border-top-right-radius:0;color:inherit}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:transparent;color:var(--bs-emphasis-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{background-color:transparent;border-color:transparent;border-bottom:0.125rem solid #4f46e5}[data-bs-theme="dark"] .nav-tabs{border-bottom:0.0625rem solid #343a40}[data-bs-theme="dark"] .nav-tabs .nav-link.active,[data-bs-theme="dark"] .nav-tabs .nav-item.show .nav-link{border-bottom:0.125rem solid #b3c7ff}.footer{border-top:1px solid #e9ecef;padding-top:1.125rem;padding-bottom:1.125rem}.footer ul{margin-bottom:0}.footer li{font-size:.875rem;margin-bottom:0}.footer .list-inline-item:not(:last-child){margin-right:1rem}@media (max-width: 991.98px){.footer .col-lg-8{margin-top:0.25rem;margin-bottom:0.25rem}}@media (min-width: 768px){.footer li{font-size:1rem}}.navbar-brand{font-weight:700}.navbar-brand svg{margin-right:0.25rem}[data-bs-theme="dark"] .navbar-brand{color:inherit}.navbar{z-index:1000;background-color:rgba(255,255,255,0.95);border-bottom:1px solid #e9ecef}@media (min-width: 992px){.navbar{z-index:1025}}@media (min-width: 768px){.navbar-brand{font-size:1.375rem}}.nav-item{margin-left:0}@media (max-width: 991.98px){.navbar-nav .nav-link{font-weight:400}}@media (min-width: 768px){.nav-item{margin-left:0.5rem}}@media (max-width: 575.98px){.navbar .offcanvas.offcanvas-start,.navbar .offcanvas.offcanvas-end{width:80vw}}.offcanvas-header{border-bottom:1px solid #dee2e6;padding-top:1.0625rem;padding-bottom:0.8125rem}h5.offcanvas-title,.offcanvas-title.h5{margin:0;color:inherit}.offcanvas .nav-link{color:#1d2d35}.offcanvas .nav-link:hover,.offcanvas .nav-link:focus{color:#4f46e5}.offcanvas .nav-link.active{color:#4f46e5}.home .navbar{border-bottom:0}@media (min-width: 992px){.navbar-brand{margin-right:0.75rem !important}}.social-link{padding-right:0.375rem;padding-left:0.375rem}@media (max-width: 991.98px){#buttonColorMode{margin:0.5rem 0}#socialMenu{margin:0.5rem 0 0.5rem -0.25rem}.navbar-nav{margin-top:1rem}.nav-item .nav-link{font-weight:400;font-size:1.125rem}}.modal-backdrop,.offcanvas-backdrop{visibility:hidden;background:rgba(23,24,28,0.5);opacity:0}[data-bs-theme="dark"] .modal-backdrop,[data-bs-theme="dark"] .offcanvas-backdrop{visibility:hidden;background:rgba(23,24,28,0.5);opacity:0}.modal-backdrop.show,.offcanvas-backdrop.show{visibility:visible;opacity:1;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.showing,.hiding{transition:none;display:none}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg{padding-right:0.75rem}.docs-content>h2[id]::before,.docs-content>[id].h2::before,.docs-content>h3[id]::before,.docs-content>[id].h3::before,.docs-content>h4[id]::before,.docs-content>[id].h4::before{display:block;height:6rem;margin-top:-6rem;content:""}.docs-content ul,.docs-content ol{margin-bottom:1rem}.anchor{visibility:hidden;margin-left:0.375rem}.edit-page a{color:var(--sl-color-gray-3)}h1:hover a,.h1:hover a,h2:hover a,.h2:hover a,h3:hover a,.h3:hover a,h4:hover a,.h4:hover a{visibility:visible;text-decoration:none}.card-list{margin-top:2.25rem}.page-footer-meta{margin-top:2rem;margin-bottom:2rem}.edit-page{font-size:.875rem;margin-top:0.25rem;margin-bottom:0.25rem}@media (min-width: 768px){.edit-page{font-size:1rem;margin-top:0.75rem;margin-bottom:0.25rem}}.edit-page a:hover{color:var(--sl-color-gray-4);text-decoration:none}[data-bs-theme="dark"] .edit-page a:hover{color:var(--sl-color-gray-2)}.edit-page svg{margin-right:0.25rem;margin-bottom:0.25rem}p.meta{margin-top:0.5rem;font-size:1rem}.toc-mobile{margin-top:2rem;margin-bottom:2rem}.page-link:hover{text-decoration:none}ul li{margin:0.25rem 0}.page-nav .card .icon-tabler-arrow-left{margin-right:0.75rem}.page-nav .card .icon-tabler-arrow-right{margin-left:0.75rem}.page-nav .card:hover{border:1px solid #d9d9d9}[data-bs-theme="dark"] .page-nav .card{border:1px solid #353841}[data-bs-theme="dark"] .page-nav .card:hover{border:1px solid #888c96}.home .card,.contributors.list .card,.categories.list .card,.tags.list .card{margin-top:2rem;margin-bottom:2rem;transition:transform 0.3s}.home .content .card:hover,.contributors.list .content .card:hover,.categories.list .content .card:hover,.tags.list .content .card:hover{transform:scale(1.025)}.home .content .card-body,.contributors.list .content .card-body,.categories.list .content .card-body,.tags.list .content .card-body{padding:0 2rem 1rem}.page-item:first-child,.page-item:last-child,.page-item.disabled{display:none}.page-item a{margin-left:0.5rem;margin-right:0.5rem;padding-left:0.875rem;padding-right:0.875rem}.docs-links,.docs-toc{scrollbar-width:thin;scrollbar-color:#fff #fff}.docs-links::-webkit-scrollbar,.docs-toc::-webkit-scrollbar{width:5px}.docs-links::-webkit-scrollbar-track,.docs-toc::-webkit-scrollbar-track{background:#fff}.docs-links::-webkit-scrollbar-thumb,.docs-toc::-webkit-scrollbar-thumb{background:#fff}.docs-links:hover,.docs-toc:hover{scrollbar-width:thin;scrollbar-color:#e9ecef #fff}.docs-links:hover::-webkit-scrollbar-thumb,.docs-toc:hover::-webkit-scrollbar-thumb{background:#e9ecef}.docs-links::-webkit-scrollbar-thumb:hover,.docs-toc::-webkit-scrollbar-thumb:hover{background:#e9ecef}.docs-links h3,.docs-links .h3,.page-links h3,.page-links .h3{font-size:1.125rem;margin:1.25rem 0 0.5rem;padding:1.5rem 0 0}@media (min-width: 992px){.docs-links h3,.docs-links .h3,.page-links h3,.page-links .h3{margin:1.125rem 1.5rem 0.75rem 0;padding:1.375rem 0 0}}.docs-links h3:not(:first-child),.docs-links .h3:not(:first-child){border-top:1px solid #e9ecef}.page-links li{margin-top:0.375rem;padding-top:0.375rem}.page-links li ul li{border-top:none;padding-left:1rem;margin-top:0.125rem;padding-top:0.125rem}.page-links li:not(:first-child){border-top:1px dashed #e9ecef}.page-links a{color:#1d2d35;display:block;padding:0.125rem 0;font-size:.9375rem;text-decoration:none}.page-links a:hover,.page-links a.active{text-decoration:none;color:#4f46e5}.nav-link.active{font-weight:500}*{-webkit-font-smoothing:antialiased}h1,.h1,h2,.h2,h3,.h3,h4,.h4,h5,.h5,.navbar-brand{font-family:Quicksand, sans-serif;font-weight:700}[data-bs-theme="dark"] .only-light{display:none}[data-bs-theme="light"] .only-dark{display:none}
diff --git a/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.json b/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.json
index c2019e168..80a1da8c1 100644
--- a/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.json
+++ b/docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.json
@@ -1 +1 @@
-{"Target":"main.e18d306ea87fc31ccdc18bc02f9617933c7a3056a2ba2de6830a8ba47283543cbbd494e24b7d778b41d54d3074f438879353bddabcc0bd0d0389eb3d84f0aa38.css","MediaType":"text/css","Data":{"Integrity":"sha512-4Y0wbqh/wxzNwYvAL5YXkzx6MFaiui3mgwqLpHKDVDy71JTiS313i0HVTTB09DiHk1O92rzAvQ0Dies9hPCqOA=="}}
\ No newline at end of file
+{"Target":"main.3e9cfa18ef8eea818e5322c5eccce2bcedbda0100f6cc0469a0d0d315f86d01a5e46c2424f782e7e0c2e12da5c5afc98309d50b5aa60f98949293da86193c731.css","MediaType":"text/css","Data":{"Integrity":"sha512-Ppz6GO+O6oGOUyLF7MzivO29oBAPbMBGmg0NMV+G0BpeRsJCT3gufgwuEtpcWvyYMJ1Qtapg+YlJKT2oYZPHMQ=="}}
\ No newline at end of file
diff --git a/docs/resources/_gen/images/_hu70523d59fb738bec5c06336403a46531_39940_c0fe83760c1bebd5a39d4ddb7fce622e.webp b/docs/resources/_gen/images/_hu70523d59fb738bec5c06336403a46531_39940_c0fe83760c1bebd5a39d4ddb7fce622e.webp
new file mode 100644
index 000000000..fa79b7353
Binary files /dev/null and b/docs/resources/_gen/images/_hu70523d59fb738bec5c06336403a46531_39940_c0fe83760c1bebd5a39d4ddb7fce622e.webp differ
diff --git a/docs/resources/_gen/images/_hu89002a090cbbd5e6897ae6b591dddabc_33739_cb9243f2e37f830fb14160ae4284ce39.webp b/docs/resources/_gen/images/_hu89002a090cbbd5e6897ae6b591dddabc_33739_cb9243f2e37f830fb14160ae4284ce39.webp
new file mode 100644
index 000000000..84bc78093
Binary files /dev/null and b/docs/resources/_gen/images/_hu89002a090cbbd5e6897ae6b591dddabc_33739_cb9243f2e37f830fb14160ae4284ce39.webp differ
diff --git a/message/router.go b/message/router.go
index 1b93e85a6..0ecf48c6d 100644
--- a/message/router.go
+++ b/message/router.go
@@ -825,15 +825,12 @@ func (h *handler) publishProducedMessages(producedMessages Messages, msgFields w
"publish_topic": h.publishTopic,
}))
- for _, msg := range producedMessages {
- if err := h.publisher.Publish(h.publishTopic, msg); err != nil {
- // todo - how to deal with it better/transactional/retry?
- h.logger.Error("Cannot publish message", err, msgFields.Add(watermill.LogFields{
- "not_sent_message": fmt.Sprintf("%#v", producedMessages),
- }))
-
- return err
- }
+ if err := h.publisher.Publish(h.publishTopic, producedMessages...); err != nil {
+ h.logger.Error("Cannot publish messages", err, msgFields.Add(watermill.LogFields{
+ "not_sent_message": fmt.Sprintf("%#v", producedMessages),
+ }))
+
+ return err
}
return nil
diff --git a/message/router/middleware/delay_on_error.go b/message/router/middleware/delay_on_error.go
new file mode 100644
index 000000000..9b70370e5
--- /dev/null
+++ b/message/router/middleware/delay_on_error.go
@@ -0,0 +1,47 @@
+package middleware
+
+import (
+ "time"
+
+ "github.com/ThreeDotsLabs/watermill/components/delay"
+ "github.com/ThreeDotsLabs/watermill/message"
+)
+
+// DelayOnError is a middleware that adds the delay metadata to the message if an error occurs.
+//
+// IMPORTANT: The delay metadata doesn't cause delays with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.
+// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/
+type DelayOnError struct {
+ // InitialInterval is the first interval between retries. Subsequent intervals will be scaled by Multiplier.
+ InitialInterval time.Duration
+ // MaxInterval sets the limit for the exponential backoff of retries. The interval will not be increased beyond MaxInterval.
+ MaxInterval time.Duration
+ // Multiplier is the factor by which the waiting interval will be multiplied between retries.
+ Multiplier float64
+}
+
+func (d *DelayOnError) Middleware(h message.HandlerFunc) message.HandlerFunc {
+ return func(msg *message.Message) ([]*message.Message, error) {
+ msgs, err := h(msg)
+ if err != nil {
+ d.applyDelay(msg)
+ }
+
+ return msgs, err
+ }
+}
+
+func (d *DelayOnError) applyDelay(msg *message.Message) {
+ delayedForStr := msg.Metadata.Get(delay.DelayedForKey)
+ delayedFor, err := time.ParseDuration(delayedForStr)
+ if delayedForStr != "" && err == nil {
+ delayedFor *= time.Duration(d.Multiplier)
+ if delayedFor > d.MaxInterval {
+ delayedFor = d.MaxInterval
+ }
+
+ delay.Message(msg, delay.For(delayedFor))
+ } else {
+ delay.Message(msg, delay.For(d.InitialInterval))
+ }
+}
diff --git a/message/router/middleware/delay_on_error_test.go b/message/router/middleware/delay_on_error_test.go
new file mode 100644
index 000000000..e8fc89fa1
--- /dev/null
+++ b/message/router/middleware/delay_on_error_test.go
@@ -0,0 +1,58 @@
+package middleware_test
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/ThreeDotsLabs/watermill/components/delay"
+ "github.com/ThreeDotsLabs/watermill/message"
+ "github.com/ThreeDotsLabs/watermill/message/router/middleware"
+)
+
+func TestDelayOnError(t *testing.T) {
+ m := middleware.DelayOnError{
+ InitialInterval: time.Second,
+ MaxInterval: time.Second * 10,
+ Multiplier: 2,
+ }
+
+ msg := message.NewMessage("1", []byte("test"))
+
+ getDelayFor := func(msg *message.Message) string {
+ return msg.Metadata.Get(delay.DelayedForKey)
+ }
+
+ okHandler := func(msg *message.Message) ([]*message.Message, error) {
+ return nil, nil
+ }
+
+ errHandler := func(msg *message.Message) ([]*message.Message, error) {
+ return nil, errors.New("error")
+ }
+
+ assert.Equal(t, "", getDelayFor(msg))
+
+ _, _ = m.Middleware(okHandler)(msg)
+ assert.Equal(t, "", getDelayFor(msg))
+
+ _, _ = m.Middleware(errHandler)(msg)
+ assert.Equal(t, "1s", getDelayFor(msg))
+
+ _, _ = m.Middleware(errHandler)(msg)
+ assert.Equal(t, "2s", getDelayFor(msg))
+
+ _, _ = m.Middleware(errHandler)(msg)
+ assert.Equal(t, "4s", getDelayFor(msg))
+
+ _, _ = m.Middleware(errHandler)(msg)
+ assert.Equal(t, "8s", getDelayFor(msg))
+
+ _, _ = m.Middleware(errHandler)(msg)
+ assert.Equal(t, "10s", getDelayFor(msg))
+
+ _, _ = m.Middleware(errHandler)(msg)
+ assert.Equal(t, "10s", getDelayFor(msg))
+}
diff --git a/message/router/middleware/retry_test.go b/message/router/middleware/retry_test.go
index cf7bd84f1..8c2fc91f7 100644
--- a/message/router/middleware/retry_test.go
+++ b/message/router/middleware/retry_test.go
@@ -5,13 +5,11 @@ import (
"testing"
"time"
- "github.com/ThreeDotsLabs/watermill"
-
+ "github.com/pkg/errors"
"github.com/stretchr/testify/assert"
+ "github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
- "github.com/pkg/errors"
-
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
)
diff --git a/netlify.toml b/netlify.toml
index e213b7c54..0388f619e 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -39,3 +39,8 @@
from = "/docs/pub-sub-implementing"
to = "/development/pub-sub-implementing/"
status = 301
+
+[[redirects]]
+ from = "/pubsubs/amazonsqs/"
+ to = "/pubsubs/aws/"
+ status = 301
diff --git a/slog.go b/slog.go
index a7f887766..47dbf930c 100644
--- a/slog.go
+++ b/slog.go
@@ -21,25 +21,40 @@ func slogAttrsFromFields(fields LogFields) []any {
// SlogLoggerAdapter wraps [slog.Logger].
type SlogLoggerAdapter struct {
slog *slog.Logger
+
+ watermillLevelToSlog map[slog.Level]slog.Level
}
// Error logs a message to [slog.LevelError].
func (s *SlogLoggerAdapter) Error(msg string, err error, fields LogFields) {
- s.slog.Error(msg, append(slogAttrsFromFields(fields), "error", err)...)
+ s.log(slog.LevelError, msg, append(slogAttrsFromFields(fields), "error", err)...)
}
// Info logs a message to [slog.LevelInfo].
func (s *SlogLoggerAdapter) Info(msg string, fields LogFields) {
- s.slog.Info(msg, slogAttrsFromFields(fields)...)
+ s.log(slog.LevelInfo, msg, slogAttrsFromFields(fields)...)
}
// Debug logs a message to [slog.LevelDebug].
func (s *SlogLoggerAdapter) Debug(msg string, fields LogFields) {
- s.slog.Debug(msg, slogAttrsFromFields(fields)...)
+ s.log(slog.LevelDebug, msg, slogAttrsFromFields(fields)...)
}
// Trace logs a message to [LevelTrace].
func (s *SlogLoggerAdapter) Trace(msg string, fields LogFields) {
+ s.log(
+ LevelTrace,
+ msg,
+ slogAttrsFromFields(fields)...,
+ )
+}
+
+func (s *SlogLoggerAdapter) log(level slog.Level, msg string, args ...any) {
+ mappedLevel, ok := s.watermillLevelToSlog[level]
+ if ok {
+ level = mappedLevel
+ }
+
s.slog.Log(
// Void context, following the slog example
// as it treats context slightly differently from
@@ -48,15 +63,15 @@ func (s *SlogLoggerAdapter) Trace(msg string, fields LogFields) {
// See the [slog] package documentation
// for more details.
context.Background(),
- LevelTrace,
+ level,
msg,
- slogAttrsFromFields(fields)...,
+ args...,
)
}
// With return a [SlogLoggerAdapter] with a set of fields injected into all consequent logging messages.
func (s *SlogLoggerAdapter) With(fields LogFields) LoggerAdapter {
- return &SlogLoggerAdapter{slog: s.slog.With(slogAttrsFromFields(fields)...)}
+ return &SlogLoggerAdapter{slog: s.slog.With(slogAttrsFromFields(fields)...), watermillLevelToSlog: s.watermillLevelToSlog}
}
// NewSlogLogger creates an adapter to the standard library's structured logging package. A `nil` logger is substituted for the result of [slog.Default].
@@ -68,3 +83,16 @@ func NewSlogLogger(logger *slog.Logger) LoggerAdapter {
slog: logger,
}
}
+
+// NewSlogLoggerWithLevelMapping creates an adapter to the standard library's structured logging package. A `nil` logger is substituted for the result of [slog.Default].
+// The `watermillLevelToSlog` parameter is a map that maps Watermill's log levels to the levels of the structured logger.
+// It's helpful, when want to for example log Watermill's info logs as debug in slog.
+func NewSlogLoggerWithLevelMapping(logger *slog.Logger, watermillLevelToSlog map[slog.Level]slog.Level) LoggerAdapter {
+ if logger == nil {
+ logger = slog.Default()
+ }
+ return &SlogLoggerAdapter{
+ slog: logger,
+ watermillLevelToSlog: watermillLevelToSlog,
+ }
+}
diff --git a/slog_test.go b/slog_test.go
index 7d27b469c..fc61568e6 100644
--- a/slog_test.go
+++ b/slog_test.go
@@ -42,12 +42,58 @@ func TestSlogLoggerAdapter(t *testing.T) {
})
assert.Equal(t,
- strings.TrimSpace(b.String()),
strings.TrimSpace(`
time=[omit] level=DEBUG-4 msg="test trace" common1=commonvalue field1=value1
time=[omit] level=ERROR msg="test error" common1=commonvalue field2=value2 error="error message"
time=[omit] level=INFO msg="test info" common1=commonvalue field3=value3
`),
+ strings.TrimSpace(b.String()),
+ "Logging output does not match saved template.",
+ )
+}
+
+func TestSlogLoggerAdapter_level_mapping(t *testing.T) {
+ b := &bytes.Buffer{}
+
+ logger := NewSlogLoggerWithLevelMapping(
+ slog.New(slog.NewTextHandler(
+ b, // output
+ &slog.HandlerOptions{
+ Level: LevelTrace,
+ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
+ if a.Key == "time" && len(groups) == 0 {
+ // omit time stamp to make the test idempotent
+ a.Value = slog.StringValue("[omit]")
+ }
+ return a
+ },
+ },
+ )),
+ map[slog.Level]slog.Level{
+ slog.LevelInfo: slog.LevelDebug,
+ },
+ )
+
+ logger = logger.With(LogFields{
+ "common1": "commonvalue",
+ })
+ logger.Trace("test trace", LogFields{
+ "field1": "value1",
+ })
+ logger.Error("test error", errors.New("error message"), LogFields{
+ "field2": "value2",
+ })
+ logger.Info("test info mapped to debug", LogFields{
+ "field3": "value3",
+ })
+
+ assert.Equal(t,
+ strings.TrimSpace(`
+time=[omit] level=DEBUG-4 msg="test trace" common1=commonvalue field1=value1
+time=[omit] level=ERROR msg="test error" common1=commonvalue field2=value2 error="error message"
+time=[omit] level=DEBUG msg="test info mapped to debug" common1=commonvalue field3=value3
+ `),
+ strings.TrimSpace(b.String()),
"Logging output does not match saved template.",
)
}
diff --git a/tools/pq/README.md b/tools/pq/README.md
new file mode 100644
index 000000000..b482cbbe6
--- /dev/null
+++ b/tools/pq/README.md
@@ -0,0 +1,38 @@
+# pq
+
+pq is a CLI tool for working with delayed messages on poison queues.
+
+For now, it supports the PostgreSQL Pub/Sub implementation.
+
+## Install
+
+```bash
+go install github.com/ThreeDotsLabs/watermill/tools/pq@latest
+```
+
+## Usage
+
+Set the `DATABASE_URL` environment variable to your PostgreSQL connection string.
+
+For example, to connect to the database used for the [delayed requeue example](../../_examples/real-world-examples/delayed-requeue):
+
+```bash
+export DATABASE_URL="postgres://watermill:password@postgres:5432/watermill?sslmode=disable"
+```
+
+```bash
+pq -backend postgres -topic requeue
+```
+
+This will use the default `watermill_` prefix, so will use the `watermill_requeue` table.
+
+If you use a custom prefix, use the `-raw-topic` flag instead:
+
+```bash
+pq -backend postgres -raw-topic my_prefix_requeue
+```
+
+## Commands
+
+- Requeue — Updates the `_watermill_delayed_until` metadata to the current time, so the message will be instantly requeued.
+- Ack — Removes the message from the queue (be careful — you will lose the message forever).
diff --git a/tools/pq/backend/postgres.go b/tools/pq/backend/postgres.go
new file mode 100644
index 000000000..f4418c127
--- /dev/null
+++ b/tools/pq/backend/postgres.go
@@ -0,0 +1,99 @@
+package backend
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+
+ "github.com/ThreeDotsLabs/watermill/components/delay"
+ "github.com/ThreeDotsLabs/watermill/tools/pq/cli"
+)
+
+type PostgresMessage struct {
+ Offset int `db:"offset"`
+ UUID string `db:"uuid"`
+ Payload string `db:"payload"`
+ Metadata string `db:"metadata"`
+}
+
+type PostgresBackend struct {
+ db *sqlx.DB
+ config cli.BackendConfig
+}
+
+func NewPostgresBackend(ctx context.Context, config cli.BackendConfig) (*PostgresBackend, error) {
+ dbURL := os.Getenv("DATABASE_URL")
+ if dbURL == "" {
+ return nil, fmt.Errorf("missing DATABASE_URL")
+ }
+
+ db, err := sqlx.Connect("postgres", dbURL)
+ if err != nil {
+ return nil, err
+ }
+
+ return &PostgresBackend{
+ db: db,
+ config: config,
+ }, nil
+}
+
+func (r *PostgresBackend) AllMessages(ctx context.Context) ([]cli.Message, error) {
+ var dbMessages []PostgresMessage
+ err := r.db.SelectContext(ctx, &dbMessages, fmt.Sprintf(`SELECT "offset", uuid, payload, metadata FROM %v WHERE acked = false ORDER BY "offset"`, r.topic()))
+ if err != nil {
+ return nil, err
+ }
+
+ var messages []cli.Message
+
+ for _, dbMsg := range dbMessages {
+ var metadata map[string]string
+ err := json.Unmarshal([]byte(dbMsg.Metadata), &metadata)
+ if err != nil {
+ return nil, err
+ }
+
+ msg, err := cli.NewMessage(fmt.Sprint(dbMsg.Offset), dbMsg.UUID, dbMsg.Payload, metadata)
+ if err != nil {
+ return nil, err
+ }
+
+ messages = append(messages, msg)
+ }
+
+ return messages, nil
+}
+
+func (r *PostgresBackend) Requeue(ctx context.Context, msg cli.Message) error {
+ _, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET metadata = metadata::jsonb || jsonb_build_object($1::text, $2::text) WHERE "offset" = $3`, r.topic()),
+ delay.DelayedUntilKey, time.Now().UTC().Format(time.RFC3339), msg.ID,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *PostgresBackend) Ack(ctx context.Context, msg cli.Message) error {
+ _, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET acked = true WHERE "offset" = %v`, r.topic(), msg.ID))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *PostgresBackend) topic() string {
+ if r.config.Topic != "" {
+ return fmt.Sprintf(`"watermill_%v"`, r.config.Topic)
+ }
+
+ return fmt.Sprintf(`"%v"`, r.config.RawTopic)
+}
diff --git a/tools/pq/cli/backend.go b/tools/pq/cli/backend.go
new file mode 100644
index 000000000..9aad18c66
--- /dev/null
+++ b/tools/pq/cli/backend.go
@@ -0,0 +1,32 @@
+package cli
+
+import (
+ "context"
+
+ "github.com/pkg/errors"
+)
+
+type BackendConfig struct {
+ Topic string
+ RawTopic string
+}
+
+func (c BackendConfig) Validate() error {
+ if c.Topic == "" && c.RawTopic == "" {
+ return errors.New("topic or raw topic must be provided")
+ }
+
+ if c.Topic != "" && c.RawTopic != "" {
+ return errors.New("only one of topic or raw topic must be provided")
+ }
+
+ return nil
+}
+
+type BackendConstructor func(ctx context.Context, cfg BackendConfig) (Backend, error)
+
+type Backend interface {
+ AllMessages(ctx context.Context) ([]Message, error)
+ Requeue(ctx context.Context, msg Message) error
+ Ack(ctx context.Context, msg Message) error
+}
diff --git a/tools/pq/cli/message.go b/tools/pq/cli/message.go
new file mode 100644
index 000000000..654171ed5
--- /dev/null
+++ b/tools/pq/cli/message.go
@@ -0,0 +1,45 @@
+package cli
+
+import (
+ "time"
+
+ "github.com/ThreeDotsLabs/watermill/components/delay"
+ "github.com/ThreeDotsLabs/watermill/message/router/middleware"
+)
+
+type Message struct {
+ // ID is a unique message ID across the Pub/Sub's topic.
+ ID string
+ UUID string
+ Payload string
+ Metadata map[string]string
+
+ OriginalTopic string
+ DelayedUntil string
+ DelayedFor string
+ RequeueIn time.Duration
+}
+
+func NewMessage(id string, uuid string, payload string, metadata map[string]string) (Message, error) {
+ originalTopic := metadata[middleware.PoisonedTopicKey]
+
+ // Calculate the time until the message should be requeued
+ delayedUntil, err := time.Parse(time.RFC3339, metadata[delay.DelayedUntilKey])
+ if err != nil {
+ return Message{}, err
+ }
+
+ delayedFor := metadata[delay.DelayedForKey]
+ requeueIn := delayedUntil.Sub(time.Now().UTC()).Round(time.Second)
+
+ return Message{
+ ID: id,
+ UUID: uuid,
+ Payload: payload,
+ Metadata: metadata,
+ OriginalTopic: originalTopic,
+ DelayedUntil: delayedUntil.String(),
+ DelayedFor: delayedFor,
+ RequeueIn: requeueIn,
+ }, nil
+}
diff --git a/tools/pq/cli/model.go b/tools/pq/cli/model.go
new file mode 100644
index 000000000..05593b831
--- /dev/null
+++ b/tools/pq/cli/model.go
@@ -0,0 +1,403 @@
+package cli
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "slices"
+ "time"
+
+ "github.com/charmbracelet/bubbles/table"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "golang.org/x/exp/maps"
+)
+
+var warningStyle = lipgloss.NewStyle().
+ Background(lipgloss.Color("196")).
+ Align(lipgloss.Center).
+ Padding(1, 10)
+
+var dialogStyle = lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(lipgloss.Color("241")).
+ Padding(1, 4)
+
+var buttonStyle = lipgloss.NewStyle()
+
+var buttonSelectedStyle = lipgloss.NewStyle().
+ Background(lipgloss.Color("57"))
+
+var readOnlyMessageActions = []string{"<- Back", "Show payload"}
+var writeMessageActions = []string{"Requeue", "Ack (drop)"}
+
+var dialogActions = []string{"Cancel", "Confirm"}
+
+type MessagesUpdated struct {
+ Messages []Message
+}
+
+type DialogResult struct {
+ Err error
+}
+
+func (m Model) FetchMessages() tea.Cmd {
+ return func() tea.Msg {
+ for {
+ msgs, err := m.backend.AllMessages(context.Background())
+ if err != nil {
+ panic(err)
+ }
+
+ m.sub <- MessagesUpdated{
+ Messages: msgs,
+ }
+
+ time.Sleep(time.Second)
+ }
+ }
+}
+
+func (m Model) WaitForMessages() tea.Cmd {
+ return func() tea.Msg {
+ return <-m.sub
+ }
+}
+
+var baseStyle = lipgloss.NewStyle().
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("240"))
+
+type Model struct {
+ backend Backend
+ sub chan MessagesUpdated
+
+ chosenMessage *Message
+ chosenMessageGone bool
+
+ table table.Model
+ messages []Message
+
+ chosenAction int
+ currentDialog *Dialog
+
+ showingPayload bool
+ payloadViewport viewport.Model
+}
+
+func (m Model) Init() tea.Cmd {
+ return tea.Batch(
+ m.FetchMessages(),
+ m.WaitForMessages(),
+ )
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case MessagesUpdated:
+ rows := make([]table.Row, len(msg.Messages))
+ for i, message := range msg.Messages {
+ rows[i] = table.Row{
+ message.ID,
+ message.UUID,
+ message.OriginalTopic,
+ message.DelayedFor,
+ message.RequeueIn.String(),
+ }
+ }
+ m.table.SetRows(rows)
+ m.messages = msg.Messages
+
+ // If the chosen message is no longer in the list, go back to the table.
+ // This is to avoid accidentally making an action on a message that has been requeued or deleted.
+ if m.chosenMessage != nil {
+ found := false
+ for _, message := range m.messages {
+ if message.ID == m.chosenMessage.ID {
+ foundMessage := message
+ m.chosenMessage = &foundMessage
+ found = true
+ break
+ }
+ }
+
+ if found {
+ m.chosenMessageGone = false
+ } else {
+ if !m.chosenMessageGone {
+ m.chosenAction = 0
+ }
+
+ m.chosenMessageGone = true
+ }
+ }
+
+ return m, m.WaitForMessages()
+ }
+
+ if m.chosenMessage == nil {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "ctrl+c", "q":
+ return m, tea.Quit
+ case " ", "enter":
+ c := m.table.Cursor()
+ m.chosenAction = 0
+ chosenMessage := m.messages[c]
+ m.chosenMessage = &chosenMessage
+ m.chosenMessageGone = false
+ }
+ }
+
+ var cmd tea.Cmd
+ m.table, cmd = m.table.Update(msg)
+ return m, cmd
+ } else if m.showingPayload {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "ctrl+c", "q":
+ return m, tea.Quit
+ case "esc", "backspace":
+ m.showingPayload = false
+ }
+ }
+
+ var cmd tea.Cmd
+ m.payloadViewport, cmd = m.payloadViewport.Update(msg)
+ return m, cmd
+ } else if m.currentDialog != nil {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "ctrl+c", "q":
+ return m, tea.Quit
+ case "esc", "backspace":
+ m.currentDialog = nil
+ case "h", "left":
+ m.currentDialog.Choice--
+ if m.currentDialog.Choice < 0 {
+ m.currentDialog.Choice = 0
+ }
+ case "l", "right":
+ m.currentDialog.Choice++
+ if m.currentDialog.Choice >= len(dialogActions) {
+ m.currentDialog.Choice = len(dialogActions) - 1
+ }
+ case " ", "enter":
+ switch m.currentDialog.Choice {
+ case 0:
+ m.currentDialog = nil
+ case 1:
+ m.currentDialog.Running = true
+ return m, m.currentDialog.Action
+ }
+ }
+ case DialogResult:
+ if msg.Err != nil {
+ // TODO Could be handled better
+ panic(msg.Err)
+ }
+
+ m.currentDialog = nil
+ }
+
+ return m, nil
+ } else {
+ messageActions := len(readOnlyMessageActions)
+ if !m.chosenMessageGone {
+ messageActions += len(writeMessageActions)
+ }
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "ctrl+c", "q":
+ return m, tea.Quit
+ case "esc", "backspace":
+ m.chosenMessage = nil
+ m.chosenMessageGone = false
+ case "j", "down":
+ m.chosenAction++
+ if m.chosenAction >= messageActions {
+ m.chosenAction = messageActions - 1
+ }
+ case "k", "up":
+ m.chosenAction--
+ if m.chosenAction < 0 {
+ m.chosenAction = 0
+ }
+ case " ", "enter":
+ switch m.chosenAction {
+ case 0:
+ m.chosenMessage = nil
+ m.chosenMessageGone = false
+ case 1:
+ // Show payload
+ m.showingPayload = true
+ m.payloadViewport = viewport.New(80, 20)
+ b := lipgloss.RoundedBorder()
+ m.payloadViewport.Style = lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
+
+ payload := m.chosenMessage.Payload
+
+ var jsonPayload any
+ err := json.Unmarshal([]byte(payload), &jsonPayload)
+ if err == nil {
+ pretty, err := json.MarshalIndent(jsonPayload, "", " ")
+ if err == nil {
+ payload = string(pretty)
+ }
+ }
+
+ m.payloadViewport.SetContent(payload)
+ case 2:
+ chosenMessage := *m.chosenMessage
+ m.currentDialog = &Dialog{
+ Prompt: "Requeue message? It will go back to the original topic.",
+ Action: func() tea.Msg {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ return DialogResult{
+ Err: m.backend.Requeue(ctx, chosenMessage),
+ }
+ },
+ }
+ case 3:
+ chosenMessage := *m.chosenMessage
+ m.currentDialog = &Dialog{
+ Prompt: "Acknowledge message? It will be dropped from the topic.",
+ Action: func() tea.Msg {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ return DialogResult{
+ Err: m.backend.Ack(ctx, chosenMessage),
+ }
+ },
+ }
+ }
+ }
+ }
+
+ return m, nil
+ }
+}
+
+func (m Model) View() string {
+ if m.chosenMessage == nil {
+ return baseStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n"
+ }
+
+ msg := m.chosenMessage
+
+ var out string
+
+ if m.chosenMessageGone {
+ out += warningStyle.Render("Read only — the message is gone.")
+ out += "\n"
+ }
+
+ out += fmt.Sprintf(
+ "ID: %v\nUUID: %v\nOriginal Topic: %v\nDelayed For: %v\nDelayed Until: %v\nRequeue In: %v\n\n",
+ msg.ID,
+ msg.UUID,
+ msg.OriginalTopic,
+ msg.DelayedFor,
+ msg.DelayedUntil,
+ msg.RequeueIn,
+ )
+
+ if m.showingPayload {
+ out += m.payloadViewport.View()
+ return out
+ }
+
+ out += "Metadata:\n"
+
+ keys := maps.Keys(msg.Metadata)
+ slices.Sort(keys)
+ for _, k := range keys {
+ v := msg.Metadata[k]
+ out += fmt.Sprintf(" %v: %v\n", k, v)
+ }
+
+ if m.currentDialog != nil {
+ prompt := m.currentDialog.Prompt + "\n\n"
+
+ if m.currentDialog.Running {
+ prompt += "Running..."
+ } else {
+ for i, action := range dialogActions {
+ style := buttonStyle
+ if i == m.currentDialog.Choice {
+ style = buttonSelectedStyle
+ }
+
+ prompt += fmt.Sprintf("%v", style.MarginLeft(13).Render(action))
+ }
+ }
+
+ out += dialogStyle.Render(prompt)
+ } else {
+ out += "\nActions:\n"
+
+ messageActions := readOnlyMessageActions
+ if !m.chosenMessageGone {
+ messageActions = append(messageActions, writeMessageActions...)
+ }
+
+ for i, action := range messageActions {
+ style := buttonStyle
+ if i == m.chosenAction {
+ style = buttonSelectedStyle
+ }
+
+ out += fmt.Sprintf("%v\n", style.MarginLeft(2).Render(action))
+ }
+ }
+
+ return out
+}
+
+func NewModel(backend Backend) Model {
+ columns := []table.Column{
+ {Title: "ID", Width: 8},
+ {Title: "UUID", Width: 40},
+ {Title: "Original Topic", Width: 20},
+ {Title: "Delayed For", Width: 14},
+ {Title: "Requeue In", Width: 14},
+ }
+
+ t := table.New(
+ table.WithColumns(columns),
+ table.WithFocused(true),
+ table.WithHeight(20),
+ )
+
+ s := table.DefaultStyles()
+ s.Header = s.Header.
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("240")).
+ BorderBottom(true).
+ Bold(false)
+ s.Selected = s.Selected.
+ Foreground(lipgloss.Color("229")).
+ Background(lipgloss.Color("57")).
+ Bold(false)
+ t.SetStyles(s)
+
+ return Model{
+ backend: backend,
+ sub: make(chan MessagesUpdated),
+ table: t,
+ }
+}
+
+type Dialog struct {
+ Prompt string
+ Action func() tea.Msg
+ Choice int
+ Running bool
+}
diff --git a/tools/pq/go.mod b/tools/pq/go.mod
new file mode 100644
index 000000000..4edb60ec2
--- /dev/null
+++ b/tools/pq/go.mod
@@ -0,0 +1,42 @@
+module github.com/ThreeDotsLabs/watermill/tools/pq
+
+go 1.23.0
+
+require (
+ github.com/ThreeDotsLabs/watermill v1.4.0-rc.2
+ github.com/charmbracelet/bubbles v0.19.0
+ github.com/charmbracelet/bubbletea v0.27.1
+ github.com/charmbracelet/lipgloss v0.13.0
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/lib/pq v1.10.9
+ github.com/pkg/errors v0.9.1
+ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561
+)
+
+require (
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/cenkalti/backoff/v3 v3.2.2 // indirect
+ github.com/charmbracelet/x/ansi v0.1.4 // indirect
+ github.com/charmbracelet/x/input v0.1.0 // indirect
+ github.com/charmbracelet/x/term v0.1.1 // indirect
+ github.com/charmbracelet/x/windows v0.1.0 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/oklog/ulid v1.3.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/sony/gobreaker v1.0.0 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+)
diff --git a/tools/pq/go.sum b/tools/pq/go.sum
new file mode 100644
index 000000000..d10441494
--- /dev/null
+++ b/tools/pq/go.sum
@@ -0,0 +1,92 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/ThreeDotsLabs/watermill v1.4.0-rc.2 h1:K62uIAKOkCHTXtAwY+Nj95vyLR0y25UMBsbf/FuWCeQ=
+github.com/ThreeDotsLabs/watermill v1.4.0-rc.2/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
+github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0=
+github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA=
+github.com/charmbracelet/bubbletea v0.27.1 h1:/yhaJKX52pxG4jZVKCNWj/oq0QouPdXycriDRA6m6r8=
+github.com/charmbracelet/bubbletea v0.27.1/go.mod h1:xc4gm5yv+7tbniEvQ0naiG9P3fzYhk16cTgDZQQW6YE=
+github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
+github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
+github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
+github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
+github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
+github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
+github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
+github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
+github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
+github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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/tools/pq/main.go b/tools/pq/main.go
new file mode 100644
index 000000000..292d50d9d
--- /dev/null
+++ b/tools/pq/main.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "log"
+
+ "github.com/ThreeDotsLabs/watermill/tools/pq/backend"
+ "github.com/ThreeDotsLabs/watermill/tools/pq/cli"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+var (
+ backendFlag = flag.String("backend", "", "backend to use")
+ topicFlag = flag.String("topic", "", "topic to use")
+ rawTopicFlag = flag.String("raw-topic", "", "raw topic to use")
+)
+
+func main() {
+ flag.Parse()
+
+ config := cli.BackendConfig{
+ Topic: *topicFlag,
+ RawTopic: *rawTopicFlag,
+ }
+
+ err := config.Validate()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var b cli.Backend
+ switch *backendFlag {
+ case "postgres":
+ b, err = backend.NewPostgresBackend(context.Background(), config)
+ if err != nil {
+ log.Fatal(err)
+ }
+ default:
+ log.Fatalf("unknown backend: %s", *backendFlag)
+ }
+
+ m := cli.NewModel(b)
+
+ p := tea.NewProgram(m, tea.WithAltScreen())
+ _, err = p.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+}