[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n<!--\nThe resources of our team are limited.\nIf you want to speed up fixing the problem, please follow the guidelines below.\nIt will help us to understand and reproduce the issue and to find a solution faster.\n-->\n\n### Steps to reproduce\n\n<!--\n⚠️  Important: Problem Reproduction Steps  ⚠️\nTo help us investigate and fix your issue effectively, we need clear reproduction steps.\nInclude relevant code snippets and descriptions that demonstrate the problem.\n-->\n\ndocker-compose.yml\n```yaml\n\n```\n\n```go\n// Your reproduction code goes here\n```\n\n### Expected behavior\n\n<!-- What should happen -->\n\n### Actual behavior\n\n<!-- What happens instead -->\n\n### Possible solution\n\n<!-- Consider submitting a pull request - it will help us provide faster feedback on your solution. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n## Feature request\n\n<!--\nThe resources of our team are limited. If you want to\nWe will be able to help you faster if you follow the guidelines below.\nIt will help us to understand and reproduce the issue and to find a solution faster.\n-->\n\n### Description\n\n<!-- Describe the new feature clearly and concisely -->\n\n### Example use case\n\n<!--\nProvide an example of how you would use this feature. \nIt will help us to understand the context of the request and to find the best solution.\n-->\n\n#### How it can look like in code\n\n```go\n\n```\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\nThanks for contributing to Watermill!\n\nThe following template aims to help contributors write a good description for their pull requests.\n**The more information you provide, the faster we will be able to review and merge your PR.**\n\nFeel free to skip this template for minor changes like typo fixes.\n\n-->\n\n### Motivation / Background\n\n<!--\n\nExplain the purpose of this Pull Request:\n- What issue or bug does it address?\n- What new functionality does it add?\n- Why are these changes needed?\nFor bug fixes, include \"Fixes #ISSUE\" to automatically link to the related issue.\n\n-->\n\n### Detail\n\n\n### Alternative approaches considered (if applicable)\n\n<!-- If applicable, describe alternative approaches you considered and why you chose this one. -->\n\n### Checklist\n\nThe resources of our team are limited. **There are a couple of things that you can do to help us merge your PR faster**:\n\n- [ ] I wrote tests for the changes.\n- [ ] All tests are passing.\n  - If you are testing a Pub/Sub, you can start Docker with `make up`.\n  - You can start with `make test_short` for a quick check.\n  - If you want to run all tests, use `make test`.\n- [ ] Code has no breaking changes.\n- [ ] _(If applicable)_ documentation on [watermill.io](https://watermill.io/) is updated.\n  - Documentation is built in the [github.com/ThreeDotsLabs/watermill/docs](https://github.com/ThreeDotsLabs/watermill/tree/master/docs).\n  - You can find development instructions in the [DEVELOP.md](https://github.com/ThreeDotsLabs/watermill/tree/master/docs/DEVELOP.md)."
  },
  {
    "path": ".github/workflows/master.yml",
    "content": "name: master\non:\n  push:\n    branches:\n      - master\njobs:\n  ci:\n    uses: ThreeDotsLabs/watermill/.github/workflows/tests.yml@master\n    with:\n      stress-tests: true\n      codecov: true\n    secrets:\n      codecov_token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/pr-examples.yml",
    "content": "name: pr-examples\non:\n  pull_request:\n    paths:\n      - '_examples/**/*'\n\njobs:\n  validate-examples:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v4\n        with:\n          go-version: '^1.21.1'\n      - run: make validate_examples\n        timeout-minutes: 30\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: pr\non:\n  pull_request:\njobs:\n  ci:\n    uses: ThreeDotsLabs/watermill/.github/workflows/tests.yml@master\n    with:\n      codecov: true\n    secrets:\n      codecov_token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: tests\non:\n  workflow_call:\n    inputs:\n      stress-tests:\n        description: 'Run stress tests'\n        required: false\n        type: boolean\n        default: false\n      codecov:\n        required: false\n        type: boolean\n        default: false\n      runs-on:\n        required: false\n        type: string\n        default: 'ubuntu-latest'\n    secrets:\n      codecov_token:\n        required: false\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v4\n        with:\n          go-version: '^1.21.1'\n      - run: make build\n\n  detect-modules:\n    runs-on: ubuntu-latest\n    outputs:\n      modules: ${{ steps.set-modules.outputs.modules }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '^1.21.1'\n      - id: set-modules\n        run: echo \"modules=$(go list -m -json | jq -s '.' | jq -c '[.[].Dir]')\" >> $GITHUB_OUTPUT\n\n  lint:\n    needs: detect-modules\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v4\n        with:\n          go-version: '^1.21.1'\n      - name: golangci-lint ${{ matrix.modules }}\n        uses: golangci/golangci-lint-action@v6.5.2\n        with:\n          working-directory: ${{ matrix.modules }}\n\n  tests:\n    needs: [build]\n    runs-on: ${{ inputs.runs-on }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v4\n        with:\n          go-version: '^1.21.1'\n      - run: cat .env >> $GITHUB_ENV || true\n      - run: make up\n      - run: make wait\n      - run: make test_short\n      - run: make test\n        timeout-minutes: 30\n      - run: make test_race\n      - run: make test_stress\n        if: ${{ inputs.stress-tests }}\n      - name: Dump docker logs on failure\n        if: failure()\n        uses: jwalton/gh-docker-logs@a8cb5301950dd4d2b86619cd487b3b281526b178 # v2.2.0\n  codecov:\n    runs-on: ${{ inputs.runs-on }}\n    if: ${{ inputs.codecov }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v4\n        with:\n          go-version: '^1.21.1'\n      - run: cat .env >> $GITHUB_ENV || true\n      - run: make up\n      - run: make wait\n      - run: make test_codecov\n      - uses: codecov/codecov-action@v4\n        with:\n          fail_ci_if_error: true\n          files: ./coverage.out\n          token: ${{ secrets.codecov_token }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\nvendor\ndocs/themes/\ndocs/node_modules/\ndocs/public/\ndocs/content/src-link\ndocs/content/middleware\ndocs/hugo_stats.json\n*.out\n*.log\n.mod-cache\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributors guide v0.1\n\n## How can I help?\n\nWe are always happy to help you in contributing to Watermill. If you have any ideas, please let us know on our [Discord server](https://watermill.io/support/).\n\nThere are multiple ways in which you can help us.\n\n### Existing issues\n\nYou can pick one of the existing issues. Most of the issues should have an estimation (S - small, M - medium, L - large).\n\n- [Good first issues list](https://github.com/ThreeDotsLabs/watermill/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - simple issues to begin with\n- [Help wanted issues list](https://github.com/ThreeDotsLabs/watermill/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) - tasks that are already more or less clear, and you can start to implement them pretty quickly\n\n### New Pub/Sub implementations\n\nIf you have an idea to create a Pub/Sub based on some technology and it is not listed yet in our issues (because we don't know it, or it is just some crazy idea, like physical mail based Pub/Sub), feel free to add your own implementation.\nYou can do it in your private repository or if you want, we can move it to `ThreeDotsLabs/watermill-[name]`.\n\n*Please keep in mind that you will not be able to push changes directly to the master branch in a project in our organization*.\n\nWhen adding a new Pub/Sub implementation, you should start with this guide: [https://watermill.io/docs/pub-sub-implementing/](https://watermill.io/docs/pub-sub-implementing/).\n\n### New ideas\n\nIf you have any idea that is not covered in the issues list, please post a new issue describing it. \nIt's recommended to discuss your idea on [Discord](https://discord.gg/QV6VFg4YQE)/GitHub before creating production-ready implementation - in some situations, it may save a lot of your time before implementing something that can be simplified or done more easily. :)\n\nIn general, it's helpful to discuss a Proof of Concept to align with the idea.\n\n## Local development\n\nMakefile and docker-compose (for Pub/Subs) are your friends. You can run all tests locally (they are running in CI in the same way).\n\nUseful commands:\n- `make up` - docker-compose up\n- `make test` - tests\n- `make test_short` - run short tests (useful to perform a very fast check after changes)\n- `make fmt` - do goimports\n\n## Code standards\n\n- you should run `make fmt`\n- [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)\n- [Effective Go](https://golang.org/doc/effective_go.html)\n- SOLID\n- code should be open for configuration and not coupled to any serialization method (for example: [AMQP marshaler](https://github.com/ThreeDotsLabs/watermill-amqp/blob/master/pkg/amqp/marshaler.go), [AMQP Config](https://github.com/ThreeDotsLabs/watermill-amqp/blob/master/pkg/amqp/config.go)\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Three Dots Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "up:\n\ntest:\n\tgo test ./...\n\ntest_v:\n\tgo test -v ./...\n\ntest_short:\n\tgo test ./... -short\n\ntest_race:\n\tgo test ./... -short -race\n\ntest_stress:\n\tgo test -tags=stress -timeout=30m ./...\n\ntest_codecov:\n\tgo test -coverprofile=coverage.out -covermode=atomic ./...\n\ntest_reconnect:\n\tgo test -tags=reconnect ./...\n\nbuild:\n\tgo build ./...\n\nwait:\n\nfmt:\n\tgo fmt ./...\n\tgoimports -l -w .\n\ngenerate_gomod:\n\trm go.mod go.sum || true\n\tgo mod init github.com/ThreeDotsLabs/watermill\n\n\tgo install ./...\n\tsed -i '\\|go |d' go.mod\n\tgo mod edit -fmt\n\nupdate_examples_deps:\n\tgo run dev/update-examples-deps/main.go\n\nvalidate_examples:\n\t(cd dev/validate-examples/ && go run main.go)\n"
  },
  {
    "path": "README.md",
    "content": "# Watermill\n<img align=\"right\" width=\"300\" src=\"https://watermill.io/img/gopher.svg\">\n\n[![CI Status](https://github.com/ThreeDotsLabs/watermill/actions/workflows/master.yml/badge.svg)](https://github.com/ThreeDotsLabs/watermill/actions/workflows/master.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/ThreeDotsLabs/watermill.svg)](https://pkg.go.dev/github.com/ThreeDotsLabs/watermill)\n[![Go Report Card](https://goreportcard.com/badge/github.com/ThreeDotsLabs/watermill)](https://goreportcard.com/report/github.com/ThreeDotsLabs/watermill)\n[![codecov](https://codecov.io/gh/ThreeDotsLabs/watermill/branch/master/graph/badge.svg)](https://codecov.io/gh/ThreeDotsLabs/watermill)\n\nWatermill is a Go library for working efficiently with message streams. It is intended\nfor building event driven applications, enabling event sourcing, RPC over messages,\nsagas and basically whatever else comes to your mind. You can use conventional pub/sub\nimplementations like Kafka or RabbitMQ, but also HTTP or PostgreSQL if that fits your use case.\n\n## Goals\n\n* **Easy** to understand.\n* **Universal** - event-driven architecture, messaging, stream processing, CQRS - use it for whatever you need.\n* **Fast** (see [Benchmarks](#benchmarks)).\n* **Flexible** with middlewares, plugins and Pub/Sub configurations.\n* **Resilient** - using proven technologies and passing stress tests (see [Stability](#stability)).\n\n## Getting Started\n\nPick what you like the best or see in order:\n\n1. [Quickstart](https://watermill.io/learn/quickstart/) — learn by coding!\n2. Follow the [Getting Started guide](https://watermill.io/learn/getting-started/).\n3. See examples below.\n4. Read the full documentation: https://watermill.io/\n\n## Our online hands-on training\n\nGo Event-Driven goes beyond Watermill Quickstart. You'll learn industry standard concepts and patterns like:\n\n* Handling at-least-once delivery\n* Asynchronous read models\n* Events & Commands\n* Observability\n* Message ordering\n* Sagas\n\n<a href=\"https://threedots.tech/event-driven/?utm_source=watermill-readme\"><img align=\"center\" width=\"400\" src=\"https://threedots.tech/event-driven-banner.png\"></a>\n\n## Examples\n\n* Basic\n    * [Your first app](_examples/basic/1-your-first-app) - **start here!**\n    * [Realtime feed](_examples/basic/2-realtime-feed)\n    * [Router](_examples/basic/3-router)\n    * [Metrics](_examples/basic/4-metrics)\n    * [CQRS with protobuf](_examples/basic/5-cqrs-protobuf)\n* [Pub/Subs usage](_examples/pubsubs)\n    * These examples are part of the [Getting started guide](https://watermill.io/learn/getting-started/) and show usage of a single Pub/Sub at a time.\n* Real-world examples\n    * [Exactly-once delivery counter](_examples/real-world-examples/exactly-once-delivery-counter)\n    * [Receiving webhooks](_examples/real-world-examples/receiving-webhooks)\n    * [Sending webhooks](_examples/real-world-examples/sending-webhooks)\n    * [Synchronizing Databases](_examples/real-world-examples/synchronizing-databases)\n    * [Persistent Event Log](_examples/real-world-examples/persistent-event-log)\n    * [Transactional Events](_examples/real-world-examples/transactional-events)\n    * [Real-time HTTP updates with Server-Sent Events](_examples/real-world-examples/server-sent-events)\n    * [Real-time HTTP updates with Server-Sent Events and htmx](_examples/real-world-examples/server-sent-events-htmx)\n* Complete projects\n    * [NATS example with live code reloading](https://github.com/ThreeDotsLabs/nats-example)\n    * [RabbitMQ, webhooks and Kafka integration](https://github.com/ThreeDotsLabs/event-driven-example)\n\n## Background\n\nBuilding distributed and scalable services is rarely as easy as some may suggest. There is a\nlot of hidden knowledge that comes with writing such systems. Just like you don't need to know the\nwhole TCP stack to create a HTTP REST server, you shouldn't need to study all of this knowledge to\nstart with building message-driven applications.\n\nWatermill's goal is to make communication with messages as easy to use as HTTP routers. It provides\nthe tools needed to begin working with event-driven architecture and allows you to learn the details\non the go.\n\nAt the heart of Watermill there is one simple interface:\n```go\nfunc(*Message) ([]*Message, error)\n```\n\nYour handler receives a message and decides whether to publish new message(s) or return\nan error. What happens next is up to the middlewares you've chosen.\n\nYou can find more about our motivations in our [*Introducing Watermill* blog post](https://threedots.tech/post/introducing-watermill/).\n\n## Pub/Subs\n\nAll publishers and subscribers have to implement an interface:\n\n```go\ntype Publisher interface {\n\tPublish(topic string, messages ...*Message) error\n\tClose() error\n}\n\ntype Subscriber interface {\n\tSubscribe(ctx context.Context, topic string) (<-chan *Message, error)\n\tClose() error\n}\n```\n\nSupported Pub/Subs:\n\n- AMQP (RabbitMQ) Pub/Sub [(`github.com/ThreeDotsLabs/watermill-amqp/v3`)](https://github.com/ThreeDotsLabs/watermill-amqp/)\n- AWS SNS/SQS Pub/Sub [(`github.com/ThreeDotsLabs/watermill-aws`)](https://github.com/ThreeDotsLabs/watermill-aws/)\n- Bolt Pub/Sub [(`github.com/ThreeDotsLabs/watermill-bolt`)](https://github.com/ThreeDotsLabs/watermill-bolt/)\n- Firestore Pub/Sub [(`github.com/ThreeDotsLabs/watermill-firestore`)](https://github.com/ThreeDotsLabs/watermill-firestore/)\n- Google Cloud Pub/Sub [(`github.com/ThreeDotsLabs/watermill-googlecloud/v2`)](https://github.com/ThreeDotsLabs/watermill-googlecloud/)\n- HTTP Pub/Sub [(`github.com/ThreeDotsLabs/watermill-http/v2`)](https://github.com/ThreeDotsLabs/watermill-http/)\n- io.Reader/io.Writer Pub/Sub [(`github.com/ThreeDotsLabs/watermill-io`)](https://github.com/ThreeDotsLabs/watermill-io/)\n- Kafka Pub/Sub [(`github.com/ThreeDotsLabs/watermill-kafka/v3`)](https://github.com/ThreeDotsLabs/watermill-kafka/)\n- NATS Jetstream Pub/Sub [(`github.com/ThreeDotsLabs/watermill-nats/v2`)](https://github.com/ThreeDotsLabs/watermill-nats/)\n- Redis Stream Pub/Sub [(`github.com/ThreeDotsLabs/watermill-redisstream`)](https://github.com/ThreeDotsLabs/watermill-redisstream/)\n- SQL (MySQL / PostgreSQL) Pub/Sub [(`github.com/ThreeDotsLabs/watermill-sql/v4`)](https://github.com/ThreeDotsLabs/watermill-sql/)\n- SQLite Pub/Sub (Beta) [(`github.com/ThreeDotsLabs/watermill-sqlite/`)](https://github.com/ThreeDotsLabs/watermill-sqlite/)\n\nAll Pub/Subs implementation documentation can be found in the [documentation](https://watermill.io/pubsubs/).\n\n## Unofficial libraries\n\nCan't find your favorite Pub/Sub or library integration? Check [Awesome Watermill](https://watermill.io/docs/awesome/).\n\nIf you know another library or are an author of one, please [add it to the list](https://github.com/ThreeDotsLabs/watermill/edit/master/docs/content/docs/awesome.md).\n\n## Contributing\n\nPlease check our [contributing guide](CONTRIBUTING.md).\n\n## Stability\n\nWatermill v1.0.0 has been released and is production-ready. The public API is stable and will not change without changing the major version.\n\nTo ensure that all Pub/Subs are stable and safe to use in production, we created a [set of tests](https://github.com/ThreeDotsLabs/watermill/blob/master/pubsub/tests/test_pubsub.go#L34) that need to pass for each of the implementations before merging to master.\nAll tests are also executed in [*stress*](https://github.com/ThreeDotsLabs/watermill/blob/master/pubsub/tests/test_pubsub.go#L171) mode - that means that we are running all the tests **20x** in parallel.\n\nAll tests are run with the race condition detector enabled (`-race` flag in tests).\n\nFor more information about debugging tests, you should check [tests troubleshooting guide](http://watermill.io/docs/troubleshooting/#debugging-pubsub-tests).\n\n## Benchmarks\n\nInitial tools for benchmarking Pub/Subs can be found in [watermill-benchmark](https://github.com/ThreeDotsLabs/watermill-benchmark).\n\nAll benchmarks are being done on a single 16 CPU VM instance, running one binary and dependencies in Docker Compose.\n\nThese numbers are meant to serve as a rough estimate of how fast messages can be processed by different Pub/Subs.\nKeep in mind that the results can be vastly different, depending on the setup and configuration (both much lower and higher).\n\nHere's the short version for message size of 16 bytes.\n\n| Pub/Sub                         | Publish (messages / s) | Subscribe (messages / s) |\n|---------------------------------|------------------------|--------------------------|\n| GoChannel                       | 315,776                | 138,743                  |\n| Redis Streams                   | 59,158                 | 12,134                   |\n| NATS Jetstream (16 Subscribers) | 50,668                 | 34,713                   |\n| Kafka (one node)                | 41,492                 | 101,669                  |\n| SQL (MySQL, batch size=100)     | 6,371                  | 2,794                    |\n| SQL (PostgreSQL, batch size=1)  | 2,831                  | 9,460                    |\n| Google Cloud Pub/Sub            | 3,027                  | 28,589                   |\n| AMQP (RabbitMQ)                 | 2,770                  | 14,604                   |\n\n## Support\n\nIf you didn't find the answer to your question in [the documentation](https://watermill.io/), feel free to ask us directly!\n\nPlease join us on the `#watermill` channel on the [Three Dots Labs Discord](https://discord.gg/QV6VFg4YQE).\n\n## Why the name?\n\nIt processes streams!\n\n## License\n\n[MIT License](./LICENSE)\n"
  },
  {
    "path": "RELEASE-PROCEDURE.md",
    "content": "# Release procedure\n\n1. Generate clean go.mod: `make generate_gomod`\n2. Push to master\n3. Update missing documentation\n4. Check snippets in documentation (sometimes `first_line_contains` or `last_line_contains` can change position and load too much)\n5. Add breaking changes to `UPGRADE-[new-version].md`\n6. Push to master\n7. [Add release in GitHub](https://github.com/ThreeDotsLabs/watermill/releases)\n8. Update Pub/Subs versions\n9. Update and validate examples: `make validate_examples`\n"
  },
  {
    "path": "UPGRADE-0.3.md",
    "content": "# UPGRADE FROM 0.2.x to 0.3\n\n# `watermill/message`\n\n- `message.Message.Ack` and `message.Message.Nack` now return `bool` instead of `error`\n- `message.Subscriber.Subscribe` now accepts `context.Context` as the first argument\n- `message.Subscriber.Subscribe` now returns `<-chan *Message` instead of `chan *Message`\n- `message.Router.AddHandler` and `message.Router.AddNoPublisherHandler` now panic, instead of returning error\n\n# `watermill/message/infrastructure`\n\n- updated all Pub/Subs to new `message.Subscriber` interface\n- `gochannel.NewGoChannel` now accepts `gochannel.Config`, instead of positional parameters\n- `http.NewSubscriber` now accepts `http.SubscriberConfig`, instead of positional parameters\n\n# `watermill/message/router/middleware`\n\n- `metrics.NewMetrics` is removed, please use the [metrics](components/metrics) component instead\n\n# `watermill`\n\n- `watermill.LoggerAdapter` interface now requires a `With(fields LogFields) LoggerAdapter` method\n"
  },
  {
    "path": "UPGRADE-0.4.md",
    "content": "# UPGRADE FROM 0.3.x to 0.4\n\n## `watermill/components/cqrs`\n\n### `CommandHandler.HandlerName` and `EventHandler.HandlerName` was added to the interface.\n\nIf you are using metrics component, you may want to keep backward capability with handler names. In other cases, you can implement your own method of generating handler name.\n\nKeeping backward capability for **event handlers**:\n\n```\nfunc (h CommandHandler) HandlerName() string {\n    return fmt.Sprintf(\"command_processor-%s\", h)\n}\n```\n\nKeeping backward capability for **command handlers**:\n\n```\nfunc (h EventHandler) HandlerName() string {\n    return fmt.Sprintf(\"event_processor-%s\", ObjectName(h))\n}\n```\n\n### Added `CommandsSubscriberConstructor` and `EventsSubscriberConstructor`\n\nFrom now on, `CommandsSubscriberConstructor` and `EventsSubscriberConstructor` are passed to constructors in CQRS component.\n\nThey allow creating customized subscribers for every handler. For usage examples please check [_examples/cqrs-protobuf](_examples/cqrs-protobuf).\n\n\n### Added context to `CommandHandler.Handle`, `CommandBus.Send`, `EventHandler.Handle` and `EventBus.Send`\n\nAdded missing context, which is passed to Publish function and handlers.\n\n### Other\n\n- `NewCommandProcessor` and `NewEventProcessor` now return an error instead of panic\n- `DuplicateCommandHandlerError` is returned instead of panic when two handlers are handling the same command\n- `CommandProcessor.routerHandlerFunc` and `EventProcessor.routerHandlerFunc` are now private\n- using `GenerateCommandsTopic` and `GenerateEventsTopic` functions instead of constant topic to allow more flexibility\n\n\n## `watermill/message/infrastructure/amqp`\n\n### `Config.QueueBindConfig.RoutingKey` was replaced with `GenerateRoutingKey`\n\nFor backward compatibility, when using the constant value you should use a function:\n\n\n```\nfunc(topic string) string {\n    return \"routing_key\"\n}\n```\n\n\n## `message/router/middleware`\n\n- `PoisonQueue` is now `PoisonQueue(pub message.Publisher, topic string) (message.HandlerMiddleware, error)`, not a struct\n\n\n## `message/router.go`\n\n- From now on, when all handlers are stopped, the router will also stop (`TestRouter_stop_when_all_handlers_stopped` test)\n"
  },
  {
    "path": "UPGRADE-1.0.md",
    "content": "# Upgrade instructions from v0.4.X\n\nIn v1.0.0 we introduced a couple of breaking changes, to keep a stable API until version v2.\n\n## Migrating Pub/Subs\n\nAll Pub/Subs (excluding go-channel implementation) were moved to separated repositories.\nYou can replace all import paths, with provided `sed`:\n\n\tfind . -type f -iname '*.go' -exec sed -i -E \"s/github\\.com\\/ThreeDotsLabs\\/watermill\\/message\\/infrastructure\\/(amqp|googlecloud|http|io|kafka|nats|sql)/github.com\\/ThreeDotsLabs\\/watermill-\\1\\/pkg\\/\\1/\" \"{}\" +;\n\tfind . -type f -iname '*.go' -exec sed -i -E \"s/github\\.com\\/ThreeDotsLabs\\/watermill\\/message\\/infrastructure\\/gochannel/github\\.com\\/ThreeDotsLabs\\/watermill\\/pubsub\\/gochannel/\" \"{}\" +;\n\n# Breaking changes\n- `message.PubSub` interface was removed\n- `message.NewPubSub` constructor was removed\n- `message.NoPublishHandlerFunc` is now passed to `message.Router.AddNoPublisherHandler`, instead of `message.HandlerFunc`.\n- `message.Router.Run` now requires `context.Context` in parameter\n- `PrometheusMetricsBuilder.DecoratePubSub` was removed (because of `message.PubSub` interface removal)\n- `cars.ObjectName` was renamed to `cqrs.FullyQualifiedStructName`\n- `github.com/ThreeDotsLabs/watermill/message/infrastructure/gochannel` was moved to `github.com/ThreeDotsLabs/watermill/pubsub/gochannel`\n- `middleware.Retry` configuration parameters have been renamed\n- Universal Pub/Sub tests have been moved from `github.com/ThreeDotsLabs/watermill/message/infrastructure` to `github.com/ThreeDotsLabs/watermill/pubsub/tests`\n- All universal tests require now `TestContext`.\n- Removed `context` from `googlecloud.NewPublisher`\n"
  },
  {
    "path": "_examples/basic/1-your-first-app/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"received event {ID:[0-9]+}\"\n"
  },
  {
    "path": "_examples/basic/1-your-first-app/README.md",
    "content": "# Your first Watermill app\n\nThis example project shows a basic setup of Watermill. The application runs in a loop, consuming events from a Kafka\ntopic, modifying them and publishing to another topic.\n\nThere's a docker-compose file included, so you can run the example and see it in action.\n\nTo understand the background and internals, see [getting started guide](https://watermill.io/learn/getting-started/).\n\n## Files\n\n- [main.go](main.go) - example source code, the **most interesting file for you**\n- [docker-compose.yml](docker-compose.yml) - local environment Docker Compose configuration, contains Golang, Kafka and Zookeeper\n- [go.mod](go.mod) - Go modules dependencies, you can find more information at [Go wiki](https://github.com/golang/go/wiki/Modules)\n- [go.sum](go.sum) - Go modules checksums\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See the [installation guide](https://docs.docker.com/compose/install/).\n\n## Running\n\n```bash\n> docker-compose up\n[some initial logs]\nserver_1     | 2019/08/29 19:41:23 received event {ID:0}\nserver_1     | 2019/08/29 19:41:23 received event {ID:1}\nserver_1     | 2019/08/29 19:41:23 received event {ID:2}\nserver_1     | 2019/08/29 19:41:23 received event {ID:3}\nserver_1     | 2019/08/29 19:41:24 received event {ID:4}\nserver_1     | 2019/08/29 19:41:25 received event {ID:5}\nserver_1     | 2019/08/29 19:41:26 received event {ID:6}\nserver_1     | 2019/08/29 19:41:27 received event {ID:7}\nserver_1     | 2019/08/29 19:41:28 received event {ID:8}\nserver_1     | 2019/08/29 19:41:29 received event {ID:9}\n```\n\nOpen another terminal and take a look at Kafka topics to see that all messages are there. The initial events should be present on the `events` topic:\n\n```bash\n> docker-compose exec server mill kafka consume -b kafka:9092 --topic events\n\n{\"id\":12}\n{\"id\":13}\n{\"id\":14}\n{\"id\":15}\n{\"id\":16}\n{\"id\":17}\n```\n\nAnd the processed messages will be stored in the `events-processed` topic:\n\n```bash\n> docker-compose exec server mill kafka consume -b kafka:9092 -t events-processed \n\n{\"processed_id\":21,\"time\":\"2019-08-29T19:42:31.4464598Z\"}\n{\"processed_id\":22,\"time\":\"2019-08-29T19:42:32.4501767Z\"}\n{\"processed_id\":23,\"time\":\"2019-08-29T19:42:33.4530692Z\"}\n{\"processed_id\":24,\"time\":\"2019-08-29T19:42:34.4561694Z\"}\n{\"processed_id\":25,\"time\":\"2019-08-29T19:42:35.4608918Z\"}\n```\n"
  },
  {
    "path": "_examples/basic/1-your-first-app/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: >\n      /bin/sh -c \"go install github.com/ThreeDotsLabs/watermill/tools/mill@latest &&\n                  go run main.go\"\n\n  kafka:\n    image: bitnami/kafka:3.5.0\n    restart: unless-stopped\n    environment:\n      ALLOW_PLAINTEXT_LISTENER: yes\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n"
  },
  {
    "path": "_examples/basic/1-your-first-app/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n"
  },
  {
    "path": "_examples/basic/1-your-first-app/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/1-your-first-app/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tbrokers      = []string{\"kafka:9092\"}\n\tconsumeTopic = \"events\"\n\tpublishTopic = \"events-processed\"\n\n\tlogger = watermill.NewStdLogger(\n\t\ttrue,  // debug\n\t\tfalse, // trace\n\t)\n\tmarshaler = kafka.DefaultMarshaler{}\n)\n\ntype event struct {\n\tID int `json:\"id\"`\n}\n\ntype processedEvent struct {\n\tProcessedID int       `json:\"processed_id\"`\n\tTime        time.Time `json:\"time\"`\n}\n\nfunc main() {\n\tpublisher := createPublisher()\n\n\t// Subscriber is created with consumer group handler_1\n\tsubscriber := createSubscriber(\"handler_1\")\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddPlugin(plugin.SignalsHandler)\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\t// Adding a handler (multiple handlers can be added)\n\trouter.AddHandler(\n\t\t\"handler_1\",  // handler name, must be unique\n\t\tconsumeTopic, // topic from which messages should be consumed\n\t\tsubscriber,\n\t\tpublishTopic, // topic to which messages should be published\n\t\tpublisher,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\tconsumedPayload := event{}\n\t\t\terr := json.Unmarshal(msg.Payload, &consumedPayload)\n\t\t\tif err != nil {\n\t\t\t\t// When a handler returns an error, the default behavior is to send a Nack (negative-acknowledgement).\n\t\t\t\t// The message will be processed again.\n\t\t\t\t//\n\t\t\t\t// You can change the default behaviour by using middlewares, like Retry or PoisonQueue.\n\t\t\t\t// You can also implement your own middleware.\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfmt.Printf(\"received event %+v\\n\", consumedPayload)\n\n\t\t\tnewPayload, err := json.Marshal(processedEvent{\n\t\t\t\tProcessedID: consumedPayload.ID,\n\t\t\t\tTime:        time.Now(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnewMessage := message.NewMessage(watermill.NewUUID(), newPayload)\n\n\t\t\treturn []*message.Message{newMessage}, nil\n\t\t},\n\t)\n\n\t// Simulate incoming events in the background\n\tgo simulateEvents(publisher)\n\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// createPublisher is a helper function that creates a Publisher, in this case - the Kafka Publisher.\nfunc createPublisher() message.Publisher {\n\tkafkaPublisher, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   brokers,\n\t\t\tMarshaler: marshaler,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn kafkaPublisher\n}\n\n// createSubscriber is a helper function similar to the previous one, but in this case it creates a Subscriber.\nfunc createSubscriber(consumerGroup string) message.Subscriber {\n\tkafkaSubscriber, err := kafka.NewSubscriber(\n\t\tkafka.SubscriberConfig{\n\t\t\tBrokers:       brokers,\n\t\t\tUnmarshaler:   marshaler,\n\t\t\tConsumerGroup: consumerGroup, // every handler will use a separate consumer group\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn kafkaSubscriber\n}\n\n// simulateEvents produces events that will be later consumed.\nfunc simulateEvents(publisher message.Publisher) {\n\ti := 0\n\tfor {\n\t\te := event{\n\t\t\tID: i,\n\t\t}\n\n\t\tpayload, err := json.Marshal(e)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\terr = publisher.Publish(consumeTopic, message.NewMessage(\n\t\t\twatermill.NewUUID(), // internal uuid of the message, useful for debugging\n\t\t\tpayload,\n\t\t))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ti++\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/.validate_example_subscribing.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"Adding to feed\"\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/README.md",
    "content": "# Realtime Feed\n\nThis example features a very busy blogging platform, with thousands of messages showing up on your feed.\n\nThere are two separate applications (microservices) integrating over a Kafka topic. The [`producer`](producer/) generates\nthousands of \"posts\" and publishes them to the topic. The [`consumer`](consumer/) subscribes to this topic and \ndisplays each post on the standard output.\n\nThe consumer has a throttling middleware enabled, so you have a chance to actually read the posts.\n\nTo understand the background and internals, see [getting started guide](https://watermill.io/learn/getting-started/).\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See the [installation guide](https://docs.docker.com/compose/install/).\n\n## Running\n\n```bash\ndocker-compose up\n```\n\nYou should see the live feed of posts on the standard output.\n\n## Exercises\n\n1. Peek into the posts counter published on `posts_count` topic.\n\n```\ndocker-compose exec consumer mill kafka consume -b kafka:9092 -t posts_count\n```\n\n2. Add a persistent storage for incoming posts in the consumer service, instead of displaying them.\n   Consider using the [SQL Publisher](https://github.com/ThreeDotsLabs/watermill-sql).\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/consumer/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/consumer/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/consumer/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tmarshaler = kafka.DefaultMarshaler{}\n\tbrokers   = []string{\"kafka:9092\"}\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\tlogger.Info(\"Starting the consumer\", nil)\n\n\tpub, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   brokers,\n\t\t\tMarshaler: marshaler,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tr, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tretryMiddleware := middleware.Retry{\n\t\tMaxRetries:      1,\n\t\tInitialInterval: time.Millisecond * 10,\n\t}\n\n\tpoisonQueue, err := middleware.PoisonQueue(pub, \"poison_queue\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tr.AddMiddleware(\n\t\t// Recoverer middleware recovers panic from handlers and middlewares\n\t\tmiddleware.Recoverer,\n\n\t\t// Limit incoming messages to 10 per second\n\t\tmiddleware.NewThrottle(10, time.Second).Middleware,\n\n\t\t// If the retries limit is exceeded (see retryMiddleware below), the message is sent\n\t\t// to the poison queue (published to poison_queue topic)\n\t\tpoisonQueue,\n\n\t\t// Retry middleware retries message processing if an error occurred in the handler\n\t\tretryMiddleware.Middleware,\n\n\t\t// Correlation ID middleware adds the correlation ID of the consumed message to each produced message.\n\t\t// It's useful for debugging.\n\t\tmiddleware.CorrelationID,\n\n\t\t// Simulate errors or panics from handler\n\t\tmiddleware.RandomFail(0.01),\n\t\tmiddleware.RandomPanic(0.01),\n\t)\n\n\t// Close the router when a SIGTERM is sent\n\tr.AddPlugin(plugin.SignalsHandler)\n\n\t// Handler that counts consumed posts\n\tr.AddHandler(\n\t\t\"posts_counter\",\n\t\t\"posts_published\",\n\t\tcreateSubscriber(\"posts_counter\", logger),\n\t\t\"posts_count\",\n\t\tpub,\n\t\tPostsCounter{memoryCountStorage{new(int64)}}.Count,\n\t)\n\n\t// Handler that generates \"feed\" from consumed posts\n\t//\n\t// This implementation just prints the posts on stdout,\n\t// but production ready implementation would save posts to some persistent storage.\n\tr.AddConsumerHandler(\n\t\t\"feed_generator\",\n\t\t\"posts_published\",\n\t\tcreateSubscriber(\"feed_generator\", logger),\n\t\tFeedGenerator{printFeedStorage{}}.UpdateFeed,\n\t)\n\n\tif err = r.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc createSubscriber(consumerGroup string, logger watermill.LoggerAdapter) message.Subscriber {\n\tsub, err := kafka.NewSubscriber(\n\t\tkafka.SubscriberConfig{\n\t\t\tBrokers:       brokers,\n\t\t\tUnmarshaler:   marshaler,\n\t\t\tConsumerGroup: consumerGroup,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn sub\n}\n\ntype postsCountUpdated struct {\n\tNewCount int64 `json:\"new_count\"`\n}\n\ntype countStorage interface {\n\tCountAdd() (int64, error)\n\tCount() (int64, error)\n}\n\ntype memoryCountStorage struct {\n\tcount *int64\n}\n\nfunc (m memoryCountStorage) CountAdd() (int64, error) {\n\treturn atomic.AddInt64(m.count, 1), nil\n}\n\nfunc (m memoryCountStorage) Count() (int64, error) {\n\treturn atomic.LoadInt64(m.count), nil\n}\n\ntype PostsCounter struct {\n\tcountStorage countStorage\n}\n\nfunc (p PostsCounter) Count(msg *message.Message) ([]*message.Message, error) {\n\t// When implementing counter for production use, you'd probably need to add some kind of deduplication here,\n\t// unless the used Pub/Sub supports exactly-once delivery.\n\n\tnewCount, err := p.countStorage.CountAdd()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot add count: %w\", err)\n\t}\n\n\tproducedMsg := postsCountUpdated{NewCount: newCount}\n\tb, err := json.Marshal(producedMsg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn []*message.Message{message.NewMessage(watermill.NewUUID(), b)}, nil\n}\n\n// postAdded might look similar to the postAdded type from producer.\n// It's intentionally not imported here. We avoid coupling the services at the cost of duplication.\n// We don't need all of its data either (content is not displayed on the feed).\ntype postAdded struct {\n\tOccurredOn time.Time `json:\"occurred_on\"`\n\tAuthor     string    `json:\"author\"`\n\tTitle      string    `json:\"title\"`\n}\n\ntype feedStorage interface {\n\tAddToFeed(title, author string, time time.Time) error\n}\n\ntype printFeedStorage struct{}\n\nfunc (printFeedStorage) AddToFeed(title, author string, time time.Time) error {\n\tfmt.Printf(\"Adding to feed: %s by %s @%s\\n\", title, author, time)\n\treturn nil\n}\n\ntype FeedGenerator struct {\n\tfeedStorage feedStorage\n}\n\nfunc (f FeedGenerator) UpdateFeed(message *message.Message) error {\n\tevent := postAdded{}\n\tif err := json.Unmarshal(message.Payload, &event); err != nil {\n\t\treturn err\n\t}\n\n\terr := f.feedStorage.AddToFeed(event.Title, event.Author, event.OccurredOn)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot update feed: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/docker-compose.yml",
    "content": "services:\n  producer:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n    - kafka\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app/producer/\n    command: go run main.go\n\n  consumer:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n    - kafka\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app/consumer/\n    command: >\n      /bin/sh -c \"go install github.com/ThreeDotsLabs/watermill/tools/mill@latest &&\n                  go run main.go\"\n\n  zookeeper:\n    image: confluentinc/cp-zookeeper:7.3.1\n    restart: unless-stopped\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n    logging:\n      driver: none\n\n  kafka:\n    image: confluentinc/cp-kafka:7.3.1\n    restart: unless-stopped\n    logging:\n      driver: none\n    depends_on:\n    - zookeeper\n    environment:\n      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/producer/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n\tgithub.com/brianvoe/gofakeit/v6 v6.28.0\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/producer/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/2-realtime-feed/producer/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/brianvoe/gofakeit/v6\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\nvar (\n\tbrokers = []string{\"kafka:9092\"}\n\n\tmessagesPerSecond = 100\n\tnumWorkers        = 20\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\tlogger.Info(\"Starting the producer\", watermill.LogFields{})\n\n\tpublisher, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   brokers,\n\t\t\tMarshaler: kafka.DefaultMarshaler{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer publisher.Close()\n\n\tcloseCh := make(chan struct{})\n\tworkersGroup := &sync.WaitGroup{}\n\tworkersGroup.Add(numWorkers)\n\n\tfor i := 0; i < numWorkers; i++ {\n\t\tgo worker(publisher, workersGroup, closeCh)\n\t}\n\n\t// wait for SIGINT\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt)\n\t<-c\n\n\t// signal for the workers to stop publishing\n\tclose(closeCh)\n\n\t// Waiting for all messages to be published\n\tworkersGroup.Wait()\n\n\tlogger.Info(\"All messages published\", nil)\n}\n\n// worker publishes messages until closeCh is closed.\nfunc worker(publisher message.Publisher, wg *sync.WaitGroup, closeCh chan struct{}) {\n\tticker := time.NewTicker(time.Duration(int(time.Second) / messagesPerSecond))\n\n\tfor {\n\t\tselect {\n\t\tcase <-closeCh:\n\t\t\tticker.Stop()\n\t\t\twg.Done()\n\t\t\treturn\n\n\t\tcase <-ticker.C:\n\t\t}\n\n\t\tmsgPayload := postAdded{\n\t\t\tOccurredOn: time.Now(),\n\t\t\tAuthor:     gofakeit.Username(),\n\t\t\tTitle:      gofakeit.Sentence(rand.Intn(5) + 1),\n\t\t\tContent:    gofakeit.Sentence(rand.Intn(10) + 5),\n\t\t}\n\n\t\tpayload, err := json.Marshal(msgPayload)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tmsg := message.NewMessage(watermill.NewUUID(), payload)\n\n\t\t// Use a middleware to set the correlation ID, it's useful for debugging\n\t\tmiddleware.SetCorrelationID(watermill.NewShortUUID(), msg)\n\t\terr = publisher.Publish(\"posts_published\", msg)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"cannot publish message:\", err)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\ntype postAdded struct {\n\tOccurredOn time.Time `json:\"occurred_on\"`\n\n\tAuthor string `json:\"author\"`\n\tTitle  string `json:\"title\"`\n\n\tContent string `json:\"content\"`\n}\n"
  },
  {
    "path": "_examples/basic/3-router/.validate_example.yml",
    "content": "validation_cmd: \"go run main.go\"\ntimeout: 120\nexpected_output: \"Received message: [0-9a-f\\\\-]+\"\n"
  },
  {
    "path": "_examples/basic/3-router/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire github.com/ThreeDotsLabs/watermill v1.5.1\n\nrequire (\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n)\n"
  },
  {
    "path": "_examples/basic/3-router/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/3-router/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nvar (\n\t// For this example, we're using just a simple logger implementation,\n\t// You probably want to ship your own implementation of `watermill.LoggerAdapter`.\n\tlogger = watermill.NewStdLogger(false, false)\n)\n\nfunc main() {\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// SignalsHandler will gracefully shutdown Router when SIGTERM is received.\n\t// You can also close the router by just calling `r.Close()`.\n\trouter.AddPlugin(plugin.SignalsHandler)\n\n\t// Router level middleware are executed for every message sent to the router\n\trouter.AddMiddleware(\n\t\t// CorrelationID will copy the correlation id from the incoming message's metadata to the produced messages\n\t\tmiddleware.CorrelationID,\n\n\t\t// The handler function is retried if it returns an error.\n\t\t// After MaxRetries, the message is Nacked and it's up to the PubSub to resend it.\n\t\tmiddleware.Retry{\n\t\t\tMaxRetries:      3,\n\t\t\tInitialInterval: time.Millisecond * 100,\n\t\t\tLogger:          logger,\n\t\t}.Middleware,\n\n\t\t// Recoverer handles panics from handlers.\n\t\t// In this case, it passes them as errors to the Retry middleware.\n\t\tmiddleware.Recoverer,\n\t)\n\n\t// For simplicity, we are using the gochannel Pub/Sub here,\n\t// You can replace it with any Pub/Sub implementation, it will work the same.\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\n\t// Producing some incoming messages in background\n\tgo publishMessages(pubSub)\n\n\t// AddHandler returns a handler which can be used to add handler level middleware\n\t// or to stop handler.\n\thandler := router.AddHandler(\n\t\t\"struct_handler\",          // handler name, must be unique\n\t\t\"incoming_messages_topic\", // topic from which we will read events\n\t\tpubSub,\n\t\t\"outgoing_messages_topic\", // topic to which we will publish events\n\t\tpubSub,\n\t\tstructHandler{}.Handler,\n\t)\n\n\t// Handler level middleware is only executed for a specific handler\n\t// Such middleware can be added the same way the router level ones\n\thandler.AddMiddleware(func(h message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(message *message.Message) ([]*message.Message, error) {\n\t\t\tlog.Println(\"executing handler specific middleware for \", message.UUID)\n\n\t\t\treturn h(message)\n\t\t}\n\t})\n\n\t// just for debug, we are printing all messages received on `incoming_messages_topic`\n\trouter.AddConsumerHandler(\n\t\t\"print_incoming_messages\",\n\t\t\"incoming_messages_topic\",\n\t\tpubSub,\n\t\tprintMessages,\n\t)\n\n\t// just for debug, we are printing all events sent to `outgoing_messages_topic`\n\trouter.AddConsumerHandler(\n\t\t\"print_outgoing_messages\",\n\t\t\"outgoing_messages_topic\",\n\t\tpubSub,\n\t\tprintMessages,\n\t)\n\n\t// Now that all handlers are registered, we're running the Router.\n\t// Run is blocking while the router is running.\n\tctx := context.Background()\n\tif err := router.Run(ctx); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\t\tmiddleware.SetCorrelationID(watermill.NewUUID(), msg)\n\n\t\tlog.Printf(\"sending message %s, correlation id: %s\\n\", msg.UUID, middleware.MessageCorrelationID(msg))\n\n\t\tif err := publisher.Publish(\"incoming_messages_topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc printMessages(msg *message.Message) error {\n\tfmt.Printf(\n\t\t\"\\n> Received message: %s\\n> %s\\n> metadata: %v\\n\\n\",\n\t\tmsg.UUID, string(msg.Payload), msg.Metadata,\n\t)\n\treturn nil\n}\n\ntype structHandler struct {\n\t// we can add some dependencies here\n}\n\nfunc (s structHandler) Handler(msg *message.Message) ([]*message.Message, error) {\n\tlog.Println(\"structHandler received message\", msg.UUID)\n\n\tmsg = message.NewMessage(watermill.NewUUID(), []byte(\"message produced by structHandler\"))\n\treturn message.Messages{msg}, nil\n}\n"
  },
  {
    "path": "_examples/basic/4-metrics/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"msg=\\\"Message acked\\\"\"\n"
  },
  {
    "path": "_examples/basic/4-metrics/README.md",
    "content": "# Prometheus metrics showcase\n\nThis is an example application that showcases how Watermill may be monitored with Prometheus metrics.\n\nThe docker-compose bundle contains the following services:\n\n#### Golang\n\nA [Golang](https://hub.docker.com/_/golang) image which runs the [example code](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/4-metrics/main.go). It consists of a router with a single handler. \n \nThe handler consumes messages from a [Gochannel PubSub](https://github.com/ThreeDotsLabs/watermill/tree/master/message/infrastructure/gochannel), and publishes 0-4 copies of the message with a preconfigured random delay.\n\nAdditionally, there is one goroutine which produces messages incoming to the handler with a gochannel publisher, and another goroutine which consumes the messages outgoing from the handler.\n\nThe router, the standalone publisher and the standalone subscriber are all decorated with the metrics code and their statistics will appear in the dashboard.\n\n#### Prometheus\n[Prometheus](https://hub.docker.com/r/prom/prometheus/), to scrape the metrics which the golang application exposes at `:8081/metrics` by default. It is configured by a [prometheus.yml](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/4-metrics/prometheus.yml) file, which declares the endpoints that Prometheus will scrape.\n\n#### Grafana\n[Grafana](https://hub.docker.com/r/grafana/grafana), to visualize the metrics in a dashboard.\n\n#### Running the example\n\nTo run the docker-compose bundle, go to `_examples/metrics` and execute:\n\n```\ndocker-compose up\n```\n\nThe golang app will start running, producing messages, passing them through the handler, and consuming the copies.\n\nWith default settings, the raw Prometheus metrics should appear at your http://localhost:8081/metrics. \n\nThe Prometheus image should expose a more advanced UI at http://localhost:9090/graph, where you can investigate all the scraped metrics.\n\nHowever, what is the most useful way to monitor is through the use of Grafana, which you can access at http://localhost:3000. \n\n#### Adding the Prometheus data source to Grafana\n\nThe fresh Grafana image should greet you with a login screen:\n\n![Grafana login screen](https://threedots.tech/watermill-io/grafana_login.png)\n\nJust use the default `admin:admin` credentials. You can skip changing the password, if you wish.\n\nThe next thing that we need to do is to add the Prometheus data source. Click on `Add data source`.\n\nIn the following screen:\n\n1. Enter a name for the Prometheus data source. Let's name this data source `prometheus`.\n1. Choose `Prometheus` from the `Type` dropdown.\n1. Enter the `http://localhost:9090` value in the HTTP/URL section.\n1. You can leave the remaining settings at default and click `Save & Test`.\n\n![Prometheus data source configuration](https://threedots.tech/watermill-io/prometheus_data_source_config.png)\n\nThe Prometheus data source is now ready to use in Grafana.\n\n#### Importing the Grafana dashboard\n\nWe have prepared a Grafana dashboard that visualizes the metrics exported by this example.\n\nTo import the Grafana dashboard, select Dashboard/Manage from the left menu, and then click on `+Import` (or go to http://localhost:3000/dashboard/import).\n\nEnter the dashboard URL https://grafana.com/dashboards/9777 (or just the ID, 9777), and click on Load.\n\n![Importing the dashboard](https://threedots.tech/watermill-io/grafana_import_dashboard.png)\n\nThen select the Prometheus data source created in the previous step. Click on `Import`, and you're done!\n\n### Find out more \n\nTo find out more, about metrics be sure to check out the [Watermill docs](https://watermill.io/docs/metrics).\n"
  },
  {
    "path": "_examples/basic/4-metrics/docker-compose.yml",
    "content": "services:\n  golang:\n    image: golang:1.25\n    restart: unless-stopped\n    ports:\n    - 8080:8080\n    - 8081:8081\n    depends_on:\n      - prometheus\n    volumes:\n    - ../../../:/watermill\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /watermill/_examples/basic/4-metrics\n    command: go run main.go -metrics :8081 -delay 0.1\n\n  prometheus:\n    image: prom/prometheus\n    restart: unless-stopped\n    network_mode: host\n    volumes:\n     - ./prometheus.yml:/etc/prometheus/prometheus.yml\n\n  grafana:\n    image: grafana/grafana:5.2.4\n    network_mode: host\n\n"
  },
  {
    "path": "_examples/basic/4-metrics/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire github.com/ThreeDotsLabs/watermill v1.5.1\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/go-chi/chi/v5 v5.2.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/client_golang v1.23.0 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.65.0 // indirect\n\tgithub.com/prometheus/procfs v0.17.0 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n)\n\n// uncomment to use local sources\n// replace github.com/ThreeDotsLabs/watermill => ../../../../watermill\n"
  },
  {
    "path": "_examples/basic/4-metrics/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=\ngithub.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=\ngithub.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=\ngithub.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=\ngithub.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=\ngithub.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/4-metrics/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"math\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/metrics\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nvar (\n\tmetricsAddr  = flag.String(\"metrics\", \":8081\", \"The address that will expose /metrics for Prometheus\")\n\thandlerDelay = flag.Float64(\"delay\", 0, \"The stdev of normal distribution of delay in handler (in seconds), to simulate load\")\n\n\tlogger = watermill.NewStdLogger(true, true)\n\trandom = rand.New(rand.NewSource(time.Now().Unix()))\n)\n\nfunc delay() {\n\tseconds := *handlerDelay\n\tif seconds == 0 {\n\t\treturn\n\t}\n\tdelay := math.Abs(random.NormFloat64() * seconds)\n\ttime.Sleep(time.Duration(float64(time.Second) * delay))\n}\n\n// handler publishes 0-4 messages with a random delay.\nfunc handler(msg *message.Message) ([]*message.Message, error) {\n\tdelay()\n\n\tnumOutgoing := random.Intn(4)\n\toutgoing := make([]*message.Message, numOutgoing)\n\n\tfor i := 0; i < numOutgoing; i++ {\n\t\toutgoing[i] = msg.Copy()\n\t}\n\treturn outgoing, nil\n}\n\n// consumeMessages consumes the messages exiting the handler.\nfunc consumeMessages(subscriber message.Subscriber) {\n\tmessages, err := subscriber.Subscribe(context.Background(), \"pub_topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor msg := range messages {\n\t\tmsg.Ack()\n\t}\n}\n\n// produceMessages produces the incoming messages in delays of 50-100 milliseconds.\nfunc produceMessages(routerClosed chan struct{}, publisher message.Publisher) {\n\tfor {\n\t\tselect {\n\t\tcase <-routerClosed:\n\t\t\treturn\n\t\tdefault:\n\t\t\t// go on\n\t\t}\n\n\t\ttime.Sleep(50*time.Millisecond + time.Duration(random.Intn(50))*time.Millisecond)\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte{})\n\t\t_ = publisher.Publish(\"sub_topic\", msg)\n\t}\n}\n\nfunc main() {\n\tflag.Parse()\n\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\n\trouter, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprometheusRegistry, closeMetricsServer := metrics.CreateRegistryAndServeHTTP(*metricsAddr)\n\tdefer closeMetricsServer()\n\n\t// we leave the namespace and subsystem empty\n\tmetricsBuilder := metrics.NewPrometheusMetricsBuilder(prometheusRegistry, \"\", \"\")\n\tmetricsBuilder.AddPrometheusRouterMetrics(router)\n\n\trouter.AddMiddleware(\n\t\tmiddleware.Recoverer,\n\t\tmiddleware.RandomFail(0.1),\n\t\tmiddleware.RandomPanic(0.1),\n\t)\n\trouter.AddPlugin(plugin.SignalsHandler)\n\n\trouter.AddHandler(\n\t\t\"metrics-example\",\n\t\t\"sub_topic\",\n\t\tpubSub,\n\t\t\"pub_topic\",\n\t\tpubSub,\n\t\thandler,\n\t)\n\n\tpub := randomFailPublisherDecorator{pubSub, 0.1}\n\n\t// The handler's publisher and subscriber will be decorated by `AddPrometheusRouterMetrics`.\n\t// We are using the same pub/sub to generate messages incoming to the handler\n\t// and consume the outgoing messages.\n\t// They will have `handler_name=<no handler>` label in Prometheus.\n\tsubWithMetrics, err := metricsBuilder.DecorateSubscriber(pubSub)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpubWithMetrics, err := metricsBuilder.DecoratePublisher(pub)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouterClosed := make(chan struct{})\n\tgo produceMessages(routerClosed, pubWithMetrics)\n\tgo consumeMessages(subWithMetrics)\n\n\t_ = router.Run(context.Background())\n\tclose(routerClosed)\n}\n\ntype randomFailPublisherDecorator struct {\n\tmessage.Publisher\n\tfailProbability float64\n}\n\nfunc (r randomFailPublisherDecorator) Publish(topic string, messages ...*message.Message) error {\n\tif random.Float64() < r.failProbability {\n\t\treturn errors.New(\"random publishing failure\")\n\t}\n\treturn r.Publisher.Publish(topic, messages...)\n}\n"
  },
  {
    "path": "_examples/basic/4-metrics/prometheus.yml",
    "content": "# my global config\nglobal:\n  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n  # scrape_timeout is set to the global default (10s).\n\nscrape_configs:\n#  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.\n#  - job_name: 'prometheus'\n#\n#    # metrics_path defaults to '/metrics'\n#    # scheme defaults to 'http'.\n#\n#    static_configs:\n#    - targets: ['localhost:9090']\n\n  - job_name: 'metrics_example'\n    \n    static_configs:\n    - targets: ['localhost:8081']\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 120\nexpected_outputs:\n  - \"beers ordered to room \\\\d+\"\n  - \"Already booked rooms for \\\\$\\\\d{2,}\"\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/Makefile",
    "content": ".PHONY: proto\n\nproto:\n\tprotoc --proto_path=proto --go_out=. --go_opt=paths=source_relative  proto/messages.proto\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/README.md",
    "content": "# Example Golang CQRS application\n\nThis application is using [Watermill CQRS](http://watermill.io/docs/cqrs) component.\n\nDetailed documentation for CQRS can be found in Watermill's docs: [http://watermill.io/docs/cqrs#usage](http://watermill.io/docs/cqrs).\n\n![CQRS Event Storming](https://threedots.tech/watermill-io/cqrs-example-storming.png)\n\n```mermaid\nsequenceDiagram\n    participant M as Main\n    participant CB as CommandBus\n    participant BRH as BookRoomHandler\n    participant EB as EventBus\n    participant OBRB as OrderBeerOnRoomBooked\n    participant OBH as OrderBeerHandler\n    participant BFR as BookingsFinancialReport\n\n    Note over M,BFR: Commands use AMQP queue, Events use AMQP pub/sub\n    \n    M->>CB: Send(BookRoom)<br/>topic: commands.BookRoom\n    CB->>BRH: Handle(BookRoom)\n    \n    BRH->>EB: Publish(RoomBooked)<br/>topic: events.RoomBooked\n    \n    par Process RoomBooked Event\n        EB->>OBRB: Handle(RoomBooked)\n        OBRB->>CB: Send(OrderBeer)<br/>topic: commands.OrderBeer\n        CB->>OBH: Handle(OrderBeer)\n        OBH->>EB: Publish(BeerOrdered)<br/>topic: events.BeerOrdered\n        \n        EB->>BFR: Handle(RoomBooked)\n        Note over BFR: Updates financial report\n    end\n```\n\n\n## Running\n\n```bash\ndocker-compose up\n```\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/docker-compose.yml",
    "content": "services:\n  golang:\n    image: golang:1.25\n    restart: unless-stopped\n    ports:\n    - 8080:8080\n    depends_on:\n      - rabbitmq\n    links:\n      - rabbitmq\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run .\n\n  rabbitmq:\n    image: rabbitmq:3.7\n    restart: unless-stopped\n    attach: false\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2\n\tgoogle.golang.org/protobuf v1.36.8\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rabbitmq/amqp091-go v1.10.0 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2 h1:aeyFSR4SUsbszmocuFiYY13nsHorc6CXIS2Hy7+xgFU=\ngithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2/go.mod h1:+8tCh6VCuBcQWhfETCwzRINKQ1uyeg9moH3h7jMKxQk=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=\ngithub.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\n// BookRoomHandler is a command handler, which handles BookRoom command and emits RoomBooked.\n//\n// In CQRS, one command must be handled by only one handler.\n// When another handler with this command is added to command processor, error will be returned.\ntype BookRoomHandler struct {\n\teventBus *cqrs.EventBus\n}\n\nfunc (b BookRoomHandler) Handle(ctx context.Context, cmd *BookRoom) error {\n\t// some random price, in production you probably will calculate in wiser way\n\tprice := (rand.Int63n(40) + 1) * 10\n\n\tslog.Info(\n\t\t\"Booked room\",\n\t\t\"room_id\", cmd.RoomId,\n\t\t\"guest_name\", cmd.GuestName,\n\t\t\"start_date\", time.Unix(cmd.StartDate.Seconds, int64(cmd.StartDate.Nanos)),\n\t\t\"end_date\", time.Unix(cmd.EndDate.Seconds, int64(cmd.EndDate.Nanos)),\n\t)\n\n\t// RoomBooked will be handled by OrderBeerOnRoomBooked event handler,\n\t// in future RoomBooked may be handled by multiple event handler\n\tif err := b.eventBus.Publish(ctx, &RoomBooked{\n\t\tReservationId: watermill.NewUUID(),\n\t\tRoomId:        cmd.RoomId,\n\t\tGuestName:     cmd.GuestName,\n\t\tPrice:         price,\n\t\tStartDate:     cmd.StartDate,\n\t\tEndDate:       cmd.EndDate,\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// OrderBeerOnRoomBooked is an event handler, which handles RoomBooked event and emits OrderBeer command.\ntype OrderBeerOnRoomBooked struct {\n\tcommandBus *cqrs.CommandBus\n}\n\nfunc (o OrderBeerOnRoomBooked) Handle(ctx context.Context, event *RoomBooked) error {\n\torderBeerCmd := &OrderBeer{\n\t\tRoomId: event.RoomId,\n\t\tCount:  rand.Int63n(10) + 1,\n\t}\n\n\treturn o.commandBus.Send(ctx, orderBeerCmd)\n}\n\n// OrderBeerHandler is a command handler, which handles OrderBeer command and emits BeerOrdered.\n// BeerOrdered is not handled by any event handler, but we may use persistent Pub/Sub to handle it in the future.\ntype OrderBeerHandler struct {\n\teventBus *cqrs.EventBus\n}\n\nfunc (o OrderBeerHandler) Handle(ctx context.Context, cmd *OrderBeer) error {\n\tif rand.Int63n(10) == 0 {\n\t\t// sometimes there is no beer left, command will be retried\n\t\treturn fmt.Errorf(\"no beer left for room %s, please try later\", cmd.RoomId)\n\t}\n\n\tif err := o.eventBus.Publish(ctx, &BeerOrdered{\n\t\tRoomId: cmd.RoomId,\n\t\tCount:  cmd.Count,\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tslog.Info(fmt.Sprintf(\"%d beers ordered to room %s\", cmd.Count, cmd.RoomId))\n\treturn nil\n}\n\n// BookingsFinancialReport is a read model, which calculates how much money we may earn from bookings.\n// Like OrderBeerOnRoomBooked, it listens for RoomBooked event.\n//\n// This implementation is just writing to the memory. In production, you will probably will use some persistent storage.\ntype BookingsFinancialReport struct {\n\thandledBookings map[string]struct{}\n\ttotalCharge     int64\n\tlock            sync.Mutex\n}\n\nfunc NewBookingsFinancialReport() *BookingsFinancialReport {\n\treturn &BookingsFinancialReport{handledBookings: map[string]struct{}{}}\n}\n\nfunc (b *BookingsFinancialReport) Handle(ctx context.Context, event *RoomBooked) error {\n\t// Handle may be called concurrently, so it need to be thread safe.\n\tb.lock.Lock()\n\tdefer b.lock.Unlock()\n\n\t// When we are using Pub/Sub which doesn't provide exactly-once delivery semantics, we need to deduplicate messages.\n\t// GoChannel Pub/Sub provides exactly-once delivery,\n\t// but let's make this example ready for other Pub/Sub implementations.\n\tif _, ok := b.handledBookings[event.ReservationId]; ok {\n\t\treturn nil\n\t}\n\tb.handledBookings[event.ReservationId] = struct{}{}\n\n\tb.totalCharge += event.Price\n\n\tslog.Info(fmt.Sprintf(\">>> Already booked rooms for $%d\\n\", b.totalCharge))\n\treturn nil\n}\n\nvar amqpAddress = \"amqp://guest:guest@rabbitmq:5672/\"\n\nfunc main() {\n\tlogger := watermill.NewSlogLoggerWithLevelMapping(nil, map[slog.Level]slog.Level{\n\t\tslog.LevelInfo: slog.LevelDebug,\n\t})\n\n\tcqrsMarshaler := cqrs.ProtoMarshaler{\n\t\t// It will generate topic names based on the event/command type.\n\t\t// So for example, for \"RoomBooked\" name will be \"RoomBooked\".\n\t\t//\n\t\t// This value is used to generate topic names with \"generateEventsTopic\" and \"generateCommandsTopic\" functions.\n\t\tGenerateName: cqrs.StructName,\n\t}\n\n\tgenerateEventsTopic := func(eventName string) string {\n\t\treturn \"events.\" + eventName\n\t}\n\n\tgenerateCommandsTopic := func(commandName string) string {\n\t\treturn \"commands.\" + commandName\n\t}\n\n\t// You can use any Pub/Sub implementation from here: https://watermill.io/pubsubs/\n\t// Detailed RabbitMQ implementation: https://watermill.io/pubsubs/amqp/\n\t// Commands will be sent to queue, because they need to be consumed once.\n\tcommandsAMQPConfig := amqp.NewDurableQueueConfig(amqpAddress)\n\tcommandsPublisher, err := amqp.NewPublisher(commandsAMQPConfig, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcommandsSubscriber, err := amqp.NewSubscriber(commandsAMQPConfig, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Events will be published to PubSub configured Rabbit, because they may be consumed by multiple consumers.\n\t// (in that case BookingsFinancialReport and OrderBeerOnRoomBooked).\n\teventsPublisher, err := amqp.NewPublisher(amqp.NewDurablePubSubConfig(amqpAddress, nil), logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// CQRS is built on messages router. Detailed documentation: https://watermill.io/docs/messages-router/\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Simple middleware which will recover panics from event or command handlers.\n\t// More about router middlewares you can find in the documentation:\n\t// https://watermill.io/docs/messages-router/#middleware\n\t//\n\t// List of available middlewares you can find in message/router/middleware.\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\tcommandBus, err := cqrs.NewCommandBusWithConfig(commandsPublisher, cqrs.CommandBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\treturn generateCommandsTopic(params.CommandName), nil\n\t\t},\n\t\tOnSend: func(params cqrs.CommandBusOnSendParams) error {\n\t\t\tlogger.Info(\"Sending command\", watermill.LogFields{\n\t\t\t\t\"command_name\": params.CommandName,\n\t\t\t})\n\n\t\t\tparams.Message.Metadata.Set(\"sent_at\", time.Now().String())\n\n\t\t\treturn nil\n\t\t},\n\t\tMarshaler: cqrsMarshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn generateCommandsTopic(params.CommandName), nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\t// we can reuse subscriber, because all commands have separated topics\n\t\t\t\treturn commandsSubscriber, nil\n\t\t\t},\n\t\t\tOnHandle: func(params cqrs.CommandProcessorOnHandleParams) error {\n\t\t\t\tstart := time.Now()\n\n\t\t\t\terr := params.Handler.Handle(params.Message.Context(), params.Command)\n\n\t\t\t\tlogger.Info(\"Command handled\", watermill.LogFields{\n\t\t\t\t\t\"command_name\": params.CommandName,\n\t\t\t\t\t\"duration\":     time.Since(start),\n\t\t\t\t\t\"err\":          err,\n\t\t\t\t})\n\n\t\t\t\treturn err\n\t\t\t},\n\t\t\tMarshaler: cqrsMarshaler,\n\t\t\tLogger:    logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\teventBus, err := cqrs.NewEventBusWithConfig(eventsPublisher, cqrs.EventBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\treturn generateEventsTopic(params.EventName), nil\n\t\t},\n\n\t\tOnPublish: func(params cqrs.OnEventSendParams) error {\n\t\t\tlogger.Info(\"Publishing event\", watermill.LogFields{\n\t\t\t\t\"event_name\": params.EventName,\n\t\t\t})\n\n\t\t\tparams.Message.Metadata.Set(\"published_at\", time.Now().String())\n\n\t\t\treturn nil\n\t\t},\n\n\t\tMarshaler: cqrsMarshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn generateEventsTopic(params.EventName), nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\tconfig := amqp.NewDurablePubSubConfig(\n\t\t\t\t\tamqpAddress,\n\t\t\t\t\tamqp.GenerateQueueNameTopicNameWithSuffix(params.HandlerName),\n\t\t\t\t)\n\n\t\t\t\treturn amqp.NewSubscriber(config, logger)\n\t\t\t},\n\n\t\t\tOnHandle: func(params cqrs.EventProcessorOnHandleParams) error {\n\t\t\t\tstart := time.Now()\n\n\t\t\t\terr := params.Handler.Handle(params.Message.Context(), params.Event)\n\n\t\t\t\tlogger.Info(\"Event handled\", watermill.LogFields{\n\t\t\t\t\t\"event_name\": params.EventName,\n\t\t\t\t\t\"duration\":   time.Since(start),\n\t\t\t\t\t\"err\":        err,\n\t\t\t\t})\n\n\t\t\t\treturn err\n\t\t\t},\n\n\t\t\tMarshaler: cqrsMarshaler,\n\t\t\tLogger:    logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = commandProcessor.AddHandlers(\n\t\tcqrs.NewCommandHandler(\"BookRoomHandler\", BookRoomHandler{eventBus}.Handle),\n\t\tcqrs.NewCommandHandler(\"OrderBeerHandler\", OrderBeerHandler{eventBus}.Handle),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = eventProcessor.AddHandlers(\n\t\tcqrs.NewEventHandler(\n\t\t\t\"OrderBeerOnRoomBooked\",\n\t\t\tOrderBeerOnRoomBooked{commandBus}.Handle,\n\t\t),\n\t\tcqrs.NewEventHandler(\n\t\t\t\"LogBeerOrdered\",\n\t\t\tfunc(ctx context.Context, event *BeerOrdered) error {\n\t\t\t\tlogger.Info(\"Beer ordered\", watermill.LogFields{\n\t\t\t\t\t\"room_id\": event.RoomId,\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t\tcqrs.NewEventHandler(\n\t\t\t\"BookingsFinancialReport\",\n\t\t\tNewBookingsFinancialReport().Handle,\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// publish BookRoom commands every second to simulate incoming traffic\n\tgo publishCommands(commandBus)\n\n\t// processors are based on router, so they will work when router will start\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc publishCommands(commandBus *cqrs.CommandBus) func() {\n\ti := 0\n\tfor {\n\t\ti++\n\n\t\tstartDate := timestamppb.New(time.Now())\n\t\tendDate := timestamppb.New(time.Now().Add(time.Hour * 24 * 3))\n\n\t\tbookRoomCmd := &BookRoom{\n\t\t\tRoomId:    fmt.Sprintf(\"%d\", i),\n\t\t\tGuestName: \"John\",\n\t\t\tStartDate: startDate,\n\t\t\tEndDate:   endDate,\n\t\t}\n\t\tif err := commandBus.Send(context.Background(), bookRoomCmd); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/messages.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.31.0\n// \tprotoc        v4.24.4\n// source: messages.proto\n\npackage main\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype BookRoom struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tRoomId    string                 `protobuf:\"bytes,1,opt,name=room_id,json=roomId,proto3\" json:\"room_id,omitempty\"`\n\tGuestName string                 `protobuf:\"bytes,2,opt,name=guest_name,json=guestName,proto3\" json:\"guest_name,omitempty\"`\n\tStartDate *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=start_date,json=startDate,proto3\" json:\"start_date,omitempty\"`\n\tEndDate   *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=end_date,json=endDate,proto3\" json:\"end_date,omitempty\"`\n}\n\nfunc (x *BookRoom) Reset() {\n\t*x = BookRoom{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *BookRoom) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BookRoom) ProtoMessage() {}\n\nfunc (x *BookRoom) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BookRoom.ProtoReflect.Descriptor instead.\nfunc (*BookRoom) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *BookRoom) GetRoomId() string {\n\tif x != nil {\n\t\treturn x.RoomId\n\t}\n\treturn \"\"\n}\n\nfunc (x *BookRoom) GetGuestName() string {\n\tif x != nil {\n\t\treturn x.GuestName\n\t}\n\treturn \"\"\n}\n\nfunc (x *BookRoom) GetStartDate() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.StartDate\n\t}\n\treturn nil\n}\n\nfunc (x *BookRoom) GetEndDate() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EndDate\n\t}\n\treturn nil\n}\n\ntype RoomBooked struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tReservationId string                 `protobuf:\"bytes,1,opt,name=reservation_id,json=reservationId,proto3\" json:\"reservation_id,omitempty\"`\n\tRoomId        string                 `protobuf:\"bytes,2,opt,name=room_id,json=roomId,proto3\" json:\"room_id,omitempty\"`\n\tGuestName     string                 `protobuf:\"bytes,3,opt,name=guest_name,json=guestName,proto3\" json:\"guest_name,omitempty\"`\n\tPrice         int64                  `protobuf:\"varint,4,opt,name=price,proto3\" json:\"price,omitempty\"`\n\tStartDate     *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=start_date,json=startDate,proto3\" json:\"start_date,omitempty\"`\n\tEndDate       *timestamppb.Timestamp `protobuf:\"bytes,6,opt,name=end_date,json=endDate,proto3\" json:\"end_date,omitempty\"`\n}\n\nfunc (x *RoomBooked) Reset() {\n\t*x = RoomBooked{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *RoomBooked) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RoomBooked) ProtoMessage() {}\n\nfunc (x *RoomBooked) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RoomBooked.ProtoReflect.Descriptor instead.\nfunc (*RoomBooked) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *RoomBooked) GetReservationId() string {\n\tif x != nil {\n\t\treturn x.ReservationId\n\t}\n\treturn \"\"\n}\n\nfunc (x *RoomBooked) GetRoomId() string {\n\tif x != nil {\n\t\treturn x.RoomId\n\t}\n\treturn \"\"\n}\n\nfunc (x *RoomBooked) GetGuestName() string {\n\tif x != nil {\n\t\treturn x.GuestName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RoomBooked) GetPrice() int64 {\n\tif x != nil {\n\t\treturn x.Price\n\t}\n\treturn 0\n}\n\nfunc (x *RoomBooked) GetStartDate() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.StartDate\n\t}\n\treturn nil\n}\n\nfunc (x *RoomBooked) GetEndDate() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EndDate\n\t}\n\treturn nil\n}\n\ntype OrderBeer struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tRoomId string `protobuf:\"bytes,1,opt,name=room_id,json=roomId,proto3\" json:\"room_id,omitempty\"`\n\tCount  int64  `protobuf:\"varint,2,opt,name=count,proto3\" json:\"count,omitempty\"`\n}\n\nfunc (x *OrderBeer) Reset() {\n\t*x = OrderBeer{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[2]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *OrderBeer) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*OrderBeer) ProtoMessage() {}\n\nfunc (x *OrderBeer) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[2]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use OrderBeer.ProtoReflect.Descriptor instead.\nfunc (*OrderBeer) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *OrderBeer) GetRoomId() string {\n\tif x != nil {\n\t\treturn x.RoomId\n\t}\n\treturn \"\"\n}\n\nfunc (x *OrderBeer) GetCount() int64 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\ntype BeerOrdered struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tRoomId string `protobuf:\"bytes,1,opt,name=room_id,json=roomId,proto3\" json:\"room_id,omitempty\"`\n\tCount  int64  `protobuf:\"varint,2,opt,name=count,proto3\" json:\"count,omitempty\"`\n}\n\nfunc (x *BeerOrdered) Reset() {\n\t*x = BeerOrdered{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[3]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *BeerOrdered) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BeerOrdered) ProtoMessage() {}\n\nfunc (x *BeerOrdered) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[3]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BeerOrdered.ProtoReflect.Descriptor instead.\nfunc (*BeerOrdered) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *BeerOrdered) GetRoomId() string {\n\tif x != nil {\n\t\treturn x.RoomId\n\t}\n\treturn \"\"\n}\n\nfunc (x *BeerOrdered) GetCount() int64 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\nvar File_messages_proto protoreflect.FileDescriptor\n\nvar file_messages_proto_rawDesc = []byte{\n\t0x0a, 0x0e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,\n\t0x12, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,\n\t0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb4, 0x01, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b,\n\t0x52, 0x6f, 0x6f, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69, 0x64, 0x18,\n\t0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12, 0x1d, 0x0a,\n\t0x0a, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,\n\t0x09, 0x52, 0x09, 0x67, 0x75, 0x65, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a,\n\t0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,\n\t0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,\n\t0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74,\n\t0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x64,\n\t0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,\n\t0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,\n\t0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x44, 0x61, 0x74, 0x65, 0x22, 0xf3,\n\t0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x6f, 0x6d, 0x42, 0x6f, 0x6f, 0x6b, 0x65, 0x64, 0x12, 0x25, 0x0a,\n\t0x0e, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18,\n\t0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69,\n\t0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69, 0x64, 0x18,\n\t0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12, 0x1d, 0x0a,\n\t0x0a, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,\n\t0x09, 0x52, 0x09, 0x67, 0x75, 0x65, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05,\n\t0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x70, 0x72, 0x69,\n\t0x63, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x65,\n\t0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,\n\t0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,\n\t0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a,\n\t0x08, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32,\n\t0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,\n\t0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64,\n\t0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x0a, 0x09, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x65, 0x65,\n\t0x72, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,\n\t0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f,\n\t0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74,\n\t0x22, 0x3c, 0x0a, 0x0b, 0x42, 0x65, 0x65, 0x72, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x65, 0x64, 0x12,\n\t0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,\n\t0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e,\n\t0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x08,\n\t0x5a, 0x06, 0x2e, 0x2f, 0x6d, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_messages_proto_rawDescOnce sync.Once\n\tfile_messages_proto_rawDescData = file_messages_proto_rawDesc\n)\n\nfunc file_messages_proto_rawDescGZIP() []byte {\n\tfile_messages_proto_rawDescOnce.Do(func() {\n\t\tfile_messages_proto_rawDescData = protoimpl.X.CompressGZIP(file_messages_proto_rawDescData)\n\t})\n\treturn file_messages_proto_rawDescData\n}\n\nvar file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 4)\nvar file_messages_proto_goTypes = []interface{}{\n\t(*BookRoom)(nil),              // 0: main.BookRoom\n\t(*RoomBooked)(nil),            // 1: main.RoomBooked\n\t(*OrderBeer)(nil),             // 2: main.OrderBeer\n\t(*BeerOrdered)(nil),           // 3: main.BeerOrdered\n\t(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp\n}\nvar file_messages_proto_depIdxs = []int32{\n\t4, // 0: main.BookRoom.start_date:type_name -> google.protobuf.Timestamp\n\t4, // 1: main.BookRoom.end_date:type_name -> google.protobuf.Timestamp\n\t4, // 2: main.RoomBooked.start_date:type_name -> google.protobuf.Timestamp\n\t4, // 3: main.RoomBooked.end_date:type_name -> google.protobuf.Timestamp\n\t4, // [4:4] is the sub-list for method output_type\n\t4, // [4:4] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_messages_proto_init() }\nfunc file_messages_proto_init() {\n\tif File_messages_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_messages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*BookRoom); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*RoomBooked); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*OrderBeer); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*BeerOrdered); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_messages_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   4,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_messages_proto_goTypes,\n\t\tDependencyIndexes: file_messages_proto_depIdxs,\n\t\tMessageInfos:      file_messages_proto_msgTypes,\n\t}.Build()\n\tFile_messages_proto = out.File\n\tfile_messages_proto_rawDesc = nil\n\tfile_messages_proto_goTypes = nil\n\tfile_messages_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "_examples/basic/5-cqrs-protobuf/proto/messages.proto",
    "content": "syntax = \"proto3\";\npackage main;\n\noption go_package = \"./main\";\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage BookRoom {\n    string room_id = 1;\n    string guest_name = 2;\n\n    google.protobuf.Timestamp start_date = 4;\n    google.protobuf.Timestamp end_date = 5;\n}\n\nmessage RoomBooked {\n    string reservation_id = 1;\n    string room_id = 2;\n    string guest_name = 3;\n    int64 price = 4;\n\n    google.protobuf.Timestamp start_date = 5;\n    google.protobuf.Timestamp end_date = 6;\n}\n\nmessage OrderBeer {\n    string room_id = 1;\n    int64 count = 2;\n}\n\n\nmessage BeerOrdered {\n    string room_id = 1;\n    int64 count = 2;\n}"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 120\nexpected_outputs:\n  - \"Subscriber added subscriber_id\"\n  - \"Subscriber updated subscriber_id\"\n  - \"Subscriber removed subscriber_id\"\n  - \"\\\\[ACTIVITY\\\\] activity_type=SUBSCRIBED\"\n  - \"\\\\[ACTIVITY\\\\] activity_type=UNSUBSCRIBED\"\n  - \"\\\\[ACTIVITY\\\\] activity_type=EMAIL_UPDATED\"\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/Makefile",
    "content": ".PHONY: proto\n\nproto:\n\tprotoc --proto_path=proto --go_out=. --go_opt=paths=source_relative  proto/messages.proto\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/README.md",
    "content": "# Example Golang CQRS application - ordered events with Kafka\n\nThis application is using [Watermill CQRS](http://watermill.io/docs/cqrs) component.\n\nDetailed documentation for CQRS can be found in Watermill's docs: [http://watermill.io/docs/cqrs#usage](http://watermill.io/docs/cqrs).\n\nThis example, uses event groups to keep order for multiple events. You can read more about them in the [Watermill documentation](https://watermill.io/docs/cqrs/).\nWe are also using Kafka's partitioning keys to increase processing throughput without losing order of events.\n\n\n## What does this application do?\n\nThis application manages an email subscription system where users can:\n\n1. Subscribe to receive emails by providing their email address\n2. Update their email address after subscribing\n3. Unsubscribe from the mailing list\n\nThe system maintains:\n- A current list of all active subscribers\n- A timeline of all subscription-related activities\n\nIn this example, keeping order of events is crucial.\nIf events won't be ordered, and `SubscriberSubscribed` would arrive after `SubscriberUnsubscribed` event,\nthe subscriber will be still subscribed.\n\n## Possible improvements\n\nIn this example, we are using global `events` and `commands` topics.\nYou can consider splitting them into smaller topics, for example, per aggregate type.\n\nThanks to that, you can scale your application horizontally and increase the throughput and processing less events.\n\n## Running\n\n```bash\ndocker-compose up\n```\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/activity.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n)\n\n// ActivityEntry represents a single event in the timeline\ntype ActivityEntry struct {\n\tTimestamp    time.Time\n\tSubscriberID string\n\tActivityType string\n\tDetails      string\n}\n\n// ActivityTimelineReadModel maintains a chronological log of all subscription-related events\ntype ActivityTimelineReadModel struct {\n\tactivities []ActivityEntry\n\tlock       sync.RWMutex\n}\n\nfunc NewActivityTimelineModel() *ActivityTimelineReadModel {\n\treturn &ActivityTimelineReadModel{\n\t\tactivities: make([]ActivityEntry, 0),\n\t}\n}\n\n// OnSubscribed handles subscription events\nfunc (m *ActivityTimelineReadModel) OnSubscribed(ctx context.Context, event *SubscriberSubscribed) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tentry := ActivityEntry{\n\t\tTimestamp:    time.Now(),\n\t\tSubscriberID: event.SubscriberId,\n\t\tActivityType: \"SUBSCRIBED\",\n\t\tDetails:      fmt.Sprintf(\"Subscribed with email: %s\", event.Email),\n\t}\n\n\tm.activities = append(m.activities, entry)\n\tm.logActivity(entry)\n\treturn nil\n}\n\n// OnUnsubscribed handles unsubscription events\nfunc (m *ActivityTimelineReadModel) OnUnsubscribed(ctx context.Context, event *SubscriberUnsubscribed) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tentry := ActivityEntry{\n\t\tTimestamp:    time.Now(),\n\t\tSubscriberID: event.SubscriberId,\n\t\tActivityType: \"UNSUBSCRIBED\",\n\t\tDetails:      \"Subscriber unsubscribed\",\n\t}\n\n\tm.activities = append(m.activities, entry)\n\tm.logActivity(entry)\n\treturn nil\n}\n\n// OnEmailUpdated handles email update events\nfunc (m *ActivityTimelineReadModel) OnEmailUpdated(ctx context.Context, event *SubscriberEmailUpdated) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tentry := ActivityEntry{\n\t\tTimestamp:    time.Now(),\n\t\tSubscriberID: event.SubscriberId,\n\t\tActivityType: \"EMAIL_UPDATED\",\n\t\tDetails:      fmt.Sprintf(\"Email updated to: %s\", event.NewEmail),\n\t}\n\n\tm.activities = append(m.activities, entry)\n\tm.logActivity(entry)\n\treturn nil\n}\n\nfunc (m *ActivityTimelineReadModel) logActivity(entry ActivityEntry) {\n\tslog.Info(\n\t\t\"[ACTIVITY]\",\n\t\t\"activity_type\", entry.ActivityType,\n\t\t\"subscriber_id\", entry.SubscriberID,\n\t\t\"details\", entry.Details,\n\t)\n}\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/docker-compose.yml",
    "content": "services:\n  golang:\n    image: golang:1.25\n    restart: unless-stopped\n    ports:\n    - 8080:8080\n    depends_on:\n      - kafka\n      - zookeeper\n    links:\n      - kafka\n      - zookeeper\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run .\n\n  zookeeper:\n    container_name: zk\n    attach: false\n    image: confluentinc/cp-zookeeper:7.7.1\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n      ZOOKEEPER_TICK_TIME: 2000\n    ports:\n      - 2181:2181\n\n  kafka:\n    container_name: kafka\n    attach: false\n    image: confluentinc/cp-kafka:7.7.1\n    depends_on:\n      - zookeeper\n    ports:\n      - 9093:9093\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_ZOOKEEPER_CONNECT: zk:2181\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9093\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT\n      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n\tgoogle.golang.org/protobuf v1.36.8\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\nfunc main() {\n\tslog.SetLogLoggerLevel(slog.LevelDebug)\n\n\tlogger := watermill.NewSlogLoggerWithLevelMapping(nil, map[slog.Level]slog.Level{\n\t\tslog.LevelInfo: slog.LevelDebug,\n\t})\n\n\t// We are decorating ProtobufMarshaler to add extra metadata to the message.\n\tcqrsMarshaler := CqrsMarshalerDecorator{\n\t\tcqrs.ProtoMarshaler{\n\t\t\t// It will generate topic names based on the event/command type.\n\t\t\t// So for example, for \"RoomBooked\" name will be \"RoomBooked\".\n\t\t\tGenerateName: cqrs.StructName,\n\t\t},\n\t}\n\n\twatermillLogger := watermill.NewSlogLoggerWithLevelMapping(\n\t\tslog.With(\"watermill\", true),\n\t\tmap[slog.Level]slog.Level{\n\t\t\tslog.LevelInfo: slog.LevelDebug,\n\t\t},\n\t)\n\n\t// This marshaler converts Watermill messages to Kafka messages.\n\t// We are using it to add partition key to the Kafka message.\n\tkafkaMarshaler := kafka.NewWithPartitioningMarshaler(GenerateKafkaPartitionKey)\n\n\t// You can use any Pub/Sub implementation from here: https://watermill.io/pubsubs/\n\tpublisher, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   []string{\"kafka:9092\"},\n\t\t\tMarshaler: kafkaMarshaler,\n\t\t},\n\t\twatermillLogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// CQRS is built on messages router. Detailed documentation: https://watermill.io/docs/messages-router/\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Simple middleware which will recover panics from event or command handlers.\n\t// More about router middlewares you can find in the documentation:\n\t// https://watermill.io/docs/messages-router/#middleware\n\t//\n\t// List of available middlewares you can find in message/router/middleware.\n\trouter.AddMiddleware(middleware.Recoverer)\n\trouter.AddMiddleware(func(h message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\t\tslog.Debug(\"Received message\", \"metadata\", msg.Metadata)\n\t\t\treturn h(msg)\n\t\t}\n\t})\n\n\tcommandBus, err := cqrs.NewCommandBusWithConfig(publisher, cqrs.CommandBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t// We are using one topic for all commands to maintain the order of commands.\n\t\t\treturn \"commands\", nil\n\t\t},\n\t\tMarshaler: cqrsMarshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\teventBus, err := cqrs.NewEventBusWithConfig(publisher, cqrs.EventBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\t// We are using one topic for all events to maintain the order of events.\n\t\t\treturn \"events\", nil\n\t\t},\n\t\tMarshaler: cqrsMarshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"commands\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn kafka.NewSubscriber(\n\t\t\t\t\tkafka.SubscriberConfig{\n\t\t\t\t\t\tBrokers:       []string{\"kafka:9092\"},\n\t\t\t\t\t\tConsumerGroup: params.HandlerName,\n\t\t\t\t\t\tUnmarshaler:   kafkaMarshaler,\n\t\t\t\t\t},\n\t\t\t\t\twatermillLogger,\n\t\t\t\t)\n\t\t\t},\n\t\t\tMarshaler: cqrsMarshaler,\n\t\t\tLogger:    logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\teventProcessor, err := cqrs.NewEventGroupProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventGroupProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn kafka.NewSubscriber(\n\t\t\t\t\tkafka.SubscriberConfig{\n\t\t\t\t\t\tBrokers:       []string{\"kafka:9092\"},\n\t\t\t\t\t\tConsumerGroup: params.EventGroupName,\n\t\t\t\t\t\tUnmarshaler:   kafkaMarshaler,\n\t\t\t\t\t},\n\t\t\t\t\twatermillLogger,\n\t\t\t\t)\n\t\t\t},\n\t\t\tMarshaler: cqrsMarshaler,\n\t\t\tLogger:    logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = commandProcessor.AddHandlers(\n\t\tcqrs.NewCommandHandler(\"SubscribeHandler\", SubscribeHandler{eventBus}.Handle),\n\t\tcqrs.NewCommandHandler(\"UnsubscribeHandler\", UnsubscribeHandler{eventBus}.Handle),\n\t\tcqrs.NewCommandHandler(\"UpdateEmailHandler\", UpdateEmailHandler{eventBus}.Handle),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsubscribersReadModel := NewSubscriberReadModel()\n\n\t// All messages from this group will have one subscription.\n\t// When message arrives, Watermill will match it with the correct handler.\n\terr = eventProcessor.AddHandlersGroup(\n\t\t\"SubscriberReadModel\",\n\t\tcqrs.NewGroupEventHandler(subscribersReadModel.OnSubscribed),\n\t\tcqrs.NewGroupEventHandler(subscribersReadModel.OnUnsubscribed),\n\t\tcqrs.NewGroupEventHandler(subscribersReadModel.OnEmailUpdated),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tactivityReadModel := NewActivityTimelineModel()\n\n\t// All messages from this group will have one subscription.\n\t// When message arrives, Watermill will match it with the correct handler.\n\terr = eventProcessor.AddHandlersGroup(\n\t\t\"ActivityTimelineReadModel\",\n\t\tcqrs.NewGroupEventHandler(activityReadModel.OnSubscribed),\n\t\tcqrs.NewGroupEventHandler(activityReadModel.OnUnsubscribed),\n\t\tcqrs.NewGroupEventHandler(activityReadModel.OnEmailUpdated),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tslog.Info(\"Starting service\")\n\n\tgo simulateTraffic(commandBus)\n\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc simulateTraffic(commandBus *cqrs.CommandBus) {\n\tfor i := 0; ; i++ {\n\t\tsubscriberID := watermill.NewUUID()\n\n\t\terr := commandBus.Send(context.Background(), &Subscribe{\n\t\t\tMetadata:     GenerateMessageMetadata(subscriberID),\n\t\t\tSubscriberId: subscriberID,\n\t\t\tEmail:        fmt.Sprintf(\"user%d@example.com\", i),\n\t\t})\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error sending Subscribe command\", \"err\", err)\n\t\t}\n\t\ttime.Sleep(time.Millisecond * 500)\n\n\t\terr = commandBus.Send(context.Background(), &UpdateEmail{\n\t\t\tMetadata:     GenerateMessageMetadata(subscriberID),\n\t\t\tSubscriberId: subscriberID,\n\t\t\tNewEmail:     fmt.Sprintf(\"updated%d@example.com\", i),\n\t\t})\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error sending UpdateEmail command\", \"err\", err)\n\t\t}\n\t\ttime.Sleep(time.Millisecond * 500)\n\n\t\tif i%3 == 0 {\n\t\t\terr = commandBus.Send(context.Background(), &Unsubscribe{\n\t\t\t\tMetadata:     GenerateMessageMetadata(subscriberID),\n\t\t\t\tSubscriberId: subscriberID,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Error sending Unsubscribe command\", \"err\", err)\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(time.Millisecond * 500)\n\t}\n}\n\ntype SubscribeHandler struct {\n\teventBus *cqrs.EventBus\n}\n\nfunc (h SubscribeHandler) Handle(ctx context.Context, cmd *Subscribe) error {\n\treturn h.eventBus.Publish(ctx, &SubscriberSubscribed{\n\t\tMetadata:     GenerateMessageMetadata(cmd.SubscriberId),\n\t\tSubscriberId: cmd.SubscriberId,\n\t\tEmail:        cmd.Email,\n\t})\n}\n\ntype UnsubscribeHandler struct {\n\teventBus *cqrs.EventBus\n}\n\nfunc (h UnsubscribeHandler) Handle(ctx context.Context, cmd *Unsubscribe) error {\n\treturn h.eventBus.Publish(ctx, &SubscriberUnsubscribed{\n\t\tMetadata:     GenerateMessageMetadata(cmd.SubscriberId),\n\t\tSubscriberId: cmd.SubscriberId,\n\t})\n}\n\ntype UpdateEmailHandler struct {\n\teventBus *cqrs.EventBus\n}\n\nfunc (h UpdateEmailHandler) Handle(ctx context.Context, cmd *UpdateEmail) error {\n\treturn h.eventBus.Publish(ctx, &SubscriberEmailUpdated{\n\t\tMetadata:     GenerateMessageMetadata(cmd.SubscriberId),\n\t\tSubscriberId: cmd.SubscriberId,\n\t\tNewEmail:     cmd.NewEmail,\n\t})\n}\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/message.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\nfunc GenerateMessageMetadata(partitionKey string) *MessageMetadata {\n\treturn &MessageMetadata{\n\t\tPartitionKey: partitionKey,\n\t\tCreatedAt:    timestamppb.Now(),\n\t}\n}\n\ntype CqrsMarshalerDecorator struct {\n\tcqrs.ProtoMarshaler\n}\n\nconst PartitionKeyMetadataField = \"partition_key\"\n\nfunc (c CqrsMarshalerDecorator) Marshal(v interface{}) (*message.Message, error) {\n\tmsg, err := c.ProtoMarshaler.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpm, ok := v.(ProtoMessage)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%T does not implement ProtoMessage and can't be marshaled\", v)\n\t}\n\n\tmetadata := pm.GetMetadata()\n\tif metadata == nil {\n\t\treturn nil, fmt.Errorf(\"%T.GetMetadata returned nil\", v)\n\t}\n\n\tmsg.Metadata.Set(PartitionKeyMetadataField, metadata.PartitionKey)\n\tmsg.Metadata.Set(\"created_at\", metadata.CreatedAt.AsTime().String())\n\n\treturn msg, nil\n}\n\ntype ProtoMessage interface {\n\tGetMetadata() *MessageMetadata\n}\n\n// GenerateKafkaPartitionKey is a function that generates a partition key for Kafka messages.\nfunc GenerateKafkaPartitionKey(topic string, msg *message.Message) (string, error) {\n\tslog.Debug(\"Setting partition key\", \"topic\", topic, \"msg_metadata\", msg.Metadata)\n\n\treturn msg.Metadata.Get(PartitionKeyMetadataField), nil\n}\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/messages.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.31.0\n// \tprotoc        v4.24.4\n// source: messages.proto\n\npackage main\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype MessageMetadata struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tPartitionKey string                 `protobuf:\"bytes,1,opt,name=partition_key,json=partitionKey,proto3\" json:\"partition_key,omitempty\"`\n\tCreatedAt    *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=created_at,json=createdAt,proto3\" json:\"created_at,omitempty\"`\n}\n\nfunc (x *MessageMetadata) Reset() {\n\t*x = MessageMetadata{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *MessageMetadata) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MessageMetadata) ProtoMessage() {}\n\nfunc (x *MessageMetadata) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MessageMetadata.ProtoReflect.Descriptor instead.\nfunc (*MessageMetadata) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *MessageMetadata) GetPartitionKey() string {\n\tif x != nil {\n\t\treturn x.PartitionKey\n\t}\n\treturn \"\"\n}\n\nfunc (x *MessageMetadata) GetCreatedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreatedAt\n\t}\n\treturn nil\n}\n\n// Commands\ntype Subscribe struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMetadata     *MessageMetadata `protobuf:\"bytes,1,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tSubscriberId string           `protobuf:\"bytes,2,opt,name=subscriber_id,json=subscriberId,proto3\" json:\"subscriber_id,omitempty\"`\n\tEmail        string           `protobuf:\"bytes,3,opt,name=email,proto3\" json:\"email,omitempty\"`\n}\n\nfunc (x *Subscribe) Reset() {\n\t*x = Subscribe{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Subscribe) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Subscribe) ProtoMessage() {}\n\nfunc (x *Subscribe) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Subscribe.ProtoReflect.Descriptor instead.\nfunc (*Subscribe) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Subscribe) GetMetadata() *MessageMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\nfunc (x *Subscribe) GetSubscriberId() string {\n\tif x != nil {\n\t\treturn x.SubscriberId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Subscribe) GetEmail() string {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn \"\"\n}\n\ntype Unsubscribe struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMetadata     *MessageMetadata `protobuf:\"bytes,1,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tSubscriberId string           `protobuf:\"bytes,2,opt,name=subscriber_id,json=subscriberId,proto3\" json:\"subscriber_id,omitempty\"`\n}\n\nfunc (x *Unsubscribe) Reset() {\n\t*x = Unsubscribe{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[2]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Unsubscribe) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Unsubscribe) ProtoMessage() {}\n\nfunc (x *Unsubscribe) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[2]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Unsubscribe.ProtoReflect.Descriptor instead.\nfunc (*Unsubscribe) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Unsubscribe) GetMetadata() *MessageMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\nfunc (x *Unsubscribe) GetSubscriberId() string {\n\tif x != nil {\n\t\treturn x.SubscriberId\n\t}\n\treturn \"\"\n}\n\ntype UpdateEmail struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMetadata     *MessageMetadata `protobuf:\"bytes,1,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tSubscriberId string           `protobuf:\"bytes,2,opt,name=subscriber_id,json=subscriberId,proto3\" json:\"subscriber_id,omitempty\"`\n\tNewEmail     string           `protobuf:\"bytes,3,opt,name=new_email,json=newEmail,proto3\" json:\"new_email,omitempty\"`\n}\n\nfunc (x *UpdateEmail) Reset() {\n\t*x = UpdateEmail{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[3]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *UpdateEmail) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateEmail) ProtoMessage() {}\n\nfunc (x *UpdateEmail) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[3]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateEmail.ProtoReflect.Descriptor instead.\nfunc (*UpdateEmail) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *UpdateEmail) GetMetadata() *MessageMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateEmail) GetSubscriberId() string {\n\tif x != nil {\n\t\treturn x.SubscriberId\n\t}\n\treturn \"\"\n}\n\nfunc (x *UpdateEmail) GetNewEmail() string {\n\tif x != nil {\n\t\treturn x.NewEmail\n\t}\n\treturn \"\"\n}\n\n// Events\ntype SubscriberSubscribed struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMetadata     *MessageMetadata `protobuf:\"bytes,1,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tSubscriberId string           `protobuf:\"bytes,2,opt,name=subscriber_id,json=subscriberId,proto3\" json:\"subscriber_id,omitempty\"`\n\tEmail        string           `protobuf:\"bytes,3,opt,name=email,proto3\" json:\"email,omitempty\"`\n}\n\nfunc (x *SubscriberSubscribed) Reset() {\n\t*x = SubscriberSubscribed{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[4]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *SubscriberSubscribed) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SubscriberSubscribed) ProtoMessage() {}\n\nfunc (x *SubscriberSubscribed) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[4]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SubscriberSubscribed.ProtoReflect.Descriptor instead.\nfunc (*SubscriberSubscribed) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *SubscriberSubscribed) GetMetadata() *MessageMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\nfunc (x *SubscriberSubscribed) GetSubscriberId() string {\n\tif x != nil {\n\t\treturn x.SubscriberId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SubscriberSubscribed) GetEmail() string {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn \"\"\n}\n\ntype SubscriberUnsubscribed struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMetadata     *MessageMetadata `protobuf:\"bytes,1,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tSubscriberId string           `protobuf:\"bytes,2,opt,name=subscriber_id,json=subscriberId,proto3\" json:\"subscriber_id,omitempty\"`\n}\n\nfunc (x *SubscriberUnsubscribed) Reset() {\n\t*x = SubscriberUnsubscribed{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[5]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *SubscriberUnsubscribed) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SubscriberUnsubscribed) ProtoMessage() {}\n\nfunc (x *SubscriberUnsubscribed) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[5]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SubscriberUnsubscribed.ProtoReflect.Descriptor instead.\nfunc (*SubscriberUnsubscribed) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *SubscriberUnsubscribed) GetMetadata() *MessageMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\nfunc (x *SubscriberUnsubscribed) GetSubscriberId() string {\n\tif x != nil {\n\t\treturn x.SubscriberId\n\t}\n\treturn \"\"\n}\n\ntype SubscriberEmailUpdated struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMetadata     *MessageMetadata `protobuf:\"bytes,1,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tSubscriberId string           `protobuf:\"bytes,2,opt,name=subscriber_id,json=subscriberId,proto3\" json:\"subscriber_id,omitempty\"`\n\tNewEmail     string           `protobuf:\"bytes,3,opt,name=new_email,json=newEmail,proto3\" json:\"new_email,omitempty\"`\n}\n\nfunc (x *SubscriberEmailUpdated) Reset() {\n\t*x = SubscriberEmailUpdated{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_messages_proto_msgTypes[6]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *SubscriberEmailUpdated) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SubscriberEmailUpdated) ProtoMessage() {}\n\nfunc (x *SubscriberEmailUpdated) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[6]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SubscriberEmailUpdated.ProtoReflect.Descriptor instead.\nfunc (*SubscriberEmailUpdated) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *SubscriberEmailUpdated) GetMetadata() *MessageMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\nfunc (x *SubscriberEmailUpdated) GetSubscriberId() string {\n\tif x != nil {\n\t\treturn x.SubscriberId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SubscriberEmailUpdated) GetNewEmail() string {\n\tif x != nil {\n\t\treturn x.NewEmail\n\t}\n\treturn \"\"\n}\n\nvar File_messages_proto protoreflect.FileDescriptor\n\nvar file_messages_proto_rawDesc = []byte{\n\t0x0a, 0x0e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,\n\t0x12, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,\n\t0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x71, 0x0a, 0x0f, 0x4d, 0x65, 0x73, 0x73, 0x61,\n\t0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61,\n\t0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,\n\t0x09, 0x52, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12,\n\t0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20,\n\t0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,\n\t0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,\n\t0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x79, 0x0a, 0x09, 0x53, 0x75,\n\t0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,\n\t0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e,\n\t0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,\n\t0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75,\n\t0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,\n\t0x09, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x49, 0x64, 0x12,\n\t0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,\n\t0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x65, 0x0a, 0x0b, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63,\n\t0x72, 0x69, 0x62, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,\n\t0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x4d, 0x65,\n\t0x73, 0x73, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d,\n\t0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63,\n\t0x72, 0x69, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,\n\t0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x49, 0x64, 0x22, 0x82, 0x01, 0x0a,\n\t0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x31, 0x0a, 0x08,\n\t0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15,\n\t0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74,\n\t0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12,\n\t0x23, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64,\n\t0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,\n\t0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x65, 0x6d, 0x61, 0x69,\n\t0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x45, 0x6d, 0x61, 0x69,\n\t0x6c, 0x22, 0x84, 0x01, 0x0a, 0x14, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72,\n\t0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65,\n\t0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d,\n\t0x61, 0x69, 0x6e, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64,\n\t0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a,\n\t0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02,\n\t0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72,\n\t0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28,\n\t0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x70, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73,\n\t0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,\n\t0x65, 0x64, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01,\n\t0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x4d, 0x65, 0x73, 0x73,\n\t0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74,\n\t0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,\n\t0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x75,\n\t0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x49, 0x64, 0x22, 0x8d, 0x01, 0x0a, 0x16, 0x53,\n\t0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x55, 0x70,\n\t0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,\n\t0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x4d,\n\t0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08,\n\t0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73,\n\t0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,\n\t0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a,\n\t0x09, 0x6e, 0x65, 0x77, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,\n\t0x52, 0x08, 0x6e, 0x65, 0x77, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x42, 0x08, 0x5a, 0x06, 0x2e, 0x2f,\n\t0x6d, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_messages_proto_rawDescOnce sync.Once\n\tfile_messages_proto_rawDescData = file_messages_proto_rawDesc\n)\n\nfunc file_messages_proto_rawDescGZIP() []byte {\n\tfile_messages_proto_rawDescOnce.Do(func() {\n\t\tfile_messages_proto_rawDescData = protoimpl.X.CompressGZIP(file_messages_proto_rawDescData)\n\t})\n\treturn file_messages_proto_rawDescData\n}\n\nvar file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 7)\nvar file_messages_proto_goTypes = []interface{}{\n\t(*MessageMetadata)(nil),        // 0: main.MessageMetadata\n\t(*Subscribe)(nil),              // 1: main.Subscribe\n\t(*Unsubscribe)(nil),            // 2: main.Unsubscribe\n\t(*UpdateEmail)(nil),            // 3: main.UpdateEmail\n\t(*SubscriberSubscribed)(nil),   // 4: main.SubscriberSubscribed\n\t(*SubscriberUnsubscribed)(nil), // 5: main.SubscriberUnsubscribed\n\t(*SubscriberEmailUpdated)(nil), // 6: main.SubscriberEmailUpdated\n\t(*timestamppb.Timestamp)(nil),  // 7: google.protobuf.Timestamp\n}\nvar file_messages_proto_depIdxs = []int32{\n\t7, // 0: main.MessageMetadata.created_at:type_name -> google.protobuf.Timestamp\n\t0, // 1: main.Subscribe.metadata:type_name -> main.MessageMetadata\n\t0, // 2: main.Unsubscribe.metadata:type_name -> main.MessageMetadata\n\t0, // 3: main.UpdateEmail.metadata:type_name -> main.MessageMetadata\n\t0, // 4: main.SubscriberSubscribed.metadata:type_name -> main.MessageMetadata\n\t0, // 5: main.SubscriberUnsubscribed.metadata:type_name -> main.MessageMetadata\n\t0, // 6: main.SubscriberEmailUpdated.metadata:type_name -> main.MessageMetadata\n\t7, // [7:7] is the sub-list for method output_type\n\t7, // [7:7] is the sub-list for method input_type\n\t7, // [7:7] is the sub-list for extension type_name\n\t7, // [7:7] is the sub-list for extension extendee\n\t0, // [0:7] is the sub-list for field type_name\n}\n\nfunc init() { file_messages_proto_init() }\nfunc file_messages_proto_init() {\n\tif File_messages_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_messages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*MessageMetadata); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Subscribe); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Unsubscribe); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*UpdateEmail); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*SubscriberSubscribed); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*SubscriberUnsubscribed); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_messages_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*SubscriberEmailUpdated); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_messages_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   7,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_messages_proto_goTypes,\n\t\tDependencyIndexes: file_messages_proto_depIdxs,\n\t\tMessageInfos:      file_messages_proto_msgTypes,\n\t}.Build()\n\tFile_messages_proto = out.File\n\tfile_messages_proto_rawDesc = nil\n\tfile_messages_proto_goTypes = nil\n\tfile_messages_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/proto/messages.proto",
    "content": "syntax = \"proto3\";\npackage main;\n\noption go_package = \"./main\";\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage MessageMetadata {\n  string partition_key = 1;\n  google.protobuf.Timestamp created_at = 2;\n}\n\n// Commands\nmessage Subscribe {\n  MessageMetadata metadata = 1;\n\n  string subscriber_id = 2;\n  string email = 3;\n}\n\nmessage Unsubscribe {\n  MessageMetadata metadata = 1;\n\n  string subscriber_id = 2;\n}\n\nmessage UpdateEmail {\n  MessageMetadata metadata = 1;\n\n  string subscriber_id = 2;\n  string new_email = 3;\n}\n\n// Events\nmessage SubscriberSubscribed {\n  MessageMetadata metadata = 1;\n\n  string subscriber_id = 2;\n  string email = 3;\n}\n\nmessage SubscriberUnsubscribed {\n  MessageMetadata metadata = 1;\n\n  string subscriber_id = 2;\n}\n\nmessage SubscriberEmailUpdated {\n  MessageMetadata metadata = 1;\n\n  string subscriber_id = 2;\n  string new_email = 3;\n}"
  },
  {
    "path": "_examples/basic/6-cqrs-ordered-events/subscribers.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sync\"\n)\n\ntype SubscriberReadModel struct {\n\tsubscribers map[string]string // map[subscriberID]email\n\tlock        sync.RWMutex\n}\n\nfunc NewSubscriberReadModel() *SubscriberReadModel {\n\treturn &SubscriberReadModel{\n\t\tsubscribers: make(map[string]string),\n\t}\n}\n\nfunc (m *SubscriberReadModel) OnSubscribed(ctx context.Context, event *SubscriberSubscribed) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tm.subscribers[event.SubscriberId] = event.Email\n\n\tslog.Info(\n\t\t\"Subscriber added\",\n\t\t\"subscriber_id\", event.SubscriberId,\n\t\t\"email\", event.Email,\n\t)\n\n\treturn nil\n}\n\nfunc (m *SubscriberReadModel) OnUnsubscribed(ctx context.Context, event *SubscriberUnsubscribed) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tdelete(m.subscribers, event.SubscriberId)\n\n\tslog.Info(\n\t\t\"Subscriber removed\",\n\t\t\"subscriber_id\", event.SubscriberId,\n\t)\n\n\treturn nil\n}\n\nfunc (m *SubscriberReadModel) OnEmailUpdated(ctx context.Context, event *SubscriberEmailUpdated) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tm.subscribers[event.SubscriberId] = event.NewEmail\n\n\tslog.Info(\n\t\t\"Subscriber updated\",\n\t\t\"subscriber_id\", event.SubscriberId,\n\t\t\"email\", event.NewEmail,\n\t)\n\n\treturn nil\n}\n\nfunc (m *SubscriberReadModel) GetSubscriberCount() int {\n\tm.lock.RLock()\n\tdefer m.lock.RUnlock()\n\treturn len(m.subscribers)\n}\n"
  },
  {
    "path": "_examples/pubsubs/amqp/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 120\nexpected_output: \"received message: [0-9a-f\\\\-]+, payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/amqp/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - rabbitmq\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  rabbitmq:\n    image: rabbitmq:3.7\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/pubsubs/amqp/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rabbitmq/amqp091-go v1.10.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/amqp/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2 h1:aeyFSR4SUsbszmocuFiYY13nsHorc6CXIS2Hy7+xgFU=\ngithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2/go.mod h1:+8tCh6VCuBcQWhfETCwzRINKQ1uyeg9moH3h7jMKxQk=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=\ngithub.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/amqp/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nvar amqpURI = \"amqp://guest:guest@rabbitmq:5672/\"\n\nfunc main() {\n\tamqpConfig := amqp.NewDurableQueueConfig(amqpURI)\n\n\tsubscriber, err := amqp.NewSubscriber(\n\t\t// This config is based on this example: https://www.rabbitmq.com/tutorials/tutorial-two-go.html\n\t\t// It works as a simple queue.\n\t\t//\n\t\t// If you want to implement a Pub/Sub style service instead, check\n\t\t// https://watermill.io/pubsubs/amqp/#amqp-consumer-groups\n\t\tamqpConfig,\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := amqp.NewPublisher(amqpConfig, watermill.NewStdLogger(false, false))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/aws-sns/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 120\nexpected_output: \"A received message: [0-9a-f\\\\-]+, payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/aws-sns/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  localstack:\n    image: localstack/localstack:3.8\n    environment:\n      - SERVICES=sqs,sns\n      - AWS_DEFAULT_REGION=us-east-1\n      - EDGE_PORT=4566\n    ports:\n      - \"4566-4597:4566-4597\"\n    healthcheck:\n      test: awslocal sqs list-queues && awslocal sns list-topics\n      interval: 5s\n      timeout: 5s\n      retries: 5\n      start_period: 30s\n"
  },
  {
    "path": "_examples/pubsubs/aws-sns/go.mod",
    "content": "module main\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-aws v1.0.1\n\tgithub.com/aws/aws-sdk-go-v2 v1.38.3\n\tgithub.com/aws/aws-sdk-go-v2/service/sns v1.38.1\n\tgithub.com/aws/aws-sdk-go-v2/service/sqs v1.42.3\n\tgithub.com/aws/smithy-go v1.23.0\n\tgithub.com/samber/lo v1.51.0\n)\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/aws-sns/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-aws v1.0.1 h1:lsXp7iIih2Eqlm9p05u9QC3G9DemAMi88qMFkq+810w=\ngithub.com/ThreeDotsLabs/watermill-aws v1.0.1/go.mod h1:jlGFr7vhmzAESlU/PE5BCyuat3w/gr5zmwx1oNm1yh8=\ngithub.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=\ngithub.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=\ngithub.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0=\ngithub.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.38.1 h1:6AqFh9gI+BEOlKRXaYryGMCwygwaTlISVUs6qEMosaU=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.38.1/go.mod h1:wZGK3CJNllAOeJ/xrnyTHotaXEvtC27KOLMMKGBeT+4=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.42.3 h1:0dWg1Tkz3FnEo48DgAh7CT22hYyMShly8WMd3sGx0xI=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.42.3/go.mod h1:hpOo4IGPfGPlHRcf2nizYAzKfz8GzbQ8tTDIUR4H4GQ=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo=\ngithub.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=\ngithub.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=\ngithub.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/aws-sns/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tamazonsns \"github.com/aws/aws-sdk-go-v2/service/sns\"\n\tamazonsqs \"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\ttransport \"github.com/aws/smithy-go/endpoints\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-aws/sns\"\n\t\"github.com/ThreeDotsLabs/watermill-aws/sqs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tsnsOpts := []func(*amazonsns.Options){\n\t\tamazonsns.WithEndpointResolverV2(sns.OverrideEndpointResolver{\n\t\t\tEndpoint: transport.Endpoint{\n\t\t\t\tURI: *lo.Must(url.Parse(\"http://localstack:4566\")),\n\t\t\t},\n\t\t}),\n\t}\n\n\tsqsOpts := []func(*amazonsqs.Options){\n\t\tamazonsqs.WithEndpointResolverV2(sqs.OverrideEndpointResolver{\n\t\t\tEndpoint: transport.Endpoint{\n\t\t\t\tURI: *lo.Must(url.Parse(\"http://localstack:4566\")),\n\t\t\t},\n\t\t}),\n\t}\n\n\ttopicResolver, err := sns.NewGenerateArnTopicResolver(\"000000000000\", \"us-east-1\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tnewSubscriber := func(name string) (message.Subscriber, error) {\n\t\tsubscriberConfig := sns.SubscriberConfig{\n\t\t\tAWSConfig: aws.Config{\n\t\t\t\tCredentials: aws.AnonymousCredentials{},\n\t\t\t},\n\t\t\tOptFns:        snsOpts,\n\t\t\tTopicResolver: topicResolver,\n\t\t\tGenerateSqsQueueName: func(ctx context.Context, snsTopic sns.TopicArn) (string, error) {\n\t\t\t\ttopic, err := sns.ExtractTopicNameFromTopicArn(snsTopic)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\n\t\t\t\treturn fmt.Sprintf(\"%v-%v\", topic, name), nil\n\t\t\t},\n\t\t}\n\n\t\tsqsSubscriberConfig := sqs.SubscriberConfig{\n\t\t\tAWSConfig: aws.Config{\n\t\t\t\tCredentials: aws.AnonymousCredentials{},\n\t\t\t},\n\t\t\tOptFns: sqsOpts,\n\t\t}\n\n\t\treturn sns.NewSubscriber(subscriberConfig, sqsSubscriberConfig, logger)\n\t}\n\n\tsubA, err := newSubscriber(\"subA\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsubB, err := newSubscriber(\"subB\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessagesA, err := subA.Subscribe(context.Background(), \"example-topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessagesB, err := subB.Subscribe(context.Background(), \"example-topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(\"A\", messagesA)\n\tgo process(\"B\", messagesB)\n\n\tpublisherConfig := sns.PublisherConfig{\n\t\tAWSConfig: aws.Config{\n\t\t\tCredentials: aws.AnonymousCredentials{},\n\t\t},\n\t\tOptFns:        snsOpts,\n\t\tTopicResolver: topicResolver,\n\t}\n\n\tpublisher, err := sns.NewPublisher(publisherConfig, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example-topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(prefix string, messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"%v received message: %s, payload: %s\", prefix, msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/aws-sqs/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 120\nexpected_output: \"received message: [0-9a-f\\\\-]+, payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/aws-sqs/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  localstack:\n    image: localstack/localstack:latest\n    environment:\n      - SERVICES=sqs,sns\n      - AWS_DEFAULT_REGION=us-east-1\n      - EDGE_PORT=4566\n    ports:\n      - \"4566-4597:4566-4597\"\n    healthcheck:\n      test: awslocal sqs list-queues && awslocal sns list-topics\n      interval: 5s\n      timeout: 5s\n      retries: 5\n      start_period: 30s\n"
  },
  {
    "path": "_examples/pubsubs/aws-sqs/go.mod",
    "content": "module main\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-aws v1.0.1\n\tgithub.com/aws/aws-sdk-go-v2 v1.38.3\n\tgithub.com/aws/aws-sdk-go-v2/service/sqs v1.42.3\n\tgithub.com/aws/smithy-go v1.23.0\n\tgithub.com/samber/lo v1.51.0\n)\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/aws-sqs/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-aws v1.0.1 h1:lsXp7iIih2Eqlm9p05u9QC3G9DemAMi88qMFkq+810w=\ngithub.com/ThreeDotsLabs/watermill-aws v1.0.1/go.mod h1:jlGFr7vhmzAESlU/PE5BCyuat3w/gr5zmwx1oNm1yh8=\ngithub.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=\ngithub.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=\ngithub.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0=\ngithub.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.42.3 h1:0dWg1Tkz3FnEo48DgAh7CT22hYyMShly8WMd3sGx0xI=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.42.3/go.mod h1:hpOo4IGPfGPlHRcf2nizYAzKfz8GzbQ8tTDIUR4H4GQ=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo=\ngithub.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=\ngithub.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=\ngithub.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/aws-sqs/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tamazonsqs \"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\ttransport \"github.com/aws/smithy-go/endpoints\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-aws/sqs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tsqsOpts := []func(*amazonsqs.Options){\n\t\tamazonsqs.WithEndpointResolverV2(sqs.OverrideEndpointResolver{\n\t\t\tEndpoint: transport.Endpoint{\n\t\t\t\tURI: *lo.Must(url.Parse(\"http://localstack:4566\")),\n\t\t\t},\n\t\t}),\n\t}\n\n\tsubscriberConfig := sqs.SubscriberConfig{\n\t\tAWSConfig: aws.Config{\n\t\t\tCredentials: aws.AnonymousCredentials{},\n\t\t},\n\t\tOptFns: sqsOpts,\n\t}\n\n\tsubscriber, err := sqs.NewSubscriber(subscriberConfig, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example-topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisherConfig := sqs.PublisherConfig{\n\t\tAWSConfig: aws.Config{\n\t\t\tCredentials: aws.AnonymousCredentials{},\n\t\t},\n\t\tOptFns: sqsOpts,\n\t}\n\n\tpublisher, err := sqs.NewPublisher(publisherConfig, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example-topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/go-channel/.validate_example.yml",
    "content": "validation_cmd: \"go run main.go\"\ntimeout: 30\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/go-channel/go.mod",
    "content": "module main.go\n\nrequire github.com/ThreeDotsLabs/watermill v1.5.1\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/go-channel/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/go-channel/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nfunc main() {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\n\tmessages, err := pubSub.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublishMessages(pubSub)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tfmt.Printf(\"received message: %s, payload: %s\\n\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/googlecloud/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/googlecloud/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - googlecloud\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    environment:\n      # use local emulator instead of google cloud engine\n      PUBSUB_EMULATOR_HOST: \"googlecloud:8085\"\n    working_dir: /app\n    command: go run main.go\n\n  googlecloud:\n    image: google/cloud-sdk:414.0.0\n    entrypoint: gcloud --quiet beta emulators pubsub start --host-port=0.0.0.0:8085 --verbosity=debug --log-http\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/pubsubs/googlecloud/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0\n)\n\nrequire (\n\tcloud.google.com/go v0.121.6 // indirect\n\tcloud.google.com/go/auth v0.16.5 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.8.0 // indirect\n\tcloud.google.com/go/iam v1.5.2 // indirect\n\tcloud.google.com/go/pubsub/v2 v2.0.0 // indirect\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/api v0.248.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/grpc v1.75.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/googlecloud/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=\ncloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=\ncloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=\ncloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=\ncloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=\ncloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=\ncloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=\ncloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=\ncloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0 h1:GXR+tsxPs/Vpmm0t4yEJUZdqLP9EytWvR+KN3Un5mNY=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0/go.mod h1:3IHyi1bNqQ8J2/wVWj4cQjzWXoEPauLm8ViyOCNaKbM=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=\ngo.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=\ngoogle.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=\ngoogle.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "_examples/pubsubs/googlecloud/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-googlecloud/v2/pkg/googlecloud\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\tsubscriber, err := googlecloud.NewSubscriber(\n\t\tgooglecloud.SubscriberConfig{\n\t\t\t// custom function to generate Subscription Name,\n\t\t\t// there are also predefined TopicSubscriptionName and TopicSubscriptionNameWithSuffix available.\n\t\t\tGenerateSubscriptionName: func(topic string) string {\n\t\t\t\treturn \"test-sub_\" + topic\n\t\t\t},\n\t\t\tProjectID: \"test-project\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Subscribe will create the subscription. Only messages that are sent after the subscription is created may be received.\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := googlecloud.NewPublisher(googlecloud.PublisherConfig{\n\t\tProjectID: \"test-project\",\n\t}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/kafka/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/kafka/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - kafka\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  zookeeper:\n    image: confluentinc/cp-zookeeper:7.3.1\n    restart: unless-stopped\n    logging:\n      driver: none\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n\n  kafka:\n    image: confluentinc/cp-kafka:7.3.1\n    restart: unless-stopped\n    depends_on:\n      - zookeeper\n    logging:\n      driver: none\n    environment:\n      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n"
  },
  {
    "path": "_examples/pubsubs/kafka/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/kafka/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/kafka/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/IBM/sarama\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tsaramaSubscriberConfig := kafka.DefaultSaramaSubscriberConfig()\n\t// equivalent of auto.offset.reset: earliest\n\tsaramaSubscriberConfig.Consumer.Offsets.Initial = sarama.OffsetOldest\n\n\tsubscriber, err := kafka.NewSubscriber(\n\t\tkafka.SubscriberConfig{\n\t\t\tBrokers:               []string{\"kafka:9092\"},\n\t\t\tUnmarshaler:           kafka.DefaultMarshaler{},\n\t\t\tOverwriteSaramaConfig: saramaSubscriberConfig,\n\t\t\tConsumerGroup:         \"test_consumer_group\",\n\t\t},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   []string{\"kafka:9092\"},\n\t\t\tMarshaler: kafka.DefaultMarshaler{},\n\t\t},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/nats-core/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/nats-core/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - nats\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  nats:\n    image: nats:2\n    ports:\n      - \"0.0.0.0:4222:4222\"\n    restart: unless-stopped\n    command: [\"-js\"]\n    ulimits:\n      nofile:\n        soft: 65536\n        hard: 65536\n"
  },
  {
    "path": "_examples/pubsubs/nats-core/go.mod",
    "content": "module main\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3\n\tgithub.com/nats-io/nats.go v1.45.0\n)\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/nats-io/nkeys v0.4.11 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n)\n"
  },
  {
    "path": "_examples/pubsubs/nats-core/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3 h1:/5IfNugBb9H+BvEHHNRnICmF3jaI9P7wVRzA12kDDDs=\ngithub.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/nats-io/nats.go v1.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA=\ngithub.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=\ngithub.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=\ngithub.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/nats-core/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-nats/v2/pkg/nats\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\tnc \"github.com/nats-io/nats.go\"\n)\n\nfunc main() {\n\tnatsURL := \"nats://nats:4222\"\n\tmarshaler := &nats.GobMarshaler{}\n\tlogger := watermill.NewStdLogger(false, false)\n\toptions := []nc.Option{\n\t\tnc.RetryOnFailedConnect(true),\n\t\tnc.Timeout(30 * time.Second),\n\t\tnc.ReconnectWait(1 * time.Second),\n\t}\n\n\tjsConfig := nats.JetStreamConfig{Disabled: true}\n\n\tsubscriber, err := nats.NewSubscriber(\n\t\tnats.SubscriberConfig{\n\t\t\tURL:            natsURL,\n\t\t\tCloseTimeout:   30 * time.Second,\n\t\t\tAckWaitTimeout: 30 * time.Second,\n\t\t\tNatsOptions:    options,\n\t\t\tUnmarshaler:    marshaler,\n\t\t\tJetStream:      jsConfig,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tc := make(chan os.Signal)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-c\n\t\tfmt.Println(\"\\r- Ctrl+C pressed in Terminal - closing subscriber\")\n\t\tsubscriber.Close()\n\t\tos.Exit(0)\n\t}()\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example_topic_nats\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := nats.NewPublisher(\n\t\tnats.PublisherConfig{\n\t\t\tURL:         natsURL,\n\t\t\tNatsOptions: options,\n\t\t\tMarshaler:   marshaler,\n\t\t\tJetStream:   jsConfig,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example_topic_nats\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/nats-jetstream/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/nats-jetstream/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - nats\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  nats:\n    image: nats:2\n    ports:\n      - \"0.0.0.0:4222:4222\"\n    restart: unless-stopped\n    command: [\"-js\"]\n    ulimits:\n      nofile:\n        soft: 65536\n        hard: 65536\n"
  },
  {
    "path": "_examples/pubsubs/nats-jetstream/go.mod",
    "content": "module main\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3\n\tgithub.com/nats-io/nats.go v1.45.0\n)\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/nats-io/nkeys v0.4.11 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n)\n"
  },
  {
    "path": "_examples/pubsubs/nats-jetstream/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3 h1:/5IfNugBb9H+BvEHHNRnICmF3jaI9P7wVRzA12kDDDs=\ngithub.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/nats-io/nats.go v1.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA=\ngithub.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=\ngithub.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=\ngithub.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/nats-jetstream/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-nats/v2/pkg/nats\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\tnc \"github.com/nats-io/nats.go\"\n)\n\nfunc main() {\n\tnatsURL := \"nats://nats:4222\"\n\tmarshaler := &nats.GobMarshaler{}\n\tlogger := watermill.NewStdLogger(false, false)\n\toptions := []nc.Option{\n\t\tnc.RetryOnFailedConnect(true),\n\t\tnc.Timeout(30 * time.Second),\n\t\tnc.ReconnectWait(1 * time.Second),\n\t}\n\tsubscribeOptions := []nc.SubOpt{\n\t\tnc.DeliverAll(),\n\t\tnc.AckExplicit(),\n\t}\n\n\tjsConfig := nats.JetStreamConfig{\n\t\tDisabled:         false,\n\t\tAutoProvision:    true,\n\t\tConnectOptions:   nil,\n\t\tSubscribeOptions: subscribeOptions,\n\t\tPublishOptions:   nil,\n\t\tTrackMsgId:       false,\n\t\tAckAsync:         false,\n\t\tDurablePrefix:    \"\",\n\t}\n\tsubscriber, err := nats.NewSubscriber(\n\t\tnats.SubscriberConfig{\n\t\t\tURL:            natsURL,\n\t\t\tCloseTimeout:   30 * time.Second,\n\t\t\tAckWaitTimeout: 30 * time.Second,\n\t\t\tNatsOptions:    options,\n\t\t\tUnmarshaler:    marshaler,\n\t\t\tJetStream:      jsConfig,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tc := make(chan os.Signal)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-c\n\t\tfmt.Println(\"\\r- Ctrl+C pressed in Terminal - closing subscriber\")\n\t\tsubscriber.Close()\n\t\tos.Exit(0)\n\t}()\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example_topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo processJS(messages)\n\n\tpublisher, err := nats.NewPublisher(\n\t\tnats.PublisherConfig{\n\t\t\tURL:         natsURL,\n\t\t\tNatsOptions: options,\n\t\t\tMarshaler:   marshaler,\n\t\t\tJetStream:   jsConfig,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example_topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc processJS(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/nats-streaming/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/nats-streaming/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - nats-streaming\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  nats-streaming:\n    image: nats-streaming:0.11.2\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/pubsubs/nats-streaming/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-nats v1.0.7\n\tgithub.com/nats-io/stan.go v0.10.4\n)\n\nrequire (\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/go-hclog v1.4.0 // indirect\n\tgithub.com/hashicorp/go-msgpack v0.5.5 // indirect\n\tgithub.com/hashicorp/raft v1.3.11 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/minio/highwayhash v1.0.2 // indirect\n\tgithub.com/nats-io/jwt/v2 v2.3.0 // indirect\n\tgithub.com/nats-io/nats.go v1.45.0 // indirect\n\tgithub.com/nats-io/nkeys v0.4.11 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgo.etcd.io/bbolt v1.3.6 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/time v0.3.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/pubsubs/nats-streaming/go.sum",
    "content": "github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-nats v1.0.7 h1:hOquWq0GAwm5jaIc3wGaDoVCPYL+If4NZPb+RUaHni4=\ngithub.com/ThreeDotsLabs/watermill-nats v1.0.7/go.mod h1:t5A8XbO/v8CPM+AIljgoO9NR1jBk3ixYBGAtvn1N4lA=\ngithub.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=\ngithub.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=\ngithub.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=\ngithub.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=\ngithub.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/raft v1.3.11 h1:p3v6gf6l3S797NnK5av3HcczOC1T5CLoaRvg0g9ys4A=\ngithub.com/hashicorp/raft v1.3.11/go.mod h1:J8naEwc6XaaCfts7+28whSeRvCqTd6e20BlCU3LtEO4=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI=\ngithub.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=\ngithub.com/nats-io/nats-server/v2 v2.6.1 h1:cJy+ia7/4EaJL+ZYDmIy2rD1mDWTfckhtPBU0GYo8xM=\ngithub.com/nats-io/nats-server/v2 v2.6.1/go.mod h1:Az91TbZiV7K4a6k/4v6YYdOKEoxCXj+iqhHVf/MlrKo=\ngithub.com/nats-io/nats-streaming-server v0.22.1 h1:YKDdLAWZud3UnEBvUPaYppMxSDuh+9czTCDriq19tJY=\ngithub.com/nats-io/nats-streaming-server v0.22.1/go.mod h1:1WpVkVV5NyZbHuGGxkaPWopLFnxNthO/TK/BkzFdnPE=\ngithub.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA=\ngithub.com/nats-io/nats.go v1.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA=\ngithub.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=\ngithub.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw=\ngithub.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=\ngithub.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=\ngo.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=\ngolang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/nats-streaming/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\tstan \"github.com/nats-io/stan.go\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-nats/pkg/nats\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tsubscriber, err := nats.NewStreamingSubscriber(\n\t\tnats.StreamingSubscriberConfig{\n\t\t\tClusterID:        \"test-cluster\",\n\t\t\tClientID:         \"example-subscriber\",\n\t\t\tQueueGroup:       \"example\",\n\t\t\tDurableName:      \"my-durable\",\n\t\t\tSubscribersCount: 4, // how many goroutines should consume messages\n\t\t\tCloseTimeout:     time.Minute,\n\t\t\tAckWaitTimeout:   time.Second * 30,\n\t\t\tStanOptions: []stan.Option{\n\t\t\t\tstan.NatsURL(\"nats://nats-streaming:4222\"),\n\t\t\t},\n\t\t\tUnmarshaler: nats.GobMarshaler{},\n\t\t},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := nats.NewStreamingPublisher(\n\t\tnats.StreamingPublisherConfig{\n\t\t\tClusterID: \"test-cluster\",\n\t\t\tClientID:  \"example-publisher\",\n\t\t\tStanOptions: []stan.Option{\n\t\t\t\tstan.NatsURL(\"nats://nats-streaming:4222\"),\n\t\t\t},\n\t\t\tMarshaler: nats.GobMarshaler{},\n\t\t},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/redisstream/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/redisstream/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - redis\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  redis:\n    image: redis:7\n    ports:\n      - 6379:6379\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/pubsubs/redisstream/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4\n\tgithub.com/redis/go-redis/v9 v9.12.1\n)\n\nrequire (\n\tgithub.com/Rican7/retry v0.3.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/vmihailenco/msgpack v4.0.4+incompatible // indirect\n\tgoogle.golang.org/appengine v1.6.8 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n)\n"
  },
  {
    "path": "_examples/pubsubs/redisstream/go.sum",
    "content": "github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=\ngithub.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4 h1:vkpSm2MZHacjN4H8R0PA9IKQ++uQMq6wA0m1bnGjipo=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4/go.mod h1:Da3wqG1OcvHPODjuJcxSCY1O7D4loIZQpVbZ5u94xRo=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=\ngithub.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/redisstream/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tsubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t\tDB:   0,\n\t})\n\tsubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient:        subClient,\n\t\t\tUnmarshaller:  redisstream.DefaultMarshallerUnmarshaller{},\n\t\t\tConsumerGroup: \"test_consumer_group\",\n\t\t},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t\tDB:   0,\n\t})\n\tpublisher, err := redisstream.NewPublisher(\n\t\tredisstream.PublisherConfig{\n\t\t\tClient:     pubClient,\n\t\t\tMarshaller: redisstream.DefaultMarshallerUnmarshaller{},\n\t\t},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/sql/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"Hello, world!\"\n"
  },
  {
    "path": "_examples/pubsubs/sql/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - mysql\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  mysql:\n    image: mysql:8.0\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_DATABASE: watermill\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n"
  },
  {
    "path": "_examples/pubsubs/sql/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n"
  },
  {
    "path": "_examples/pubsubs/sql/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/pubsubs/sql/main.go",
    "content": "// Sources for https://watermill.io/learn/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"log\"\n\t\"time\"\n\n\tdriver \"github.com/go-sql-driver/mysql\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tdb := createDB()\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tsubscriber, err := sql.NewSubscriber(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.SubscriberConfig{\n\t\t\tSchemaAdapter:    sql.DefaultMySQLSchema{},\n\t\t\tOffsetsAdapter:   sql.DefaultMySQLOffsetsAdapter{},\n\t\t\tInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example_topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := sql.NewPublisher(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter: sql.DefaultMySQLSchema{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"watermill\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(`{\"message\": \"Hello, world!\"}`))\n\n\t\tif err := publisher.Publish(\"example_topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/sqlite/.gitignore",
    "content": "db.sqlite3*"
  },
  {
    "path": "_examples/pubsubs/sqlite/.validate_example.yml",
    "content": "validation_cmd: \"go run .\"\ntimeout: 30\nexpected_stdout:\n  - \"received message:\"\n"
  },
  {
    "path": "_examples/pubsubs/sqlite/go.mod",
    "content": "module sqlite\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.1.1\n\tmodernc.org/sqlite v1.39.0\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect\n\tgolang.org/x/sys v0.36.0 // indirect\n\tmodernc.org/libc v1.66.8 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n)\n"
  },
  {
    "path": "_examples/pubsubs/sqlite/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.0.7 h1:yrHtw0WxXn+aoZU0+PdVCS+AC0d5wTC3v73sVhMLgXc=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.0.7/go.mod h1:+sg/sEWQtVzzzDnrcd/Lva2CP9D3gDTE9nBkM2toujI=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.0.8/go.mod h1:Os8F1QAkBJFoWNABp6iql3lJaKzG3b70mesQV5Iu+oU=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.1.0 h1:NNgUlhjuG5oAcXlXcB9HHYBcqwG12VcJz1DaVBaUwQg=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.1.0/go.mod h1:Os8F1QAkBJFoWNABp6iql3lJaKzG3b70mesQV5Iu+oU=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.1.1 h1:t4f8bPmZfGv8+/VO3prla5rklVbex4OpmsK6+SkPR+c=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc v0.1.1/go.mod h1:Os8F1QAkBJFoWNABp6iql3lJaKzG3b70mesQV5Iu+oU=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngolang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=\ngolang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=\ngolang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\nmodernc.org/libc v1.66.8 h1:/awsvTnyN/sNjvJm6S3lb7KZw5WV4ly/sBEG7ZUzmIE=\nmodernc.org/libc v1.66.8/go.mod h1:aVdcY7udcawRqauu0HukYYxtBSizV+R80n/6aQe9D5k=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=\nmodernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=\n"
  },
  {
    "path": "_examples/pubsubs/sqlite/main.go",
    "content": "// Sources for https://watermill.io/docs/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t_ \"modernc.org/sqlite\"\n)\n\nfunc main() {\n\tdb := createDB()\n\tdefer db.Close()\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tsubscriber, err := wmsqlitemodernc.NewSubscriber(\n\t\tdb,\n\t\twmsqlitemodernc.SubscriberOptions{\n\t\t\tInitializeSchema: true,\n\t\t\tLogger:           logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example_topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\n\tpublisher, err := wmsqlitemodernc.NewPublisher(\n\t\tdb,\n\t\twmsqlitemodernc.PublisherOptions{\n\t\t\tInitializeSchema: true,\n\t\t\tLogger:           logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpublishMessages(publisher)\n}\n\nfunc createDB() *sql.DB {\n\tconnectionDSN := \":memory:\" // or \"db.sqlite3?journal_mode=WAL&busy_timeout=1000&cache=shared\"\n\tdb, err := sql.Open(\"sqlite\", connectionDSN)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// limit the number of concurrent connections to one\n\t// this is a limitation of `modernc.org/sqlite` driver\n\tdb.SetMaxOpenConns(1)\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(`{\"message\": \"Hello, world!\"}`))\n\n\t\tif err := publisher.Publish(\"example_topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\n\t\t// we need to Acknowledge that we received and processed the message,\n\t\t// otherwise, it will be resent over and over again.\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/sqlite/transaction.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc publishWithInTransaction(db *sql.DB) {\n\ttx, err := db.BeginTx(context.Background(), &sql.TxOptions{\n\t\tIsolation: sql.LevelReadCommitted,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer func() {\n\t\t_ = tx.Commit()\n\t}()\n\n\tpublisher, err := wmsqlitemodernc.NewPublisher(\n\t\ttx, // transaction presented as database\n\t\twmsqlitemodernc.PublisherOptions{\n\t\t\t// schema must be initialized elsewhere before using\n\t\t\t// the publisher within the transaction\n\t\t\tInitializeSchema: false,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmsg := message.NewMessage(watermill.NewUUID(), []byte(`{\"message\": \"Hello, world!\"}`))\n\tif err := publisher.Publish(\"example_topic\", msg); err != nil {\n\t\t_ = tx.Rollback()\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/sqlite-zombiezen/.gitignore",
    "content": "db.sqlite3*"
  },
  {
    "path": "_examples/pubsubs/sqlite-zombiezen/.validate_example.yml",
    "content": "validation_cmd: \"go run main.go\"\ntimeout: 30\nexpected_stdout:\n  - \"received message:\""
  },
  {
    "path": "_examples/pubsubs/sqlite-zombiezen/go.mod",
    "content": "module sqlite\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen v0.1.1\n\tmodernc.org/sqlite v1.39.0\n\tzombiezen.com/go/sqlite v1.4.2\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect\n\tgolang.org/x/sys v0.36.0 // indirect\n\tmodernc.org/libc v1.66.8 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n)\n"
  },
  {
    "path": "_examples/pubsubs/sqlite-zombiezen/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-sqlite/test v0.0.5 h1:50Y9mhgsbskpxOqJiqNMZWs6RAmDJaQkH00ukGXTdcg=\ngithub.com/ThreeDotsLabs/watermill-sqlite/test v0.0.5/go.mod h1:0pqGSkSBj849FqsgYGbuO0k1Coav/i2cXgF8j+wRGtE=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen v0.0.8 h1:sslOW/2x2m4vBVoZ9eNP/VG3/YeIZCBVEYoVegB8dg4=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen v0.0.8/go.mod h1:XudXyl3g3JuyBvZ8Di8dZcuGLP450MlLadGIiKTek+g=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen v0.1.1 h1:/u/c3KdnbQcLK7+RK7vK3m9mnrl/Ish+euwtFIRx7Tc=\ngithub.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen v0.1.1/go.mod h1:XudXyl3g3JuyBvZ8Di8dZcuGLP450MlLadGIiKTek+g=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=\ngolang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=\ngolang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=\ngolang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=\ngolang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=\ngolang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=\ngolang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=\ngolang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=\ngolang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=\ngolang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nmodernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=\nmodernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/cc/v4 v4.26.4 h1:jPhG8oNjtTYuP2FA4YefTJ/wioNUGALmGuEWt7SUR6s=\nmodernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=\nmodernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=\nmodernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=\nmodernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=\nmodernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=\nmodernc.org/fileutil v1.3.28 h1:Vp156KUA2nPu9F1NEv036x9UGOjg2qsi5QlWTjZmtMk=\nmodernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=\nmodernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=\nmodernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=\nmodernc.org/libc v1.66.8 h1:/awsvTnyN/sNjvJm6S3lb7KZw5WV4ly/sBEG7ZUzmIE=\nmodernc.org/libc v1.66.8/go.mod h1:aVdcY7udcawRqauu0HukYYxtBSizV+R80n/6aQe9D5k=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=\nmodernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=\nmodernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=\nmodernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=\nmodernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nzombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=\nzombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=\nzombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=\nzombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=\n"
  },
  {
    "path": "_examples/pubsubs/sqlite-zombiezen/main.go",
    "content": "// Sources for https://watermill.io/docs/getting-started/\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t_ \"modernc.org/sqlite\"\n\t\"zombiezen.com/go/sqlite\"\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\t// &cache=shared is critical, see: https://github.com/zombiezen/go-sqlite/issues/92#issuecomment-2052330643\n\t// connectionDSN := \"file:db.sqlite3?journal_mode=WAL&busy_timeout=1000&secure_delete=true&foreign_keys=true&cache=shared\"\n\tconnectionDSN := \"file:ephemeral?mode=memory&cache=shared\"\n\tconn, err := sqlite.OpenConn(connectionDSN)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer conn.Close()\n\n\tpublisher, err := wmsqlitezombiezen.NewPublisher(conn, wmsqlitezombiezen.PublisherOptions{\n\t\tInitializeSchema: true,\n\t\tLogger:           logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsubscriber, err := wmsqlitezombiezen.NewSubscriber(connectionDSN, wmsqlitezombiezen.SubscriberOptions{\n\t\tInitializeSchema: true,\n\t\tLogger:           logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessages, err := subscriber.Subscribe(context.Background(), \"example_topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo process(messages)\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(`{\"message\": \"Hello from ZombieZen!\"}`))\n\n\t\tif err := publisher.Publish(\"example_topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc process(messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"ZombieZen received message: %s, payload: %s\", msg.UUID, string(msg.Payload))\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "_examples/pubsubs/sqlite-zombiezen/transaction.go",
    "content": "package main\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"zombiezen.com/go/sqlite\"\n\t\"zombiezen.com/go/sqlite/sqlitex\"\n)\n\nfunc publishWithInTransaction(connectionDSN string) {\n\tvar err error\n\t// create a new connection for each transaction\n\t// unless you guard it with a sync.Mutex\n\tconn, err := sqlite.OpenConn(connectionDSN)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer conn.Close()\n\n\tcloser := sqlitex.Transaction(conn)\n\tdefer func() {\n\t\tif closer(&err); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tpublisher, err := wmsqlitezombiezen.NewPublisher(\n\t\tconn,\n\t\twmsqlitezombiezen.PublisherOptions{\n\t\t\t// schema must be initialized elsewhere before using\n\t\t\t// the publisher within the transaction\n\t\t\tInitializeSchema: false,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmsg := message.NewMessage(watermill.NewUUID(), []byte(`{\"message\": \"Hello, world!\"}`))\n\tif err = publisher.Publish(\"example_topic\", msg); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/README.md",
    "content": "# Interactive Consumer Groups Example (Routing Events)\n\nThis example shows how Customer Groups work, i.e. how to decide which handlers receive which events.\n\nConsumer Group is a concept used in Apache Kafka®, but many other Pub/Subs use a similar mechanism.\n\nThe example uses Watermill and Redis Streams Pub/Sub, but the same idea applies to other Pub/Subs as well.\n\n## Live video\n\nThis example was showcased on the Watermill v1.2 Launch Event. You can see the [recording on YouTube](https://www.youtube.com/live/wjnd0Hj6CaM?t=1020) (starts at 17:00).\n\n[![Live Recording](https://img.youtube.com/vi/wjnd0Hj6CaM/0.jpg)](https://www.youtube.com/live/wjnd0Hj6CaM?t=1020)\n\n## Running\n\n```\ndocker-compose up\n```\n\nThen visit [localhost:8080](http://localhost:8080) and check the examples in each tab.\n\n## Screenshots\n\n![](docs/screen2.png)\n\n![](docs/screen1.png)\n\n## Code\n\nSee [crm-service](crm-service) and [newsletter-service](newsletter-service) for the Watermill handlers setup.\n\n## How does it work?\n\nThis example uses SSE for pushing events to the frontend UI. See the [other example on SEE](../server-sent-events) for more details.\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/api/http.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\twatermillHTTP \"github.com/ThreeDotsLabs/watermill-http/pkg/http\"\n\t\"github.com/ThreeDotsLabs/watermill-routing-example/server/common\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/go-chi/chi/v5\"\n)\n\ntype Handler struct {\n\tstorage *storage\n\n\tsubscriber message.Subscriber\n\tpublisher  message.Publisher\n\tlogger     watermill.LoggerAdapter\n\n\tlastIDs map[string]int\n}\n\nfunc (h Handler) Mux() *chi.Mux {\n\tr := chi.NewRouter()\n\n\tfileServer(r, \"/\", http.Dir(\"./public\"))\n\n\tsseRouter, err := watermillHTTP.NewSSERouter(\n\t\twatermillHTTP.SSERouterConfig{\n\t\t\tUpstreamSubscriber: h.subscriber,\n\t\t},\n\t\th.logger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmessagesHandler := sseRouter.AddHandler(common.UpdatesTopic, messagesStream{\n\t\tlogger:  h.logger,\n\t\tstorage: h.storage,\n\t})\n\n\tr.Route(\"/api\", func(r chi.Router) {\n\t\tr.Use(requestIDMiddleware)\n\t\tr.Get(\"/messages\", messagesHandler)\n\t\tr.Post(\"/messages/{topic}\", h.SendMessage)\n\t})\n\n\tgo func() {\n\t\terr = sseRouter.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-sseRouter.Running()\n\n\treturn r\n}\n\nfunc (h Handler) SendMessage(w http.ResponseWriter, r *http.Request) {\n\ttopic := chi.URLParam(r, \"topic\")\n\n\th.lastIDs[topic]++\n\n\tmsgID := fmt.Sprintf(\"%v\", h.lastIDs[topic])\n\n\tevent := common.UserSignedUp{\n\t\tUserID: watermill.NewUUID(),\n\t\tConsents: common.Consents{\n\t\t\tMarketing: true,\n\t\t\tNews:      true,\n\t\t},\n\t}\n\n\tpayload, err := json.Marshal(event)\n\tif err != nil {\n\t\tlogAndWriteError(h.logger, w, err)\n\t\treturn\n\t}\n\n\tmsg := message.NewMessage(msgID, payload)\n\tmsg.Metadata.Set(\"name\", \"UserSignedUp\")\n\n\terr = h.publisher.Publish(topic, msg)\n\tif err != nil {\n\t\tlogAndWriteError(h.logger, w, err)\n\t\treturn\n\t}\n\n\tw.WriteHeader(204)\n}\n\ntype messagesStream struct {\n\tstorage *storage\n\tlogger  watermill.LoggerAdapter\n}\n\nfunc (p messagesStream) GetResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {\n\treqID := r.Context().Value(\"request_id\").(string)\n\tmessages := p.storage.PopAll(reqID)\n\treturn messages, true\n}\n\nfunc (p messagesStream) Validate(r *http.Request, msg *message.Message) (ok bool) {\n\tvar payload common.MessageReceived\n\terr := json.Unmarshal(msg.Payload, &payload)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tp.storage.Append(payload)\n\n\treturn true\n}\n\nfunc fileServer(r chi.Router, path string, root http.FileSystem) {\n\tif strings.ContainsAny(path, \"{}*\") {\n\t\tpanic(\"FileServer does not permit URL parameters.\")\n\t}\n\n\tfs := http.StripPrefix(path, http.FileServer(root))\n\n\tif path != \"/\" && path[len(path)-1] != '/' {\n\t\tr.Get(path, http.RedirectHandler(path+\"/\", 301).ServeHTTP)\n\t\tpath += \"/\"\n\t}\n\tpath += \"*\"\n\n\tr.Get(path, func(w http.ResponseWriter, r *http.Request) {\n\t\tfs.ServeHTTP(w, r)\n\t})\n}\n\nfunc logAndWriteError(logger watermill.LoggerAdapter, w http.ResponseWriter, err error) {\n\tlogger.Error(\"Error\", err, nil)\n\tw.WriteHeader(500)\n}\n\nfunc requestIDMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\trequestID := watermill.NewUUID()\n\t\tctx = context.WithValue(ctx, \"request_id\", requestID)\n\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t})\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/api/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream\"\n\t\"github.com/ThreeDotsLabs/watermill-routing-example/server/common\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\tpubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t})\n\tpublisher, err := redisstream.NewPublisher(\n\t\tredisstream.PublisherConfig{\n\t\t\tClient: pubClient,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t})\n\tsubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient: subClient,\n\t\t},\n\t\tlogger,\n\t)\n\n\tgo func() {\n\t\terr = router.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-router.Running()\n\n\tstorage := &storage{\n\t\tlock:             &sync.Mutex{},\n\t\treceivedMessages: map[string][]common.MessageReceived{},\n\t}\n\n\thttpRouter := Handler{\n\t\tstorage:    storage,\n\t\tsubscriber: subscriber,\n\t\tpublisher:  publisher,\n\t\tlogger:     logger,\n\t\tlastIDs:    map[string]int{},\n\t}\n\n\terr = http.ListenAndServe(\":8080\", httpRouter.Mux())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/api/public/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <title>Watermill Consumer Groups Example</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD\" crossorigin=\"anonymous\">\n    <link href=\"https://unpkg.com/@highlightjs/cdn-assets@11.7.0/styles/default.min.css\" rel=\"stylesheet\">\n    <style>\n    </style>\n</head>\n\n<body>\n\n<nav class=\"navbar bg-body-tertiary mb-5\">\n    <div class=\"container-fluid\">\n        <span class=\"navbar-brand mb-0 h1\">Watermill Consumer Groups Example</span>\n    </div>\n</nav>\n\n<div id=\"root\"></div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN\" crossorigin=\"anonymous\"></script>\n<script src=\"https://unpkg.com/react@18/umd/react.development.js\"></script>\n<script src=\"https://unpkg.com/react-dom@18/umd/react-dom.development.js\"></script>\n<script src=\"https://unpkg.com/@babel/standalone/babel.min.js\"></script>\n<script src=\"https://unpkg.com/@highlightjs/cdn-assets@11.7.0/highlight.min.js\"></script>\n\n<script type=\"text/babel\">\n    function App() {\n        const [chapters, setChapters] = React.useState([\n            {\n                number: 0,\n                title: \"Consumer Groups\",\n                description: `\n                <p>Consumer groups let you decide which handler receives a message.</p>\n                <p>They are related to subscribers.</p>\n                <p>Some Pub/Sub take a different approach, but the idea stays the same.</p>\n                <p>Follow the tabs above to see all examples in order.</p>\n                `,\n                services: [],\n                code: [],\n            },\n            {\n                number: 1,\n                title: \"Signing up for the newsletter\",\n                description: `\n                    <p>When a user signs up, a <span class=\"badge bg-dark\">UserSignedUp</span> event is published.</p>\n\n                    <p>In the simplest scenario, there's a single service with one handler subscribed to this event.\n                    It's adds the user to the newsletter list.\n                    </p>\n\n                    <p>Flip the switch below to start sending messages and see what happens.</p>\n                `,\n                topic: \"UserSignedUp-1\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: null,\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    }\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `router.AddNoPublisherHandler(\n    \"OnUserSignedUp\",\n    \"UserSignedUp\",\n    subscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)`\n                }],\n            },\n            {\n                number: 2,\n                title: \"Introducing the CRM service\",\n                description: `\n                <p>We add one more service. It adds the signed up users to the CRM software.</p>\n                <p>Handlers in both services receive the same message.</p>\n                `,\n                topic: \"UserSignedUp-2\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: null,\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"crm-service-1\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: null,\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    }\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `router.AddNoPublisherHandler(\n    \"OnUserSignedUp\",\n    \"UserSignedUp\",\n    subscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)`,\n                }, {\n                    service: 'crm-service',\n                    code: `router.AddNoPublisherHandler(\n    \"OnUserSignedUp\",\n    \"UserSignedUp\",\n    subscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the CRM\")\n\n        return nil\n    },\n)`,\n                }],\n            },\n            {\n                number: 3,\n                topic: \"UserSignedUp-3\",\n                title: \"Two replicas of the newsletter service\",\n                description: `\n<p>We scale the newsletter service horizontally, adding an identical replica.</p>\n\n<p>This can be done for performance or availability reasons.</p>\n\n<div class=\"alert alert-warning\" role=\"alert\">\n    <strong>Anti-pattern:</strong> each message is processed twice.\n</div>\n`,\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: null,\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"newsletter-service-2\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: null,\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `router.AddNoPublisherHandler(\n    \"OnUserSignedUp\",\n    \"UserSignedUp\",\n    subscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)`\n                }],\n            },\n            {\n                number: 4,\n                title: \"Introducing a consumer group\",\n                description: `\n                <p>Introducing a consumer group makes messages delivered in a round-robin fashion.</p>\n                <p>Each message is processed only once. It is delivered to the group, and the Pub/Sub decides which subscriber receives it.</p>\n\n                <p>The consumer group is a string.</p>\n                <p>Some Pub/Subs use different primitives to achieve the same thing (e.g., AMQP).</p>\n`,\n                topic: \"UserSignedUp-4\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: {\n                                    name: \"newsletter-service\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"newsletter-service-2\",\n                        handlers: [\n                            {\n                                name: \"OnUserSignedUp\",\n                                group: {\n                                    name: \"newsletter-service\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `newsletterServiceGroupSubscriber, err := redisstream.NewSubscriber(\n    redisstream.SubscriberConfig{\n        Client:        subClient,\n        ConsumerGroup: \"newsletter-service\",\n    },\n    logger,\n)\n\nrouter.AddNoPublisherHandler(\n    \"OnUserSignedUp\",\n    \"UserSignedUp\",\n    newsletterServiceGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)`\n                }],\n            },\n            {\n                number: 5,\n                title: \"Adding a second handler to the newsletter service\",\n                description: `\n<p>We now have two newsletter lists: one for promotions, and one for news.</p>\n\n<p>We add another handler subscribed to the same event.</p>\n\n<p>Note: we changed the first handler's name.</p>\n\n<p>We keep the same consumer groups.</p>\n\n<div class=\"alert alert-warning\" role=\"alert\">\n    <strong>Anti-pattern:</strong> every other message is delivered to a different handler.\n</div>\n`,\n                topic: \"UserSignedUp-5\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"newsletter-service\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"newsletter-service\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `newsletterServiceGroupSubscriber, err := redisstream.NewSubscriber(\n    redisstream.SubscriberConfig{\n        Client:        subClient,\n        ConsumerGroup: \"newsletter-service\",\n    },\n    logger,\n)\n\nrouter.AddNoPublisherHandler(\n    \"AddToPromotionsList\",\n    \"UserSignedUp\",\n    newsletterServiceGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)\n\nrouter.AddNoPublisherHandler(\n    \"AddToNewsList\",\n    \"UserSignedUp\",\n    newsletterServiceGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.News {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the news list\")\n\n        return nil\n    },\n)`\n                }],\n            },\n            {\n                number: 6,\n                title: \"Changing the consumer group to the handler name\",\n                description: `\n                <p>Introducing a consumer group per handler makes messages delivered to both handlers.</p>\n`,\n                topic: \"UserSignedUp-6\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"AddToPromotionsList\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"AddToNewsList\",\n                                    color: \"warning\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `addToPromotionsListGroupSubscriber, err := redisstream.NewSubscriber(\n    redisstream.SubscriberConfig{\n        Client:        subClient,\n        ConsumerGroup: \"AddToPromotionsList\",\n    },\n    logger,\n)\n\naddToNewsListGroupSubscriber, err := redisstream.NewSubscriber(\n    redisstream.SubscriberConfig{\n        Client:        subClient,\n        ConsumerGroup: \"AddToNewsList\",\n    },\n    logger,\n)\n\nrouter.AddNoPublisherHandler(\n    \"AddToPromotionsList\",\n    \"UserSignedUp\",\n    addToPromotionsListGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)\n\nrouter.AddNoPublisherHandler(\n    \"AddToNewsList\",\n    \"UserSignedUp\",\n    addToNewsListGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.News {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the news list\")\n\n        return nil\n    },\n)`\n                }],\n            },\n            {\n                number: 7,\n                title: \"Two replicas with two handlers\",\n                description: `\n                <p>We keep one consumer group per handler.</p>\n                <p>Events are delivered to each handler in a round-robin fashion.</p>\n`,\n                topic: \"UserSignedUp-7\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"AddToPromotionsList\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"AddToNewsList\",\n                                    color: \"warning\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"newsletter-service-2\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"AddToPromotionsList\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"AddToNewsList\",\n                                    color: \"warning\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: 'newsletter-service',\n                    code: `addToPromotionsListGroupSubscriber, err := redisstream.NewSubscriber(\n    redisstream.SubscriberConfig{\n        Client:        subClient,\n        ConsumerGroup: \"AddToPromotionsList\",\n    },\n    logger,\n)\n\naddToNewsListGroupSubscriber, err := redisstream.NewSubscriber(\n    redisstream.SubscriberConfig{\n        Client:        subClient,\n        ConsumerGroup: \"AddToNewsList\",\n    },\n    logger,\n)\n\nrouter.AddNoPublisherHandler(\n    \"AddToPromotionsList\",\n    \"UserSignedUp\",\n    addToPromotionsListGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.Marketing {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n        return nil\n    },\n)\n\nrouter.AddNoPublisherHandler(\n    \"AddToNewsList\",\n    \"UserSignedUp\",\n    addToNewsListGroupSubscriber,\n    func(msg *message.Message) error {\n        var event common.UserSignedUp\n        err := json.Unmarshal(msg.Payload, &event)\n        if err != nil {\n            return err\n        }\n\n        if !event.Consents.News {\n            return nil\n        }\n\n        fmt.Println(\"Adding user\", event.UserID, \"to the news list\")\n\n        return nil\n    },\n)`\n                }],\n            },\n            {\n                number: 8,\n                title: \"Adding a second handler to the CRM service\",\n                description: `\n                <p>The CRM service now has one more responsibility: adding users to the Support software.</p>\n\n                <p>Handler names as consumer groups still make sense. However, a good practice is to include the service name as well.</p>\n\n                <p>Thanks to this, there's no risk of conflicts of the handler names between services.</p>\n\n                <p>Note: the replica number is not part of the service name!</p>\n\n                <p>This example features Watermill's CQRS component for easier creation of subscribers.</p>\n`,\n                topic: \"UserSignedUp-8\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"newsletter-service_AddToPromotionsList\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"newsletter-service_AddToNewsList\",\n                                    color: \"warning\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"crm-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToCRM\",\n                                group: {\n                                    name: \"crm-service_AddToCRM\",\n                                    color: \"danger\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToSupport\",\n                                group: {\n                                    name: \"crm-service_AddToSupport\",\n                                    color: \"info\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: 'newsletter-service',\n            code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n    GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n        return params.EventName, nil\n    },\n    SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n        return redisstream.NewSubscriber(\n            redisstream.SubscriberConfig{\n                Client:        subClient,\n                ConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, params.HandlerName),\n            },\n            logger,\n        )\n    },\n    Marshaler: cqrs.JSONMarshaler{\n        GenerateName: cqrs.StructName,\n    },\n    Logger: logger,\n})\nif err != nil {\n    panic(err)\n}\nerr = eventProc.AddHandlers(\n    cqrs.NewEventHandler(\"AddToPromotionsList\", HandlePromotions),\n    cqrs.NewEventHandler(\"AddToNewsList\", HandleNews),\n)\nif err != nil {\n    panic(err)\n}\nfunc HandleNews(ctx context.Context, e *common.UserSignedUp) error {\n\tif !e.Consents.News {\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Adding user\", e.UserID, \"to the news list\")\n\n\treturn nil\n}\n\nfunc HandlePromotions(ctx context.Context, e *common.UserSignedUp) error {\n\tif !e.Consents.Marketing {\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Adding user\", e.UserID, \"to the promotions list\")\n\n\treturn nil\n}`,\n                }, {\n                    service: 'crm-service',\n                    code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n    GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n        return params.EventName, nil\n    },\n    SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n        return redisstream.NewSubscriber(\n            redisstream.SubscriberConfig{\n                Client:        subClient,\n                ConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, params.HandlerName),\n            },\n            logger,\n        )\n    },\n    Marshaler: cqrs.JSONMarshaler{\n        GenerateName: cqrs.StructName,\n    },\n    Logger: logger,\n})\nif err != nil {\n    panic(err)\n}\nerr = eventProc.AddHandlers(\n    cqrs.NewEventHandler(\"AddToCRM\", HandleCRM),\n    cqrs.NewEventHandler(\"AddToSupport\", HandleSupport),\n)\nif err != nil {\n    panic(err)\n}\n\nfunc HandleCRM(ctx context.Context, e *common.UserSignedUp) error {\n\tfmt.Println(\"Adding user\", e.UserID, \"to the CRM\")\n\n\treturn nil\n}\n\nfunc HandleSupport(ctx context.Context, e *common.UserSignedUp) error {\n\tfmt.Println(\"Adding user\", e.UserID, \"to the support channel\")\n\n\treturn nil\n}`\n                }],\n            },\n            {\n                number: 9,\n                title: \"Running two replicas of both services\",\n                description: `\n                <p>This example is identical as the previous one except both services run two replicas.</p>\n                `,\n                topic: \"UserSignedUp-9\",\n                services: [\n                    {\n                        name: \"newsletter-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"newsletter-service_AddToPromotionsList\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"newsletter-service_AddToNewsList\",\n                                    color: \"warning\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"crm-service-1\",\n                        handlers: [\n                            {\n                                name: \"AddToCRM\",\n                                group: {\n                                    name: \"crm-service_AddToCRM\",\n                                    color: \"danger\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToSupport\",\n                                group: {\n                                    name: \"crm-service_AddToSupport\",\n                                    color: \"info\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"newsletter-service-2\",\n                        handlers: [\n                            {\n                                name: \"AddToPromotionsList\",\n                                group: {\n                                    name: \"newsletter-service_AddToPromotionsList\",\n                                    color: \"primary\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToNewsList\",\n                                group: {\n                                    name: \"newsletter-service_AddToNewsList\",\n                                    color: \"warning\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                    {\n                        name: \"crm-service-2\",\n                        handlers: [\n                            {\n                                name: \"AddToCRM\",\n                                group: {\n                                    name: \"crm-service_AddToCRM\",\n                                    color: \"danger\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            },\n                            {\n                                name: \"AddToSupport\",\n                                group: {\n                                    name: \"crm-service_AddToSupport\",\n                                    color: \"info\",\n                                },\n                                received_message_id: \"\",\n                                received_message_at: 0,\n                            }\n                        ]\n                    },\n                ],\n                code: [{\n                    service: \"newsletter-service\",\n                    code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n    GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n        return params.EventName, nil\n    },\n    SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n        return redisstream.NewSubscriber(\n            redisstream.SubscriberConfig{\n                Client:        subClient,\n                ConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, params.HandlerName),\n            },\n            logger,\n        )\n    },\n    Marshaler: cqrs.JSONMarshaler{\n        GenerateName: cqrs.StructName,\n    },\n    Logger: logger,\n})\nif err != nil {\n    panic(err)\n}\nerr = eventProc.AddHandlers(\n    cqrs.NewEventHandler(\"AddToPromotionsList\", HandlePromotions),\n    cqrs.NewEventHandler(\"AddToNewsList\", HandleNews),\n)\nif err != nil {\n    panic(err)\n}\nfunc HandleNews(ctx context.Context, e *common.UserSignedUp) error {\n\tif !e.Consents.News {\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Adding user\", e.UserID, \"to the news list\")\n\n\treturn nil\n}\n\nfunc HandlePromotions(ctx context.Context, e *common.UserSignedUp) error {\n\tif !e.Consents.Marketing {\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Adding user\", e.UserID, \"to the promotions list\")\n\n\treturn nil\n}`,\n                }, {\n                    service: \"crm-service\",\n                    code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n    GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n        return params.EventName, nil\n    },\n    SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n        return redisstream.NewSubscriber(\n            redisstream.SubscriberConfig{\n                Client:        subClient,\n                ConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, params.HandlerName),\n            },\n            logger,\n        )\n    },\n    Marshaler: cqrs.JSONMarshaler{\n        GenerateName: cqrs.StructName,\n    },\n    Logger: logger,\n})\nif err != nil {\n    panic(err)\n}\nerr = eventProc.AddHandlers(\n    cqrs.NewEventHandler(\"AddToCRM\", HandleCRM),\n    cqrs.NewEventHandler(\"AddToSupport\", HandleSupport),\n)\nif err != nil {\n    panic(err)\n}\n\nfunc HandleCRM(ctx context.Context, e *common.UserSignedUp) error {\n\tfmt.Println(\"Adding user\", e.UserID, \"to the CRM\")\n\n\treturn nil\n}\n\nfunc HandleSupport(ctx context.Context, e *common.UserSignedUp) error {\n\tfmt.Println(\"Adding user\", e.UserID, \"to the support channel\")\n\n\treturn nil\n}`\n                }],\n            },\n        ]);\n\n        React.useEffect(() => {\n            const es = new EventSource(\"/api/messages\");\n            es.addEventListener('data', event => {\n                console.log(event.data);\n                const data = JSON.parse(event.data);\n\n                let newChapters = [...chapters];\n\n                const now = Date.now();\n\n                for (let message of data) {\n                    for (let i = 0; i < newChapters.length; i++) {\n                        const chapter = newChapters[i];\n                        if (chapter.topic === message.topic) {\n                            for (let j = 0; j < chapter.services.length; j++) {\n                                const service = chapter.services[j];\n                                if (service.name === message.service) {\n                                    for (let k = 0; k < service.handlers.length; k++) {\n                                        const handler = service.handlers[k];\n                                        if (handler.name === message.handler) {\n                                            handler.received_message_id = message.id;\n                                            handler.received_message_at = now;\n                                            break;\n                                        }\n                                    }\n                                    break;\n                                }\n                            }\n                            break;\n                        }\n                    }\n                }\n\n                setChapters(newChapters);\n            }, false);\n        }, []);\n\n        React.useEffect(() => {\n            const interval = setInterval(() => {\n                let newChapters = [...chapters];\n\n                const now = Date.now();\n\n                for (let i = 0; i < newChapters.length; i++) {\n                    const chapter = newChapters[i];\n                    for (let j = 0; j < chapter.services.length; j++) {\n                        const service = chapter.services[j];\n                        for (let k = 0; k < service.handlers.length; k++) {\n                            const handler = service.handlers[k];\n                            if (now - handler.received_message_at > 1200) {\n                                handler.received_message_at = 0;\n                            }\n                        }\n                    }\n                }\n\n                setChapters(newChapters);\n            }, 100);\n            return () => clearInterval(interval);\n        }, []);\n\n        return (\n            <div className=\"container\">\n                <ul className=\"nav nav-tabs\" role=\"tablist\" style={{borderBottom: 0}}>\n                    {chapters.map(chapter => (\n                        <li key={chapter.number} className=\"nav-item mx-1\" role=\"presentation\">\n                            <button\n                                className={`nav-link ${chapter.number === 0 ? \"active\" : \"\"}`}\n                                id={\"example-tab-\" + chapter.number }\n                                data-bs-toggle=\"tab\"\n                                data-bs-target={\"#tab-pane-\" + chapter.number}\n                                type=\"button\"\n                                role=\"tab\"\n                                aria-controls={\"tab-pane-\" + chapter.number}\n                                aria-selected={chapter.number === 0 ? \"true\" : \"false\"}><span className=\"px-1\">{chapter.number}</span></button>\n                        </li>\n                    ))}\n                </ul>\n                <div className=\"tab-content\">\n                    {chapters.map(chapter => <Chapter key={chapter.number} chapter={chapter} />)}\n                </div>\n            </div>\n        );\n    }\n\n    function Chapter(props) {\n        const [sending, setSending] = React.useState(false);\n\n        React.useEffect(() => {\n            const interval = setInterval(() => {\n                if (!sending) {\n                    return;\n                }\n                fetch('/api/messages/' + props.chapter.topic, {\n                    method: 'POST',\n                    headers: {\n                        'Accept': 'application/json',\n                    }\n                });\n            }, 1500);\n            return () => clearInterval(interval);\n        }, [sending]);\n\n        const columns = 2;\n        const rows = props.chapter.services.map((service, i) => {\n            return <Service key={i} service={service} />\n        }).reduce((r, element, index) => {\n            if (index % columns === 0) {\n                r.push([]);\n            }\n            r[r.length - 1].push(element);\n            return r;\n        }, []).map((row, i) => {\n            return (\n                <div key={i} className=\"row my-3\">\n                    {row}\n                </div>\n            );\n        })\n\n        React.useEffect(() => {\n            hljs.highlightAll();\n        }, []);\n\n        return (\n            <div\n                className={`tab-pane fade ${props.chapter.number === 0 ? \" show active\" : \"\"}`}\n                id={\"tab-pane-\" + props.chapter.number}\n                role=\"tabpanel\"\n                aria-labelledby={\"tab-\" + props.chapter.number}\n                tabIndex=\"0\"\n            >\n                <div className=\"card\">\n                    <div className=\"card-header\">\n                        <h3>{props.chapter.number}. {props.chapter.title}</h3>\n                    </div>\n                    <div className=\"card-body\">\n                        <p dangerouslySetInnerHTML={{__html: props.chapter.description}}></p>\n\n                        {props.chapter.services.length > 0 &&\n                            <div className=\"form-check form-switch mx-3 mt-5 mb-4\">\n                                <input className=\"form-check-input\" onChange={() => setSending(!sending) } type=\"checkbox\" role=\"switch\" id={`flexSwitchCheckDefault-${props.chapter.number}`} />\n                                <label className=\"form-check-label\" htmlFor={`flexSwitchCheckDefault-${props.chapter.number}`}>Send <span className=\"badge bg-dark\">UserSignedUp</span> Events</label>\n                                {sending && <div className=\"mx-3 spinner-grow spinner-grow-sm text-success\" role=\"status\"></div>}\n                            </div>\n                        }\n\n                        <div className=\"container\">\n                            {rows}\n                        </div>\n\n                            <div className=\"accordion mt-5\" id={\"code\" + props.chapter.number}>\n                                {props.chapter.code.map(code => (\n                                    <div className=\"accordion-item\">\n                                        <h2 className=\"accordion-header\" id={\"codeHeader\" + props.chapter.number+code.service}>\n                                            <button className=\"accordion-button collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target={\"#codeCollapse\" + props.chapter.number+code.service} aria-expanded=\"false\" aria-controls={\"#codeCollapse\" + props.chapter.number+code.service}>\n                                                Code: {code.service}\n                                            </button>\n                                        </h2>\n                                        <div id={\"codeCollapse\" + props.chapter.number + code.service} className=\"accordion-collapse collapse\" aria-labelledby={\"codeHeader\"+props.chapter.number+code.service} data-bs-parent={\"code\"+props.chapter.number+code.service}>\n                                            <div className=\"accordion-body\">\n                                            <pre>\n                                                <code className=\"language-go\">\n                                                    {code.code}\n                                                </code>\n                                            </pre>\n                                            </div>\n                                        </div>\n                                    </div>\n                                ))}\n                            </div>\n                    </div>\n                </div>\n            </div>\n        );\n    }\n\n    function Service(props) {\n        return (\n            <div className=\"col-6\">\n                <div className=\"card\">\n                    <div className=\"card-header\">\n                        <h3>{props.service.name}</h3>\n                    </div>\n                    <ul className=\"list-group list-group-flush\">\n                        {props.service.handlers.map(handler => <Handler key={handler.name} handler={handler} />)}\n                    </ul>\n                </div>\n            </div>\n        );\n    }\n\n    function Handler(props) {\n        return (\n            <li className=\"list-group-item\">\n                <h4>{props.handler.name}</h4>\n                {props.handler.group &&\n                    <h4><span className={`badge text-bg-${props.handler.group.color}`}>Group: {props.handler.group.name}</span></h4>\n                }\n                <h1><span className={\"badge \" + (props.handler.received_message_at > 0 ? \"text-bg-success\" : \"text-bg-light\")}>{props.handler.received_message_id > 0 ? `Received #${props.handler.received_message_id}` : \"Waiting for messages...\"}</span></h1>\n            </li>\n        );\n    }\n\n    const container = document.getElementById('root');\n    const root = ReactDOM.createRoot(container);\n    root.render(<App />);\n\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/api/storage.go",
    "content": "package main\n\nimport (\n\t\"sync\"\n\n\t\"github.com/ThreeDotsLabs/watermill-routing-example/server/common\"\n)\n\ntype storage struct {\n\tlock             *sync.Mutex\n\treceivedMessages map[string][]common.MessageReceived\n}\n\nfunc (s *storage) Append(message common.MessageReceived) {\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\n\tfor k, v := range s.receivedMessages {\n\t\ts.receivedMessages[k] = append(v, message)\n\t}\n}\n\nfunc (s *storage) PopAll(key string) []common.MessageReceived {\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\n\tif _, ok := s.receivedMessages[key]; !ok {\n\t\ts.receivedMessages[key] = []common.MessageReceived{}\n\t\treturn []common.MessageReceived{}\n\t}\n\n\tmessages := s.receivedMessages[key]\n\ts.receivedMessages[key] = []common.MessageReceived{}\n\treturn messages\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/common/events.go",
    "content": "package common\n\ntype UserSignedUp struct {\n\tUserID   string   `json:\"id\"`\n\tConsents Consents `json:\"consents\"`\n}\n\ntype Consents struct {\n\tMarketing bool `json:\"marketing\"`\n\tNews      bool `json:\"news\"`\n}\n\ntype MessageReceived struct {\n\tID      string `json:\"id\"`\n\tService string `json:\"service\"`\n\tHandler string `json:\"handler\"`\n\tTopic   string `json:\"topic\"`\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/common/messaging.go",
    "content": "package common\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nconst UpdatesTopic = \"updates\"\n\nfunc NotifyMiddleware(pub message.Publisher, serviceName string) func(message.HandlerFunc) message.HandlerFunc {\n\treturn func(next message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\t\ttopic := message.SubscribeTopicFromCtx(msg.Context())\n\t\t\thandler := strings.Split(message.HandlerNameFromCtx(msg.Context()), \"-\")[0]\n\n\t\t\tmsgs, err := next(msg)\n\t\t\tif err != nil {\n\t\t\t\treturn msgs, err\n\t\t\t}\n\n\t\t\tpayload := MessageReceived{\n\t\t\t\tID:      msg.UUID,\n\t\t\t\tService: serviceName,\n\t\t\t\tHandler: handler,\n\t\t\t\tTopic:   topic,\n\t\t\t}\n\n\t\t\tjsonPayload, err := json.Marshal(payload)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnewMsg := message.NewMessage(watermill.NewUUID(), jsonPayload)\n\n\t\t\terr = pub.Publish(UpdatesTopic, newMsg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn msgs, nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/crm-service/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream\"\n\t\"github.com/ThreeDotsLabs/watermill-routing-example/server/common\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar (\n\treplica                = os.Getenv(\"REPLICA\")\n\tserviceName            = \"crm-service\"\n\tserviceNameWithReplica = serviceName + \"-\" + replica\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t})\n\tpublisher, err := redisstream.NewPublisher(\n\t\tredisstream.PublisherConfig{\n\t\t\tClient: pubClient,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddMiddleware(middleware.Recoverer)\n\trouter.AddMiddleware(common.NotifyMiddleware(publisher, serviceNameWithReplica))\n\n\tsubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t})\n\n\tsubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient:        subClient,\n\t\t\tConsumerGroup: \"\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif replica == \"1\" {\n\t\trouter.AddConsumerHandler(\n\t\t\t\"OnUserSignedUp-2\",\n\t\t\t\"UserSignedUp-2\",\n\t\t\tsubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the CRM\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\t}\n\n\tif replica == \"1\" {\n\t\teventProc8, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn fmt.Sprintf(\"%s-8\", params.EventName), nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\thandlerName := strings.Split(params.HandlerName, \"-\")[0]\n\t\t\t\treturn redisstream.NewSubscriber(\n\t\t\t\t\tredisstream.SubscriberConfig{\n\t\t\t\t\t\tClient:        subClient,\n\t\t\t\t\t\tConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, handlerName),\n\t\t\t\t\t},\n\t\t\t\t\tlogger,\n\t\t\t\t)\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{\n\t\t\t\tGenerateName: cqrs.StructName,\n\t\t\t},\n\t\t\tLogger: logger,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\terr = eventProc8.AddHandlers(\n\t\t\tcqrs.NewEventHandler(\"AddToCRM-8\", HandleCRM),\n\t\t\tcqrs.NewEventHandler(\"AddToSupport-8\", HandleSupport),\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\teventProc9, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn fmt.Sprintf(\"%s-9\", params.EventName), nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\thandlerName := strings.Split(params.HandlerName, \"-\")[0]\n\t\t\treturn redisstream.NewSubscriber(\n\t\t\t\tredisstream.SubscriberConfig{\n\t\t\t\t\tClient:        subClient,\n\t\t\t\t\tConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, handlerName),\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t},\n\t\tMarshaler: cqrs.JSONMarshaler{\n\t\t\tGenerateName: cqrs.StructName,\n\t\t},\n\t\tLogger: logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = eventProc9.AddHandlers(\n\t\tcqrs.NewEventHandler(\"AddToCRM-9\", HandleCRM),\n\t\tcqrs.NewEventHandler(\"AddToSupport-9\", HandleSupport),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = router.Run(context.Background())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc HandleCRM(ctx context.Context, e *common.UserSignedUp) error {\n\tfmt.Println(\"Adding user\", e.UserID, \"to the CRM\")\n\n\treturn nil\n}\n\nfunc HandleSupport(ctx context.Context, e *common.UserSignedUp) error {\n\tfmt.Println(\"Adding user\", e.UserID, \"to the support channel\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/docker-compose.yml",
    "content": "services:\n  api:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - redis\n    volumes:\n      - .:/app\n    working_dir: /app/api\n    ports:\n      - \"8080:8080\"\n    command: go run .\n\n  newsletter-1:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - redis\n    volumes:\n      - .:/app\n    working_dir: /app/newsletter-service\n    command: go run .\n    environment:\n      REPLICA: 1\n\n  newsletter-2:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - redis\n    volumes:\n      - .:/app\n    working_dir: /app/newsletter-service\n    command: go run .\n    environment:\n      REPLICA: 2\n\n  crm-1:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - redis\n    volumes:\n      - .:/app\n    working_dir: /app/crm-service\n    command: go run .\n    environment:\n      REPLICA: 1\n\n  crm-2:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - redis\n    volumes:\n      - .:/app\n    working_dir: /app/crm-service\n    command: go run .\n    environment:\n      REPLICA: 2\n\n  redis:\n    image: redis:7\n    ports:\n      - \"6379:6379\"\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill-routing-example/server\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-http v1.1.4\n\tgithub.com/go-chi/chi/v5 v5.2.3\n\tgithub.com/go-chi/render v1.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n)\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4\n\tgithub.com/redis/go-redis/v9 v9.12.1\n)\n\nrequire (\n\tgithub.com/Rican7/retry v0.3.1 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/go-chi/chi v4.1.2+incompatible // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/vmihailenco/msgpack v4.0.4+incompatible // indirect\n\tgoogle.golang.org/appengine v1.6.8 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/go.sum",
    "content": "github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=\ngithub.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=\ngithub.com/ThreeDotsLabs/watermill v1.1.0/go.mod h1:Qd1xNFxolCAHCzcMrm6RnjW0manbvN+DJVWc1MWRFlI=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-http v1.1.4 h1:wRM54z/BPnIWjGbXMrOnwOlrCAESzoSNxTAHiLysFA4=\ngithub.com/ThreeDotsLabs/watermill-http v1.1.4/go.mod h1:mkQ9CC0pxTZerNwr281rBoOy355vYt/lePkmYSX/BRg=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4 h1:vkpSm2MZHacjN4H8R0PA9IKQ++uQMq6wA0m1bnGjipo=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4/go.mod h1:Da3wqG1OcvHPODjuJcxSCY1O7D4loIZQpVbZ5u94xRo=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=\ngithub.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=\ngithub.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=\ngithub.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/consumer-groups/newsletter-service/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream\"\n\t\"github.com/ThreeDotsLabs/watermill-routing-example/server/common\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar (\n\treplica                = os.Getenv(\"REPLICA\")\n\tserviceName            = \"newsletter-service\"\n\tserviceNameWithReplica = serviceName + \"-\" + replica\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t})\n\tpublisher, err := redisstream.NewPublisher(\n\t\tredisstream.PublisherConfig{\n\t\t\tClient: pubClient,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddMiddleware(middleware.Recoverer)\n\trouter.AddMiddleware(common.NotifyMiddleware(publisher, serviceNameWithReplica))\n\n\tsubClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"redis:6379\",\n\t})\n\tsubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient:        subClient,\n\t\t\tConsumerGroup: \"\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tnewsletterServiceGroupSubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient:        subClient,\n\t\t\tConsumerGroup: \"newsletter-service\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\taddToPromotionsListGroupSubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient:        subClient,\n\t\t\tConsumerGroup: \"AddToPromotionsList\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\taddToNewsListGroupSubscriber, err := redisstream.NewSubscriber(\n\t\tredisstream.SubscriberConfig{\n\t\t\tClient:        subClient,\n\t\t\tConsumerGroup: \"AddToNewsList\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif replica == \"1\" {\n\t\trouter.AddConsumerHandler(\n\t\t\t\"OnUserSignedUp-1\",\n\t\t\t\"UserSignedUp-1\",\n\t\t\tsubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !event.Consents.Marketing {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\trouter.AddConsumerHandler(\n\t\t\t\"OnUserSignedUp-2\",\n\t\t\t\"UserSignedUp-2\",\n\t\t\tsubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !event.Consents.Marketing {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\trouter.AddConsumerHandler(\n\t\t\t\"AddToPromotionsList-5\",\n\t\t\t\"UserSignedUp-5\",\n\t\t\tnewsletterServiceGroupSubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !event.Consents.Marketing {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\trouter.AddConsumerHandler(\n\t\t\t\"AddToNewsList-5\",\n\t\t\t\"UserSignedUp-5\",\n\t\t\tnewsletterServiceGroupSubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !event.Consents.News {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the news list\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\trouter.AddConsumerHandler(\n\t\t\t\"AddToPromotionsList-6\",\n\t\t\t\"UserSignedUp-6\",\n\t\t\taddToPromotionsListGroupSubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !event.Consents.Marketing {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\trouter.AddConsumerHandler(\n\t\t\t\"AddToNewsList-6\",\n\t\t\t\"UserSignedUp-6\",\n\t\t\taddToNewsListGroupSubscriber,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tvar event common.UserSignedUp\n\t\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !event.Consents.News {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the news list\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\t}\n\n\trouter.AddConsumerHandler(\n\t\t\"OnUserSignedUp-3\",\n\t\t\"UserSignedUp-3\",\n\t\tsubscriber,\n\t\tfunc(msg *message.Message) error {\n\t\t\tvar event common.UserSignedUp\n\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !event.Consents.Marketing {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\trouter.AddConsumerHandler(\n\t\t\"OnUserSignedUp-4\",\n\t\t\"UserSignedUp-4\",\n\t\tnewsletterServiceGroupSubscriber,\n\t\tfunc(msg *message.Message) error {\n\t\t\tvar event common.UserSignedUp\n\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !event.Consents.Marketing {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\trouter.AddConsumerHandler(\n\t\t\"AddToPromotionsList-7\",\n\t\t\"UserSignedUp-7\",\n\t\taddToPromotionsListGroupSubscriber,\n\t\tfunc(msg *message.Message) error {\n\t\t\tvar event common.UserSignedUp\n\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !event.Consents.Marketing {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the promotions list\")\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\trouter.AddConsumerHandler(\n\t\t\"AddToNewsList-7\",\n\t\t\"UserSignedUp-7\",\n\t\taddToNewsListGroupSubscriber,\n\t\tfunc(msg *message.Message) error {\n\t\t\tvar event common.UserSignedUp\n\t\t\terr := json.Unmarshal(msg.Payload, &event)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !event.Consents.News {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfmt.Println(\"Adding user\", event.UserID, \"to the news list\")\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tif replica == \"1\" {\n\t\teventProc8, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn fmt.Sprintf(\"%s-8\", params.EventName), nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\thandlerName := strings.Split(params.HandlerName, \"-\")[0]\n\t\t\t\treturn redisstream.NewSubscriber(\n\t\t\t\t\tredisstream.SubscriberConfig{\n\t\t\t\t\t\tClient:        subClient,\n\t\t\t\t\t\tConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, handlerName),\n\t\t\t\t\t},\n\t\t\t\t\tlogger,\n\t\t\t\t)\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{\n\t\t\t\tGenerateName: cqrs.StructName,\n\t\t\t},\n\t\t\tLogger: logger,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\terr = eventProc8.AddHandlers(\n\t\t\tcqrs.NewEventHandler(\"AddToPromotionsList-8\", HandlePromotions),\n\t\t\tcqrs.NewEventHandler(\"AddToNewsList-8\", HandleNews),\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\teventProc9, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn fmt.Sprintf(\"%s-9\", params.EventName), nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\thandlerName := strings.Split(params.HandlerName, \"-\")[0]\n\t\t\treturn redisstream.NewSubscriber(\n\t\t\t\tredisstream.SubscriberConfig{\n\t\t\t\t\tClient:        subClient,\n\t\t\t\t\tConsumerGroup: fmt.Sprintf(\"%s_%s\", serviceName, handlerName),\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t},\n\t\tMarshaler: cqrs.JSONMarshaler{\n\t\t\tGenerateName: cqrs.StructName,\n\t\t},\n\t\tLogger: logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = eventProc9.AddHandlers(\n\t\tcqrs.NewEventHandler(\"AddToPromotionsList-9\", HandlePromotions),\n\t\tcqrs.NewEventHandler(\"AddToNewsList-9\", HandleNews),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = router.Run(context.Background())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc HandleNews(ctx context.Context, e *common.UserSignedUp) error {\n\tif !e.Consents.News {\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Adding user\", e.UserID, \"to the news list\")\n\n\treturn nil\n}\n\nfunc HandlePromotions(ctx context.Context, e *common.UserSignedUp) error {\n\tif !e.Consents.Marketing {\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Adding user\", e.UserID, \"to the promotions list\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-messages/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  redis:\n    image: redis:7\n    ports:\n      - 6379:6379\n    restart: unless-stopped\n\n  postgres:\n    image: postgres:15\n    restart: unless-stopped\n    ports:\n      - 5432:5432\n    environment:\n      POSTGRES_USER: watermill\n      POSTGRES_DB: watermill\n      POSTGRES_PASSWORD: \"password\"\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-messages/go.mod",
    "content": "module delayed-messages\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/brianvoe/gofakeit/v6 v6.28.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/redis/go-redis/v9 v9.12.1\n)\n\nrequire (\n\tgithub.com/Rican7/retry v0.3.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/vmihailenco/msgpack v4.0.4+incompatible // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgoogle.golang.org/appengine v1.6.8 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-messages/go.sum",
    "content": "github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=\ngithub.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4 h1:vkpSm2MZHacjN4H8R0PA9IKQ++uQMq6wA0m1bnGjipo=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4/go.mod h1:Da3wqG1OcvHPODjuJcxSCY1O7D4loIZQpVbZ5u94xRo=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=\ngithub.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-messages/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/brianvoe/gofakeit/v6\"\n\t\"github.com/google/uuid\"\n\t_ \"github.com/lib/pq\"\n\t\"github.com/redis/go-redis/v9\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/components/delay\"\n\t\"github.com/ThreeDotsLabs/watermill/components/forwarder\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tdb, err := stdSQL.Open(\"postgres\", \"postgres://watermill:password@postgres:5432/watermill?sslmode=disable\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tredisClient := redis.NewClient(&redis.Options{Addr: \"redis:6379\"})\n\tmarshaler := cqrs.JSONMarshaler{\n\t\tGenerateName: cqrs.StructName,\n\t}\n\n\tredisPublisher, err := redisstream.NewPublisher(redisstream.PublisherConfig{\n\t\tClient: redisClient,\n\t}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar sqlPublisher message.Publisher\n\tsqlPublisher, err = sql.NewDelayedPostgreSQLPublisher(db, sql.DelayedPostgreSQLPublisherConfig{\n\t\tDelayPublisherConfig: delay.PublisherConfig{},\n\t\tLogger:               logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsqlPublisher = forwarder.NewPublisher(sqlPublisher, forwarder.PublisherConfig{\n\t\tForwarderTopic: \"forwarder\",\n\t})\n\n\teventBus, err := cqrs.NewEventBusWithConfig(redisPublisher, cqrs.EventBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\treturn params.EventName, nil\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcommandBus, err := cqrs.NewCommandBusWithConfig(sqlPublisher, cqrs.CommandBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\treturn params.CommandName, nil\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter := message.NewDefaultRouter(logger)\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn params.EventName, nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn redisstream.NewSubscriber(redisstream.SubscriberConfig{\n\t\t\t\tClient:        redisClient,\n\t\t\t\tConsumerGroup: params.HandlerName,\n\t\t\t}, logger)\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cqrs.CommandProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn params.CommandName, nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn redisstream.NewSubscriber(redisstream.SubscriberConfig{\n\t\t\t\tClient:        redisClient,\n\t\t\t\tConsumerGroup: params.HandlerName,\n\t\t\t}, logger)\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = eventProcessor.AddHandlers(\n\t\tcqrs.NewEventHandler(\n\t\t\t\"OnOrderPlacedHandler\",\n\t\t\tfunc(ctx context.Context, event *OrderPlaced) error {\n\t\t\t\tfmt.Printf(\"💰 Received order from %v <%v>\\n\", event.Customer.Name, event.Customer.Email)\n\n\t\t\t\tcmd := SendFeedbackForm{\n\t\t\t\t\tTo:   event.Customer.Email,\n\t\t\t\t\tName: event.Customer.Name,\n\t\t\t\t}\n\n\t\t\t\t// In a real world scenario, we would delay the command by a few days\n\t\t\t\tctx = delay.WithContext(ctx, delay.For(8*time.Second))\n\n\t\t\t\terr := commandBus.Send(ctx, cmd)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = commandProcessor.AddHandlers(\n\t\tcqrs.NewCommandHandler(\n\t\t\t\"OnSendFeedbackForm\",\n\t\t\tfunc(ctx context.Context, cmd *SendFeedbackForm) error {\n\t\t\t\tfmt.Printf(\"📧 Sending feedback form to %v <%v>\\n\", cmd.Name, cmd.To)\n\n\t\t\t\t// In a real world scenario, we would send an email to the customer here\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsqlSubscriber, err := sql.NewDelayedPostgreSQLSubscriber(db, sql.DelayedPostgreSQLSubscriberConfig{\n\t\tDeleteOnAck: true,\n\t\tLogger:      logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = forwarder.NewForwarder(\n\t\tsqlSubscriber,\n\t\tredisPublisher,\n\t\tlogger,\n\t\tforwarder.Config{\n\t\t\tForwarderTopic: \"forwarder\",\n\t\t\tRouter:         router,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo func() {\n\t\terr = router.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-router.Running()\n\n\tfor {\n\t\tname := gofakeit.FirstName()\n\t\te := OrderPlaced{\n\t\t\tOrderID: uuid.NewString(),\n\t\t\tCustomer: Customer{\n\t\t\t\tName:  name,\n\t\t\t\tEmail: fmt.Sprintf(\"%v@%v\", strings.ToLower(name), gofakeit.DomainName()),\n\t\t\t},\n\t\t}\n\n\t\terr = eventBus.Publish(context.Background(), e)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(5 * time.Second)\n\t}\n}\n\ntype Customer struct {\n\tName  string `json:\"name\"`\n\tEmail string `json:\"email\"`\n}\n\ntype OrderPlaced struct {\n\tOrderID  string   `json:\"order_id\"`\n\tCustomer Customer `json:\"customer\"`\n}\n\ntype SendFeedbackForm struct {\n\tTo   string `json:\"to\"`\n\tName string `json:\"name\"`\n}\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-requeue/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  redis:\n    image: redis:7\n    ports:\n      - 6379:6379\n    restart: unless-stopped\n\n  postgres:\n    image: postgres:15\n    restart: unless-stopped\n    ports:\n      - 5432:5432\n    environment:\n      POSTGRES_USER: watermill\n      POSTGRES_DB: watermill\n      POSTGRES_PASSWORD: \"password\"\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-requeue/go.mod",
    "content": "module delayed-requeue\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/brianvoe/gofakeit/v6 v6.28.0\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/redis/go-redis/v9 v9.12.1\n)\n\nrequire (\n\tgithub.com/Rican7/retry v0.3.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/go-sql-driver/mysql v1.8.1 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/vmihailenco/msgpack v4.0.4+incompatible // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgoogle.golang.org/appengine v1.6.8 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-requeue/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=\ngithub.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4 h1:vkpSm2MZHacjN4H8R0PA9IKQ++uQMq6wA0m1bnGjipo=\ngithub.com/ThreeDotsLabs/watermill-redisstream v1.4.4/go.mod h1:Da3wqG1OcvHPODjuJcxSCY1O7D4loIZQpVbZ5u94xRo=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=\ngithub.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/delayed-requeue/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\n\t\"github.com/brianvoe/gofakeit/v6\"\n\t_ \"github.com/lib/pq\"\n\t\"github.com/redis/go-redis/v9\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tdb, err := stdSQL.Open(\"postgres\", \"postgres://watermill:password@postgres:5432/watermill?sslmode=disable\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tredisClient := redis.NewClient(&redis.Options{Addr: \"redis:6379\"})\n\n\tredisPublisher, err := redisstream.NewPublisher(redisstream.PublisherConfig{\n\t\tClient: redisClient,\n\t}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdelayedRequeuer, err := sql.NewPostgreSQLDelayedRequeuer(sql.DelayedRequeuerConfig{\n\t\tDB:        db,\n\t\tPublisher: redisPublisher,\n\t\tDelayOnError: &middleware.DelayOnError{\n\t\t\tInitialInterval: 10 * time.Second,\n\t\t\tMaxInterval:     3 * time.Minute,\n\t\t\tMultiplier:      2,\n\t\t},\n\t\tLogger: logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmarshaler := cqrs.JSONMarshaler{\n\t\tGenerateName: cqrs.StructName,\n\t}\n\n\teventBus, err := cqrs.NewEventBusWithConfig(redisPublisher, cqrs.EventBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\treturn params.EventName, nil\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter := message.NewDefaultRouter(logger)\n\trouter.AddMiddleware(delayedRequeuer.Middleware()...)\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn params.EventName, nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn redisstream.NewSubscriber(redisstream.SubscriberConfig{\n\t\t\t\tClient:        redisClient,\n\t\t\t\tConsumerGroup: params.HandlerName,\n\t\t\t}, logger)\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = eventProcessor.AddHandlers(\n\t\tcqrs.NewEventHandler(\n\t\t\t\"OnOrderPlacedHandler\",\n\t\t\tfunc(ctx context.Context, event *OrderPlaced) error {\n\t\t\t\tif event.OrderID == \"\" {\n\t\t\t\t\tfmt.Println(\"ERROR: Received order placed without order_id\")\n\t\t\t\t\treturn fmt.Errorf(\"empty order_id\")\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"Received order placed:\", event.OrderID)\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo func() {\n\t\terr = delayedRequeuer.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\terr = router.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-router.Running()\n\n\ti := 0\n\n\tfor {\n\t\te := newFakeOrderPlaced()\n\n\t\ti++\n\n\t\tif i == 10 {\n\t\t\te.OrderID = \"\"\n\t\t\ti = 0\n\t\t}\n\n\t\terr = eventBus.Publish(context.Background(), e)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(1 * time.Second)\n\t}\n}\n\nfunc newFakeOrderPlaced() OrderPlaced {\n\tvar products []Product\n\n\tfor i := 0; i < rand.Intn(5)+1; i++ {\n\t\tproducts = append(products, Product{\n\t\t\tID:   watermill.NewShortUUID(),\n\t\t\tName: gofakeit.ProductName(),\n\t\t})\n\t}\n\n\treturn OrderPlaced{\n\t\tOrderID: watermill.NewUUID(),\n\t\tCustomer: Customer{\n\t\t\tID:    watermill.NewULID(),\n\t\t\tName:  gofakeit.Name(),\n\t\t\tEmail: gofakeit.Email(),\n\t\t\tPhone: gofakeit.Phone(),\n\t\t},\n\t\tAddress: Address{\n\t\t\tStreet:  gofakeit.Street(),\n\t\t\tCity:    gofakeit.City(),\n\t\t\tZip:     gofakeit.Zip(),\n\t\t\tCountry: gofakeit.Country(),\n\t\t},\n\t\tProducts: products,\n\t}\n}\n\ntype OrderPlaced struct {\n\tOrderID  string    `json:\"order_id\"`\n\tCustomer Customer  `json:\"customer\"`\n\tAddress  Address   `json:\"address\"`\n\tProducts []Product `json:\"products\"`\n}\n\ntype Customer struct {\n\tID    string `json:\"id\"`\n\tName  string `json:\"name\"`\n\tEmail string `json:\"email\"`\n\tPhone string `json:\"phone\"`\n}\n\ntype Address struct {\n\tStreet  string `json:\"street\"`\n\tCity    string `json:\"city\"`\n\tZip     string `json:\"zip\"`\n\tCountry string `json:\"country\"`\n}\n\ntype Product struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/README.md",
    "content": "# Exactly-once delivery counter\n\nIs exactly-once delivery impossible? Well, it depends a lot on the definition of exactly-once delivery.\nWhen we assume we want to avoid the situation when a message is delivered more than once when our broker or worker died -- it's possible.\nI'll say more, it's even possible with Watermill!\n\n![](./at-least-once-delivery.jpg)\n\n*At-least once delivery - this is not what we want!*\n\nThere are just two constraints:\n1. you need to use a Pub/Sub implementation that does support exactly-once delivery (only [MySQL/PostgreSQL](https://github.com/ThreeDotsLabs/watermill-sql) for now),\n2. writes need to go to the same DB.\n\nIn practice, our model is pretty similar to how does it work with Kafka exactly-once delivery. If you want to know more details, you can check [their article](https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/).\n\nIn our example, we use a MySQL database to implement a **simple counter**. It can be triggered by calling the `http://localhost:8080/count/{counterUUID}` endpoint.\nCalling this endpoint will publish a message to MySQL via our [Pub/Sub implementation](https://github.com/ThreeDotsLabs/watermill-sql).\nThe endpoint is provided by [server/main.go](server/main.go).\n\nLater, the message is consumed by [worker/main.go](worker/main.go). The only responsibility of the worker is to update the counter in the MySQL database.\n**Counter update is done in the same transaction as message consumption.**\n\nNormally, we would need to de-duplicate messages. \nBut thanks to that fact and [A.C.I.D](https://en.wikipedia.org/wiki/ACID) even if server, worker or network failure happens during processing our data will stay consistent.\n\n![](./architecture.jpg)\n\n*Watermill's exactly-once delivery*\n\nTo check if the created code works, I've created a small `run.go` program, that sends 10k requests to the server and verifies if the count at the end is equal to 10k.\nBut to not make it too easy, I'm restarting the worker and MySQL a couple of times. I also forgot about graceful shutdown in my worker. ;-)\n\nThe biggest downside of this approach is performance. Due to [our benchmark](https://github.com/ThreeDotsLabs/watermill-benchmark#sql-mysql), MySQL subscriber can consume up to 154 messages per second.\nFortunately, it's still 13,305,600 messages per day. It's more than enough for a lot of systems.\n\n## Running\n\n    docker-compose up\n\n    go run run.go\n\n*Please note that `run.go` needs to be executed by a user having privileges to manage Docker.\nIt's due to the fact that `run.go` is restarting containers.*\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    ports:\n      - 8080:8080\n    volumes:\n      - ./server:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: 'go run .'\n\n  worker:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n      - ./worker:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: 'go run .'\n\n  mysql:\n    image: mysql:8.0\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_DATABASE: example\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n    volumes:\n      - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/run.go",
    "content": "package main\n\nimport (\n\tstdSQL \"database/sql\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os/exec\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cheggaaa/pb/v3\"\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/google/uuid\"\n)\n\nconst messagesCount = 5000\n\n// at these messages we will restart MySQL\nvar restartMySQLAt = map[int]struct{}{\n\t50:   {},\n\t1000: {},\n\t1500: {},\n\t3000: {},\n}\n\n// at these messages we will restart counter worker\nvar restartWorkerAt = map[int]struct{}{\n\t100:  {},\n\t1500: {},\n\t1600: {},\n\t3000: {},\n}\n\nconst senderGoroutines = 5\n\nfunc main() {\n\tdb := createDB()\n\tcounterUUID := uuid.New().String()\n\n\twg := &sync.WaitGroup{}\n\twg.Add(messagesCount)\n\n\tbar := pb.StartNew(messagesCount)\n\n\t// sending value to sendCounter counter HTTP call\n\tsendCounter := make(chan struct{}, 0)\n\tgo func() {\n\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\tsendCounter <- struct{}{}\n\n\t\t\t// let's challenge exactly-once delivery a bit\n\t\t\t// normally it should trigger re-delivery of the message\n\t\t\tif _, ok := restartMySQLAt[i]; ok {\n\t\t\t\trestartMySQL()\n\t\t\t}\n\t\t\tif _, ok := restartWorkerAt[i]; ok {\n\t\t\t\trestartWorker()\n\t\t\t}\n\t\t}\n\t\tclose(sendCounter)\n\t}()\n\n\tfor i := 0; i < senderGoroutines; i++ {\n\t\tgo func() {\n\t\t\tfor range sendCounter {\n\t\t\t\tsendCountRequest(counterUUID)\n\t\t\t\twg.Done()\n\t\t\t\tbar.Increment()\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\tbar.Finish()\n\n\ttimeout := time.Now().Add(time.Second * 30)\n\n\tfmt.Println(\"checking counter with DB, expected count:\", messagesCount)\n\n\tmatchedOnce := true\n\n\tfor {\n\t\tif time.Now().After(timeout) {\n\t\t\tfmt.Println(\"timeout\")\n\t\t\tbreak\n\t\t}\n\n\t\tdbCounterValue, err := getDbCounterValue(db, counterUUID)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"err:\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tfmt.Println(\"db counter value\", dbCounterValue)\n\t\tif dbCounterValue == messagesCount {\n\t\t\tif !matchedOnce {\n\t\t\t\t// let's ensure that nothing new will arrive\n\t\t\t\tmatchedOnce = true\n\t\t\t\ttime.Sleep(time.Second * 2)\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"expected counter value is matching DB value\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc getDbCounterValue(db *stdSQL.DB, counterUUID string) (int, error) {\n\tvar dbCounterValue int\n\trow := db.QueryRow(\"SELECT value from counter WHERE id = ?\", counterUUID)\n\n\tif err := row.Scan(&dbCounterValue); err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn dbCounterValue, nil\n}\n\nfunc restartWorker() {\n\tfmt.Println(\"restarting worker\")\n\terr := exec.Command(\"docker-compose\", \"restart\", \"worker\").Run()\n\tif err != nil {\n\t\tfmt.Println(\"restarting worker failed\", err)\n\t}\n}\n\nfunc restartMySQL() {\n\tfmt.Println(\"restarting mysql\")\n\terr := exec.Command(\"docker-compose\", \"restart\", \"mysql\").Run()\n\tif err != nil {\n\t\tfmt.Println(\"restarting mysql failed\", err)\n\t}\n}\n\nfunc sendCountRequest(counterUUID string) {\n\tfor {\n\t\tresp, err := http.Post(\"http://localhost:8080/count/\"+counterUUID, \"\", nil)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusNoContent {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := mysql.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"localhost\"\n\tconf.DBName = \"example\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/schema.sql",
    "content": "CREATE TABLE counter (\n    id VARCHAR(36) NOT NULL UNIQUE,\n    value int NOT NULL\n);\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/server/go.mod",
    "content": "module exactly-once-delivery\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/go-chi/chi/v5 v5.2.3\n\tgithub.com/go-sql-driver/mysql v1.9.3\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/server/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=\ngithub.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/server/main.go",
    "content": "package main\n\nimport (\n\tstdSQL \"database/sql\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\tdriver \"github.com/go-sql-driver/mysql\"\n)\n\nconst topic = \"counter\"\n\nfunc main() {\n\tdb := createDB()\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tr := chi.NewRouter()\n\tr.Use(middleware.Recoverer)\n\tr.Use(middleware.Logger)\n\n\tpublisher, err := sql.NewPublisher(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter: sql.DefaultMySQLSchema{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tr.Post(\"/count/{counterUUID}\", func(w http.ResponseWriter, r *http.Request) {\n\t\tpayload, err := json.Marshal(messagePayload{\n\t\t\tCounterUUID: chi.URLParam(r, \"counterUUID\"),\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Print(err)\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tmsg := message.NewMessage(watermill.NewUUID(), payload)\n\n\t\tif err := publisher.Publish(topic, msg); err != nil {\n\t\t\tlog.Print(err)\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusNoContent)\n\t})\n\n\terr = http.ListenAndServe(\":8080\", r)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\ntype messagePayload struct {\n\tCounterUUID string `json:\"counter_uuid\"`\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"example\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/worker/go.mod",
    "content": "module exactly-once-delivery\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/pkg/errors v0.9.1\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/worker/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/exactly-once-delivery-counter/worker/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\tdriver \"github.com/go-sql-driver/mysql\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n)\n\nconst topic = \"counter\"\n\nfunc main() {\n\tdb := createDB()\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tgo runWatermillRouter(db, logger)\n\n\tsigs := make(chan os.Signal, 1)\n\tsignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)\n\n\t// no graceful shutdown, to increase chance of problems :-)\n\t<-sigs\n}\n\ntype messagePayload struct {\n\tCounterUUID string `json:\"counter_uuid\"`\n}\n\nfunc runWatermillRouter(db *stdSQL.DB, logger watermill.LoggerAdapter) {\n\tsubscriber, err := sql.NewSubscriber(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.SubscriberConfig{\n\t\t\tSchemaAdapter:    sql.DefaultMySQLSchema{},\n\t\t\tOffsetsAdapter:   sql.DefaultMySQLOffsetsAdapter{},\n\t\t\tInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddConsumerHandler(\n\t\t\"counter\",\n\t\ttopic,\n\t\tsubscriber,\n\t\tprocessMessage,\n\t)\n\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc processMessage(msg *message.Message) error {\n\ttx, ok := sql.TxFromContext(msg.Context())\n\tif !ok {\n\t\treturn errors.New(\"tx not found in message context\")\n\t}\n\n\tpayload := messagePayload{}\n\terr := json.Unmarshal(msg.Payload, &payload)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to unmarshal payload\")\n\t}\n\n\t// let's do it more fragile, let's get the value from DB instead of simple increment\n\tcounterValue, err := dbCounterValue(msg.Context(), tx, payload.CounterUUID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcounterValue += 1\n\n\tif err := updateDbCounter(msg.Context(), tx, payload.CounterUUID, counterValue); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc updateDbCounter(ctx context.Context, tx sql.Tx, counterUUD string, counterValue int) error {\n\t_, err := tx.ExecContext(\n\t\tctx,\n\t\t\"INSERT INTO counter (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?\",\n\t\tcounterUUD,\n\t\tcounterValue,\n\t\tcounterValue,\n\t)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"can't update counter value\")\n\t}\n\n\treturn nil\n}\n\nfunc dbCounterValue(ctx context.Context, tx sql.Tx, counterUUID string) (int, error) {\n\tvar counterValue int\n\trows, err := tx.QueryContext(ctx, \"SELECT value from counter WHERE id = ?\", counterUUID)\n\tif err != nil {\n\t\treturn 0, errors.Wrap(err, \"can't get counter value\")\n\t}\n\n\tif !rows.Next() {\n\t\treturn 0, nil\n\t}\n\n\terr = rows.Scan(&counterValue)\n\tif err != nil {\n\t\treturn 0, errors.Wrap(err, \"can't get counter value\")\n\t}\n\n\treturn counterValue, nil\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"example\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n"
  },
  {
    "path": "_examples/real-world-examples/persistent-event-log/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"received event\"\n"
  },
  {
    "path": "_examples/real-world-examples/persistent-event-log/README.md",
    "content": "# Persistent Event Log (Google Cloud Pub/Sub to MySQL)\n\nThis example shows how to use the SQL Publisher from [SQL Pub/Sub](https://github.com/ThreeDotsLabs/watermill-sql).\n\n## Background\n\nSome PubSubs (e.g. Kafka) come with support for storing processed messages, possibly even with no expiration date.\nThis can be useful for audit purposes or to reply selected messages again in the future. But what if you'd like to use\na PubSub that offers no storage and an event log is needed?\n\nFor more detailed description, see [When an SQL database makes a great Pub/Sub](https://threedots.tech/post/when-sql-database-makes-great-pub-sub/).\n\n## Solution\n\nPlugging a pair of a publisher and a subscriber into Watermill's `Router` can work as a proxy from one PubSub to another.\nTo ensure that events are written to a persistent storage, you can use the SQL publisher.\n\nGoogle Cloud Pub/Sub subscriber consumes events from a topic and inserts them into a MySQL table. While this particular \nPubSub doesn't guarantee proper order of messages, you can use `OccurredAt` field of the payload for sorting.\nGoogle Cloud Pub/Sub is used just as an example and any other subscriber can be used instead.\n\nThe example uses `DefaultMySQLSchema`, but you can define your own table definition and queries.\nSee [SQL Pub/Sub documentation](https://watermill.io/pubsubs/sql) for details.\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See installation guide at https://docs.docker.com/compose/install/\n\n## Running\n\n```bash\ndocker-compose up\n```\n\nAfter few seconds, some events should be saved in the table:\n\n```\ndocker-compose exec mysql mysql -e 'select * from watermill.watermill_events;'\n+--------+--------------------------------------+---------------------+---------+----------+\n| offset | uuid                                 | created_at          | payload | metadata |\n+--------+--------------------------------------+---------------------+---------+----------+\n|      1 | 2faf6a14-f52a-4d6c-a4be-7355db428be1 | 2019-08-17 12:23:35 | {...}   | {}       |\n|      2 | cccfe73c-1968-4e20-b8b7-3763f68dc60b | 2019-08-17 12:23:35 | {...}   | {}       |\n|      3 | e8585f50-5e38-4569-bd93-fe4f6e960e61 | 2019-08-17 12:23:36 | {...}   | {}       |\n|      4 | 2d364b7e-fc4d-459c-972a-8859c8f1a655 | 2019-08-17 12:23:37 | {...}   | {}       |\n|      5 | 3b9da717-aad8-4e4b-a6e2-2d7040454015 | 2019-08-17 12:23:38 | {...}   | {}       |\n|      6 | 5c07a2e7-464e-4ffb-8ada-0e2f02e48111 | 2019-08-17 12:23:39 | {...}   | {}       |\n|      7 | 60a30b9e-6a40-4f41-94f9-8e7c8a38a998 | 2019-08-17 12:23:40 | {...}   | {}       |\n|      8 | 3d28a15a-7448-4535-9b79-27111579e341 | 2019-08-17 12:23:41 | {...}   | {}       |\n|      9 | 3c448aff-6bdd-4fc4-9b56-8bacab0b2746 | 2019-08-17 12:23:42 | {...}   | {}       |\n|     10 | 9b56ca67-4c47-4bcd-931f-86f9af62775d | 2019-08-17 12:23:43 | {...}   | {}       |\n+--------+--------------------------------------+---------------------+---------+----------+\n```\n"
  },
  {
    "path": "_examples/real-world-examples/persistent-event-log/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - mysql\n      - googlecloud\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    environment:\n      PUBSUB_EMULATOR_HOST: googlecloud:8085\n    command: go run main.go\n\n  mysql:\n    image: mysql:8.0\n    logging:\n      driver: none\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_DATABASE: watermill\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n\n  googlecloud:\n    image: google/cloud-sdk:228.0.0\n    logging:\n      driver: none\n    entrypoint: gcloud --quiet beta emulators pubsub start --host-port=0.0.0.0:8085 --verbosity=debug --log-http\n    ports:\n      - 8085:8085\n    environment:\n      PUBSUB_EMULATOR_HOST: googlecloud:8085\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/real-world-examples/persistent-event-log/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n)\n\nrequire (\n\tcloud.google.com/go v0.121.6 // indirect\n\tcloud.google.com/go/auth v0.16.5 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.8.0 // indirect\n\tcloud.google.com/go/iam v1.5.2 // indirect\n\tcloud.google.com/go/pubsub/v2 v2.0.0 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/api v0.248.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/grpc v1.75.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/persistent-event-log/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=\ncloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=\ncloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=\ncloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=\ncloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=\ncloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=\ncloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=\ncloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=\ncloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0 h1:GXR+tsxPs/Vpmm0t4yEJUZdqLP9EytWvR+KN3Un5mNY=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0/go.mod h1:3IHyi1bNqQ8J2/wVWj4cQjzWXoEPauLm8ViyOCNaKbM=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=\ngo.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=\ngoogle.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=\ngoogle.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "_examples/real-world-examples/persistent-event-log/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"time\"\n\n\tdriver \"github.com/go-sql-driver/mysql\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-googlecloud/v2/pkg/googlecloud\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tlogger           = watermill.NewStdLogger(false, false)\n\tgoogleCloudTopic = \"events\"\n\tmysqlTable       = \"events\"\n)\n\ntype event struct {\n\tName       string `json:\"name\"`\n\tOccurredAt string `json:\"occurred_at\"`\n}\n\nfunc main() {\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddPlugin(plugin.SignalsHandler)\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\tdb := createDB()\n\n\tsubscriber := createSubscriber()\n\tpublisher := createPublisher(db)\n\n\tgo simulateEvents()\n\n\trouter.AddHandler(\n\t\t\"googlecloud-to-mysql\",\n\t\tgoogleCloudTopic,\n\t\tsubscriber,\n\t\tmysqlTable,\n\t\tpublisher,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\tconsumedEvent := event{}\n\t\t\terr := json.Unmarshal(msg.Payload, &consumedEvent)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tlog.Printf(\"received event %+v with UUID %s\", consumedEvent, msg.UUID)\n\n\t\t\treturn []*message.Message{msg}, nil\n\t\t},\n\t)\n\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"watermill\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n\nfunc createSubscriber() message.Subscriber {\n\tsub, err := googlecloud.NewSubscriber(\n\t\tgooglecloud.SubscriberConfig{\n\t\t\tProjectID: \"example\",\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn sub\n}\n\nfunc createPublisher(db *stdSQL.DB) message.Publisher {\n\tpub, err := sql.NewPublisher(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter:        sql.DefaultMySQLSchema{},\n\t\t\tAutoInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn pub\n}\n\nfunc simulateEvents() {\n\tpub, err := googlecloud.NewPublisher(googlecloud.PublisherConfig{\n\t\tProjectID: \"example\",\n\t}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\te := event{\n\t\t\tName:       \"UserSignedUp\",\n\t\t\tOccurredAt: time.Now().UTC().Format(time.RFC3339),\n\t\t}\n\t\tpayload, err := json.Marshal(e)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\terr = pub.Publish(googleCloudTopic, message.NewMessage(\n\t\t\twatermill.NewUUID(),\n\t\t\tpayload,\n\t\t))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/receiving-webhooks/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"Starting handler\"\n"
  },
  {
    "path": "_examples/real-world-examples/receiving-webhooks/README.md",
    "content": "# Receiving webhooks (HTTP to Kafka)\n\nThis example showcases the use of the **HTTP Subscriber** to receive webhooks with HTTP POST requests. \n\nReceived messages are then published to a Kafka topic.\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See installation guide at https://docs.docker.com/compose/install/\n\n## Running\n\nTo run all services, execute:\n\n```\ndocker-compose up\n```\n"
  },
  {
    "path": "_examples/real-world-examples/receiving-webhooks/docker-compose.yml",
    "content": "services:\n  golang:\n    image: golang:1.25\n    restart: unless-stopped\n    ports:\n    - 8080:8080\n    depends_on:\n    - kafka\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go -kafka kafka:9092 -http :8080\n\n  zookeeper:\n    image: confluentinc/cp-zookeeper:7.3.1\n    restart: unless-stopped\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n    logging:\n      driver: none\n\n  kafka:\n    image: confluentinc/cp-kafka:7.3.1\n    restart: unless-stopped\n    logging:\n      driver: none\n    depends_on:\n    - zookeeper\n    environment:\n      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n\n"
  },
  {
    "path": "_examples/real-world-examples/receiving-webhooks/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-http v1.1.4\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-chi/chi v4.1.2+incompatible // indirect\n\tgithub.com/go-chi/render v1.0.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/real-world-examples/receiving-webhooks/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.1.0/go.mod h1:Qd1xNFxolCAHCzcMrm6RnjW0manbvN+DJVWc1MWRFlI=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-http v1.1.4 h1:wRM54z/BPnIWjGbXMrOnwOlrCAESzoSNxTAHiLysFA4=\ngithub.com/ThreeDotsLabs/watermill-http v1.1.4/go.mod h1:mkQ9CC0pxTZerNwr281rBoOy355vYt/lePkmYSX/BRg=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=\ngithub.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/receiving-webhooks/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\tstdHttp \"net/http\"\n\t_ \"net/http/pprof\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-http/pkg/http\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tkafkaAddr = flag.String(\"kafka\", \"localhost:9092\", \"The address of the kafka broker\")\n\thttpAddr  = flag.String(\"http\", \":8080\", \"The address for the http subscriber\")\n)\n\ntype Webhook struct {\n\tObjectKind string `json:\"object_kind\"`\n}\n\nfunc main() {\n\tflag.Parse()\n\tlogger := watermill.NewStdLogger(true, true)\n\n\tkafkaPublisher, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   []string{*kafkaAddr},\n\t\t\tMarshaler: kafka.DefaultMarshaler{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\thttpSubscriber, err := http.NewSubscriber(\n\t\t*httpAddr,\n\t\thttp.SubscriberConfig{\n\t\t\tUnmarshalMessageFunc: func(topic string, request *stdHttp.Request) (*message.Message, error) {\n\t\t\t\tb, err := io.ReadAll(request.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"cannot read body: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn message.NewMessage(watermill.NewUUID(), b), nil\n\t\t\t},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tr.AddMiddleware(\n\t\tmiddleware.Recoverer,\n\t\tmiddleware.CorrelationID,\n\t)\n\tr.AddPlugin(plugin.SignalsHandler)\n\n\tr.AddHandler(\n\t\t\"http_to_kafka\",\n\t\t\"/webhooks\", // this is the URL of our API\n\t\thttpSubscriber,\n\t\t\"webhooks\", // this is the topic the message will be published to\n\t\tkafkaPublisher,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\twebhook := Webhook{}\n\n\t\t\tif err := json.Unmarshal(msg.Payload, &webhook); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot unmarshal message: %w\", err)\n\t\t\t}\n\n\t\t\t// Add simple validation\n\t\t\tif webhook.ObjectKind == \"\" {\n\t\t\t\treturn nil, errors.New(\"empty object kind\")\n\t\t\t}\n\n\t\t\t// Simply forward the message from HTTP Subscriber to Kafka Publisher\n\t\t\treturn []*message.Message{msg}, nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\t// HTTP server needs to be started after the router is ready.\n\t\t<-r.Running()\n\t\t_ = httpSubscriber.StartHTTPServer()\n\t}()\n\n\t_ = r.Run(context.Background())\n}\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"POST /foo_or_bar: message\"\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/README.md",
    "content": "# Sending webhooks (Kafka to HTTP)\n\nThis example showcases the use of the **HTTP Publisher** to call webhooks with HTTP POST requests. It consists of three services:\n\n1. `producer` publishes messages on Kafka. The messages come in three varieties: `Foo`, `Bar`, and `Baz`. The event type is encoded in metadata, under the key `event_type`.\n1. `webhook_server` is a HTTP server that listens for requests and prints the path, method, and payload on stdout.\n1. `router` consumes the Kafka messages, and uses the HTTP producer to send requests to `webhook_server`. To illustrate how one message can spawn multiple webhooks, the following paths are called based on `event_type`:\n    1. `/foo` for events of type `Foo`\n    1. `/foo_or_bar` for events of type `Foo` or `Bar`\n    1. `/all` for all events.\n\nAdditionally, services `zookeeper` and `kafka` are present to provide backend for the Kafka producer and subscriber.\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See installation guide at https://docs.docker.com/compose/install/\n\n## Running\n\nTo run all services, execute:\n\n```\ndocker-compose up\n```\n\nTo filter messages from a specific service, execute:\n\n```\ndocker-compose logs [-f] {service}\n```\n\nin a separate terminal window while the services are running. Use the `-f` flag to emulate `tail -f` behavior, i.e. follow the output.\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/docker-compose.yml",
    "content": "services:\n  webhooks-server:\n    image: golang:1.25\n    restart: unless-stopped\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app/webhooks-server/\n    command: go run main.go\n\n  router:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - kafka\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app/router/\n    command: go run main.go\n\n  producer:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n    - kafka\n    - webhooks-server\n    - router\n    volumes:\n    - .:/app\n    - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app/producer/\n    command: go run main.go\n\n  zookeeper:\n    image: confluentinc/cp-zookeeper:7.3.1\n    restart: unless-stopped\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n    logging:\n      driver: none\n\n  kafka:\n    image: confluentinc/cp-kafka:7.3.1\n    restart: unless-stopped\n    logging:\n      driver: none\n    depends_on:\n    - zookeeper\n    environment:\n      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/producer/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/producer/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/producer/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nvar (\n\tbrokers = []string{\"kafka:9092\"}\n\tlogger  = watermill.NewStdLogger(false, false)\n)\n\ntype eventType string\n\nconst (\n\tFoo eventType = \"Foo\"\n\tBar eventType = \"Bar\"\n\tBaz eventType = \"Baz\"\n)\n\nfunc main() {\n\tpub, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   brokers,\n\t\t\tMarshaler: kafka.DefaultMarshaler{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\teventTypes := []eventType{Foo, Bar, Baz}\n\n\tfor {\n\t\teventType := eventTypes[rand.Intn(3)]\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"message\"))\n\t\tmsg.Metadata.Set(\"event_type\", string(eventType))\n\n\t\tfmt.Printf(\"%s Publishing %s\\n\\n\", time.Now().String(), eventType)\n\t\tif err := pub.Publish(\"kafka_to_http_example\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n\n}\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/router/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-http v1.1.4\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-chi/chi v4.1.2+incompatible // indirect\n\tgithub.com/go-chi/render v1.0.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/router/go.sum",
    "content": "github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.1.0/go.mod h1:Qd1xNFxolCAHCzcMrm6RnjW0manbvN+DJVWc1MWRFlI=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-http v1.1.4 h1:wRM54z/BPnIWjGbXMrOnwOlrCAESzoSNxTAHiLysFA4=\ngithub.com/ThreeDotsLabs/watermill-http v1.1.4/go.mod h1:mkQ9CC0pxTZerNwr281rBoOy355vYt/lePkmYSX/BRg=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=\ngithub.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/router/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\twatermill_http \"github.com/ThreeDotsLabs/watermill-http/pkg/http\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tlogger = watermill.NewStdLogger(false, false)\n)\n\n// filterMessages passes the message along if its event type is one of acceptedTypes.\nfunc filterMessages(acceptedTypes ...string) message.HandlerFunc {\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\t// the kafka producer sets this metadata so that we don't have to unmarshal the body\n\t\t// just sort the messages based on event type metadata\n\t\tmsgEventType := msg.Metadata.Get(\"event_type\")\n\n\t\tfor _, typ := range acceptedTypes {\n\t\t\tif typ == msgEventType {\n\t\t\t\treturn message.Messages{msg}, nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil, nil\n\t}\n}\n\nfunc main() {\n\tpublisher, err := watermill_http.NewPublisher(watermill_http.PublisherConfig{\n\t\tMarshalMessageFunc: watermill_http.DefaultMarshalMessageFunc,\n\t}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsubscriber, err := kafka.NewSubscriber(\n\t\tkafka.SubscriberConfig{\n\t\t\tBrokers:     []string{\"kafka:9092\"},\n\t\t\tUnmarshaler: kafka.DefaultMarshaler{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\ttopic := \"kafka_to_http_example\"\n\turl := \"http://webhooks-server:8001/\"\n\n\trouter.AddHandler(\"foo\", topic, subscriber, url+\"foo\", publisher, filterMessages(\"Foo\"))\n\trouter.AddHandler(\"foo_or_bar\", topic, subscriber, url+\"foo_or_bar\", publisher, filterMessages(\"Foo\", \"Bar\"))\n\trouter.AddHandler(\"all\", topic, subscriber, url+\"all\", publisher, filterMessages(\"Foo\", \"Bar\", \"Baz\"))\n\trouter.AddPlugin(plugin.SignalsHandler)\n\n\terr = router.Run(context.Background())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/sending-webhooks/webhooks-server/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// handler receives the webhook requests and logs them in stdout.\nfunc handler(w http.ResponseWriter, r *http.Request) {\n\tbody, err := ioutil.ReadAll(r.Body)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\treturn\n\t}\n\tfmt.Printf(\n\t\t\"[%s] %s %s: %s\\n\\n\",\n\t\ttime.Now().String(),\n\t\tr.Method,\n\t\tr.URL.String(),\n\t\tstring(body),\n\t)\n\tw.WriteHeader(http.StatusOK)\n}\n\nfunc main() {\n\thttp.HandleFunc(\"/\", handler)\n\thttp.ListenAndServe(\":8001\", http.DefaultServeMux)\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/README.md",
    "content": "# HTTP Server push using SSE (Server-Sent Events)\n\nThis example is a Twitter-like web application using [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) to support real-time refreshing.\n\n![](./screen.gif)\n\n## Running\n\n```\ndocker-compose up\n```\n\nThen, open  http://localhost:8080\n\nYou can add your own post or click the button to get randomly generated posts.\n\nEither way, the feeds list and posts in a feed should be always up-to-date. Try using a second browser window to see the update.\n\n## How it works\n\n* Posts can be created and updated.\n* Posts can contain tags.\n* Each tag has its own feed that contains all posts from that tag.\n* All posts are stored in MySQL. This is the Write Model.\n* All feeds are updated asynchronously and stored in MongoDB. This is the Read Model.\n\n### Why use separate write and read models? \n\nFor this example application, using polyglot persistence (two database engines) is, of course, an overkill.\nWe did it to showcase this technique and how easy it is to apply it with Watermill.\n\nA dedicated read model is a useful pattern for applications with high read/write ratio. All writes are applied atomically\nto the write model (MySQL in our case). Event handlers asynchronously update the read model (we use Mongo).\n\nThe data in the read model is ready to serve as it is. It can also be scaled independently of the write model.\n\nKeep in mind that eventual consistency has to be acceptable in your application to use this pattern.\nAlso, you probably won't need to use it for most use cases. Be pragmatic!\n\n![](./diagram.jpg)\n\n### SSE Router\n\nThe `SSERouter` comes from [watermill-http](https://github.com/ThreeDotsLabs/watermill-http).\nWhen creating a new router, you pass an upstream subscriber. Messages coming from that subscriber will trigger pushing updates over HTTP.\n\nIn this example, we use [NATS](https://nats.io/) as Pub/Sub, but this can be any Pub/Sub supported by Watermill.\n\n```go\nsseRouter, err := watermillHTTP.NewSSERouter(\n    watermillHTTP.SSERouterConfig{\n        UpstreamSubscriber: router.Subscriber,\n        ErrorHandler:       watermillHTTP.DefaultErrorHandler,\n    },\n    router.Logger,\n)\n```\n\n### Stream Adapters\n\nTo work with `SSERouter` you need to prepare a `StreamAdapter` with two methods.\n\n`GetResponse` is similar to a standard HTTP handler. It should be super easy to modify an existing handler to match this signature.\n\n`Validate` is an extra method that tells whether an update should be pushed for a particular `Message`.\n\n```go\ntype StreamAdapter interface {\n\t// GetResponse returns the response to be sent back to client.\n\t// Any errors that occur should be handled and written to `w`, returning false as `ok`.\n\tGetResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool)\n\t// Validate validates if the incoming message should be handled by this handler.\n\t// Typically this involves checking some kind of model ID.\n\tValidate(r *http.Request, msg *message.Message) (ok bool)\n}\n```\n\nAn example `Validate` can look like this. It checks whether the message came for the same post ID that the user sent over the HTTP request.\n\n```go\nfunc (p postStreamAdapter) Validate(r *http.Request, msg *message.Message) (ok bool) {\n\tpostUpdated := PostUpdated{}\n\n\terr := json.Unmarshal(msg.Payload, &postUpdated)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tpostID := chi.URLParam(r, \"id\")\n\n\treturn postUpdated.OriginalPost.ID == postID\n}\n```\n\nIf you'd like to trigger an update for every message, you can simply return `true`.\n\n```go\nfunc (f allFeedsStreamAdapter) Validate(r *http.Request, msg *message.Message) (ok bool) {\n\treturn true\n}\n```\n\nBefore starting the `SSERouter`, you need to add the handler with particular topic.\n`AddHandler` returns a standard HTTP handler that can be used in any routing library.\n\n```go\npostHandler := sseRouter.AddHandler(PostUpdatedTopic, postStream)\n\n// ...\n\nr.Get(\"/posts/{id}\", postHandler)\n```\n\n## Event handlers\n\nThe example uses Watermill for all asynchronous communication, including SSE.\n\nThere are several events published:\n\n* `PostCreated`\n    * Adds the post to all feeds with tags present in the post.\n* `FeedUpdated`\n    * Pushes update to all clients currently visiting the feed page.\n* `PostUpdated`\n    * Pushes update to all clients currently visiting the post page.\n    * Updates post in all feeds with tags present in the post\n        * a) For existing tags, the post content will be updated in the tag.\n        * b) If a new tag has been added, the post will be added to the tag's feed.\n        * c) If a tag has been deleted, the post will be removed from the tag's feed.\n\n## Frontend app\n\nThe frontend application is built using Vue.js and Bootstrap.\n\nThe most interesting part is the use of `EventSource`.\n\n```js\nthis.es = new EventSource('/api/feeds/' + this.feed)\n\nthis.es.addEventListener('data', event => {\n    let data = JSON.parse(event.data);\n    this.posts_stream = data.posts;\n}, false);\n```\n\nPlease note the author is not a frontend developer and the code in `index.html` is probably not idiomatic. PRs are welcome. :)\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    ports:\n      - 8080:8080\n    volumes:\n      - ./server:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: 'go run .'\n\n  mysql:\n    image: mysql:8.0\n    restart: unless-stopped\n    environment:\n      MYSQL_DATABASE: example\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n    volumes:\n      - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql\n\n  mongo:\n    image: mongo:3.6\n    restart: unless-stopped\n    environment:\n      MONGO_INITDB_ROOT_USERNAME: root\n      MONGO_INITDB_ROOT_PASSWORD: password\n\n  nats-streaming:\n    image: nats-streaming:0.11.2\n    restart: unless-stopped\n    logging:\n      driver: none\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/schema.sql",
    "content": "CREATE TABLE example.posts (\n    id VARCHAR(36) NOT NULL PRIMARY KEY,\n    title VARCHAR(255) NOT NULL DEFAULT '',\n    content TEXT NOT NULL,\n    author VARCHAR(255) NOT NULL DEFAULT ''\n);\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/event_handlers.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-nats/pkg/nats\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/nats-io/stan.go\"\n)\n\nconst (\n\tPostCreatedTopic = \"post-created\"\n\tPostUpdatedTopic = \"post-updated\"\n\tFeedUpdatedTopic = \"feed-updated\"\n)\n\nfunc SetupMessageRouter(\n\tfeedsStorage FeedsStorage,\n\tlogger watermill.LoggerAdapter,\n) (message.Publisher, message.Subscriber, error) {\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\tnatsURL := stan.NatsURL(\"nats://nats-streaming:4222\")\n\tpub, err := nats.NewStreamingPublisher(nats.StreamingPublisherConfig{\n\t\tClusterID:   \"test-cluster\",\n\t\tClientID:    \"publisher\",\n\t\tStanOptions: []stan.Option{natsURL},\n\t\tMarshaler:   nats.GobMarshaler{},\n\t}, logger)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tsub, err := nats.NewStreamingSubscriber(nats.StreamingSubscriberConfig{\n\t\tClusterID:   \"test-cluster\",\n\t\tClientID:    \"subscriber\",\n\t\tStanOptions: []stan.Option{natsURL},\n\t\tUnmarshaler: nats.GobMarshaler{},\n\t}, logger)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\trouter.AddHandler(\n\t\t\"update-feeds-on-post-created\",\n\t\tPostCreatedTopic,\n\t\tsub,\n\t\tFeedUpdatedTopic,\n\t\tpub,\n\t\tfunc(msg *message.Message) (messages []*message.Message, err error) {\n\t\t\tdefer func() {\n\t\t\t\tif err == nil {\n\t\t\t\t\tlogger.Info(\"Successfully updated feeds on new post created\", nil)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Error(\"Error while updating feeds on new post created\", err, nil)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tevent := PostCreated{}\n\t\t\terr = json.Unmarshal(msg.Payload, &event)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tlogger.Info(\"Adding post\", watermill.LogFields{\"post\": event.Post})\n\n\t\t\tif len(event.Post.Tags) > 0 {\n\t\t\t\tfor _, tag := range event.Post.Tags {\n\t\t\t\t\tlogger.Info(\"Adding tag\", watermill.LogFields{\"tag\": tag})\n\t\t\t\t\terr = feedsStorage.Add(msg.Context(), tag)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\terr = feedsStorage.AppendPost(msg.Context(), event.Post)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn createFeedUpdatedEvents(event.Post.Tags)\n\t\t},\n\t)\n\n\trouter.AddHandler(\n\t\t\"update-feeds-on-post-updated\",\n\t\tPostUpdatedTopic,\n\t\tsub,\n\t\tFeedUpdatedTopic,\n\t\tpub,\n\t\tfunc(msg *message.Message) (messages []*message.Message, err error) {\n\t\t\tdefer func() {\n\t\t\t\tif err == nil {\n\t\t\t\t\tlogger.Info(\"Successfully updated feeds on post updated\", nil)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Error(\"Error while updating feeds on post updated\", err, nil)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tevent := PostUpdated{}\n\t\t\terr = json.Unmarshal(msg.Payload, &event)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfor _, tag := range event.NewPost.Tags {\n\t\t\t\tlogger.Info(\"Adding tag\", watermill.LogFields{\"tag\": tag})\n\t\t\t\terr = feedsStorage.Add(msg.Context(), tag)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr = feedsStorage.UpdatePost(msg.Context(), event.NewPost)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn createFeedUpdatedEvents(append(event.NewPost.Tags, event.OriginalPost.Tags...))\n\t\t},\n\t)\n\n\tgo func() {\n\t\terr = router.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-router.Running()\n\n\treturn pub, sub, nil\n}\n\nfunc createFeedUpdatedEvents(tags []string) ([]*message.Message, error) {\n\tvar messages []*message.Message\n\n\tfor _, tag := range tags {\n\t\tevent := FeedUpdated{\n\t\t\tName:       tag,\n\t\t\tOccurredAt: time.Now().UTC(),\n\t\t}\n\n\t\tpayload, err := json.Marshal(event)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmsg := message.NewMessage(watermill.NewUUID(), payload)\n\n\t\tmessages = append(messages, msg)\n\t}\n\n\treturn messages, nil\n}\n\ntype Publisher struct {\n\tpublisher message.Publisher\n}\n\nfunc (p Publisher) Publish(topic string, event interface{}) error {\n\tpayload, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmsg := message.NewMessage(watermill.NewUUID(), payload)\n\n\treturn p.publisher.Publish(topic, msg)\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/feeds_storage.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\t\"go.mongodb.org/mongo-driver/mongo/readpref\"\n)\n\nconst collectionName = \"feeds\"\n\ntype FeedsStorage struct {\n\tcollection *mongo.Collection\n}\n\nfunc NewFeedsStorage() FeedsStorage {\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tclient, err := mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://root:password@mongo:27017\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = client.Ping(ctx, readpref.Primary())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdb := client.Database(\"example\")\n\tnames, err := db.ListCollectionNames(ctx, bson.M{})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfound := false\n\tfor _, n := range names {\n\t\tif n == collectionName {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\terr := db.CreateCollection(ctx, collectionName)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\treturn FeedsStorage{\n\t\tcollection: db.Collection(collectionName),\n\t}\n}\n\nfunc (s FeedsStorage) Add(ctx context.Context, name string) error {\n\tfeed := Feed{\n\t\tName:  name,\n\t\tPosts: []Post{},\n\t}\n\n\t_, err := s.collection.InsertOne(ctx, feed)\n\tif err != nil {\n\t\tif !isDuplicateError(err) {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s FeedsStorage) All(ctx context.Context) ([]Feed, error) {\n\tcursor, err := s.collection.Find(ctx, bson.M{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar feeds []Feed\n\n\terr = cursor.All(ctx, &feeds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn feeds, nil\n}\n\nfunc (s FeedsStorage) ByName(ctx context.Context, name string) (Feed, error) {\n\tfilter := bson.M{\n\t\t\"_id\": name,\n\t}\n\n\tvar feed Feed\n\terr := s.collection.FindOne(ctx, filter).Decode(&feed)\n\tif err != nil {\n\t\treturn Feed{}, err\n\t}\n\n\treturn feed, nil\n}\n\nfunc (s FeedsStorage) AppendPost(ctx context.Context, post Post) error {\n\treturn s.appendPostIfNotPresent(ctx, post)\n}\n\nfunc (s FeedsStorage) UpdatePost(ctx context.Context, post Post) error {\n\terr := s.updatePostIfPresent(ctx, post)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.appendPostIfNotPresent(ctx, post)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.removePostIfNotInFeed(ctx, post)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s FeedsStorage) updatePostIfPresent(ctx context.Context, post Post) error {\n\tif len(post.Tags) == 0 {\n\t\treturn nil\n\t}\n\n\tfilter := bson.M{\n\t\t\"_id\": bson.M{\n\t\t\t\"$in\": post.Tags,\n\t\t},\n\t\t\"posts.id\": post.ID,\n\t}\n\n\tupdate := bson.M{\n\t\t\"$set\": bson.M{\n\t\t\t\"posts.$\": post,\n\t\t},\n\t}\n\n\t_, err := s.collection.UpdateMany(ctx, filter, update)\n\treturn err\n}\n\nfunc (s FeedsStorage) appendPostIfNotPresent(ctx context.Context, post Post) error {\n\tif len(post.Tags) == 0 {\n\t\treturn nil\n\t}\n\n\tfilter := bson.M{\n\t\t\"_id\": bson.M{\n\t\t\t\"$in\": post.Tags,\n\t\t},\n\t\t\"posts.id\": bson.M{\n\t\t\t\"$ne\": post.ID,\n\t\t},\n\t}\n\n\tupdate := bson.M{\n\t\t\"$push\": bson.M{\n\t\t\t\"posts\": bson.M{\n\t\t\t\t\"$each\":     bson.A{post},\n\t\t\t\t\"$position\": 0,\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err := s.collection.UpdateMany(ctx, filter, update)\n\treturn err\n}\n\nfunc (s FeedsStorage) removePostIfNotInFeed(ctx context.Context, post Post) error {\n\ttags := post.Tags\n\tif tags == nil {\n\t\ttags = []string{}\n\t}\n\n\tfilter := bson.M{\n\t\t\"_id\": bson.M{\n\t\t\t\"$nin\": tags,\n\t\t},\n\t\t\"posts.id\": post.ID,\n\t}\n\n\tupdate := bson.M{\n\t\t\"$pull\": bson.M{\n\t\t\t\"posts\": bson.M{\n\t\t\t\t\"id\": post.ID,\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err := s.collection.UpdateMany(ctx, filter, update)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc isDuplicateError(err error) bool {\n\tmErr, ok := err.(mongo.WriteException)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn mErr.WriteErrors[0].Code == 11000\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-nats v1.0.7\n\tgithub.com/go-chi/chi/v5 v5.2.3\n\tgithub.com/go-chi/render v1.0.3\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/nats-io/stan.go v0.10.4\n\tgo.mongodb.org/mongo-driver v1.17.4\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill-http/v2 v2.3.1\n\tgithub.com/brianvoe/gofakeit/v6 v6.28.0\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/go-chi/chi v4.1.2+incompatible // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/hashicorp/go-hclog v1.4.0 // indirect\n\tgithub.com/hashicorp/go-msgpack v0.5.5 // indirect\n\tgithub.com/hashicorp/raft v1.3.11 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/minio/highwayhash v1.0.2 // indirect\n\tgithub.com/montanaflynn/stats v0.7.1 // indirect\n\tgithub.com/nats-io/jwt/v2 v2.3.0 // indirect\n\tgithub.com/nats-io/nats.go v1.45.0 // indirect\n\tgithub.com/nats-io/nkeys v0.4.11 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/xdg-go/pbkdf2 v1.0.0 // indirect\n\tgithub.com/xdg-go/scram v1.1.2 // indirect\n\tgithub.com/xdg-go/stringprep v1.0.4 // indirect\n\tgithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect\n\tgo.etcd.io/bbolt v1.3.6 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/time v0.3.0 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ=\ngithub.com/ThreeDotsLabs/watermill-http/v2 v2.3.1/go.mod h1:RwGHEzGsEEXC/rQNLWQqR83+WPlABgOgnv2kTB56Y4Y=\ngithub.com/ThreeDotsLabs/watermill-nats v1.0.7 h1:hOquWq0GAwm5jaIc3wGaDoVCPYL+If4NZPb+RUaHni4=\ngithub.com/ThreeDotsLabs/watermill-nats v1.0.7/go.mod h1:t5A8XbO/v8CPM+AIljgoO9NR1jBk3ixYBGAtvn1N4lA=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=\ngithub.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=\ngithub.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=\ngithub.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=\ngithub.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=\ngithub.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=\ngithub.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/raft v1.3.11 h1:p3v6gf6l3S797NnK5av3HcczOC1T5CLoaRvg0g9ys4A=\ngithub.com/hashicorp/raft v1.3.11/go.mod h1:J8naEwc6XaaCfts7+28whSeRvCqTd6e20BlCU3LtEO4=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=\ngithub.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=\ngithub.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI=\ngithub.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=\ngithub.com/nats-io/nats-server/v2 v2.6.1 h1:cJy+ia7/4EaJL+ZYDmIy2rD1mDWTfckhtPBU0GYo8xM=\ngithub.com/nats-io/nats-server/v2 v2.6.1/go.mod h1:Az91TbZiV7K4a6k/4v6YYdOKEoxCXj+iqhHVf/MlrKo=\ngithub.com/nats-io/nats-streaming-server v0.22.1 h1:YKDdLAWZud3UnEBvUPaYppMxSDuh+9czTCDriq19tJY=\ngithub.com/nats-io/nats-streaming-server v0.22.1/go.mod h1:1WpVkVV5NyZbHuGGxkaPWopLFnxNthO/TK/BkzFdnPE=\ngithub.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA=\ngithub.com/nats-io/nats.go v1.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA=\ngithub.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=\ngithub.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw=\ngithub.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=\ngithub.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=\ngithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=\ngo.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=\ngo.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=\ngo.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=\ngolang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/http.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/brianvoe/gofakeit/v6\"\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\twatermillHTTP \"github.com/ThreeDotsLabs/watermill-http/v2/pkg/http\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nvar generatedTags = []string{\"watermill\", \"golang\", \"pubsub\", \"unicorn\", \"HelloWorld\", \"example\", \"ThreeDotsLabs\"}\n\ntype Router struct {\n\tSubscriber   message.Subscriber\n\tPublisher    Publisher\n\tPostsStorage PostsStorage\n\tFeedsStorage FeedsStorage\n\tLogger       watermill.LoggerAdapter\n}\n\nfunc (router Router) Mux() *chi.Mux {\n\tr := chi.NewRouter()\n\n\troot := http.Dir(\"./public\")\n\tFileServer(r, \"/\", root)\n\n\tsseRouter, err := watermillHTTP.NewSSERouter(\n\t\twatermillHTTP.SSERouterConfig{\n\t\t\tUpstreamSubscriber: router.Subscriber,\n\t\t\tErrorHandler:       watermillHTTP.DefaultErrorHandler,\n\t\t},\n\t\trouter.Logger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpostStream := postStreamAdapter{storage: router.PostsStorage, logger: router.Logger}\n\tfeedStream := feedStreamAdapter{storage: router.FeedsStorage, logger: router.Logger}\n\tallFeedsStream := allFeedsStreamAdapter{storage: router.FeedsStorage, logger: router.Logger}\n\n\tpostHandler := sseRouter.AddHandler(PostUpdatedTopic, postStream)\n\tfeedHandler := sseRouter.AddHandler(FeedUpdatedTopic, feedStream)\n\tallFeedsHandler := sseRouter.AddHandler(FeedUpdatedTopic, allFeedsStream)\n\n\tr.Route(\"/api\", func(r chi.Router) {\n\t\tr.Get(\"/posts/{id}\", postHandler)\n\t\tr.Post(\"/posts\", router.CreatePost)\n\t\tr.Post(\"/generate/post\", router.GeneratePost)\n\t\tr.Patch(\"/posts/{id}\", router.UpdatePost)\n\t\tr.Get(\"/feeds/{name}\", feedHandler)\n\t\tr.Get(\"/feeds\", allFeedsHandler)\n\t})\n\n\tgo func() {\n\t\terr = sseRouter.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-sseRouter.Running()\n\n\treturn r\n}\n\ntype feedSummary struct {\n\tName  string `json:\"name\"`\n\tPosts int    `json:\"posts\"`\n}\n\ntype AllFeedsResponse struct {\n\tFeeds []feedSummary `json:\"feeds\"`\n}\n\ntype allFeedsStreamAdapter struct {\n\tstorage FeedsStorage\n\tlogger  watermill.LoggerAdapter\n}\n\nfunc (f allFeedsStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {\n\tresp, err := f.getResponse(r)\n\tif err != nil {\n\t\tlogAndWriteError(f.logger, w, err)\n\t\treturn resp, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (f allFeedsStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {\n\tresp, err := f.getResponse(r)\n\tif err != nil {\n\t\treturn resp, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (f allFeedsStreamAdapter) getResponse(r *http.Request) (interface{}, error) {\n\tfeeds, err := f.storage.All(r.Context())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := AllFeedsResponse{\n\t\tFeeds: []feedSummary{},\n\t}\n\n\tfor _, f := range feeds {\n\t\tresponse.Feeds = append(response.Feeds, feedSummary{\n\t\t\tName:  f.Name,\n\t\t\tPosts: len(f.Posts),\n\t\t})\n\t}\n\n\treturn response, nil\n}\n\ntype CreatePostRequest struct {\n\tTitle   string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tAuthor  string `json:\"author\"`\n}\n\nfunc (router Router) CreatePost(w http.ResponseWriter, r *http.Request) {\n\treq := CreatePostRequest{}\n\terr := render.Decode(r, &req)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tpost := NewPost(\n\t\twatermill.NewUUID(),\n\t\treq.Title,\n\t\treq.Content,\n\t\treq.Author,\n\t)\n\n\terr = router.addPost(r.Context(), post)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tw.WriteHeader(204)\n}\n\nfunc (router Router) GeneratePost(w http.ResponseWriter, r *http.Request) {\n\ttitle := gofakeit.Sentence(5)\n\tcontent := gofakeit.Sentence(20)\n\tauthor := gofakeit.Name()\n\n\ttagsCount := rand.Intn(3) + 1\n\tfor i := 0; i < tagsCount; i++ {\n\t\tcontent += fmt.Sprintf(\" #%v\", generatedTags[rand.Intn(len(generatedTags))])\n\t}\n\n\tpost := NewPost(\n\t\twatermill.NewUUID(),\n\t\ttitle,\n\t\tcontent,\n\t\tauthor,\n\t)\n\n\terr := router.addPost(r.Context(), post)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tw.WriteHeader(204)\n}\n\nfunc (router Router) addPost(ctx context.Context, post Post) error {\n\terr := router.PostsStorage.Add(ctx, post)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tevent := PostCreated{\n\t\tPost:       post,\n\t\tOccurredAt: time.Now().UTC(),\n\t}\n\n\terr = router.Publisher.Publish(PostCreatedTopic, event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype UpdatePostRequest struct {\n\tID      string `json:\"id\"`\n\tTitle   string `json:\"title\"`\n\tContent string `json:\"content\"`\n}\n\nfunc (router Router) UpdatePost(w http.ResponseWriter, r *http.Request) {\n\treq := UpdatePostRequest{}\n\terr := render.Decode(r, &req)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tpost, err := router.PostsStorage.ByID(r.Context(), req.ID)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tnewPost := NewPost(\n\t\tpost.ID,\n\t\treq.Title,\n\t\treq.Content,\n\t\tpost.Author,\n\t)\n\n\terr = router.PostsStorage.Update(r.Context(), newPost)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tevent := PostUpdated{\n\t\tOriginalPost: post,\n\t\tNewPost:      newPost,\n\t\tOccurredAt:   time.Now().UTC(),\n\t}\n\n\terr = router.Publisher.Publish(PostUpdatedTopic, event)\n\tif err != nil {\n\t\tlogAndWriteError(router.Logger, w, err)\n\t\treturn\n\t}\n\n\tw.WriteHeader(204)\n}\n\ntype feedStreamAdapter struct {\n\tstorage FeedsStorage\n\tlogger  watermill.LoggerAdapter\n}\n\nfunc (f feedStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {\n\tresp, err := f.getResponse(r)\n\tif err != nil {\n\t\tlogAndWriteError(f.logger, w, err)\n\t\treturn nil, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (f feedStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {\n\tfeedUpdated := FeedUpdated{}\n\n\terr := json.Unmarshal(msg.Payload, &feedUpdated)\n\tif err != nil {\n\t\treturn nil, false\n\t}\n\n\tfeedName := chi.URLParam(r, \"name\")\n\n\tif feedUpdated.Name != feedName {\n\t\treturn nil, false\n\t}\n\n\tresp, err := f.getResponse(r)\n\tif err != nil {\n\t\treturn nil, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (f feedStreamAdapter) getResponse(r *http.Request) (response interface{}, err error) {\n\tfeedName := chi.URLParam(r, \"name\")\n\n\tfeed, err := f.storage.ByName(r.Context(), feedName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn feed, nil\n}\n\ntype postStreamAdapter struct {\n\tstorage PostsStorage\n\tlogger  watermill.LoggerAdapter\n}\n\nfunc (p postStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {\n\tresp, err := p.getResponse(r)\n\tif err != nil {\n\t\tlogAndWriteError(p.logger, w, err)\n\t\treturn nil, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (p postStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {\n\tpostUpdated := PostUpdated{}\n\n\terr := json.Unmarshal(msg.Payload, &postUpdated)\n\tif err != nil {\n\t\treturn nil, false\n\t}\n\n\tpostID := chi.URLParam(r, \"id\")\n\n\tif postUpdated.OriginalPost.ID != postID {\n\t\treturn nil, false\n\t}\n\n\tresp, err := p.getResponse(r)\n\tif err != nil {\n\t\treturn nil, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (p postStreamAdapter) getResponse(r *http.Request) (response interface{}, err error) {\n\tpostID := chi.URLParam(r, \"id\")\n\n\tpost, err := p.storage.ByID(r.Context(), postID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn post, nil\n}\n\nfunc FileServer(r chi.Router, path string, root http.FileSystem) {\n\tif strings.ContainsAny(path, \"{}*\") {\n\t\tpanic(\"FileServer does not permit URL parameters.\")\n\t}\n\n\tfs := http.StripPrefix(path, http.FileServer(root))\n\n\tif path != \"/\" && path[len(path)-1] != '/' {\n\t\tr.Get(path, http.RedirectHandler(path+\"/\", 301).ServeHTTP)\n\t\tpath += \"/\"\n\t}\n\tpath += \"*\"\n\n\tr.Get(path, func(w http.ResponseWriter, r *http.Request) {\n\t\tfs.ServeHTTP(w, r)\n\t})\n}\n\nfunc logAndWriteError(logger watermill.LoggerAdapter, w http.ResponseWriter, err error) {\n\tlogger.Error(\"Error\", err, nil)\n\tw.WriteHeader(500)\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/main.go",
    "content": "package main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n)\n\nfunc main() {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tpostsStorage := NewPostsStorage()\n\tfeedsStorage := NewFeedsStorage()\n\n\tpub, sub, err := SetupMessageRouter(feedsStorage, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\thttpRouter := Router{\n\t\tSubscriber:   sub,\n\t\tPublisher:    Publisher{publisher: pub},\n\t\tPostsStorage: postsStorage,\n\t\tFeedsStorage: feedsStorage,\n\t\tLogger:       logger,\n\t}\n\n\tmux := httpRouter.Mux()\n\n\terr = http.ListenAndServe(\":8080\", mux)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/models.go",
    "content": "package main\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Note that in a real application using both \"json\" and \"bson\" tags in the same structure is strongly discouraged.\n// We use common models for database storage and HTTP API just to make this example simple and easy to grasp.\n// See our article about the idea behind this: https://threedots.tech/post/things-to-know-about-dry/\n\ntype Post struct {\n\tID      string   `json:\"id\" bson:\"id\"`\n\tTitle   string   `json:\"title\" bson:\"title\"`\n\tContent string   `json:\"content\" bson:\"content\"`\n\tAuthor  string   `json:\"author\" bson:\"author\"`\n\tTags    []string `json:\"tags\" bson:\"tags\"`\n}\n\nfunc NewPost(id, title, content, author string) Post {\n\tpattern := regexp.MustCompile(\"#([a-zA-Z0-9]+)\")\n\tmatches := pattern.FindAllStringSubmatch(content, -1)\n\n\tvar tags []string\n\ttagsMap := map[string]struct{}{}\n\n\tfor _, tag := range matches {\n\t\ttagSlug := strings.ToLower(tag[1])\n\n\t\t_, ok := tagsMap[tagSlug]\n\t\tif ok {\n\t\t\tcontinue\n\t\t}\n\n\t\ttagsMap[tagSlug] = struct{}{}\n\t\ttags = append(tags, tagSlug)\n\t}\n\n\treturn Post{\n\t\tID:      id,\n\t\tTitle:   title,\n\t\tContent: content,\n\t\tAuthor:  author,\n\t\tTags:    tags,\n\t}\n}\n\ntype Feed struct {\n\tName  string `json:\"name\" bson:\"_id\"`\n\tPosts []Post `json:\"posts\" bson:\"posts\"`\n}\n\ntype PostCreated struct {\n\tPost Post `json:\"post\"`\n\n\tOccurredAt time.Time `json:\"occurred_at\"`\n}\n\ntype PostUpdated struct {\n\tOriginalPost Post `json:\"original_post\"`\n\tNewPost      Post `json:\"new_post\"`\n\n\tOccurredAt time.Time `json:\"occurred_at\"`\n}\n\ntype FeedUpdated struct {\n\tName string `json:\"name\"`\n\n\tOccurredAt time.Time `json:\"occurred_at\"`\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/posts_storage.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-sql-driver/mysql\"\n)\n\ntype PostsStorage struct {\n\tdb *sql.DB\n}\n\nfunc NewPostsStorage() PostsStorage {\n\tconf := mysql.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"example\"\n\n\tdb, err := sql.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\terr = db.Ping()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t} else {\n\t\t\tfmt.Println(\"Could not connect to MySQL, retrying...\")\n\t\t\ttime.Sleep(time.Second * 3)\n\t\t}\n\t}\n\n\treturn PostsStorage{\n\t\tdb: db,\n\t}\n}\n\nfunc (s PostsStorage) ByID(ctx context.Context, id string) (Post, error) {\n\tquery := \"SELECT title, content, author FROM posts WHERE id=?\"\n\trow := s.db.QueryRowContext(ctx, query, id)\n\n\tvar title, content, author string\n\terr := row.Scan(&title, &content, &author)\n\tif err != nil {\n\t\treturn Post{}, err\n\t}\n\n\treturn NewPost(id, title, content, author), nil\n}\n\nfunc (s PostsStorage) Add(ctx context.Context, post Post) error {\n\tquery := \"INSERT INTO posts (id, title, content, author) VALUES (?, ?, ?, ?)\"\n\t_, err := s.db.ExecContext(ctx, query, post.ID, post.Title, post.Content, post.Author)\n\treturn err\n}\n\nfunc (s PostsStorage) Update(ctx context.Context, post Post) error {\n\tquery := \"UPDATE posts SET title=?, content=?, author=? WHERE id=?\"\n\t_, err := s.db.ExecContext(ctx, query, post.Title, post.Content, post.Author, post.ID)\n\treturn err\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events/server/public/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <title>Watermill Server-Sent Events Example</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\" integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\" crossorigin=\"anonymous\">\n</head>\n<body>\n\n<style>\n    body {\n        padding-top: 5rem;\n    }\n    .example {\n        padding: 3rem 1.5rem;\n    }\n    #feeds li {\n        display: inline;\n    }\n    .breadcrumb-item + .breadcrumb-item::before {\n        content: \"|\";\n    }\n</style>\n\n<div id=\"app\">\n    <nav class=\"navbar navbar-expand-md navbar-dark bg-dark fixed-top\">\n        <a class=\"navbar-brand\" href=\"/\">Home</a>\n    </nav>\n\n    <main role=\"main\" class=\"container\">\n        <div class=\"example\">\n            <feeds-list></feeds-list>\n            <add-post></add-post>\n\n            <hr />\n\n            <router-view></router-view>\n        </div>\n\n    </main>\n</div>\n</body>\n\n<script src=\"https://code.jquery.com/jquery-3.4.1.slim.min.js\" integrity=\"sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js\" integrity=\"sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo\" crossorigin=\"anonymous\"></script>\n<script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js\" integrity=\"sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6\" crossorigin=\"anonymous\"></script>\n\n<script src=\"https://unpkg.com/vue@2.7.16/dist/vue.js\"></script>\n<script src=\"https://unpkg.com/vue-router@3.6.5/dist/vue-router.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/vue-resource@1.3.5\"></script>\n\n<script type=\"text/x-template\" id=\"feeds-list\">\n    <nav aria-label=\"breadcrumb\">\n        <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\" v-bind:class=\"{ active: feed.active }\" v-for=\"feed in feeds\">\n            <router-link v-if=\"!feed.active\" :to=\"{ name: 'show-feed', params: { feed_name: feed.name }}\">\n                {{ feed.text }}\n            </router-link>\n            <span v-else>\n                {{ feed.text }}\n            </span>\n        </li>\n    </ol>\n    </nav>\n</script>\n\n<script type=\"text/x-template\" id=\"add-post\">\n    <div>\n        <p>\n           <button class=\"btn btn-primary\" type=\"button\" v-on:click=\"toggle\">\n              Add new post\n           </button>\n\n            <button class=\"btn btn-info\" type=\"button\" v-on:click=\"generatePost\">Add randomly generated post</button>\n        </p>\n        <div id=\"addPostForm\" v-show=\"showForm\">\n            <div  class=\"card card-body\">\n                <h2>Add new post</h2>\n                <form v-on:submit.prevent=\"submitNewPost\">\n                    <div class=\"form-group row\">\n                        <label for=\"inputTitle\" class=\"col-sm-2 col-form-label\">Title</label>\n                        <div class=\"col-sm-10\">\n                            <input type=\"title\" class=\"form-control\" id=\"inputTitle\" placeholder=\"Title\" v-model=\"post.title\">\n                        </div>\n                    </div>\n                    <div class=\"form-group row\">\n                        <label for=\"inputAuthor\" class=\"col-sm-2 col-form-label\">Author</label>\n                        <div class=\"col-sm-10\">\n                            <input type=\"author\" class=\"form-control\" id=\"inputAuthor\" placeholder=\"Author\" v-model=\"post.author\">\n                        </div>\n                    </div>\n                    <div class=\"form-group row\">\n                        <label for=\"inputContent\" class=\"col-sm-2 col-form-label\">Content</label>\n                        <div class=\"col-sm-10\">\n                            <textarea class=\"form-control\" id=\"inputContent\" rows=\"3\" v-model=\"post.content\"></textarea>\n                        </div>\n                    </div>\n                    <div class=\"form-group row\">\n                        <div class=\"col-sm-2\"></div>\n                        <div class=\"col-sm-10\">\n                            <input type=\"submit\" class=\"btn btn-success\" value=\"Add post\" />\n                        </div>\n                    </div>\n                </form>\n            </div>\n        </div>\n    </div>\n</script>\n\n<script type=\"text/x-template\" id=\"home\">\n    <div>\n        <h3>1. Add some posts using the buttons above (use #hashtags).</h3>\n        <h3>2. Click one of feeds to see a live stream.</h3>\n    </div>\n</script>\n\n<script type=\"text/x-template\" id=\"feed-template\">\n    <div>\n        <h2>{{ feed }}</h2>\n        <div v-for=\"post in posts\">\n            <div class=\"card\">\n                <div class=\"card-body\">\n                    <h4 class=\"card-title\">\n                    {{post.title}}\n                        <router-link :to=\"{ name: 'edit-post', params: { post_id: post.id }}\"\n                                     class=\"btn btn-light class-right btn-sm\">Edit\n                        </router-link>\n                    </h4>\n                    <p class=\"card-text\" v-html=\"post.content\"></p>\n                    <p class=\"card-text\">\n                        <small class=\"text-muted\">Author: {{post.author}}</small>\n                    </p>\n                </div>\n            </div>\n            <br>\n        </div>\n    </div>\n</script>\n\n<script type=\"text/x-template\" id=\"post-edit-template\">\n    <div>\n        <h2>Edit post</h2>\n        <div class=\"card\">\n            <div class=\"card-body\">\n                <form v-on:submit.prevent=\"submitData\">\n                    <div class=\"form-group row\">\n                        <label for=\"inputTitle\" class=\"col-sm-2 col-form-label\">Title</label>\n                        <div class=\"col-sm-10\">\n                            <input type=\"title\" class=\"form-control\" id=\"inputTitle\" placeholder=\"Title\" v-model=\"post.title\">\n                        </div>\n                    </div>\n                    <div class=\"form-group row\">\n                        <label for=\"inputContent\" class=\"col-sm-2 col-form-label\">Content</label>\n                        <div class=\"col-sm-10\">\n                            <textarea class=\"form-control\" id=\"inputContent\" rows=\"3\" v-model=\"post.content\"></textarea>\n                        </div>\n                    </div>\n                    <div class=\"form-group row\">\n                        <div class=\"col-sm-2\"></div>\n                        <div class=\"col-sm-10\">\n                            <input type=\"submit\" class=\"btn btn-success\" value=\"Update post\" />\n                        </div>\n                    </div>\n                </form>\n            </div>\n        </div>\n    </div>\n</script>\n\n<script>\n    Vue.component('feeds-list', {\n        template: '#feeds-list',\n        data: function () {\n            return {\n                feeds_stream: [],\n                current_feed: \"\",\n            }\n        },\n        created() {\n            this.es = new EventSource('/api/feeds')\n\n            this.es.addEventListener('data', event => {\n                let data = JSON.parse(event.data);\n                this.feeds_stream = data.feeds;\n            }, false);\n        },\n        watch: {\n            $route(to, from) {\n                if (to.name === \"show-feed\") {\n                    this.current_feed = to.params.feed_name;\n                } else {\n                    this.current_feed = \"\";\n                }\n            }\n        },\n        computed: {\n            feeds: function (){\n                return this.feeds_stream.map(f => ({text: `${f.name} (${f.posts})`, name: f.name, active: f.name === this.current_feed}));\n            },\n        },\n    });\n\n    const Home = {\n        template: '#home',\n    }\n\n    Vue.component('add-post', {\n        template: '#add-post',\n        data: function () {\n            return {\n                post: {},\n                showForm: false,\n            }\n        },\n        methods: {\n            submitNewPost: function() {\n                postData = this.post;\n                this.$http.post('/api/posts', JSON.stringify(postData))\n                this.post = {};\n            },\n            generatePost: function() {\n                this.$http.post('/api/generate/post')\n            },\n            toggle: function () {\n                this.showForm = !this.showForm;\n            },\n        },\n        created() {},\n    });\n\n    const ShowFeed = {\n        template: '#feed-template',\n        data: function () {\n            return {\n                feed: \"\",\n                posts_stream: [],\n            }\n        },\n        watch: {\n            $route(to, from) {\n                console.log(to)\n                this.setupFeed(to.params.feed_name);\n            }\n        },\n        created() {\n            this.setupFeed(this.$route.params.feed_name);\n        },\n        destroyed() {\n            if (this.es) {\n                this.es.close();\n            }\n        },\n        methods: {\n            setupFeed: function(feed_name) {\n                this.feed = feed_name;\n\n                if (this.es) {\n                    this.es.close();\n                }\n                this.es = new EventSource('/api/feeds/' + this.feed)\n\n                this.es.addEventListener('data', event => {\n                    let data = JSON.parse(event.data);\n                    this.posts_stream = data.posts;\n                }, false);\n            },\n        },\n        computed: {\n            posts: function() {\n                var posts = this.posts_stream.slice();\n                for (var p of posts) {\n                    p.content = p.content.replace(/#([\\w]+)/g,'<a href=\"#/feed/$1\">#$1</a>')\n                }\n                return posts;\n            },\n        },\n    };\n    const EditPost = {\n        template: \"#post-edit-template\",\n        data: function () {\n            return {\n                post_id: this.$route.params.post_id,\n                post: {},\n            }\n        },\n        created() {\n            this.setupPost();\n        },\n        destroyed() {\n            if (this.es) {\n                console.log(\"closing\")\n                this.es.close();\n            }\n        },\n        methods: {\n            submitData: function () {\n                postData = this.post;\n                this.$http.patch('/api/posts/' + this.post_id, JSON.stringify(postData));\n                router.go(-1);\n            },\n            setupPost() {\n                this.es = new EventSource('/api/posts/' + this.post_id)\n\n                this.es.addEventListener('data', event => {\n                    let data = JSON.parse(event.data);\n                    this.post = data;\n                }, false);\n\n                this.es.addEventListener('error', function (e) {\n                    if (e.readyState == EventSource.CLOSED) {\n                        console.log(\"close\", e)\n                    } else {\n                        console.log(\"other error\", e)\n                    }\n                }, false);\n            },\n        },\n    };\n\n    const routes = [\n        {path: '/', component: Home, name: 'home'},\n        {path: '/edit-post/:post_id', component: EditPost, name: 'edit-post'},\n        {path: '/feed/:feed_name', component: ShowFeed, name: 'show-feed'},\n    ];\n\n    const router = new VueRouter({\n        routes\n    });\n\n    const app = new Vue({\n        router\n    }).$mount('#app')\n</script>\n</html>\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/Dockerfile",
    "content": "FROM golang:1.25 AS builder\n\nCOPY . /src\nWORKDIR /src/\n\nRUN CGO_ENABLED=0 go build -ldflags=\"-s -w\" -trimpath -o /main .\n\nFROM alpine\nRUN apk add --no-cache ca-certificates\nCOPY --from=builder /main /main\nCMD [\"/main\"]\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/README.md",
    "content": "# Server Sent Events (htmx)\n\nThis is an example project described in [Live website updates with Go, SSE, and htmx](https://threedots.tech/post/live-website-updates-go-sse-htmx/).\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/docker/Dockerfile",
    "content": "FROM golang:1.25\n\nRUN go install github.com/cespare/reflex@latest\nRUN go install github.com/a-h/templ/cmd/templ@latest\n\nCOPY reflex.conf /\n\nENTRYPOINT [\"/go/bin/reflex\", \"-c\", \"/reflex.conf\"]\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/docker/reflex.conf",
    "content": "-r '(\\.go$|go\\.mod$)' -s go run .\n-r '\\.templ$' templ generate\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/docker-compose.yml",
    "content": "services:\n  server:\n    build:\n      context: docker\n    volumes:\n      - ./:/src\n      - go_pkg:/go/pkg\n      - go_cache:/go-cache\n    working_dir: /src\n    ports:\n      - '8080:8080'\n    environment:\n      - PORT=8080\n      - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable\n      - PUBSUB_PROJECT_ID=local\n      - PUBSUB_EMULATOR_HOST=pubsub:8681\n    restart: unless-stopped\n    networks:\n      - sse\n\n  postgres:\n    image: postgres:15\n    restart: unless-stopped\n    environment:\n      - POSTGRES_PASSWORD=postgres\n    ports:\n      - 5432:5432\n    networks:\n      - sse\n\n  pubsub:\n    image: messagebird/gcloud-pubsub-emulator:latest\n    restart: unless-stopped\n    ports:\n      - '8681:8681'\n    networks:\n      - sse\n\nnetworks:\n  sse:\n\nvolumes:\n  go_pkg:\n  go_cache:\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/events.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"cloud.google.com/go/pubsub/v2/apiv1/pubsubpb\"\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-googlecloud/v2/pkg/googlecloud\"\n\t\"github.com/ThreeDotsLabs/watermill-http/v2/pkg/http\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"google.golang.org/protobuf/types/known/durationpb\"\n)\n\ntype PostViewed struct {\n\tPostID int `json:\"post_id\"`\n}\n\ntype PostReactionAdded struct {\n\tPostID     int    `json:\"post_id\"`\n\tReactionID string `json:\"reaction_id\"`\n}\n\ntype PostStatsUpdated struct {\n\tPostID          int            `json:\"post_id\"`\n\tViews           int            `json:\"views\"`\n\tViewsUpdated    bool           `json:\"views_updated\"`\n\tReactions       map[string]int `json:\"reactions\"`\n\tReactionUpdated *string        `json:\"reaction_updated\"`\n}\n\ntype Routers struct {\n\tEventsRouter *message.Router\n\tSSERouter    http.SSERouter\n\tEventBus     *cqrs.EventBus\n}\n\nfunc NewRouters(cfg config, repo *Repository) (Routers, error) {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tpublisher, err := googlecloud.NewPublisher(\n\t\tgooglecloud.PublisherConfig{\n\t\t\tProjectID: cfg.PubSubProjectID,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\teventBus, err := cqrs.NewEventBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.EventBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\t\treturn params.EventName, nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\tLogger:    logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\teventsRouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\teventsRouter.AddMiddleware(middleware.Recoverer)\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(\n\t\teventsRouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn params.EventName, nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn googlecloud.NewSubscriber(\n\t\t\t\t\tgooglecloud.SubscriberConfig{\n\t\t\t\t\t\tProjectID: cfg.PubSubProjectID,\n\t\t\t\t\t\tGenerateSubscriptionName: func(topic string) string {\n\t\t\t\t\t\t\treturn fmt.Sprintf(\"%v_%v\", topic, params.HandlerName)\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tlogger,\n\t\t\t\t)\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\tLogger:    logger,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\terr = eventProcessor.AddHandlers(\n\t\tcqrs.NewEventHandler(\n\t\t\t\"UpdateViews\",\n\t\t\tfunc(ctx context.Context, event *PostViewed) error {\n\t\t\t\tvar views int\n\t\t\t\tvar reactions map[string]int\n\t\t\t\terr = repo.UpdatePost(ctx, event.PostID, func(post *Post) {\n\t\t\t\t\tpost.Views++\n\t\t\t\t\tviews = post.Views\n\t\t\t\t\treactions = post.Reactions\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tstatsUpdated := PostStatsUpdated{\n\t\t\t\t\tPostID:       event.PostID,\n\t\t\t\t\tViewsUpdated: true,\n\t\t\t\t\tViews:        views,\n\t\t\t\t\tReactions:    reactions,\n\t\t\t\t}\n\n\t\t\t\treturn eventBus.Publish(ctx, statsUpdated)\n\t\t\t},\n\t\t),\n\t\tcqrs.NewEventHandler(\n\t\t\t\"UpdateReactions\",\n\t\t\tfunc(ctx context.Context, event *PostReactionAdded) error {\n\t\t\t\tvar views int\n\t\t\t\tvar reactions map[string]int\n\t\t\t\terr := repo.UpdatePost(ctx, event.PostID, func(post *Post) {\n\t\t\t\t\tpost.Reactions[event.ReactionID]++\n\t\t\t\t\tviews = post.Views\n\t\t\t\t\treactions = post.Reactions\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tstatsUpdated := PostStatsUpdated{\n\t\t\t\t\tPostID:          event.PostID,\n\t\t\t\t\tViews:           views,\n\t\t\t\t\tReactionUpdated: &event.ReactionID,\n\t\t\t\t\tReactions:       reactions,\n\t\t\t\t}\n\n\t\t\t\treturn eventBus.Publish(ctx, statsUpdated)\n\t\t\t},\n\t\t),\n\t)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\tsseSubscriber, err := googlecloud.NewSubscriber(\n\t\tgooglecloud.SubscriberConfig{\n\t\t\tProjectID: cfg.PubSubProjectID,\n\t\t\tGenerateSubscriptionName: func(topic string) string {\n\t\t\t\treturn fmt.Sprintf(\"%v_%v\", topic, watermill.NewShortUUID())\n\t\t\t},\n\t\t\tGenerateSubscription: func(params googlecloud.GenerateSubscriptionParams) *pubsubpb.Subscription {\n\t\t\t\treturn &pubsubpb.Subscription{\n\t\t\t\t\tExpirationPolicy: &pubsubpb.ExpirationPolicy{\n\t\t\t\t\t\tTtl: durationpb.New(time.Hour * 24),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\tsseRouter, err := http.NewSSERouter(\n\t\thttp.SSERouterConfig{\n\t\t\tUpstreamSubscriber: sseSubscriber,\n\t\t\tMarshaler:          http.StringSSEMarshaler{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\treturn Routers{}, err\n\t}\n\n\treturn Routers{\n\t\tEventsRouter: eventsRouter,\n\t\tSSERouter:    sseRouter,\n\t\tEventBus:     eventBus,\n\t}, nil\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/go.mod",
    "content": "module main\n\ngo 1.25\n\nrequire (\n\tcloud.google.com/go/pubsub/v2 v2.0.0\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0\n\tgithub.com/ThreeDotsLabs/watermill-http/v2 v2.3.1\n\tgithub.com/a-h/templ v0.3.943\n\tgithub.com/kelseyhightower/envconfig v1.4.0\n\tgithub.com/labstack/echo/v4 v4.13.4\n\tgithub.com/lib/pq v1.10.9\n\tgoogle.golang.org/protobuf v1.36.8\n)\n\nrequire (\n\tcloud.google.com/go v0.121.6 // indirect\n\tcloud.google.com/go/auth v0.16.5 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.8.0 // indirect\n\tcloud.google.com/go/iam v1.5.2 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-chi/chi v4.1.2+incompatible // indirect\n\tgithub.com/go-chi/render v1.0.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/labstack/gommon v0.4.2 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasttemplate v1.2.2 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/api v0.248.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/grpc v1.75.0 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=\ncloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=\ncloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=\ncloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=\ncloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=\ncloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=\ncloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=\ncloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=\ncloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0 h1:GXR+tsxPs/Vpmm0t4yEJUZdqLP9EytWvR+KN3Un5mNY=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0/go.mod h1:3IHyi1bNqQ8J2/wVWj4cQjzWXoEPauLm8ViyOCNaKbM=\ngithub.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ=\ngithub.com/ThreeDotsLabs/watermill-http/v2 v2.3.1/go.mod h1:RwGHEzGsEEXC/rQNLWQqR83+WPlABgOgnv2kTB56Y4Y=\ngithub.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=\ngithub.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=\ngithub.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=\ngithub.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=\ngithub.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=\ngithub.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=\ngithub.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=\ngithub.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=\ngo.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=\ngoogle.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=\ngoogle.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/http.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"main/views\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\twatermillhttp \"github.com/ThreeDotsLabs/watermill-http/v2/pkg/http\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/echo/v4/middleware\"\n)\n\ntype Handler struct {\n\trepo     *Repository\n\teventBus *cqrs.EventBus\n}\n\nfunc NewHandler(repo *Repository, eventBus *cqrs.EventBus, sseRouter watermillhttp.SSERouter) *echo.Echo {\n\th := Handler{\n\t\trepo:     repo,\n\t\teventBus: eventBus,\n\t}\n\n\tmarshaler := cqrs.JSONMarshaler{}\n\ttopic := marshaler.Name(PostStatsUpdated{})\n\tstatsHandler := sseRouter.AddHandler(topic, &statsStream{repo: repo})\n\n\te := echo.New()\n\te.Use(middleware.Recover())\n\te.Use(middleware.Logger())\n\n\tcounter := sseHandlersCounter{}\n\n\te.GET(\"/\", h.Index)\n\te.GET(\"/posts\", h.Posts)\n\te.GET(\"/idle\", h.Idle)\n\te.POST(\"/posts/:id/reactions\", h.AddReaction)\n\te.GET(\"/posts/:id/stats\", func(c echo.Context) error {\n\t\tpostID := c.Param(\"id\")\n\t\tc.Request().SetPathValue(\"id\", postID)\n\n\t\tstatsHandler(c.Response(), c.Request())\n\t\treturn nil\n\t}, counter.Middleware)\n\n\tgo func() {\n\t\tfor {\n\t\t\tfmt.Println(\"SSE handlers count:\", counter.Count.Load())\n\t\t\ttime.Sleep(60 * time.Second)\n\t\t}\n\t}()\n\n\treturn e\n}\n\nfunc (h Handler) Index(c echo.Context) error {\n\tposts, err := h.allPosts(c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn views.Index(posts).Render(c.Request().Context(), c.Response())\n}\n\nfunc (h Handler) Posts(c echo.Context) error {\n\tposts, err := h.allPosts(c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn views.Posts(posts).Render(c.Request().Context(), c.Response())\n}\n\nfunc (h Handler) Idle(c echo.Context) error {\n\treturn views.Idle().Render(c.Request().Context(), c.Response())\n}\n\nfunc (h Handler) allPosts(c echo.Context) ([]views.Post, error) {\n\tposts, err := h.repo.AllPosts(c.Request().Context())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, post := range posts {\n\t\tevent := PostViewed{\n\t\t\tPostID: post.ID,\n\t\t}\n\n\t\terr = h.eventBus.Publish(c.Request().Context(), event)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar postViews []views.Post\n\tfor _, post := range posts {\n\t\tpostViews = append(postViews, newPostView(post))\n\t}\n\n\treturn postViews, nil\n}\n\nfunc (h Handler) AddReaction(c echo.Context) error {\n\tpostID, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treactionID := c.FormValue(\"reaction_id\")\n\n\tvar found bool\n\tfor _, r := range allReactions {\n\t\tif r.ID == reactionID {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn c.String(http.StatusBadRequest, \"invalid reaction ID\")\n\t}\n\n\tevent := PostReactionAdded{\n\t\tPostID:     postID,\n\t\tReactionID: reactionID,\n\t}\n\n\terr = h.eventBus.Publish(c.Request().Context(), event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treaction := mustReactionByID(reactionID)\n\n\treturn views.UpdatedButton(reaction.Label).Render(c.Request().Context(), c.Response())\n}\n\ntype statsStream struct {\n\trepo *Repository\n}\n\nfunc (s *statsStream) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {\n\tpostIDStr := r.PathValue(\"id\")\n\tpostID, err := strconv.Atoi(postIDStr)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tw.Write([]byte(\"invalid post ID\"))\n\t\treturn nil, false\n\t}\n\n\tpost, err := s.repo.PostByID(r.Context(), postID)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tw.Write([]byte(\"could not get post\"))\n\t\treturn nil, false\n\t}\n\n\tstats := PostStats{\n\t\tID:        post.ID,\n\t\tViews:     post.Views,\n\t\tReactions: post.Reactions,\n\t}\n\n\tresp, err := newPostStatsView(r.Context(), stats)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tw.Write([]byte(err.Error()))\n\t\treturn nil, false\n\t}\n\n\treturn resp, true\n}\n\nfunc (s *statsStream) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {\n\tpostIDStr := r.PathValue(\"id\")\n\tpostID, err := strconv.Atoi(postIDStr)\n\tif err != nil {\n\t\tfmt.Println(\"invalid post ID\")\n\t\treturn nil, false\n\t}\n\n\tvar event PostStatsUpdated\n\terr = json.Unmarshal(msg.Payload, &event)\n\tif err != nil {\n\t\tfmt.Println(\"cannot unmarshal: \" + err.Error())\n\t\treturn \"\", false\n\t}\n\n\tif event.PostID != postID {\n\t\treturn \"\", false\n\t}\n\n\tstats := PostStats{\n\t\tID:              event.PostID,\n\t\tViews:           event.Views,\n\t\tViewsUpdated:    event.ViewsUpdated,\n\t\tReactions:       event.Reactions,\n\t\tReactionUpdated: event.ReactionUpdated,\n\t}\n\n\tresp, err := newPostStatsView(r.Context(), stats)\n\tif err != nil {\n\t\tfmt.Println(\"could not get response: \" + err.Error())\n\t\treturn nil, false\n\t}\n\n\treturn resp, true\n}\n\nfunc newPostStatsView(ctx context.Context, stats PostStats) (interface{}, error) {\n\tvar reactions []views.Reaction\n\n\tfor _, r := range allReactions {\n\t\treactions = append(reactions, views.Reaction{\n\t\t\tID:          r.ID,\n\t\t\tLabel:       r.Label,\n\t\t\tCount:       fmt.Sprint(stats.Reactions[r.ID]),\n\t\t\tJustChanged: stats.ReactionUpdated != nil && *stats.ReactionUpdated == r.ID,\n\t\t})\n\t}\n\n\tview := views.PostStats{\n\t\tPostID: fmt.Sprint(stats.ID),\n\t\tViews: views.PostViews{\n\t\t\tCount:       fmt.Sprint(stats.Views),\n\t\t\tJustChanged: stats.ViewsUpdated,\n\t\t},\n\t\tReactions: reactions,\n\t}\n\n\tvar buffer bytes.Buffer\n\terr := views.PostStatsView(view).Render(ctx, &buffer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buffer.String(), nil\n}\n\nfunc newPostView(p Post) views.Post {\n\treturn views.Post{\n\t\tID:      fmt.Sprint(p.ID),\n\t\tContent: p.Content,\n\t\tAuthor:  p.Author,\n\t\tDate:    p.CreatedAt.Format(\"02 Jan 2006 15:04\"),\n\t}\n}\n\ntype sseHandlersCounter struct {\n\tCount atomic.Int64\n}\n\nfunc (s *sseHandlersCounter) Middleware(next echo.HandlerFunc) echo.HandlerFunc {\n\treturn func(c echo.Context) error {\n\t\ts.Count.Add(1)\n\t\tdefer s.Count.Add(-1)\n\t\treturn next(c)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/kelseyhightower/envconfig\"\n\t_ \"github.com/lib/pq\"\n)\n\ntype config struct {\n\tPort            int    `envconfig:\"PORT\" required:\"true\"`\n\tDatabaseURL     string `envconfig:\"DATABASE_URL\" required:\"true\"`\n\tPubSubProjectID string `envconfig:\"PUBSUB_PROJECT_ID\" required:\"true\"`\n}\n\nfunc main() {\n\tvar cfg config\n\terr := envconfig.Process(\"\", &cfg)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdb, err := sql.Open(\"postgres\", cfg.DatabaseURL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = MigrateDB(db)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trepo := NewRepository(db)\n\n\trouters, err := NewRouters(cfg, repo)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tgo func() {\n\t\terr := routers.EventsRouter.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\terr := routers.SSERouter.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t// This goroutine simulates some events being published in the background\n\t\tctx := context.Background()\n\t\tfor {\n\t\t\tpostID := 1 + rand.Intn(2)\n\t\t\tif rand.Intn(2) == 0 {\n\t\t\t\t_ = routers.EventBus.Publish(ctx, PostViewed{\n\t\t\t\t\tPostID: postID,\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\t_ = routers.EventBus.Publish(ctx, PostReactionAdded{\n\t\t\t\t\tPostID:     postID,\n\t\t\t\t\tReactionID: allReactions[rand.Intn(len(allReactions))].ID,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\ttime.Sleep(time.Millisecond * time.Duration(3000+rand.Intn(5000)))\n\t\t}\n\t}()\n\n\thandler := NewHandler(repo, routers.EventBus, routers.SSERouter)\n\n\terr = handler.Start(fmt.Sprintf(\":%d\", cfg.Port))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/models.go",
    "content": "package main\n\nimport \"time\"\n\nvar allReactions = []Reaction{\n\t{\n\t\tID:    \"fire\",\n\t\tLabel: \"🔥\",\n\t},\n\t{\n\t\tID:    \"thinking\",\n\t\tLabel: \"🤔\",\n\t},\n\t{\n\t\tID:    \"heart\",\n\t\tLabel: \"🩵\",\n\t},\n\t{\n\t\tID:    \"laugh\",\n\t\tLabel: \"😂\",\n\t},\n\t{\n\t\tID:    \"sad\",\n\t\tLabel: \"😢\",\n\t},\n}\n\nfunc mustReactionByID(id string) Reaction {\n\tfor _, r := range allReactions {\n\t\tif r.ID == id {\n\t\t\treturn r\n\t\t}\n\t}\n\n\tpanic(\"reaction not found\")\n}\n\ntype Reaction struct {\n\tID    string\n\tLabel string\n}\n\ntype Post struct {\n\tID        int\n\tAuthor    string\n\tContent   string\n\tCreatedAt time.Time\n\tViews     int\n\tReactions map[string]int\n}\n\ntype PostStats struct {\n\tID              int\n\tViews           int\n\tViewsUpdated    bool\n\tReactions       map[string]int\n\tReactionUpdated *string\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/repository.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"time\"\n)\n\nconst migration = `\nCREATE TABLE IF NOT EXISTS posts (\n\tid serial PRIMARY KEY,\n\tauthor VARCHAR NOT NULL,\n\tcontent TEXT NOT NULL,\n\tviews INT NOT NULL DEFAULT 0,\n\treactions JSONB NOT NULL DEFAULT '{}',\n\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nINSERT INTO posts (id, author, content) VALUES\n\t(1, 'Miłosz', 'Oh, I remember the days when we used to write code in PHP!'),\n\t(2, 'Robert', 'Back in my days, we used to write code in assembly!')\nON CONFLICT (id) DO NOTHING;\n`\n\nfunc MigrateDB(db *sql.DB) error {\n\t_, err := db.Exec(migration)\n\treturn err\n}\n\ntype Repository struct {\n\tdb *sql.DB\n}\n\nfunc NewRepository(db *sql.DB) *Repository {\n\treturn &Repository{\n\t\tdb: db,\n\t}\n}\n\nfunc (s *Repository) PostByID(ctx context.Context, id int) (Post, error) {\n\trow := s.db.QueryRowContext(ctx, `SELECT id, author, content, views, reactions, created_at FROM posts WHERE id = $1`, id)\n\tpost, err := scanPost(row)\n\tif err != nil {\n\t\treturn Post{}, err\n\t}\n\n\treturn post, nil\n}\n\nfunc (s *Repository) AllPosts(ctx context.Context) ([]Post, error) {\n\trows, err := s.db.QueryContext(ctx, `SELECT id, author, content, views, reactions, created_at FROM posts ORDER BY id ASC`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar posts []Post\n\tfor rows.Next() {\n\t\tpost, err := scanPost(rows)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tposts = append(posts, post)\n\t}\n\n\treturn posts, nil\n}\n\nfunc (s *Repository) UpdatePost(ctx context.Context, id int, updateFn func(post *Post)) (err error) {\n\ttx, err := s.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tif err == nil {\n\t\t\terr = tx.Commit()\n\t\t} else {\n\t\t\ttxErr := tx.Rollback()\n\t\t\tif txErr != nil {\n\t\t\t\terr = txErr\n\t\t\t}\n\t\t}\n\t}()\n\n\trow := s.db.QueryRowContext(ctx, `SELECT id, author, content, views, reactions, created_at FROM posts WHERE id = $1 FOR UPDATE`, id)\n\tpost, err := scanPost(row)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tupdateFn(&post)\n\n\treactionsJSON, err := json.Marshal(post.Reactions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = tx.ExecContext(ctx, `UPDATE posts SET views = $1, reactions = $2 WHERE id = $3`, post.Views, reactionsJSON, post.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype scanner interface {\n\tScan(dest ...any) error\n}\n\nfunc scanPost(s scanner) (Post, error) {\n\tvar id, postViews int\n\tvar author, content string\n\tvar reactions []byte\n\tvar createdAt time.Time\n\n\terr := s.Scan(&id, &author, &content, &postViews, &reactions, &createdAt)\n\tif err != nil {\n\t\treturn Post{}, err\n\t}\n\n\tvar reactionsMap map[string]int\n\terr = json.Unmarshal(reactions, &reactionsMap)\n\tif err != nil {\n\t\treturn Post{}, err\n\t}\n\n\treturn Post{\n\t\tID:        id,\n\t\tAuthor:    author,\n\t\tContent:   content,\n\t\tCreatedAt: createdAt,\n\t\tViews:     postViews,\n\t\tReactions: reactionsMap,\n\t}, nil\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/views/base.templ",
    "content": "package views\n\ntempl base() {\n    <!doctype html>\n    <html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <title>Server Sent Events</title>\n        <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH\" crossorigin=\"anonymous\" />\n\n        <style>\n          @keyframes reaction-animation {\n                0% {\n                  transform: translateY(0);\n                }\n                25% {\n                  transform: translateY(-5px);\n                }\n                50% {\n                  transform: translateY(0);\n                }\n                100% {\n                  transform: translateY(0);\n                }\n          }\n\n          .animated {\n            animation: reaction-animation 0.5s;\n          }\n\n          .reaction-buttons form {\n            display: inline;\n          }\n\n          .reaction-buttons button {\n            font-size: 0.8rem !important;\n          }\n        </style>\n    </head>\n    <body>\n        <div class=\"container p-4\">\n            { children... }\n        </div>\n        <script src=\"https://unpkg.com/htmx.org@1.9.11\"></script>\n        <script src=\"https://unpkg.com/htmx.org@1.9.11/dist/ext/sse.js\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz\" crossorigin=\"anonymous\"></script>\n    </body>\n    </html>\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/views/base_templ.go",
    "content": "// Code generated by templ - DO NOT EDIT.\n\n// templ: version: v0.2.663\npackage views\n\n//lint:file-ignore SA4006 This context is only used if a nested component is present.\n\nimport \"github.com/a-h/templ\"\nimport \"context\"\nimport \"io\"\nimport \"bytes\"\n\nfunc base() templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var1 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var1 == nil {\n\t\t\ttempl_7745c5c3_Var1 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<!doctype html><html lang=\\\"en\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\"><title>Server Sent Events</title><link href=\\\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\\\" rel=\\\"stylesheet\\\" integrity=\\\"sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH\\\" crossorigin=\\\"anonymous\\\"><style>\\n          @keyframes reaction-animation {\\n                0% {\\n                  transform: translateY(0);\\n                }\\n                25% {\\n                  transform: translateY(-5px);\\n                }\\n                50% {\\n                  transform: translateY(0);\\n                }\\n                100% {\\n                  transform: translateY(0);\\n                }\\n          }\\n\\n          .animated {\\n            animation: reaction-animation 0.5s;\\n          }\\n\\n          .reaction-buttons form {\\n            display: inline;\\n          }\\n\\n          .reaction-buttons button {\\n            font-size: 0.8rem !important;\\n          }\\n        </style></head><body><div class=\\\"container p-4\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\ttempl_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</div><script src=\\\"https://unpkg.com/htmx.org@1.9.11\\\"></script><script src=\\\"https://unpkg.com/htmx.org@1.9.11/dist/ext/sse.js\\\"></script><script src=\\\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\\\" integrity=\\\"sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz\\\" crossorigin=\\\"anonymous\\\"></script></body></html>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/views/pages.templ",
    "content": "package views\n\ntype Post struct {\n    ID      string\n    Content string\n    Author  string\n    Date    string\n}\n\ntype Reaction struct {\n    ID          string\n    Label       string\n    Count       string\n    JustChanged bool\n}\n\ntype PostStats struct {\n    PostID    string\n    Views     PostViews\n    Reactions []Reaction\n}\n\ntype PostViews struct {\n    Count       string\n    JustChanged bool\n}\n\ntempl Index(posts []Post) {\n    @base() {\n        @Posts(posts)\n    }\n}\n\ntempl Posts(posts []Post) {\n    <div class=\"row\" id=\"main\">\n        for _, p := range posts {\n            @postView(p)\n        }\n\n        <div hx-get=\"/idle\" hx-trigger=\"load delay:600s\" hx-target=\"#main\" hx-swap=\"outerHTML\" ></div>\n    </div>\n}\n\ntempl Idle() {\n    <div class=\"row\" id=\"main\">\n        <div class=\"card mb-3\">\n            <div class=\"card-body\">\n                <h5 class=\"card-title mb-2\">Paused to save resources. 🫢</h5>\n\n                <h5 class=\"card-title mb-1\">Still around?</h5>\n\n                <button\n                    class=\"btn btn-outline-secondary m-1\"\n                    hx-get=\"/posts\"\n                    hx-target=\"#main\"\n                    hx-swap=\"outerHTML\"\n                >\n                    Reload\n                </button>\n            </div>\n        </div>\n    </div>\n}\n\ntempl postView(post Post) {\n    <div class=\"card mb-3\">\n      <div class=\"card-body\">\n        <div class=\"d-flex justify-content-between align-items-center mb-2\">\n          <div>\n            <h5 class=\"card-title mb-0\">{ post.Author }</h5>\n            <div class=\"d-flex align-items-center\">\n              <small class=\"text-muted me-2\">{ post.Date }</small>\n            </div>\n          </div>\n        </div>\n        <p class=\"card-text\">{ post.Content }</p>\n        <div hx-ext=\"sse\" sse-connect={ \"/posts/\" + post.ID + \"/stats\" } sse-swap=\"data\"></div>\n      </div>\n    </div>\n}\n\ntempl PostStatsView(stats PostStats) {\n    <div class={ \"d-flex\", \"align-items-center\", templ.KV(\"animated\", stats.Views.JustChanged)}>\n        <span class=\"me-1\">👁️</span>\n        <small class=\"text-muted\">{ stats.Views.Count + \" views\" }</small>\n    </div>\n\n    <div class=\"mt-2 d-flex justify-content-start align-items-center\">\n        <div class=\"reaction-buttons\">\n            for _, r := range stats.Reactions {\n                @reactionButton(stats.PostID, r)\n            }\n        </div>\n    </div>\n}\n\ntempl reactionButton(postID string, reaction Reaction) {\n    <form hx-post={ \"/posts/\" + postID + \"/reactions\"} hx-swap=\"outerHTML\">\n        <input type=\"hidden\" name=\"reaction_id\" value={ reaction.ID } />\n        <button type=\"submit\" class={\"btn\", \"btn-outline-secondary\", \"m-1\", templ.KV(\"animated\", reaction.JustChanged)}>\n          <span class=\"emoji\">{ reaction.Label }</span>\n          <span class=\"counter\">{ reaction.Count }</span>\n        </button>\n    </form>\n}\n\ntempl UpdatedButton(label string) {\n    <button class={\"btn\", \"btn-outline-secondary\", \"m-1\" } disabled>\n      <span class=\"emoji\">{ label }</span>\n      <span class=\"counter\">✅</span>\n    </button>\n}\n"
  },
  {
    "path": "_examples/real-world-examples/server-sent-events-htmx/views/pages_templ.go",
    "content": "// Code generated by templ - DO NOT EDIT.\n\n// templ: version: v0.2.663\npackage views\n\n//lint:file-ignore SA4006 This context is only used if a nested component is present.\n\nimport \"github.com/a-h/templ\"\nimport \"context\"\nimport \"io\"\nimport \"bytes\"\n\ntype Post struct {\n\tID      string\n\tContent string\n\tAuthor  string\n\tDate    string\n}\n\ntype Reaction struct {\n\tID          string\n\tLabel       string\n\tCount       string\n\tJustChanged bool\n}\n\ntype PostStats struct {\n\tPostID    string\n\tViews     PostViews\n\tReactions []Reaction\n}\n\ntype PostViews struct {\n\tCount       string\n\tJustChanged bool\n}\n\nfunc Index(posts []Post) templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var1 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var1 == nil {\n\t\t\ttempl_7745c5c3_Var1 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\ttempl_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t\t}\n\t\t\ttempl_7745c5c3_Err = Posts(posts).Render(ctx, templ_7745c5c3_Buffer)\n\t\t\tif templ_7745c5c3_Err != nil {\n\t\t\t\treturn templ_7745c5c3_Err\n\t\t\t}\n\t\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t\t_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)\n\t\t\t}\n\t\t\treturn templ_7745c5c3_Err\n\t\t})\n\t\ttempl_7745c5c3_Err = base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n\nfunc Posts(posts []Post) templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var3 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var3 == nil {\n\t\t\ttempl_7745c5c3_Var3 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<div class=\\\"row\\\" id=\\\"main\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tfor _, p := range posts {\n\t\t\ttempl_7745c5c3_Err = postView(p).Render(ctx, templ_7745c5c3_Buffer)\n\t\t\tif templ_7745c5c3_Err != nil {\n\t\t\t\treturn templ_7745c5c3_Err\n\t\t\t}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<div hx-get=\\\"/idle\\\" hx-trigger=\\\"load delay:600s\\\" hx-target=\\\"#main\\\" hx-swap=\\\"outerHTML\\\"></div></div>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n\nfunc Idle() templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var4 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var4 == nil {\n\t\t\ttempl_7745c5c3_Var4 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<div class=\\\"row\\\" id=\\\"main\\\"><div class=\\\"card mb-3\\\"><div class=\\\"card-body\\\"><h5 class=\\\"card-title mb-2\\\">Paused to save resources. 🫢</h5><h5 class=\\\"card-title mb-1\\\">Still around?</h5><button class=\\\"btn btn-outline-secondary m-1\\\" hx-get=\\\"/posts\\\" hx-target=\\\"#main\\\" hx-swap=\\\"outerHTML\\\">Reload</button></div></div></div>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n\nfunc postView(post Post) templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var5 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var5 == nil {\n\t\t\ttempl_7745c5c3_Var5 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<div class=\\\"card mb-3\\\"><div class=\\\"card-body\\\"><div class=\\\"d-flex justify-content-between align-items-center mb-2\\\"><div><h5 class=\\\"card-title mb-0\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var6 string\n\t\ttempl_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 70, Col: 53}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</h5><div class=\\\"d-flex align-items-center\\\"><small class=\\\"text-muted me-2\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var7 string\n\t\ttempl_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(post.Date)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 72, Col: 56}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</small></div></div></div><p class=\\\"card-text\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var8 string\n\t\ttempl_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(post.Content)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 76, Col: 43}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</p><div hx-ext=\\\"sse\\\" sse-connect=\\\"\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var9 string\n\t\ttempl_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(\"/posts/\" + post.ID + \"/stats\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 77, Col: 70}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"\\\" sse-swap=\\\"data\\\"></div></div></div>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n\nfunc PostStatsView(stats PostStats) templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var10 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var10 == nil {\n\t\t\ttempl_7745c5c3_Var10 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\tvar templ_7745c5c3_Var11 = []any{\"d-flex\", \"align-items-center\", templ.KV(\"animated\", stats.Views.JustChanged)}\n\t\ttempl_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<div class=\\\"\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var12 string\n\t\ttempl_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"\\\"><span class=\\\"me-1\\\">👁️</span> <small class=\\\"text-muted\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var13 string\n\t\ttempl_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(stats.Views.Count + \" views\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 85, Col: 64}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</small></div><div class=\\\"mt-2 d-flex justify-content-start align-items-center\\\"><div class=\\\"reaction-buttons\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tfor _, r := range stats.Reactions {\n\t\t\ttempl_7745c5c3_Err = reactionButton(stats.PostID, r).Render(ctx, templ_7745c5c3_Buffer)\n\t\t\tif templ_7745c5c3_Err != nil {\n\t\t\t\treturn templ_7745c5c3_Err\n\t\t\t}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</div></div>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n\nfunc reactionButton(postID string, reaction Reaction) templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var14 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var14 == nil {\n\t\t\ttempl_7745c5c3_Var14 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<form hx-post=\\\"\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var15 string\n\t\ttempl_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(\"/posts/\" + postID + \"/reactions\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 98, Col: 53}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"\\\" hx-swap=\\\"outerHTML\\\"><input type=\\\"hidden\\\" name=\\\"reaction_id\\\" value=\\\"\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var16 string\n\t\ttempl_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(reaction.ID)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 99, Col: 67}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"\\\"> \")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var17 = []any{\"btn\", \"btn-outline-secondary\", \"m-1\", templ.KV(\"animated\", reaction.JustChanged)}\n\t\ttempl_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<button type=\\\"submit\\\" class=\\\"\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var18 string\n\t\ttempl_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"\\\"><span class=\\\"emoji\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var19 string\n\t\ttempl_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(reaction.Label)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 101, Col: 46}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</span> <span class=\\\"counter\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var20 string\n\t\ttempl_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(reaction.Count)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 102, Col: 48}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</span></button></form>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n\nfunc UpdatedButton(label string) templ.Component {\n\treturn templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {\n\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\ttempl_7745c5c3_Buffer = templ.GetBuffer()\n\t\t\tdefer templ.ReleaseBuffer(templ_7745c5c3_Buffer)\n\t\t}\n\t\tctx = templ.InitializeContext(ctx)\n\t\ttempl_7745c5c3_Var21 := templ.GetChildren(ctx)\n\t\tif templ_7745c5c3_Var21 == nil {\n\t\t\ttempl_7745c5c3_Var21 = templ.NopComponent\n\t\t}\n\t\tctx = templ.ClearChildren(ctx)\n\t\tvar templ_7745c5c3_Var22 = []any{\"btn\", \"btn-outline-secondary\", \"m-1\"}\n\t\ttempl_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"<button class=\\\"\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var23 string\n\t\ttempl_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String())\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"\\\" disabled><span class=\\\"emoji\\\">\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tvar templ_7745c5c3_Var24 string\n\t\ttempl_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(label)\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 109, Col: 33}\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"</span> <span class=\\\"counter\\\">✅</span></button>\")\n\t\tif templ_7745c5c3_Err != nil {\n\t\t\treturn templ_7745c5c3_Err\n\t\t}\n\t\tif !templ_7745c5c3_IsBuffer {\n\t\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)\n\t\t}\n\t\treturn templ_7745c5c3_Err\n\t})\n}\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"received user:\"\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/README.md",
    "content": "# Synchronizing Databases (MySQL to PostgreSQL)\n\nThis example shows how to use [SQL Pub/Sub](https://github.com/ThreeDotsLabs/watermill-sql) across two different databases.\n\nSee also [SQL Pub/Sub documentation](https://watermill.io/pubsubs/sql).\n\n## Background\n\nSynchronizing two databases can be a tough task, especially with different data formats.\nThis example shows how to migrate a MySQL table to PostgreSQL table using watermill.\n\nThe application will first transfer all existing rows and then keep listening for any new inserts,\ncopying them to the new table as soon, as they appear. Only new rows will be detected, there's no\nsupport for updates or deletes.\n\nThe `main.go` file contains watermill-related setup, database connections and the handler translating events\nfrom one format to another. In `mysql.go` and `postgres.go` you will find definitions of `SchemaAdapters` for \neach database.\n\nFor more detailed description, see [When an SQL database makes a great Pub/Sub](https://threedots.tech/post/when-sql-database-makes-great-pub-sub/).\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See installation guide at https://docs.docker.com/compose/install/\n\n## Running\n\nRun the command and observe standard output. It should print out incoming users.\n\n```bash\ndocker-compose up\n```\n\nCheck what's inside MySQL by running:\n\n```\ndocker-compose exec mysql mysql -e 'select * from watermill.users;'\n```\n\n```\n+----+------------------+------------+-----------+---------------------+\n| id | user             | first_name | last_name | created_at          |\n+----+------------------+------------+-----------+---------------------+\n|  1 | Carroll8506      | Marc       | Murphy    | 2019-09-28 13:51:53 |\n|  2 | Metz8415         | Briana     | Bauch     | 2019-09-28 13:51:54 |\n|  3 | Lebsack6887      | Tomasa     | Steuber   | 2019-09-28 13:51:55 |\n|  4 | Hauck4518        | Alexandra  | Halvorson | 2019-09-28 13:51:56 |\n|  5 | Reynolds7156     | Ariane     | Lebsack   | 2019-09-28 13:51:57 |\n+----+------------------+------------+-----------+---------------------+\n```\n\nAnd the same for PostgreSQL:\n\n```\ndocker-compose exec postgres psql -U watermill -d watermill -c 'select * from users;'\n```\n\n```\n id  |     username     |      full_name       |     created_at\n-----+------------------+----------------------+---------------------\n   1 | Carroll8506      | Marc Murphy          | 2019-09-28 13:51:53\n   2 | Metz8415         | Briana Bauch         | 2019-09-28 13:51:54\n   3 | Lebsack6887      | Tomasa Steuber       | 2019-09-28 13:51:55\n   4 | Hauck4518        | Alexandra Halvorson  | 2019-09-28 13:51:56\n   5 | Reynolds7156     | Ariane Lebsack       | 2019-09-28 13:51:57\n```\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - mysql\n      - postgres\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run .\n\n  mysql:\n    image: mysql:8.0\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_DATABASE: watermill\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n\n  postgres:\n    image: postgres:11\n    restart: unless-stopped\n    ports:\n      - 5432:5432\n    environment:\n      POSTGRES_USER: watermill\n      POSTGRES_DB: watermill\n      POSTGRES_PASSWORD: \"password\"\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/brianvoe/gofakeit/v6 v6.28.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/lib/pq v1.10.9\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/brianvoe/gofakeit/v6\"\n\tdriver \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/lib/pq\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tlogger        = watermill.NewStdLogger(false, false)\n\tpostgresTable = \"users\"\n\tmysqlTable    = \"users\"\n)\n\nfunc main() {\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddPlugin(plugin.SignalsHandler)\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\tmysqlDB := createMySQLConnection()\n\tpostgresDB := createPostgresConnection()\n\n\tsubscriber := createSubscriber(mysqlDB)\n\tpublisher := createPublisher(postgresDB)\n\n\tgo simulateEvents(mysqlDB)\n\n\trouter.AddHandler(\n\t\t\"mysql-to-postgres\",\n\t\tmysqlTable,\n\t\tsubscriber,\n\t\tpostgresTable,\n\t\tpublisher,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\toriginUser := mysqlUser{}\n\n\t\t\tdecoder := gob.NewDecoder(bytes.NewBuffer(msg.Payload))\n\t\t\terr := decoder.Decode(&originUser)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tlog.Printf(\"received user: %+v\", originUser)\n\n\t\t\tnewUser := postgresUser{\n\t\t\t\tID:        originUser.ID,\n\t\t\t\tUsername:  originUser.User,\n\t\t\t\tFullName:  fmt.Sprintf(\"%s %s\", originUser.FirstName, originUser.LastName),\n\t\t\t\tCreatedAt: originUser.CreatedAt,\n\t\t\t}\n\n\t\t\tvar payload bytes.Buffer\n\t\t\tencoder := gob.NewEncoder(&payload)\n\t\t\terr = encoder.Encode(newUser)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnewMessage := message.NewMessage(watermill.NewULID(), payload.Bytes())\n\n\t\t\treturn []*message.Message{newMessage}, nil\n\t\t},\n\t)\n\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc createMySQLConnection() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"watermill\"\n\tconf.ParseTime = true\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n\nfunc createPostgresConnection() *stdSQL.DB {\n\tdsn := \"postgres://watermill:password@postgres/watermill?sslmode=disable\"\n\tdb, err := stdSQL.Open(\"postgres\", dsn)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n\nfunc createSubscriber(db *stdSQL.DB) message.Subscriber {\n\tsub, err := sql.NewSubscriber(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.SubscriberConfig{\n\t\t\tSchemaAdapter:    mysqlSchemaAdapter{},\n\t\t\tOffsetsAdapter:   sql.DefaultMySQLOffsetsAdapter{},\n\t\t\tInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn sub\n}\n\nfunc createPublisher(db *stdSQL.DB) message.Publisher {\n\tpub, err := sql.NewPublisher(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter:        postgresSchemaAdapter{},\n\t\t\tAutoInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn pub\n}\n\nfunc simulateEvents(db *stdSQL.DB) {\n\tpub, err := sql.NewPublisher(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter:        mysqlSchemaAdapter{},\n\t\t\tAutoInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\tuser := mysqlUser{\n\t\t\tUser:      gofakeit.Username(),\n\t\t\tFirstName: gofakeit.FirstName(),\n\t\t\tLastName:  gofakeit.LastName(),\n\t\t\tCreatedAt: time.Now().UTC(),\n\t\t}\n\n\t\tvar payload bytes.Buffer\n\t\tencoder := gob.NewEncoder(&payload)\n\t\terr := encoder.Encode(user)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\terr = pub.Publish(mysqlTable, message.NewMessage(\n\t\t\twatermill.NewUUID(),\n\t\t\tpayload.Bytes(),\n\t\t))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/mysql.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype mysqlUser struct {\n\tID        int64\n\tUser      string\n\tFirstName string\n\tLastName  string\n\tCreatedAt time.Time\n}\n\ntype mysqlSchemaAdapter struct {\n\tsql.DefaultMySQLSchema\n}\n\nfunc (m mysqlSchemaAdapter) SchemaInitializingQueries(params sql.SchemaInitializingQueriesParams) ([]sql.Query, error) {\n\tcreateQuery := `\n\t\tCREATE TABLE IF NOT EXISTS ` + params.Topic + ` (\n\t\t\tid INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n\t\t\tuser VARCHAR(36) NOT NULL,\n\t\t\tfirst_name VARCHAR(36) NOT NULL,\n\t\t\tlast_name VARCHAR(36) NOT NULL,\n\t\t\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n\t\t);\n\t`\n\n\treturn []sql.Query{{Query: createQuery}}, nil\n}\n\nfunc (m mysqlSchemaAdapter) InsertQuery(params sql.InsertQueryParams) (sql.Query, error) {\n\tinsertQuery := fmt.Sprintf(\n\t\t`INSERT INTO %s (user, first_name, last_name, created_at) VALUES %s`,\n\t\tparams.Topic,\n\t\tstrings.TrimRight(strings.Repeat(`(?,?,?,?),`, len(params.Msgs)), \",\"),\n\t)\n\n\tvar args []interface{}\n\tfor _, msg := range params.Msgs {\n\t\tuser := mysqlUser{}\n\n\t\tdecoder := gob.NewDecoder(bytes.NewBuffer(msg.Payload))\n\t\terr := decoder.Decode(&user)\n\t\tif err != nil {\n\t\t\treturn sql.Query{}, err\n\t\t}\n\n\t\targs = append(args, user.User, user.FirstName, user.LastName, user.CreatedAt)\n\t}\n\n\treturn sql.Query{Query: insertQuery, Args: args}, nil\n}\n\nfunc (m mysqlSchemaAdapter) SelectQuery(params sql.SelectQueryParams) (sql.Query, error) {\n\tnextOffsetQuery, err := params.OffsetsAdapter.NextOffsetQuery(sql.NextOffsetQueryParams{\n\t\tTopic:         params.Topic,\n\t\tConsumerGroup: params.ConsumerGroup,\n\t})\n\tif err != nil {\n\t\treturn sql.Query{}, err\n\t}\n\n\tselectQuery := `\n\t\tSELECT id, user, first_name, last_name, created_at FROM ` + params.Topic + `\n\t\tWHERE\n\t\t\tid > (` + nextOffsetQuery.Query + `)\n\t\tORDER BY\n\t\t\tid ASC\n\t\tLIMIT 1`\n\n\treturn sql.Query{Query: selectQuery, Args: nextOffsetQuery.Args}, nil\n}\n\nfunc (m mysqlSchemaAdapter) UnmarshalMessage(params sql.UnmarshalMessageParams) (_ sql.Row, err error) {\n\tuser := mysqlUser{}\n\terr = params.Row.Scan(&user.ID, &user.User, &user.FirstName, &user.LastName, &user.CreatedAt)\n\tif err != nil {\n\t\treturn sql.Row{}, err\n\t}\n\n\tvar payload bytes.Buffer\n\tencoder := gob.NewEncoder(&payload)\n\n\terr = encoder.Encode(user)\n\tif err != nil {\n\t\treturn sql.Row{}, err\n\t}\n\n\tmsg := message.NewMessage(watermill.NewULID(), payload.Bytes())\n\n\treturn sql.Row{\n\t\tOffset: user.ID,\n\t\tMsg:    msg,\n\t}, nil\n}\n"
  },
  {
    "path": "_examples/real-world-examples/synchronizing-databases/postgres.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n)\n\ntype postgresUser struct {\n\tID        int64\n\tUsername  string\n\tFullName  string\n\tCreatedAt time.Time\n}\n\ntype postgresSchemaAdapter struct {\n\tsql.DefaultPostgreSQLSchema\n}\n\nfunc (p postgresSchemaAdapter) SchemaInitializingQueries(params sql.SchemaInitializingQueriesParams) ([]sql.Query, error) {\n\tcreateQuery := `\n\t\tCREATE TABLE IF NOT EXISTS ` + params.Topic + ` (\n\t\t\tid INT NOT NULL PRIMARY KEY,\n\t\t\tusername VARCHAR(36) NOT NULL,\n\t\t\tfull_name VARCHAR(36) NOT NULL,\n\t\t\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n\t\t);\n\t`\n\n\treturn []sql.Query{{Query: createQuery}}, nil\n}\n\nfunc (p postgresSchemaAdapter) InsertQuery(params sql.InsertQueryParams) (sql.Query, error) {\n\tinsertQuery := fmt.Sprintf(\n\t\t`INSERT INTO %s (id, username, full_name, created_at) VALUES %s`,\n\t\tparams.Topic,\n\t\tstrings.TrimRight(strings.Repeat(`($1,$2,$3,$4),`, len(params.Msgs)), \",\"),\n\t)\n\n\tvar args []interface{}\n\tfor _, msg := range params.Msgs {\n\t\tuser := postgresUser{}\n\n\t\tdecoder := gob.NewDecoder(bytes.NewBuffer(msg.Payload))\n\t\terr := decoder.Decode(&user)\n\t\tif err != nil {\n\t\t\treturn sql.Query{}, err\n\t\t}\n\n\t\targs = append(args, user.ID, user.Username, user.FullName, user.CreatedAt)\n\t}\n\n\treturn sql.Query{Query: insertQuery, Args: args}, nil\n}\n\nfunc (p postgresSchemaAdapter) SelectQuery(params sql.SelectQueryParams) (sql.Query, error) {\n\t// No need to implement this method, as PostgreSQL subscriber is not used in this example.\n\treturn sql.Query{}, nil\n}\n\nfunc (p postgresSchemaAdapter) UnmarshalMessage(params sql.UnmarshalMessageParams) (sql.Row, error) {\n\treturn sql.Row{}, errors.New(\"not implemented\")\n}\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"received event\"\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events/README.md",
    "content": "# Transactional Events (MySQL to Kafka)\n\nThis example shows how to use the SQL Subscriber from the [SQL Pub/Sub](https://github.com/ThreeDotsLabs/watermill-sql). \n\n## Background\n\nWhen producing domain events, you may stumble on a dilemma: should you first persist the aggregate to the storage and\nthen publish a domain event or the other way around? Whatever order you choose, one of the operations can fail and\nyou will end up with inconsistent state.\n\nFor more detailed description, see [When an SQL database makes a great Pub/Sub](https://threedots.tech/post/when-sql-database-makes-great-pub-sub/).\n\n## Solution\n\nThis example presents a solution to this problem: saving domain events in transaction with the aggregate in the same\ndatabase and publishing it asynchronously.\n\nThe SQL subscriber listens for new records on a MySQL table. Each new record will result in a new event published\non the Kafka topic. Kafka Publisher is used just as an example and any other publisher can be used instead.\n\nThe example uses `DefaultMySQLSchema` as the schema adapter, but you can define your own table definition and queries.\nSee [SQL Pub/Sub documentation](https://watermill.io/pubsubs/sql) for details.\n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See installation guide at https://docs.docker.com/compose/install/\n\n## Running\n\n```bash\ndocker-compose up\n```\n\nObserve the log output. You will notice new events, generated by the example.\n\nIn another terminal, run the following command to consume events produced on the Kafka topic.\n\n```bash\ndocker-compose exec server mill kafka consume -b kafka:9092 -t events\n```\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - mysql\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: > \n      /bin/sh -c \"go install github.com/ThreeDotsLabs/watermill/tools/mill@latest &&\n                  go run main.go\"\n\n  mysql:\n    image: mysql:8.0\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_DATABASE: watermill\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n\n  zookeeper:\n    image: confluentinc/cp-zookeeper:7.3.1\n    logging:\n      driver: none\n    restart: unless-stopped\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n\n  kafka:\n    image: confluentinc/cp-kafka:7.3.1\n    logging:\n      driver: none\n    restart: unless-stopped\n    depends_on:\n      - zookeeper\n    environment:\n      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/IBM/sarama v1.46.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"time\"\n\n\tdriver \"github.com/go-sql-driver/mysql\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\nvar (\n\tlogger     = watermill.NewStdLogger(false, false)\n\tkafkaTopic = \"events\"\n\tmysqlTable = \"events\"\n)\n\nfunc main() {\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\trouter.AddPlugin(plugin.SignalsHandler)\n\trouter.AddMiddleware(middleware.Recoverer)\n\n\tdb := createDB()\n\n\tsubscriber := createSubscriber(db)\n\tpublisher := createPublisher()\n\n\trouter.AddHandler(\n\t\t\"mysql-to-kafka\",\n\t\tmysqlTable,\n\t\tsubscriber,\n\t\tkafkaTopic,\n\t\tpublisher,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\tconsumedEvent := event{}\n\t\t\terr := json.Unmarshal(msg.Payload, &consumedEvent)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tlog.Printf(\"received event %+v with UUID %s\", consumedEvent, msg.UUID)\n\n\t\t\treturn []*message.Message{msg}, nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\t<-router.Running()\n\t\tsimulateEvents(db)\n\t}()\n\n\tif err := router.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"watermill\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\terr = db.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn db\n}\n\nfunc createSubscriber(db *stdSQL.DB) message.Subscriber {\n\tsub, err := sql.NewSubscriber(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.SubscriberConfig{\n\t\t\tSchemaAdapter:    sql.DefaultMySQLSchema{},\n\t\t\tOffsetsAdapter:   sql.DefaultMySQLOffsetsAdapter{},\n\t\t\tInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn sub\n}\n\nfunc createPublisher() message.Publisher {\n\tpub, err := kafka.NewPublisher(\n\t\tkafka.PublisherConfig{\n\t\t\tBrokers:   []string{\"kafka:9092\"},\n\t\t\tMarshaler: kafka.DefaultMarshaler{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn pub\n}\n\ntype event struct {\n\tName       string `json:\"name\"`\n\tOccurredAt string `json:\"occurred_at\"`\n}\n\nfunc simulateEvents(db *stdSQL.DB) {\n\tfor {\n\t\ttx, err := db.Begin()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// In an actual application, this is the place where some aggregate would be persisted\n\t\t// using the same transaction.\n\t\t// tx.Exec(\"INSERT INTO (...)\")\n\n\t\terr = publishEvent(tx)\n\t\tif err != nil {\n\t\t\trollbackErr := tx.Rollback()\n\t\t\tif rollbackErr != nil {\n\t\t\t\tpanic(rollbackErr)\n\t\t\t}\n\t\t\tpanic(err)\n\t\t}\n\n\t\terr = tx.Commit()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\n// publishEvent publishes a new event.\n// To publish the event in a separate transaction, a new SQL Publisher\n// has to be created each time, passing the proper transaction handle.\nfunc publishEvent(tx *stdSQL.Tx) error {\n\tpub, err := sql.NewPublisher(\n\t\tsql.TxFromStdSQL(tx),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter: sql.DefaultMySQLSchema{},\n\t\t}, logger)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\te := event{\n\t\tName:       \"UserSignedUp\",\n\t\tOccurredAt: time.Now().UTC().Format(time.RFC3339),\n\t}\n\tpayload, err := json.Marshal(e)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn pub.Publish(mysqlTable, message.NewMessage(\n\t\twatermill.NewUUID(),\n\t\tpayload,\n\t))\n}\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events-forwarder/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 180\nexpected_output: \"Sending a prize to the winner\"\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events-forwarder/README.md",
    "content": "# Publishing events in transactions with help of Forwarder component (MySQL to Google Pub/Sub)  \n\nWhile working with an event-driven application, you may in some point need to store an application state and publish a message\ntelling the rest of the system about what just happened. As it may look trivial at a first glance, it could become\na bit tricky if we consider what can go wrong in case we won't pay enough attention to details.\n\n## Solution\n\nThis example presents a solution to this problem: saving events in transaction along with persisting application state. \nIt also compares two other approaches which lack transactional publishing therefore expose application to a risk \nof inconsistency across the system. \n\n## Requirements\n\nTo run this example you will need Docker and docker-compose installed. See installation guide at https://docs.docker.com/compose/install/\n\n## Running\n\n```bash\ndocker-compose up\n```\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events-forwarder/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    environment:\n      - PUBSUB_EMULATOR_HOST=googlecloud:8085\n    depends_on:\n      - mysql\n      - googlecloud\n    restart: unless-stopped\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run .\n\n  mysql:\n    image: mysql:8.0\n    restart: unless-stopped\n    logging:\n      driver: none\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_DATABASE: watermill\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n\n  googlecloud:\n    image: google/cloud-sdk:414.0.0\n    logging:\n      driver: none\n    entrypoint: gcloud --quiet beta emulators pubsub start --host-port=0.0.0.0:8085 --verbosity=debug --log-http\n    ports:\n      - 8085:8085\n    restart: unless-stopped\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events-forwarder/go.mod",
    "content": "module main.go\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0\n\tgithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n)\n\nrequire (\n\tcloud.google.com/go v0.121.6 // indirect\n\tcloud.google.com/go/auth v0.16.5 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.8.0 // indirect\n\tcloud.google.com/go/iam v1.5.2 // indirect\n\tcloud.google.com/go/pubsub/v2 v2.0.0 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.5 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/api v0.248.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/grpc v1.75.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n)\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events-forwarder/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=\ncloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=\ncloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=\ncloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=\ncloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=\ncloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=\ncloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=\ncloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=\ncloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0 h1:GXR+tsxPs/Vpmm0t4yEJUZdqLP9EytWvR+KN3Un5mNY=\ngithub.com/ThreeDotsLabs/watermill-googlecloud/v2 v2.0.0/go.mod h1:3IHyi1bNqQ8J2/wVWj4cQjzWXoEPauLm8ViyOCNaKbM=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0 h1:vd0eoMPriNjiFTEo7tQ1wmN933lNAWyVhxX931JG5Pw=\ngithub.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0/go.mod h1:Ce2GVZVnyajAh0AkwxSJXwx8ajBBveu1DI/yatan5jc=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=\ngithub.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=\ngo.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=\ngoogle.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=\ngoogle.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "_examples/real-world-examples/transactional-events-forwarder/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\tstdSQL \"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-googlecloud/v2/pkg/googlecloud\"\n\t\"github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql\"\n\t\"github.com/ThreeDotsLabs/watermill/components/forwarder\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\tdriver \"github.com/go-sql-driver/mysql\"\n)\n\nconst (\n\tprojectID             = \"transactional-events\"\n\tforwarderSQLTopic     = \"eventsToForward\"\n\tgoogleCloudEventTopic = \"lottery-concluded\"\n\n\tsimulatedErrorProbability = 0.5\n)\n\nvar (\n\tlogger = watermill.NewStdLogger(false, false)\n\tdb     = createDB()\n)\n\ntype LotteryConcludedEvent struct {\n\tLotteryID int `json:\"lottery_id\"`\n}\n\nfunc main() {\n\t// Setup the Forwarder component so it takes messages from MySQL subscription and pushes them to Google Pub/Sub.\n\tsqlSubscriber, err := sql.NewSubscriber(\n\t\tsql.BeginnerFromStdSQL(db),\n\t\tsql.SubscriberConfig{\n\t\t\tSchemaAdapter:    sql.DefaultMySQLSchema{},\n\t\t\tOffsetsAdapter:   sql.DefaultMySQLOffsetsAdapter{},\n\t\t\tInitializeSchema: true,\n\t\t},\n\t\tlogger,\n\t)\n\texpectNoErr(err)\n\n\tgcpPublisher, err := googlecloud.NewPublisher(\n\t\tgooglecloud.PublisherConfig{\n\t\t\tProjectID: projectID,\n\t\t},\n\t\tlogger,\n\t)\n\texpectNoErr(err)\n\n\tfwd, err := forwarder.NewForwarder(sqlSubscriber, gcpPublisher, logger, forwarder.Config{\n\t\tForwarderTopic: forwarderSQLTopic,\n\t})\n\texpectNoErr(err)\n\n\tgo func() {\n\t\terr := fwd.Run(context.Background())\n\t\texpectNoErr(err)\n\t}()\n\n\tgo runLotteryService(logger)\n\tgo runPrizeSenderService(logger)\n\n\ttime.Sleep(time.Second * 60)\n}\n\n// Lottery service picks a random user at fixed intervals and makes him win a lottery.\nfunc runLotteryService(logger watermill.LoggerAdapter) {\n\tlogger = logger.With(watermill.LogFields{\"service\": \"lottery\"})\n\n\t// We'd like to persist in a database that for a given lottery id, a drawn user is a winner.\n\t// At the same time, we want to emit an event that will tell the rest of the system this happened.\n\t// We could approach implementing this handler in at least 3 ways:\n\tavailableServiceHandlers := []func(int, string, watermill.LoggerAdapter) error{\n\t\tpublishEventAndPersistData,\n\t\tpersistDataAndPublishEvent,\n\t\tpersistDataAndPublishEventInTransaction,\n\t}\n\n\tusers := []string{\"Mike\", \"Dwight\", \"Jim\", \"Pamela\"}\n\tlotteryID := 1\n\tfor range time.Tick(time.Second * 5) {\n\t\tpickedUser := users[rand.Intn(len(users))]\n\t\tlogger := logger.With(watermill.LogFields{\"user\": pickedUser, \"lottery_id\": lotteryID})\n\t\tlogger.Info(\"User has been picked as a winner\", nil)\n\n\t\tpickedHandler := availableServiceHandlers[rand.Intn(len(availableServiceHandlers))]\n\t\terr := pickedHandler(lotteryID, pickedUser, logger)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Handler failed\", err, nil)\n\t\t}\n\n\t\tlotteryID++\n\t}\n}\n\n// 1. Publishes event to Google Cloud Pub/Sub first, then stores data in MySQL.\nfunc publishEventAndPersistData(lotteryID int, pickedUser string, logger watermill.LoggerAdapter) error {\n\tpublisher, err := googlecloud.NewPublisher(\n\t\tgooglecloud.PublisherConfig{\n\t\t\tProjectID: projectID,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tevent := LotteryConcludedEvent{LotteryID: lotteryID}\n\tpayload, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = publisher.Publish(googleCloudEventTopic, message.NewMessage(watermill.NewULID(), payload))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// In case this fails, we have an event emitted, but no data persisted yet.\n\tif err = simulateError(); err != nil {\n\t\tlogger.Error(\"Failed to persist data\", err, nil)\n\t\treturn err\n\t}\n\n\t_, err = db.Exec(`INSERT INTO lotteries (lottery_id, winner) VALUES(?, ?)`, lotteryID, pickedUser)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// 2. Persists data to MySQL first, then publishes an event straight to Google Cloud Pub/Sub.\nfunc persistDataAndPublishEvent(lotteryID int, pickedUser string, logger watermill.LoggerAdapter) error {\n\t_, err := db.Exec(`INSERT INTO lotteries (lottery_id, winner) VALUES(?, ?)`, lotteryID, pickedUser)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar publisher message.Publisher\n\tpublisher, err = googlecloud.NewPublisher(\n\t\tgooglecloud.PublisherConfig{\n\t\t\tProjectID: projectID,\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tevent := LotteryConcludedEvent{LotteryID: lotteryID}\n\tpayload, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// In case this fails, we have data persisted, but no event emitted.\n\tif err = simulateError(); err != nil {\n\t\tlogger.Error(\"Failed to emit event\", err, nil)\n\t\treturn err\n\t}\n\n\terr = publisher.Publish(googleCloudEventTopic, message.NewMessage(watermill.NewULID(), payload))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// 3. Persists data in MySQL and emits an event through MySQL to Google Cloud Pub/Sub, all in one transaction.\nfunc persistDataAndPublishEventInTransaction(lotteryID int, pickedUser string, logger watermill.LoggerAdapter) error {\n\ttx, err := db.Begin()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tif err == nil {\n\t\t\ttx.Commit()\n\t\t} else {\n\t\t\tlogger.Info(\"Rolling transaction back due to error\", watermill.LogFields{\"error\": err.Error()})\n\t\t\t// In case of an error, we're 100% sure that thanks to MySQL transaction rollback, we won't have any of the undesired situations:\n\t\t\t// - event is emitted, but no data is persisted,\n\t\t\t// - data is persisted, but no event is emitted.\n\t\t\ttx.Rollback()\n\t\t}\n\t}()\n\n\t_, err = tx.Exec(`INSERT INTO lotteries (lottery_id, winner) VALUES(?, ?)`, lotteryID, pickedUser)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar publisher message.Publisher\n\tpublisher, err = sql.NewPublisher(\n\t\tsql.TxFromStdSQL(tx),\n\t\tsql.PublisherConfig{\n\t\t\tSchemaAdapter: sql.DefaultMySQLSchema{},\n\t\t},\n\t\tlogger,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Decorate publisher so it wraps an event in an envelope understood by the Forwarder component.\n\tpublisher = forwarder.NewPublisher(publisher, forwarder.PublisherConfig{\n\t\tForwarderTopic: forwarderSQLTopic,\n\t})\n\n\t// Publish an event announcing the lottery winner. Please note we're publishing to a Google Cloud topic here,\n\t// while using decorated MySQL publisher.\n\tevent := LotteryConcludedEvent{LotteryID: lotteryID}\n\tpayload, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = publisher.Publish(googleCloudEventTopic, message.NewMessage(watermill.NewULID(), payload))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// PrizeSender service listens to UserWonLottery events and sends a prize straight to the user that has won.\nfunc runPrizeSenderService(logger watermill.LoggerAdapter) {\n\tlogger = logger.With(watermill.LogFields{\"service\": \"prize_sender\"})\n\tctx := context.Background()\n\tgoogleCloudSubscriber, err := googlecloud.NewSubscriber(\n\t\tgooglecloud.SubscriberConfig{\n\t\t\tProjectID: projectID,\n\t\t},\n\t\tlogger,\n\t)\n\texpectNoErr(err)\n\n\tevents, err := googleCloudSubscriber.Subscribe(ctx, googleCloudEventTopic)\n\tfor rawEvent := range events {\n\t\tevent := LotteryConcludedEvent{}\n\t\terr := json.Unmarshal(rawEvent.Payload, &event)\n\t\texpectNoErr(err)\n\n\t\trawEvent.Ack()\n\n\t\tlogger := logger.With(watermill.LogFields{\"lottery_id\": event.LotteryID})\n\n\t\trow := db.QueryRow(\"SELECT winner FROM lotteries WHERE lottery_id=?\", event.LotteryID)\n\t\tvar winner string\n\t\terr = row.Scan(&winner)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Could not get lottery winner\", err, nil)\n\t\t\tcontinue\n\t\t}\n\n\t\tlogger.Info(\"Sending a prize to the winner\", watermill.LogFields{\n\t\t\t\"winner\":     winner,\n\t\t\t\"lottery_id\": event.LotteryID,\n\t\t})\n\t}\n}\n\nfunc expectNoErr(err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\"expected no error, got: %s\", err)\n\t}\n}\n\nfunc simulateError() error {\n\tif simulatedErrorProbability >= rand.Float64() {\n\t\treturn errors.New(\"simulated error occurred\")\n\t}\n\n\treturn nil\n}\n\nfunc createDB() *stdSQL.DB {\n\tconf := driver.NewConfig()\n\tconf.Net = \"tcp\"\n\tconf.User = \"root\"\n\tconf.Addr = \"mysql\"\n\tconf.DBName = \"watermill\"\n\n\tdb, err := stdSQL.Open(\"mysql\", conf.FormatDSN())\n\texpectNoErr(err)\n\n\terr = db.Ping()\n\texpectNoErr(err)\n\n\t_, err = db.Exec(`DROP TABLE IF EXISTS lotteries`)\n\texpectNoErr(err)\n\n\t_, err = db.Exec(`\nCREATE TABLE IF NOT EXISTS lotteries (\n    lottery_id INT NOT NULL PRIMARY KEY,\n\twinner VARCHAR(255) NOT NULL\n) ENGINE=INNODB;\n`)\n\texpectNoErr(err)\n\n\treturn db\n}\n"
  },
  {
    "path": "codecov.yml",
    "content": "ignore:\n  - \"pubsub/tests\" # test helpers used to test Pub/Subs\n\ncomment: no # do not comment PR with the result\n\ncoverage:\n  precision: 0\n  status:\n    patch: false # do not run coverage on patch nor changes\n    project:\n      default:\n        target: auto\n        threshold: 5%\n"
  },
  {
    "path": "components/cqrs/command_bus.go",
    "content": "package cqrs\n\nimport (\n\t\"context\"\n\tstdErrors \"errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype CommandBusConfig struct {\n\t// GeneratePublishTopic is used to generate topic for publishing command.\n\tGeneratePublishTopic CommandBusGeneratePublishTopicFn\n\n\t// OnSend is called before publishing the command.\n\t// The *message.Message can be modified.\n\t//\n\t// This option is not required.\n\tOnSend CommandBusOnSendFn\n\n\t// Marshaler is used to marshal and unmarshal commands.\n\t// It is required.\n\tMarshaler CommandEventMarshaler\n\n\t// Logger instance used to log.\n\t// If not provided, watermill.NopLogger is used.\n\tLogger watermill.LoggerAdapter\n}\n\nfunc (c *CommandBusConfig) setDefaults() {\n\tif c.Logger == nil {\n\t\tc.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (c CommandBusConfig) Validate() error {\n\tvar err error\n\n\tif c.Marshaler == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing Marshaler\"))\n\t}\n\n\tif c.GeneratePublishTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing GeneratePublishTopic\"))\n\t}\n\n\treturn err\n}\n\ntype CommandBusGeneratePublishTopicFn func(CommandBusGeneratePublishTopicParams) (string, error)\n\ntype CommandBusGeneratePublishTopicParams struct {\n\tCommandName string\n\tCommand     any\n}\n\ntype CommandBusOnSendFn func(params CommandBusOnSendParams) error\n\ntype CommandBusOnSendParams struct {\n\tCommandName string\n\tCommand     any\n\n\t// Message is never nil and can be modified.\n\tMessage *message.Message\n}\n\n// CommandBus transports commands to command handlers.\ntype CommandBus struct {\n\tpublisher message.Publisher\n\n\tconfig CommandBusConfig\n}\n\n// NewCommandBusWithConfig creates a new CommandBus.\nfunc NewCommandBusWithConfig(publisher message.Publisher, config CommandBusConfig) (*CommandBus, error) {\n\tif publisher == nil {\n\t\treturn nil, errors.New(\"missing publisher\")\n\t}\n\n\tconfig.setDefaults()\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config\")\n\t}\n\n\treturn &CommandBus{publisher, config}, nil\n}\n\n// NewCommandBus creates a new CommandBus.\n// Deprecated: use NewCommandBusWithConfig instead.\nfunc NewCommandBus(\n\tpublisher message.Publisher,\n\tgenerateTopic func(commandName string) string,\n\tmarshaler CommandEventMarshaler,\n) (*CommandBus, error) {\n\tif publisher == nil {\n\t\treturn nil, errors.New(\"missing publisher\")\n\t}\n\tif generateTopic == nil {\n\t\treturn nil, errors.New(\"missing generateTopic\")\n\t}\n\tif marshaler == nil {\n\t\treturn nil, errors.New(\"missing marshaler\")\n\t}\n\n\treturn &CommandBus{publisher, CommandBusConfig{\n\t\tGeneratePublishTopic: func(params CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\treturn generateTopic(params.CommandName), nil\n\t\t},\n\t\tMarshaler: marshaler,\n\t}}, nil\n}\n\n// Send sends command to the command bus.\nfunc (c CommandBus) Send(ctx context.Context, cmd any) error {\n\treturn c.SendWithModifiedMessage(ctx, cmd, nil)\n}\n\nfunc (c CommandBus) SendWithModifiedMessage(ctx context.Context, cmd any, modify func(*message.Message) error) error {\n\tmsg, topicName, err := c.newMessage(ctx, cmd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif modify != nil {\n\t\tif err := modify(msg); err != nil {\n\t\t\treturn errors.Wrap(err, \"cannot modify message\")\n\t\t}\n\t}\n\n\tif err := c.publisher.Publish(topicName, msg); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c CommandBus) newMessage(ctx context.Context, command any) (*message.Message, string, error) {\n\tmsg, err := c.config.Marshaler.Marshal(command)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tcommandName := c.config.Marshaler.Name(command)\n\ttopicName, err := c.config.GeneratePublishTopic(CommandBusGeneratePublishTopicParams{\n\t\tCommandName: commandName,\n\t\tCommand:     command,\n\t})\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"cannot generate topic name\")\n\t}\n\n\tmsg.SetContext(ctx)\n\n\tif c.config.OnSend != nil {\n\t\terr := c.config.OnSend(CommandBusOnSendParams{\n\t\t\tCommandName: commandName,\n\t\t\tCommand:     command,\n\t\t\tMessage:     msg,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Wrap(err, \"cannot execute OnSend\")\n\t\t}\n\t}\n\n\treturn msg, topicName, nil\n}\n"
  },
  {
    "path": "components/cqrs/command_bus_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n)\n\nfunc TestCommandBusConfig_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tName              string\n\t\tModifyValidConfig func(*cqrs.CommandBusConfig)\n\t\tExpectedErr       error\n\t}{\n\t\t{\n\t\t\tName:              \"valid_config\",\n\t\t\tModifyValidConfig: nil,\n\t\t\tExpectedErr:       nil,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_Marshaler\",\n\t\t\tModifyValidConfig: func(c *cqrs.CommandBusConfig) {\n\t\t\t\tc.Marshaler = nil\n\t\t\t},\n\t\t\tExpectedErr: errors.Errorf(\"missing Marshaler\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GeneratePublishTopic\",\n\t\t\tModifyValidConfig: func(c *cqrs.CommandBusConfig) {\n\t\t\t\tc.GeneratePublishTopic = nil\n\t\t\t},\n\t\t\tExpectedErr: errors.Errorf(\"missing GeneratePublishTopic\"),\n\t\t},\n\t}\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tvalidConfig := cqrs.CommandBusConfig{\n\t\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t},\n\t\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\t}\n\n\t\t\tif tc.ModifyValidConfig != nil {\n\t\t\t\ttc.ModifyValidConfig(&validConfig)\n\t\t\t}\n\n\t\t\terr := validConfig.Validate()\n\t\t\tif tc.ExpectedErr == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, tc.ExpectedErr.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewCommandBus(t *testing.T) {\n\tpub := newPublisherStub()\n\n\tconfig := cqrs.CommandBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\treturn \"\", nil\n\t\t},\n\t\tMarshaler: cqrs.JSONMarshaler{},\n\t}\n\n\trequire.NoError(t, config.Validate())\n\n\tcb, err := cqrs.NewCommandBusWithConfig(pub, config)\n\tassert.NotNil(t, cb)\n\tassert.NoError(t, err)\n\n\tconfig.GeneratePublishTopic = nil\n\trequire.Error(t, config.Validate())\n\n\tcb, err = cqrs.NewCommandBusWithConfig(pub, config)\n\tassert.Nil(t, cb)\n\tassert.Error(t, err)\n}\n\ntype contextKey string\n\nfunc TestCommandBus_Send_ContextPropagation(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\tcommandBus, err := cqrs.NewCommandBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.CommandBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\tctx := context.WithValue(context.Background(), contextKey(\"key\"), \"value\")\n\n\terr = commandBus.Send(ctx, \"message\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, ctx, publisher.messages[\"whatever\"][0].Context())\n}\n\nfunc TestCommandBus_Send_topic_name(t *testing.T) {\n\tcb, err := cqrs.NewCommandBusWithConfig(\n\t\tassertPublishTopicPublisher{ExpectedTopic: \"cqrs_test.TestCommand\", T: t},\n\t\tcqrs.CommandBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\treturn params.CommandName, nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cb.Send(context.Background(), TestCommand{})\n\trequire.NoError(t, err)\n}\n\nfunc TestCommandBus_Send_OnSend(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\tcb, err := cqrs.NewCommandBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.CommandBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\tOnSend: func(params cqrs.CommandBusOnSendParams) error {\n\t\t\t\tparams.Message.Metadata.Set(\"key\", \"value\")\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cb.Send(context.Background(), TestCommand{})\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"value\", publisher.messages[\"whatever\"][0].Metadata.Get(\"key\"))\n}\n\nfunc TestCommandBus_SendWithModifiedMessage(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\tcb, err := cqrs.NewCommandBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.CommandBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cb.SendWithModifiedMessage(context.Background(), TestCommand{}, func(message *message.Message) error {\n\t\tmessage.Metadata.Set(\"key\", \"value\")\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"value\", publisher.messages[\"whatever\"][0].Metadata.Get(\"key\"))\n}\n\nfunc TestCommandBus_SendWithModifiedMessage_modify_error(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\tcb, err := cqrs.NewCommandBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.CommandBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\texpectedErr := errors.New(\"some error\")\n\n\terr = cb.SendWithModifiedMessage(\n\t\tcontext.Background(),\n\t\tTestCommand{},\n\t\tfunc(message *message.Message) error {\n\t\t\treturn expectedErr\n\t\t},\n\t)\n\tassert.ErrorContains(t, err, expectedErr.Error())\n}\n\nfunc TestCommandBus_Send_OnSend_error(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\texpectedErr := errors.New(\"some error\")\n\n\tcb, err := cqrs.NewCommandBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.CommandBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\tOnSend: func(params cqrs.CommandBusOnSendParams) error {\n\t\t\t\treturn expectedErr\n\t\t\t},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cb.Send(context.Background(), TestCommand{})\n\trequire.EqualError(t, err, \"cannot execute OnSend: some error\")\n}\n"
  },
  {
    "path": "components/cqrs/command_handler.go",
    "content": "package cqrs\n\nimport (\n\t\"context\"\n)\n\n// CommandHandler receives a command defined by NewCommand and handles it with the Handle method.\n// If using DDD, CommandHandler may modify and persist the aggregate.\n//\n// In contrast to EventHandler, every Command must have only one CommandHandler.\n//\n// One instance of CommandHandler is used during handling messages.\n// When multiple commands are delivered at the same time, Handle method can be executed multiple times at the same time.\n// Because of that, Handle method needs to be thread safe!\ntype CommandHandler interface {\n\t// HandlerName is the name used in message.Router while creating handler.\n\t//\n\t// It will be also passed to CommandsSubscriberConstructor.\n\t// May be useful, for example, to create a consumer group per each handler.\n\t//\n\t// WARNING: If HandlerName was changed and is used for generating consumer groups,\n\t// it may result with **reconsuming all messages**!\n\tHandlerName() string\n\n\tNewCommand() any\n\n\tHandle(ctx context.Context, cmd any) error\n}\n\ntype genericCommandHandler[Command any] struct {\n\thandleFunc  func(ctx context.Context, cmd *Command) error\n\thandlerName string\n}\n\n// NewCommandHandler creates a new CommandHandler implementation based on provided function\n// and command type inferred from function argument.\nfunc NewCommandHandler[Command any](\n\thandlerName string,\n\thandleFunc func(ctx context.Context, cmd *Command) error,\n) CommandHandler {\n\treturn &genericCommandHandler[Command]{\n\t\thandleFunc:  handleFunc,\n\t\thandlerName: handlerName,\n\t}\n}\n\nfunc (c genericCommandHandler[Command]) HandlerName() string {\n\treturn c.handlerName\n}\n\nfunc (c genericCommandHandler[Command]) NewCommand() any {\n\ttVar := new(Command)\n\treturn tVar\n}\n\nfunc (c genericCommandHandler[Command]) Handle(ctx context.Context, cmd any) error {\n\tcommand := cmd.(*Command)\n\treturn c.handleFunc(ctx, command)\n}\n"
  },
  {
    "path": "components/cqrs/command_handler_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype SomeCommand struct {\n\tFoo string\n}\n\nfunc TestNewCommandHandler(t *testing.T) {\n\tcmdToSend := &SomeCommand{\"bar\"}\n\n\tch := cqrs.NewCommandHandler(\n\t\t\"some_handler\",\n\t\tfunc(ctx context.Context, cmd *SomeCommand) error {\n\t\t\tassert.Equal(t, cmdToSend, cmd)\n\t\t\treturn fmt.Errorf(\"some error\")\n\t\t},\n\t)\n\n\tassert.Equal(t, \"some_handler\", ch.HandlerName())\n\tassert.Equal(t, &SomeCommand{}, ch.NewCommand())\n\n\terr := ch.Handle(context.Background(), cmdToSend)\n\tassert.EqualError(t, err, \"some error\")\n}\n"
  },
  {
    "path": "components/cqrs/command_processor.go",
    "content": "package cqrs\n\nimport (\n\tstdErrors \"errors\"\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype CommandProcessorConfig struct {\n\t// GenerateSubscribeTopic is used to generate topic for subscribing command.\n\tGenerateSubscribeTopic CommandProcessorGenerateSubscribeTopicFn\n\n\t// SubscriberConstructor is used to create subscriber for CommandHandler.\n\tSubscriberConstructor CommandProcessorSubscriberConstructorFn\n\n\t// OnHandle is called before handling command.\n\t// OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling a command.\n\t//\n\t// Because of that, you need to explicitly call params.Handler.Handle() to handle the command.\n\t//  func(params CommandProcessorOnHandleParams) (err error) {\n\t//      // logic before handle\n\t//      //  (...)\n\t//\n\t//      err := params.Handler.Handle(params.Message.Context(), params.Command)\n\t//\n\t//      // logic after handle\n\t//      //  (...)\n\t//\n\t//      return err\n\t//  }\n\t//\n\t// This option is not required.\n\tOnHandle CommandProcessorOnHandleFn\n\n\t// Marshaler is used to marshal and unmarshal commands.\n\t// It is required.\n\tMarshaler CommandEventMarshaler\n\n\t// Logger instance used to log.\n\t// If not provided, watermill.NopLogger is used.\n\tLogger watermill.LoggerAdapter\n\n\t// If true, CommandProcessor will ack messages even if CommandHandler returns an error.\n\t// If RequestReplyBackend is not null and sending reply fails, the message will be nack-ed anyway.\n\t//\n\t// Warning: It's not recommended to use this option when you are using requestreply component\n\t// (requestreply.NewCommandHandler or requestreply.NewCommandHandlerWithResult), as it may ack the\n\t// command when sending reply failed.\n\t//\n\t// When you are using requestreply, you should use requestreply.PubSubBackendConfig.AckCommandErrors.\n\tAckCommandHandlingErrors bool\n\n\t// disableRouterAutoAddHandlers is used to keep backwards compatibility.\n\t// it is set when CommandProcessor is created by NewCommandProcessor.\n\t// Deprecated: please migrate to NewCommandProcessorWithConfig.\n\tdisableRouterAutoAddHandlers bool\n}\n\nfunc (c *CommandProcessorConfig) setDefaults() {\n\tif c.Logger == nil {\n\t\tc.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (c CommandProcessorConfig) Validate() error {\n\tvar err error\n\n\tif c.Marshaler == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing Marshaler\"))\n\t}\n\n\tif c.GenerateSubscribeTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing GenerateSubscribeTopic\"))\n\t}\n\tif c.SubscriberConstructor == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing SubscriberConstructor\"))\n\t}\n\n\treturn err\n}\n\ntype CommandProcessorGenerateSubscribeTopicFn func(CommandProcessorGenerateSubscribeTopicParams) (string, error)\n\ntype CommandProcessorGenerateSubscribeTopicParams struct {\n\tCommandName    string\n\tCommandHandler CommandHandler\n}\n\n// CommandProcessorSubscriberConstructorFn creates subscriber for CommandHandler.\n// It allows you to create a separate customized Subscriber for every command handler.\ntype CommandProcessorSubscriberConstructorFn func(CommandProcessorSubscriberConstructorParams) (message.Subscriber, error)\n\ntype CommandProcessorSubscriberConstructorParams struct {\n\tCommandName string\n\tHandlerName string\n\tHandler     CommandHandler\n}\n\ntype CommandProcessorOnHandleFn func(params CommandProcessorOnHandleParams) error\n\ntype CommandProcessorOnHandleParams struct {\n\tHandler CommandHandler\n\n\tCommandName string\n\tCommand     any\n\n\t// Message is never nil and can be modified.\n\tMessage *message.Message\n}\n\n// CommandProcessor determines which CommandHandler should handle the command received from the command bus.\ntype CommandProcessor struct {\n\trouter *message.Router\n\n\thandlers []CommandHandler\n\n\tconfig CommandProcessorConfig\n}\n\nfunc NewCommandProcessorWithConfig(router *message.Router, config CommandProcessorConfig) (*CommandProcessor, error) {\n\tconfig.setDefaults()\n\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif router == nil && !config.disableRouterAutoAddHandlers {\n\t\treturn nil, errors.New(\"missing router\")\n\t}\n\n\treturn &CommandProcessor{\n\t\trouter: router,\n\t\tconfig: config,\n\t}, nil\n}\n\n// NewCommandProcessor creates a new CommandProcessor.\n// Deprecated. Use NewCommandProcessorWithConfig instead.\nfunc NewCommandProcessor(\n\thandlers []CommandHandler,\n\tgenerateTopic func(commandName string) string,\n\tsubscriberConstructor CommandsSubscriberConstructor,\n\tmarshaler CommandEventMarshaler,\n\tlogger watermill.LoggerAdapter,\n) (*CommandProcessor, error) {\n\tif len(handlers) == 0 {\n\t\treturn nil, errors.New(\"missing handlers\")\n\t}\n\tif generateTopic == nil {\n\t\treturn nil, errors.New(\"missing generateTopic\")\n\t}\n\tif subscriberConstructor == nil {\n\t\treturn nil, errors.New(\"missing subscriberConstructor\")\n\t}\n\n\tcp, err := NewCommandProcessorWithConfig(\n\t\tnil,\n\t\tCommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn generateTopic(params.CommandName), nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn subscriberConstructor(params.HandlerName)\n\t\t\t},\n\t\t\tMarshaler:                    marshaler,\n\t\t\tLogger:                       logger,\n\t\t\tdisableRouterAutoAddHandlers: true,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, handler := range handlers {\n\t\tif err := cp.AddHandlers(handler); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn cp, nil\n}\n\n// CommandsSubscriberConstructor creates subscriber for CommandHandler.\n// It allows you to create a separate customized Subscriber for every command handler.\n//\n// Deprecated: please use CommandProcessorSubscriberConstructorFn instead.\ntype CommandsSubscriberConstructor func(handlerName string) (message.Subscriber, error)\n\n// AddHandlers adds a new CommandHandler to the CommandProcessor and adds it to the router.\nfunc (p *CommandProcessor) AddHandlers(handlers ...CommandHandler) error {\n\thandledCommands := map[string]struct{}{}\n\tfor _, handler := range handlers {\n\t\tcommandName := p.config.Marshaler.Name(handler.NewCommand())\n\t\tif _, ok := handledCommands[commandName]; ok {\n\t\t\treturn DuplicateCommandHandlerError{commandName}\n\t\t}\n\n\t\thandledCommands[commandName] = struct{}{}\n\t}\n\n\tif p.config.disableRouterAutoAddHandlers {\n\t\tp.handlers = append(p.handlers, handlers...)\n\t\treturn nil\n\t}\n\n\tfor _, handler := range handlers {\n\t\tif _, err := p.addHandlerToRouter(p.router, handler); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tp.handlers = append(p.handlers, handler)\n\t}\n\n\treturn nil\n}\n\n// AddHandler adds a new CommandHandler to the CommandProcessor and adds it to the router.\nfunc (p *CommandProcessor) AddHandler(handler CommandHandler) (*message.Handler, error) {\n\tif p.config.disableRouterAutoAddHandlers {\n\t\tp.handlers = append(p.handlers, handler)\n\n\t\treturn nil, nil\n\t}\n\n\th, err := p.addHandlerToRouter(p.router, handler)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.handlers = append(p.handlers, handler)\n\n\treturn h, nil\n}\n\n// DuplicateCommandHandlerError occurs when a handler with the same name already exists.\ntype DuplicateCommandHandlerError struct {\n\tCommandName string\n}\n\nfunc (d DuplicateCommandHandlerError) Error() string {\n\treturn fmt.Sprintf(\"command handler for command %s already exists\", d.CommandName)\n}\n\n// AddHandlersToRouter adds the CommandProcessor's handlers to the given router.\n// It should be called only once per CommandProcessor instance.\n//\n// It is required to call AddHandlersToRouter only if command processor is created with NewCommandProcessor (disableRouterAutoAddHandlers is set to true).\n// Deprecated: please migrate to command processor created by NewCommandProcessorWithConfig.\nfunc (p CommandProcessor) AddHandlersToRouter(r *message.Router) error {\n\tif !p.config.disableRouterAutoAddHandlers {\n\t\treturn errors.New(\"AddHandlersToRouter should be called only when using deprecated NewCommandProcessor\")\n\t}\n\n\tfor i := range p.Handlers() {\n\t\thandler := p.handlers[i]\n\n\t\tif _, err := p.addHandlerToRouter(r, handler); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p CommandProcessor) addHandlerToRouter(r *message.Router, handler CommandHandler) (*message.Handler, error) {\n\thandlerName := handler.HandlerName()\n\tcommandName := p.config.Marshaler.Name(handler.NewCommand())\n\n\ttopicName, err := p.config.GenerateSubscribeTopic(CommandProcessorGenerateSubscribeTopicParams{\n\t\tCommandName:    commandName,\n\t\tCommandHandler: handler,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"cannot generate topic for command handler %s\", handlerName)\n\t}\n\n\tlogger := p.config.Logger.With(watermill.LogFields{\n\t\t\"command_handler_name\": handlerName,\n\t\t\"topic\":                topicName,\n\t})\n\n\thandlerFunc, err := p.routerHandlerFunc(handler, logger)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogger.Debug(\"Adding CQRS command handler to router\", nil)\n\n\tsubscriber, err := p.config.SubscriberConstructor(CommandProcessorSubscriberConstructorParams{\n\t\tCommandName: commandName,\n\t\tHandlerName: handlerName,\n\t\tHandler:     handler,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot create subscriber for command processor\")\n\t}\n\n\treturn r.AddConsumerHandler(\n\t\thandlerName,\n\t\ttopicName,\n\t\tsubscriber,\n\t\thandlerFunc,\n\t), nil\n}\n\n// Handlers returns the CommandProcessor's handlers.\nfunc (p CommandProcessor) Handlers() []CommandHandler {\n\treturn p.handlers\n}\n\nfunc (p CommandProcessor) routerHandlerFunc(handler CommandHandler, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) {\n\tcmd := handler.NewCommand()\n\tcmdName := p.config.Marshaler.Name(cmd)\n\n\tif err := p.validateCommand(cmd); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn func(msg *message.Message) error {\n\t\tcmd := handler.NewCommand()\n\t\tmessageCmdName := p.config.Marshaler.NameFromMessage(msg)\n\n\t\tif messageCmdName != cmdName {\n\t\t\tlogger.Trace(\"Received different command type than expected, ignoring\", watermill.LogFields{\n\t\t\t\t\"message_uuid\":          msg.UUID,\n\t\t\t\t\"expected_command_type\": cmdName,\n\t\t\t\t\"received_command_type\": messageCmdName,\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\n\t\tlogger.Debug(\"Handling command\", watermill.LogFields{\n\t\t\t\"message_uuid\":          msg.UUID,\n\t\t\t\"received_command_type\": messageCmdName,\n\t\t})\n\n\t\tctx := CtxWithOriginalMessage(msg.Context(), msg)\n\t\tmsg.SetContext(ctx)\n\n\t\tif err := p.config.Marshaler.Unmarshal(msg, cmd); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thandle := func(params CommandProcessorOnHandleParams) (err error) {\n\t\t\treturn params.Handler.Handle(ctx, params.Command)\n\t\t}\n\t\tif p.config.OnHandle != nil {\n\t\t\thandle = p.config.OnHandle\n\t\t}\n\n\t\terr := handle(CommandProcessorOnHandleParams{\n\t\t\tHandler:     handler,\n\t\t\tCommandName: messageCmdName,\n\t\t\tCommand:     cmd,\n\t\t\tMessage:     msg,\n\t\t})\n\n\t\tif p.config.AckCommandHandlingErrors && err != nil {\n\t\t\tlogger.Error(\"Error when handling command, acking (AckCommandHandlingErrors is enabled)\", err, nil)\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"Error when handling command, nacking\", watermill.LogFields{\"err\": err})\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}, nil\n}\n\nfunc (p CommandProcessor) validateCommand(cmd interface{}) error {\n\t// CommandHandler's NewCommand must return a pointer, because it is used to unmarshal\n\tif err := isPointer(cmd); err != nil {\n\t\treturn errors.Wrap(err, \"command must be a non-nil pointer\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "components/cqrs/command_processor_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCommandProcessorConfig_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tName              string\n\t\tModifyValidConfig func(*cqrs.CommandProcessorConfig)\n\t\tExpectedErr       error\n\t}{\n\t\t{\n\t\t\tName:              \"valid_config\",\n\t\t\tModifyValidConfig: nil,\n\t\t\tExpectedErr:       nil,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_Marshaler\",\n\t\t\tModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {\n\t\t\t\tc.Marshaler = nil\n\t\t\t},\n\t\t\tExpectedErr: errors.Errorf(\"missing Marshaler\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_SubscriberConstructor\",\n\t\t\tModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {\n\t\t\t\tc.SubscriberConstructor = nil\n\t\t\t},\n\t\t\tExpectedErr: errors.Errorf(\"missing SubscriberConstructor\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateHandlerSubscribeTopic\",\n\t\t\tModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {\n\t\t\t\tc.GenerateSubscribeTopic = nil\n\t\t\t},\n\t\t\tExpectedErr: errors.Errorf(\"missing GenerateSubscribeTopic\"),\n\t\t},\n\t}\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tvalidConfig := cqrs.CommandProcessorConfig{\n\t\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t},\n\t\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\t\treturn nil, nil\n\t\t\t\t},\n\t\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\t}\n\n\t\t\tif tc.ModifyValidConfig != nil {\n\t\t\t\ttc.ModifyValidConfig(&validConfig)\n\t\t\t}\n\n\t\t\terr := validConfig.Validate()\n\t\t\tif tc.ExpectedErr == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, tc.ExpectedErr.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewCommandProcessor(t *testing.T) {\n\tconfig := cqrs.CommandProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn \"\", nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t\tMarshaler: cqrs.JSONMarshaler{},\n\t}\n\trequire.NoError(t, config.Validate())\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, watermill.NewStdLogger(false, false))\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewCommandProcessorWithConfig(router, config)\n\tassert.NotNil(t, cp)\n\tassert.NoError(t, err)\n\n\tconfig.SubscriberConstructor = nil\n\trequire.Error(t, config.Validate())\n\n\tcp, err = cqrs.NewCommandProcessorWithConfig(router, config)\n\tassert.Nil(t, cp)\n\tassert.Error(t, err)\n}\n\ntype nonPointerCommandHandler struct {\n}\n\nfunc (nonPointerCommandHandler) HandlerName() string {\n\treturn \"nonPointerCommandHandler\"\n}\n\nfunc (nonPointerCommandHandler) NewCommand() interface{} {\n\treturn TestCommand{}\n}\n\nfunc (nonPointerCommandHandler) Handle(ctx context.Context, cmd interface{}) error {\n\tpanic(\"not implemented\")\n}\n\nfunc TestCommandProcessor_non_pointer_command(t *testing.T) {\n\tts := NewTestServices()\n\n\thandler := nonPointerCommandHandler{}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = commandProcessor.AddHandlers(handler)\n\tassert.IsType(t, cqrs.NonPointerError{}, errors.Cause(err))\n}\n\n// TestCommandProcessor_multiple_same_command_handlers checks, that we don't register multiple handlers for the same command.\nfunc TestCommandProcessor_multiple_same_command_handlers(t *testing.T) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = commandProcessor.AddHandlers(\n\t\t&CaptureCommandHandler{},\n\t\t&CaptureCommandHandler{},\n\t)\n\trequire.Error(t, err)\n\tassert.EqualValues(t, cqrs.DuplicateCommandHandlerError{CommandName: \"cqrs_test.TestCommand\"}, err)\n\tassert.Equal(t, \"command handler for command cqrs_test.TestCommand already exists\", err.Error())\n}\n\ntype mockSubscriber struct {\n\tMessagesToSend              []*message.Message\n\tWaitForAckBeforeSendingNext bool\n\n\tout chan *message.Message\n}\n\nfunc (m *mockSubscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {\n\tm.out = make(chan *message.Message)\n\n\tgo func() {\n\t\tfor _, msg := range m.MessagesToSend {\n\t\t\tm.out <- msg\n\n\t\t\tif m.WaitForAckBeforeSendingNext {\n\t\t\t\t<-msg.Acked()\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn m.out, nil\n}\n\nfunc (m mockSubscriber) Close() error {\n\tclose(m.out)\n\treturn nil\n}\n\nfunc TestCommandProcessor_AckCommandHandlingErrors_option_true(t *testing.T) {\n\tlogger := watermill.NewCaptureLogger()\n\n\tmarshaler := cqrs.JSONMarshaler{}\n\n\tmsgToSend, err := marshaler.Marshal(&TestCommand{ID: \"1\"})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsgToSend,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"commands\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tMarshaler:                marshaler,\n\t\t\tLogger:                   logger,\n\t\t\tAckCommandHandlingErrors: true,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\texpectedErr := errors.New(\"test error\")\n\n\terr = commandProcessor.AddHandlers(cqrs.NewCommandHandler(\n\t\t\"handler\", func(ctx context.Context, cmd *TestCommand) error {\n\t\t\treturn expectedErr\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msgToSend.Acked():\n\t\t// ok\n\tcase <-msgToSend.Nacked():\n\t\t// nack received\n\t\tt.Fatal(\"nack received, message should be acked\")\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"timeout waiting for ack\")\n\t}\n\n\t// it's pretty important to not ack message silently, so let's assert if it's logged properly\n\texpectedLogMessage := watermill.CapturedMessage{\n\t\tLevel: watermill.ErrorLogLevel,\n\t\tFields: map[string]any{\n\t\t\t\"command_handler_name\": \"handler\",\n\t\t\t\"topic\":                \"commands\",\n\t\t},\n\t\tMsg: \"Error when handling command, acking (AckCommandHandlingErrors is enabled)\",\n\t\tErr: expectedErr,\n\t}\n\tassert.True(\n\t\tt,\n\t\tlogger.Has(expectedLogMessage),\n\t\t\"expected log message not found, logs: %#v\",\n\t\tlogger.Captured(),\n\t)\n}\n\nfunc TestCommandProcessor_AckCommandHandlingErrors_option_false(t *testing.T) {\n\tlogger := watermill.NewCaptureLogger()\n\n\tmarshaler := cqrs.JSONMarshaler{}\n\n\tmsgToSend, err := marshaler.Marshal(&TestCommand{ID: \"1\"})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsgToSend,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"commands\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tMarshaler:                marshaler,\n\t\t\tLogger:                   logger,\n\t\t\tAckCommandHandlingErrors: false,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\texpectedErr := errors.New(\"test error\")\n\n\terr = commandProcessor.AddHandlers(cqrs.NewCommandHandler(\n\t\t\"handler\", func(ctx context.Context, cmd *TestCommand) error {\n\t\t\treturn expectedErr\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msgToSend.Acked():\n\t\t// nack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\tcase <-msgToSend.Nacked():\n\t\t// ok\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"timeout waiting for ack\")\n\t}\n}\n\nfunc TestNewCommandProcessor_OnHandle(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg1, err := ts.Marshaler.Marshal(&TestCommand{ID: \"1\"})\n\trequire.NoError(t, err)\n\n\tmsg2, err := ts.Marshaler.Marshal(&TestCommand{ID: \"2\"})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg1,\n\t\t\tmsg2,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\thandlerCalled := 0\n\n\tdefer func() {\n\t\t// for msg 1 we are not calling handler - but returning before\n\t\tassert.Equal(t, 1, handlerCalled)\n\t}()\n\n\thandler := cqrs.NewCommandHandler(\"test\", func(ctx context.Context, cmd *TestCommand) error {\n\t\thandlerCalled++\n\t\treturn nil\n\t})\n\n\tonHandleCalled := int64(0)\n\n\tconfig := cqrs.CommandProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn \"commands\", nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn mockSub, nil\n\t\t},\n\t\tOnHandle: func(params cqrs.CommandProcessorOnHandleParams) error {\n\t\t\tatomic.AddInt64(&onHandleCalled, 1)\n\n\t\t\tassert.IsType(t, &TestCommand{}, params.Command)\n\t\t\tassert.Equal(t, \"cqrs_test.TestCommand\", params.CommandName)\n\t\t\tassert.Equal(t, handler, params.Handler)\n\n\t\t\tif params.Command.(*TestCommand).ID == \"1\" {\n\t\t\t\tassert.Equal(t, msg1, params.Message)\n\t\t\t\treturn errors.New(\"test error\")\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, msg2, params.Message)\n\t\t\t}\n\n\t\t\treturn params.Handler.Handle(params.Message.Context(), params.Command)\n\t\t},\n\t\tMarshaler: ts.Marshaler,\n\t\tLogger:    ts.Logger,\n\t}\n\tcp, err := cqrs.NewCommandProcessorWithConfig(router, config)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlers(handler)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg1.Nacked():\n\t\t// ok\n\tcase <-msg1.Acked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n\n\tselect {\n\tcase <-msg2.Acked():\n\t\t// ok\n\tcase <-msg2.Nacked():\n\t\t// nack received\n\t}\n\n\tassert.EqualValues(t, 2, onHandleCalled)\n}\n\nfunc TestCommandProcessor_AddHandlersToRouter_without_disableRouterAutoAddHandlers(t *testing.T) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"commands\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn ts.CommandsPubSub, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlersToRouter(router)\n\tassert.ErrorContains(t, err, \"AddHandlersToRouter should be called only when using deprecated NewCommandProcessor\")\n}\n\nfunc TestCommandProcessor_original_msg_set_to_ctx(t *testing.T) {\n\tlogger := watermill.NewCaptureLogger()\n\n\tmarshaler := cqrs.JSONMarshaler{}\n\n\tmsgToSend, err := marshaler.Marshal(&TestCommand{ID: \"1\"})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsgToSend,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"commands\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tMarshaler:                marshaler,\n\t\t\tLogger:                   logger,\n\t\t\tAckCommandHandlingErrors: true,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\tvar msgFromCtx *message.Message\n\n\terr = commandProcessor.AddHandlers(cqrs.NewCommandHandler(\n\t\t\"handler\", func(ctx context.Context, cmd *TestCommand) error {\n\t\t\tmsgFromCtx = cqrs.OriginalMessageFromCtx(ctx)\n\t\t\treturn nil\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msgToSend.Acked():\n\t\t// ok\n\tcase <-msgToSend.Nacked():\n\t\t// nack received\n\t\tt.Fatal(\"nack received, message should be acked\")\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"timeout waiting for ack\")\n\t}\n\n\trequire.NotNil(t, msgFromCtx)\n\tassert.Equal(t, msgToSend, msgFromCtx)\n}\n"
  },
  {
    "path": "components/cqrs/cqrs.go",
    "content": "package cqrs\n\nimport (\n\tstdErrors \"errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// Deprecated: use CommandProcessor and EventProcessor instead.\ntype FacadeConfig struct {\n\t// GenerateCommandsTopic generates topic name based on the command name.\n\t// Command name is generated by CommandEventMarshaler's Name method.\n\t//\n\t// It allows you to use topic per command or one topic for every command.\n\tGenerateCommandsTopic func(commandName string) string\n\n\t// CommandHandlers return command handlers which should be executed.\n\tCommandHandlers func(commandBus *CommandBus, eventBus *EventBus) []CommandHandler\n\n\t// CommandsPublisher is Publisher used to publish commands.\n\tCommandsPublisher message.Publisher\n\n\t// CommandsSubscriberConstructor is constructor for subscribers which will subscribe for messages.\n\t// It will be called for every command handler.\n\t// It allows you to create separated customized Subscriber for every command handler.\n\tCommandsSubscriberConstructor CommandsSubscriberConstructor\n\n\t// GenerateEventsTopic generates topic name based on the event name.\n\t// Event name is generated by CommandEventMarshaler's Name method.\n\t//\n\t// It allows you to use topic per command or one topic for every command.\n\tGenerateEventsTopic func(eventName string) string\n\n\t// EventHandlers return event handlers which should be executed.\n\tEventHandlers func(commandBus *CommandBus, eventBus *EventBus) []EventHandler\n\n\t// EventsPublisher is Publisher used to publish commands.\n\tEventsPublisher message.Publisher\n\n\t// EventsSubscriberConstructor is constructor for subscribers which will subscribe for messages.\n\t// It will be called for every event handler.\n\t// It allows you to create separated customized Subscriber for every event handler.\n\tEventsSubscriberConstructor EventsSubscriberConstructor\n\n\t// Router is a Watermill router, which will be used to handle events and commands.\n\t// Router handlers will be automatically generated by AddHandlersToRouter of Command and Event handlers.\n\tRouter *message.Router\n\n\tCommandEventMarshaler CommandEventMarshaler\n\n\tLogger watermill.LoggerAdapter\n}\n\nfunc (c FacadeConfig) Validate() error {\n\tvar err error\n\n\tif c.CommandsEnabled() {\n\t\tif c.GenerateCommandsTopic == nil {\n\t\t\terr = stdErrors.Join(err, errors.New(\"GenerateCommandsTopic is nil\"))\n\t\t}\n\t\tif c.CommandsSubscriberConstructor == nil {\n\t\t\terr = stdErrors.Join(err, errors.New(\"CommandsSubscriberConstructor is nil\"))\n\t\t}\n\t\tif c.CommandsPublisher == nil {\n\t\t\terr = stdErrors.Join(err, errors.New(\"CommandsPublisher is nil\"))\n\t\t}\n\t}\n\tif c.EventsEnabled() {\n\t\tif c.GenerateEventsTopic == nil {\n\t\t\terr = stdErrors.Join(err, errors.New(\"GenerateEventsTopic is nil\"))\n\t\t}\n\t\tif c.EventsSubscriberConstructor == nil {\n\t\t\terr = stdErrors.Join(err, errors.New(\"EventsSubscriberConstructor is nil\"))\n\t\t}\n\t\tif c.EventsPublisher == nil {\n\t\t\terr = stdErrors.Join(err, errors.New(\"EventsPublisher is nil\"))\n\t\t}\n\t}\n\n\tif c.Router == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"Router is nil\"))\n\t}\n\tif c.Logger == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"Logger is nil\"))\n\t}\n\tif c.CommandEventMarshaler == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"CommandEventMarshaler is nil\"))\n\t}\n\n\treturn err\n}\n\nfunc (c FacadeConfig) EventsEnabled() bool {\n\treturn c.GenerateEventsTopic != nil || c.EventsPublisher != nil || c.EventsSubscriberConstructor != nil\n}\n\nfunc (c FacadeConfig) CommandsEnabled() bool {\n\treturn c.GenerateCommandsTopic != nil || c.CommandsPublisher != nil || c.CommandsSubscriberConstructor != nil\n}\n\n// Deprecated: use CommandHandler and EventHandler instead.\n//\n// Facade is a facade for creating the Command and Event buses and processors.\n// It was created to avoid boilerplate, when using CQRS in the standard way.\n// You can also create buses and processors manually, drawing inspiration from how it's done in NewFacade.\ntype Facade struct {\n\tcommandsTopic func(commandName string) string\n\tcommandBus    *CommandBus\n\n\teventsTopic func(eventName string) string\n\teventBus    *EventBus\n\n\tcommandEventMarshaler CommandEventMarshaler\n}\n\nfunc (f Facade) CommandBus() *CommandBus {\n\treturn f.commandBus\n}\n\nfunc (f Facade) EventBus() *EventBus {\n\treturn f.eventBus\n}\n\nfunc (f Facade) CommandEventMarshaler() CommandEventMarshaler {\n\treturn f.commandEventMarshaler\n}\n\n// Deprecated: use CommandHandler and EventHandler instead.\nfunc NewFacade(config FacadeConfig) (*Facade, error) {\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config\")\n\t}\n\n\tc := &Facade{\n\t\tcommandsTopic:         config.GenerateCommandsTopic,\n\t\teventsTopic:           config.GenerateEventsTopic,\n\t\tcommandEventMarshaler: config.CommandEventMarshaler,\n\t}\n\n\tif config.CommandsEnabled() {\n\t\tvar err error\n\t\tc.commandBus, err = NewCommandBus(\n\t\t\tconfig.CommandsPublisher,\n\t\t\tconfig.GenerateCommandsTopic,\n\t\t\tconfig.CommandEventMarshaler,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot create command bus\")\n\t\t}\n\t} else {\n\t\tconfig.Logger.Info(\"Empty GenerateCommandsTopic, command bus will be not created\", nil)\n\t}\n\tif config.EventsEnabled() {\n\t\tvar err error\n\t\tc.eventBus, err = NewEventBus(config.EventsPublisher, config.GenerateEventsTopic, config.CommandEventMarshaler)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot create event bus\")\n\t\t}\n\t} else {\n\t\tconfig.Logger.Info(\"Empty GenerateEventsTopic, event bus will be not created\", nil)\n\t}\n\n\tif config.CommandHandlers != nil {\n\t\tcommandProcessor, err := NewCommandProcessor(\n\t\t\tconfig.CommandHandlers(c.commandBus, c.eventBus),\n\t\t\tconfig.GenerateCommandsTopic,\n\t\t\tconfig.CommandsSubscriberConstructor,\n\t\t\tconfig.CommandEventMarshaler,\n\t\t\tconfig.Logger,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot create command processor\")\n\t\t}\n\n\t\tif err := commandProcessor.AddHandlersToRouter(config.Router); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif config.EventHandlers != nil {\n\t\teventProcessor, err := NewEventProcessor(\n\t\t\tconfig.EventHandlers(c.commandBus, c.eventBus),\n\t\t\tconfig.GenerateEventsTopic,\n\t\t\tconfig.EventsSubscriberConstructor,\n\t\t\tconfig.CommandEventMarshaler,\n\t\t\tconfig.Logger,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot create event processor\")\n\t\t}\n\n\t\tif err := eventProcessor.AddHandlersToRouter(config.Router); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn c, nil\n}\n"
  },
  {
    "path": "components/cqrs/cqrs_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\n// TestCQRS is functional test of CQRS command handler and event handler.\nfunc TestCQRS(t *testing.T) {\n\ttestCases := []struct {\n\t\tName       string\n\t\tCreateCqrs func(t *testing.T, cc *CaptureCommandHandler, ce *CaptureEventHandler) (*message.Router, *cqrs.CommandBus, *cqrs.EventBus)\n\t}{\n\t\t{\n\t\t\t// facade is deprecated, testing backwards compatibility\n\t\t\tName: \"facade\",\n\t\t\tCreateCqrs: func(t *testing.T, cc *CaptureCommandHandler, ce *CaptureEventHandler) (*message.Router, *cqrs.CommandBus, *cqrs.EventBus) {\n\t\t\t\trouter, cqrsFacade := createRouterAndFacade(t, cc, ce)\n\t\t\t\treturn router, cqrsFacade.CommandBus(), cqrsFacade.EventBus()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:       \"constructors\",\n\t\t\tCreateCqrs: createCqrsComponents,\n\t\t},\n\t}\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tcaptureCommandHandler := &CaptureCommandHandler{}\n\t\t\tcaptureEventHandler := &CaptureEventHandler{}\n\n\t\t\trouter, commandBus, eventBus := tc.CreateCqrs(t, captureCommandHandler, captureEventHandler)\n\n\t\t\tpointerCmd := &TestCommand{ID: watermill.NewULID()}\n\t\t\trequire.NoError(t, commandBus.Send(context.Background(), pointerCmd))\n\t\t\tassert.EqualValues(t, []interface{}{pointerCmd}, captureCommandHandler.HandledCommands())\n\t\t\tcaptureCommandHandler.Reset()\n\n\t\t\tnonPointerCmd := TestCommand{ID: watermill.NewULID()}\n\t\t\trequire.NoError(t, commandBus.Send(context.Background(), nonPointerCmd))\n\t\t\t// command is always unmarshaled to pointer value\n\t\t\tassert.EqualValues(t, []interface{}{&nonPointerCmd}, captureCommandHandler.HandledCommands())\n\t\t\tcaptureCommandHandler.Reset()\n\n\t\t\tpointerEvent := &TestEvent{ID: watermill.NewULID()}\n\t\t\trequire.NoError(t, eventBus.Publish(context.Background(), pointerEvent))\n\t\t\tassert.EqualValues(t, []interface{}{pointerEvent}, captureEventHandler.HandledEvents())\n\t\t\tcaptureEventHandler.Reset()\n\n\t\t\tnonPointerEvent := TestEvent{ID: watermill.NewULID()}\n\t\t\trequire.NoError(t, eventBus.Publish(context.Background(), nonPointerEvent))\n\t\t\t// event is always unmarshaled to pointer value\n\t\t\tassert.EqualValues(t, []interface{}{&nonPointerEvent}, captureEventHandler.HandledEvents())\n\t\t\tcaptureEventHandler.Reset()\n\n\t\t\tassert.NoError(t, router.Close())\n\t\t})\n\t}\n}\n\nfunc createCqrsComponents(t *testing.T, commandHandler *CaptureCommandHandler, eventHandler *CaptureEventHandler) (*message.Router, *cqrs.CommandBus, *cqrs.EventBus) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn params.EventName, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: true,\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\tassert.Equal(t, \"CaptureEventHandler\", params.HandlerName)\n\n\t\t\t\tassert.Implements(t, new(cqrs.EventHandler), params.EventHandler)\n\t\t\t\tassert.NotNil(t, params.EventHandler)\n\n\t\t\t\treturn ts.EventsPubSub, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = eventProcessor.AddHandlers(eventHandler)\n\trequire.NoError(t, err)\n\n\teventBus, err := cqrs.NewEventBusWithConfig(\n\t\tts.EventsPubSub,\n\t\tcqrs.EventBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\t\tassert.Equal(t, \"cqrs_test.TestEvent\", params.EventName)\n\n\t\t\t\tswitch cmd := params.Event.(type) {\n\t\t\t\tcase *TestEvent:\n\t\t\t\t\tassert.NotEmpty(t, cmd.ID)\n\t\t\t\tcase TestEvent:\n\t\t\t\t\tassert.NotEmpty(t, cmd.ID)\n\t\t\t\tdefault:\n\t\t\t\t\tassert.Fail(t, \"unexpected command type: %T\", cmd)\n\t\t\t\t}\n\n\t\t\t\tassert.NotEmpty(t, params.Event)\n\n\t\t\t\treturn params.EventName, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.CommandProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\tassert.Equal(t, \"cqrs_test.TestCommand\", params.CommandName)\n\n\t\t\t\tassert.Implements(t, new(cqrs.CommandHandler), params.CommandHandler)\n\t\t\t\tassert.NotNil(t, params.CommandHandler)\n\n\t\t\t\treturn params.CommandName, nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\tassert.Equal(t, \"CaptureCommandHandler\", params.HandlerName)\n\n\t\t\t\treturn ts.CommandsPubSub, nil\n\t\t\t},\n\t\t\tMarshaler:                ts.Marshaler,\n\t\t\tLogger:                   ts.Logger,\n\t\t\tAckCommandHandlingErrors: false,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = commandProcessor.AddHandlers(commandHandler)\n\trequire.NoError(t, err)\n\n\tcommandBus, err := cqrs.NewCommandBusWithConfig(ts.CommandsPubSub, cqrs.CommandBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\tassert.Equal(t, \"cqrs_test.TestCommand\", params.CommandName)\n\n\t\t\tswitch cmd := params.Command.(type) {\n\t\t\tcase *TestCommand:\n\t\t\t\tassert.NotEmpty(t, cmd.ID)\n\t\t\tcase TestCommand:\n\t\t\t\tassert.NotEmpty(t, cmd.ID)\n\t\t\tdefault:\n\t\t\t\tassert.Fail(t, \"unexpected command type: %T\", cmd)\n\t\t\t}\n\n\t\t\tassert.NotNil(t, params.Command)\n\n\t\t\treturn params.CommandName, nil\n\t\t},\n\t\tMarshaler: ts.Marshaler,\n\t\tLogger:    ts.Logger,\n\t})\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\trequire.NoError(t, router.Run(context.Background()))\n\t}()\n\n\t<-router.Running()\n\n\treturn router, commandBus, eventBus\n}\n\nfunc createRouterAndFacade(t *testing.T, commandHandler *CaptureCommandHandler, eventHandler *CaptureEventHandler) (*message.Router, *cqrs.Facade) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tc, err := cqrs.NewFacade(cqrs.FacadeConfig{\n\t\tGenerateCommandsTopic: func(commandName string) string {\n\t\t\tassert.Equal(t, \"cqrs_test.TestCommand\", commandName)\n\n\t\t\treturn commandName\n\t\t},\n\t\tGenerateEventsTopic: func(eventName string) string {\n\t\t\tassert.Equal(t, \"cqrs_test.TestEvent\", eventName)\n\n\t\t\treturn eventName\n\t\t},\n\t\tCommandHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.CommandHandler {\n\t\t\trequire.NotNil(t, cb)\n\t\t\trequire.NotNil(t, eb)\n\n\t\t\treturn []cqrs.CommandHandler{commandHandler}\n\t\t},\n\t\tEventHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.EventHandler {\n\t\t\trequire.NotNil(t, cb)\n\t\t\trequire.NotNil(t, eb)\n\n\t\t\treturn []cqrs.EventHandler{eventHandler}\n\t\t},\n\t\tRouter:            router,\n\t\tCommandsPublisher: ts.CommandsPubSub,\n\t\tCommandsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {\n\t\t\tassert.Equal(t, \"CaptureCommandHandler\", handlerName)\n\n\t\t\treturn ts.CommandsPubSub, nil\n\t\t},\n\t\tEventsPublisher: ts.EventsPubSub,\n\t\tEventsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {\n\t\t\tassert.Equal(t, \"CaptureEventHandler\", handlerName)\n\n\t\t\treturn ts.EventsPubSub, nil\n\t\t},\n\t\tLogger:                ts.Logger,\n\t\tCommandEventMarshaler: ts.Marshaler,\n\t})\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\trequire.NoError(t, router.Run(context.Background()))\n\t}()\n\n\t<-router.Running()\n\n\tassert.Equal(t, c.CommandEventMarshaler(), ts.Marshaler)\n\n\treturn router, c\n}\n\ntype TestServices struct {\n\tLogger         watermill.LoggerAdapter\n\tCommandsPubSub *gochannel.GoChannel\n\tEventsPubSub   *gochannel.GoChannel\n\tMarshaler      cqrs.CommandEventMarshaler\n}\n\nfunc NewTestServices() TestServices {\n\tlogger := watermill.NewStdLogger(true, true)\n\n\treturn TestServices{\n\t\tLogger: logger,\n\t\tCommandsPubSub: gochannel.NewGoChannel(\n\t\t\tgochannel.Config{BlockPublishUntilSubscriberAck: true},\n\t\t\tlogger,\n\t\t),\n\t\tEventsPubSub: gochannel.NewGoChannel(\n\t\t\tgochannel.Config{BlockPublishUntilSubscriberAck: true},\n\t\t\tlogger,\n\t\t),\n\t\tMarshaler: cqrs.JSONMarshaler{},\n\t}\n}\n\ntype TestCommand struct {\n\tID string\n}\n\ntype CaptureCommandHandler struct {\n\thandledCommands []interface{}\n}\n\nfunc (h CaptureCommandHandler) HandlerName() string {\n\treturn \"CaptureCommandHandler\"\n}\n\nfunc (h CaptureCommandHandler) HandledCommands() []interface{} {\n\treturn h.handledCommands\n}\n\nfunc (h *CaptureCommandHandler) Reset() {\n\th.handledCommands = nil\n}\n\nfunc (CaptureCommandHandler) NewCommand() interface{} {\n\treturn &TestCommand{}\n}\n\nfunc (h *CaptureCommandHandler) Handle(ctx context.Context, cmd interface{}) error {\n\th.handledCommands = append(h.handledCommands, cmd.(*TestCommand))\n\treturn nil\n}\n\ntype TestEvent struct {\n\tID   string\n\tWhen time.Time\n}\n\ntype AnotherTestEvent struct {\n\tID string\n}\n\ntype CaptureEventHandler struct {\n\thandledEvents []interface{}\n}\n\nfunc (h CaptureEventHandler) HandlerName() string {\n\treturn \"CaptureEventHandler\"\n}\n\nfunc (h CaptureEventHandler) HandledEvents() []interface{} {\n\treturn h.handledEvents\n}\n\nfunc (h *CaptureEventHandler) Reset() {\n\th.handledEvents = nil\n}\n\nfunc (CaptureEventHandler) NewEvent() interface{} {\n\treturn &TestEvent{}\n}\n\nfunc (h *CaptureEventHandler) Handle(ctx context.Context, event interface{}) error {\n\th.handledEvents = append(h.handledEvents, event.(*TestEvent))\n\treturn nil\n}\n\ntype assertPublishTopicPublisher struct {\n\tExpectedTopic string\n\tT             *testing.T\n}\n\nfunc (a assertPublishTopicPublisher) Publish(topic string, messages ...*message.Message) error {\n\tassert.Equal(a.T, a.ExpectedTopic, topic)\n\treturn nil\n}\n\nfunc (assertPublishTopicPublisher) Close() error {\n\treturn nil\n}\n\ntype publisherStub struct {\n\tmessages map[string]message.Messages\n\n\tmu sync.Mutex\n}\n\nfunc newPublisherStub() *publisherStub {\n\treturn &publisherStub{\n\t\tmessages: make(map[string]message.Messages),\n\t}\n}\n\nfunc (*publisherStub) Close() error {\n\treturn nil\n}\n\nfunc (p *publisherStub) Publish(topic string, messages ...*message.Message) error {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.messages[topic] = append(p.messages[topic], messages...)\n\n\treturn nil\n}\n\nfunc TestFacadeConfig_Validate(t *testing.T) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tvalidConfig := cqrs.FacadeConfig{\n\t\tGenerateCommandsTopic: func(commandName string) string {\n\t\t\treturn commandName\n\t\t},\n\t\tGenerateEventsTopic: func(eventName string) string {\n\t\t\treturn eventName\n\t\t},\n\t\tCommandHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.CommandHandler {\n\t\t\treturn []cqrs.CommandHandler{}\n\t\t},\n\t\tEventHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.EventHandler {\n\t\t\treturn []cqrs.EventHandler{}\n\t\t},\n\t\tRouter:            router,\n\t\tCommandsPublisher: ts.CommandsPubSub,\n\t\tCommandsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {\n\t\t\treturn ts.CommandsPubSub, nil\n\t\t},\n\t\tEventsPublisher: ts.EventsPubSub,\n\t\tEventsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {\n\t\t\treturn ts.EventsPubSub, nil\n\t\t},\n\t\tLogger:                ts.Logger,\n\t\tCommandEventMarshaler: ts.Marshaler,\n\t}\n\n\ttestCases := []struct {\n\t\tName   string\n\t\tConfig cqrs.FacadeConfig\n\t\tValid  bool\n\t}{\n\t\t{\n\t\t\tName:   \"valid\",\n\t\t\tConfig: validConfig,\n\t\t\tValid:  true,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateCommandsTopic\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.GenerateCommandsTopic = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_CommandsSubscriberConstructor\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.CommandsSubscriberConstructor = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_CommandsPublisher\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.CommandsPublisher = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateEventsTopic\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.GenerateEventsTopic = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateEventsTopic\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.EventsSubscriberConstructor = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_EventsPublisher\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.EventsPublisher = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_Router\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.Router = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_Logger\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.Logger = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_CommandEventMarshaler\",\n\t\t\tConfig: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {\n\t\t\t\tconfig.CommandEventMarshaler = nil\n\t\t\t}),\n\t\t\tValid: false,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tif c.Valid {\n\t\t\t\tassert.NoError(t, c.Config.Validate())\n\t\t\t} else {\n\t\t\t\tassert.Error(t, c.Config.Validate())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc transformConfig(config cqrs.FacadeConfig, transformFn func(config *cqrs.FacadeConfig)) cqrs.FacadeConfig {\n\ttransformFn(&config)\n\treturn config\n}\n"
  },
  {
    "path": "components/cqrs/ctx.go",
    "content": "package cqrs\n\nimport (\n\t\"context\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype ctxKey string\n\nconst (\n\toriginalMessage ctxKey = \"original_message\"\n)\n\n// OriginalMessageFromCtx returns the original message that was received by the event/command handler.\nfunc OriginalMessageFromCtx(ctx context.Context) *message.Message {\n\tval, ok := ctx.Value(originalMessage).(*message.Message)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn val\n}\n\n// CtxWithOriginalMessage returns a new context with the original message attached.\nfunc CtxWithOriginalMessage(ctx context.Context, msg *message.Message) context.Context {\n\treturn context.WithValue(ctx, originalMessage, msg)\n}\n"
  },
  {
    "path": "components/cqrs/doc.go",
    "content": "// Detailed CQRS documentation can be found in https://watermill.io/docs/cqrs/\n\npackage cqrs\n"
  },
  {
    "path": "components/cqrs/event_bus.go",
    "content": "package cqrs\n\nimport (\n\t\"context\"\n\tstdErrors \"errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\ntype EventBusConfig struct {\n\t// GeneratePublishTopic is used to generate topic name for publishing event.\n\tGeneratePublishTopic GenerateEventPublishTopicFn\n\n\t// OnPublish is called before sending the event.\n\t// The *message.Message can be modified.\n\t//\n\t// This option is not required.\n\tOnPublish OnEventSendFn\n\n\t// Marshaler is used to marshal and unmarshal events.\n\t// It is required.\n\tMarshaler CommandEventMarshaler\n\n\t// Logger instance used to log.\n\t// If not provided, watermill.NopLogger is used.\n\tLogger watermill.LoggerAdapter\n}\n\nfunc (c *EventBusConfig) setDefaults() {\n\tif c.Logger == nil {\n\t\tc.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (c EventBusConfig) Validate() error {\n\tvar err error\n\n\tif c.Marshaler == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing Marshaler\"))\n\t}\n\n\tif c.GeneratePublishTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing GenerateHandlerTopic\"))\n\t}\n\n\treturn err\n}\n\ntype GenerateEventPublishTopicFn func(GenerateEventPublishTopicParams) (string, error)\n\ntype GenerateEventPublishTopicParams struct {\n\tEventName string\n\tEvent     any\n}\n\ntype OnEventSendFn func(params OnEventSendParams) error\n\ntype OnEventSendParams struct {\n\tEventName string\n\tEvent     any\n\n\t// Message is never nil and can be modified.\n\tMessage *message.Message\n}\n\n// EventBus transports events to event handlers.\ntype EventBus struct {\n\tpublisher message.Publisher\n\tconfig    EventBusConfig\n}\n\n// NewEventBus creates a new CommandBus.\n// Deprecated: use NewEventBusWithConfig instead.\nfunc NewEventBus(\n\tpublisher message.Publisher,\n\tgenerateTopic func(eventName string) string,\n\tmarshaler CommandEventMarshaler,\n) (*EventBus, error) {\n\tif publisher == nil {\n\t\treturn nil, errors.New(\"missing publisher\")\n\t}\n\tif generateTopic == nil {\n\t\treturn nil, errors.New(\"missing generateTopic\")\n\t}\n\tif marshaler == nil {\n\t\treturn nil, errors.New(\"missing marshaler\")\n\t}\n\n\treturn &EventBus{\n\t\tpublisher: publisher,\n\t\tconfig: EventBusConfig{\n\t\t\tGeneratePublishTopic: func(params GenerateEventPublishTopicParams) (string, error) {\n\t\t\t\treturn generateTopic(params.EventName), nil\n\t\t\t},\n\t\t\tMarshaler: marshaler,\n\t\t},\n\t}, nil\n}\n\n// NewEventBusWithConfig creates a new EventBus.\nfunc NewEventBusWithConfig(publisher message.Publisher, config EventBusConfig) (*EventBus, error) {\n\tif publisher == nil {\n\t\treturn nil, errors.New(\"missing publisher\")\n\t}\n\n\tconfig.setDefaults()\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config\")\n\t}\n\n\treturn &EventBus{publisher, config}, nil\n}\n\n// Publish sends event to the event bus.\nfunc (c EventBus) Publish(ctx context.Context, event any) error {\n\tmsg, err := c.config.Marshaler.Marshal(event)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\teventName := c.config.Marshaler.Name(event)\n\ttopicName, err := c.config.GeneratePublishTopic(GenerateEventPublishTopicParams{\n\t\tEventName: eventName,\n\t\tEvent:     event,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"cannot generate topic\")\n\t}\n\n\tmsg.SetContext(ctx)\n\n\tif c.config.OnPublish != nil {\n\t\terr := c.config.OnPublish(OnEventSendParams{\n\t\t\tEventName: eventName,\n\t\t\tEvent:     event,\n\t\t\tMessage:   msg,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"cannot execute OnPublish\")\n\t\t}\n\t}\n\n\treturn c.publisher.Publish(topicName, msg)\n}\n"
  },
  {
    "path": "components/cqrs/event_bus_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEventBusConfig_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tName              string\n\t\tModifyValidConfig func(*cqrs.EventBusConfig)\n\t\tExpectedErr       error\n\t}{\n\t\t{\n\t\t\tName:              \"valid_config\",\n\t\t\tModifyValidConfig: nil,\n\t\t\tExpectedErr:       nil,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateEventPublishTopic\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventBusConfig) {\n\t\t\t\tconfig.GeneratePublishTopic = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing GenerateHandlerTopic\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_marshaler\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventBusConfig) {\n\t\t\t\tconfig.Marshaler = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing Marshaler\"),\n\t\t},\n\t}\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tvalidConfig := cqrs.EventBusConfig{\n\t\t\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t},\n\t\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\t}\n\n\t\t\tif tc.ModifyValidConfig != nil {\n\t\t\t\ttc.ModifyValidConfig(&validConfig)\n\t\t\t}\n\n\t\t\terr := validConfig.Validate()\n\t\t\tif tc.ExpectedErr == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, tc.ExpectedErr.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewEventBus(t *testing.T) {\n\tpub := newPublisherStub()\n\tgenerateTopic := func(commandName string) string {\n\t\treturn \"\"\n\t}\n\tmarshaler := cqrs.JSONMarshaler{}\n\n\tcb, err := cqrs.NewEventBus(pub, generateTopic, marshaler)\n\tassert.NotNil(t, cb)\n\tassert.NoError(t, err)\n\n\tcb, err = cqrs.NewEventBus(nil, generateTopic, marshaler)\n\tassert.Nil(t, cb)\n\tassert.Error(t, err)\n\n\tcb, err = cqrs.NewEventBus(pub, nil, marshaler)\n\tassert.Nil(t, cb)\n\tassert.Error(t, err)\n\n\tcb, err = cqrs.NewEventBus(pub, generateTopic, nil)\n\tassert.Nil(t, cb)\n\tassert.Error(t, err)\n}\n\nfunc TestEventBus_Send_ContextPropagation(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\teventBus, err := cqrs.NewEventBus(\n\t\tpublisher,\n\t\tfunc(eventName string) string {\n\t\t\treturn \"whatever\"\n\t\t},\n\t\tcqrs.JSONMarshaler{},\n\t)\n\trequire.NoError(t, err)\n\n\tctx := context.WithValue(context.Background(), contextKey(\"key\"), \"value\")\n\n\terr = eventBus.Publish(ctx, \"message\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, ctx, publisher.messages[\"whatever\"][0].Context())\n}\n\nfunc TestEventBus_Send_topic_name(t *testing.T) {\n\tcb, err := cqrs.NewEventBus(\n\t\tassertPublishTopicPublisher{ExpectedTopic: \"cqrs_test.TestEvent\", T: t},\n\t\tfunc(commandName string) string {\n\t\t\treturn commandName\n\t\t},\n\t\tcqrs.JSONMarshaler{},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cb.Publish(context.Background(), TestEvent{})\n\trequire.NoError(t, err)\n}\n\nfunc TestEventBus_Send_OnPublish(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\teb, err := cqrs.NewEventBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.EventBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\tOnPublish: func(params cqrs.OnEventSendParams) error {\n\t\t\t\tparams.Message.Metadata.Set(\"key\", \"value\")\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = eb.Publish(context.Background(), TestEvent{})\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"value\", publisher.messages[\"whatever\"][0].Metadata.Get(\"key\"))\n}\n\nfunc TestEventBus_Send_OnPublish_error(t *testing.T) {\n\tpublisher := newPublisherStub()\n\n\texpectedErr := errors.New(\"some error\")\n\n\teb, err := cqrs.NewEventBusWithConfig(\n\t\tpublisher,\n\t\tcqrs.EventBusConfig{\n\t\t\tGeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {\n\t\t\t\treturn \"whatever\", nil\n\t\t\t},\n\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\tOnPublish: func(params cqrs.OnEventSendParams) error {\n\t\t\t\treturn expectedErr\n\t\t\t},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = eb.Publish(context.Background(), TestEvent{})\n\trequire.EqualError(t, err, \"cannot execute OnPublish: some error\")\n}\n"
  },
  {
    "path": "components/cqrs/event_handler.go",
    "content": "package cqrs\n\nimport (\n\t\"context\"\n)\n\n// EventHandler receives events defined by NewEvent and handles them with its Handle method.\n// If using DDD, CommandHandler may modify and persist the aggregate.\n// It can also invoke a process manager, a saga or just build a read model.\n//\n// In contrast to CommandHandler, every Event can have multiple EventHandlers.\n//\n// One instance of EventHandler is used during handling messages.\n// When multiple events are delivered at the same time, Handle method can be executed multiple times at the same time.\n// Because of that, Handle method needs to be thread safe!\ntype EventHandler interface {\n\t// HandlerName is the name used in message.Router while creating handler.\n\t//\n\t// It will be also passed to EventsSubscriberConstructor.\n\t// May be useful, for example, to create a consumer group per each handler.\n\t//\n\t// WARNING: If HandlerName was changed and is used for generating consumer groups,\n\t// it may result with **reconsuming all messages** !!!\n\tHandlerName() string\n\n\tNewEvent() any\n\n\tHandle(ctx context.Context, event any) error\n}\n\ntype genericEventHandler[T any] struct {\n\thandleFunc  func(ctx context.Context, event *T) error\n\thandlerName string\n}\n\n// NewEventHandler creates a new EventHandler implementation based on provided function\n// and event type inferred from function argument.\nfunc NewEventHandler[T any](\n\thandlerName string,\n\thandleFunc func(ctx context.Context, event *T) error,\n) EventHandler {\n\treturn &genericEventHandler[T]{\n\t\thandleFunc:  handleFunc,\n\t\thandlerName: handlerName,\n\t}\n}\n\nfunc (c genericEventHandler[T]) HandlerName() string {\n\treturn c.handlerName\n}\n\nfunc (c genericEventHandler[T]) NewEvent() any {\n\ttVar := new(T)\n\treturn tVar\n}\n\nfunc (c genericEventHandler[T]) Handle(ctx context.Context, e any) error {\n\tevent := e.(*T)\n\treturn c.handleFunc(ctx, event)\n}\n\ntype GroupEventHandler interface {\n\tNewEvent() interface{}\n\tHandle(ctx context.Context, event interface{}) error\n}\n\n// NewGroupEventHandler creates a new GroupEventHandler implementation based on provided function\n// and event type inferred from function argument.\nfunc NewGroupEventHandler[T any](handleFunc func(ctx context.Context, event *T) error) GroupEventHandler {\n\treturn &genericEventHandler[T]{\n\t\thandleFunc: handleFunc,\n\t}\n}\n"
  },
  {
    "path": "components/cqrs/event_handler_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype SomeEvent struct {\n\tFoo string\n}\n\nfunc TestNewEventHandler(t *testing.T) {\n\tcmdToSend := &SomeEvent{\"bar\"}\n\n\tch := cqrs.NewEventHandler(\n\t\t\"some_handler\",\n\t\tfunc(ctx context.Context, cmd *SomeEvent) error {\n\t\t\tassert.Equal(t, cmdToSend, cmd)\n\t\t\treturn fmt.Errorf(\"some error\")\n\t\t},\n\t)\n\n\tassert.Equal(t, \"some_handler\", ch.HandlerName())\n\tassert.Equal(t, &SomeEvent{}, ch.NewEvent())\n\n\terr := ch.Handle(context.Background(), cmdToSend)\n\tassert.EqualError(t, err, \"some error\")\n}\n\nfunc TestNewGroupEventHandler(t *testing.T) {\n\tcmdToSend := &SomeEvent{\"bar\"}\n\n\tch := cqrs.NewGroupEventHandler(\n\t\tfunc(ctx context.Context, cmd *SomeEvent) error {\n\t\t\tassert.Equal(t, cmdToSend, cmd)\n\t\t\treturn fmt.Errorf(\"some error\")\n\t\t},\n\t)\n\n\tassert.Equal(t, &SomeEvent{}, ch.NewEvent())\n\n\terr := ch.Handle(context.Background(), cmdToSend)\n\tassert.EqualError(t, err, \"some error\")\n}\n"
  },
  {
    "path": "components/cqrs/event_processor.go",
    "content": "package cqrs\n\nimport (\n\tstdErrors \"errors\"\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype EventProcessorConfig struct {\n\t// GenerateSubscribeTopic is used to generate topic for subscribing to events.\n\t// If event processor is using handler groups, GenerateSubscribeTopic is used instead.\n\tGenerateSubscribeTopic EventProcessorGenerateSubscribeTopicFn\n\n\t// SubscriberConstructor is used to create subscriber for EventHandler.\n\t//\n\t// This function is called for every EventHandler instance.\n\t// If you want to re-use one subscriber for multiple handlers, use GroupEventProcessor instead.\n\tSubscriberConstructor EventProcessorSubscriberConstructorFn\n\n\t// OnHandle is called before handling event.\n\t// OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling an event.\n\t//\n\t// Because of that, you need to explicitly call params.Handler.Handle() to handle the event.\n\t//\n\t//  func(params EventProcessorOnHandleParams) (err error) {\n\t//      // logic before handle\n\t//      //  (...)\n\t//\n\t//      err := params.Handler.Handle(params.Message.Context(), params.Event)\n\t//\n\t//      // logic after handle\n\t//      //  (...)\n\t//\n\t//      return err\n\t//  }\n\t//\n\t// This option is not required.\n\tOnHandle EventProcessorOnHandleFn\n\n\t// AckOnUnknownEvent is used to decide if message should be acked if event has no handler defined.\n\tAckOnUnknownEvent bool\n\n\t// Marshaler is used to marshal and unmarshal events.\n\t// It is required.\n\tMarshaler CommandEventMarshaler\n\n\t// Logger instance used to log.\n\t// If not provided, watermill.NopLogger is used.\n\tLogger watermill.LoggerAdapter\n\n\t// disableRouterAutoAddHandlers is used to keep backwards compatibility.\n\t// it is set when EventProcessor is created by NewEventProcessor.\n\t// Deprecated: please migrate to NewEventProcessorWithConfig.\n\tdisableRouterAutoAddHandlers bool\n}\n\nfunc (c *EventProcessorConfig) setDefaults() {\n\tif c.Logger == nil {\n\t\tc.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (c EventProcessorConfig) Validate() error {\n\tvar err error\n\n\tif c.Marshaler == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing Marshaler\"))\n\t}\n\n\tif c.GenerateSubscribeTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing GenerateHandlerTopic\"))\n\t}\n\tif c.SubscriberConstructor == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing SubscriberConstructor\"))\n\t}\n\n\treturn err\n}\n\ntype EventProcessorGenerateSubscribeTopicFn func(EventProcessorGenerateSubscribeTopicParams) (string, error)\n\ntype EventProcessorGenerateSubscribeTopicParams struct {\n\tEventName    string\n\tEventHandler EventHandler\n}\n\ntype EventProcessorSubscriberConstructorFn func(EventProcessorSubscriberConstructorParams) (message.Subscriber, error)\n\ntype EventProcessorSubscriberConstructorParams struct {\n\tEventName    string\n\tHandlerName  string\n\tEventHandler EventHandler\n}\n\ntype EventProcessorOnHandleFn func(params EventProcessorOnHandleParams) error\n\ntype EventProcessorOnHandleParams struct {\n\tHandler EventHandler\n\n\tEvent     any\n\tEventName string\n\n\t// Message is never nil and can be modified.\n\tMessage *message.Message\n}\n\n// EventProcessor determines which EventHandler should handle event received from event bus.\ntype EventProcessor struct {\n\trouter   *message.Router\n\thandlers []EventHandler\n\tconfig   EventProcessorConfig\n}\n\n// NewEventProcessorWithConfig creates a new EventProcessor.\nfunc NewEventProcessorWithConfig(router *message.Router, config EventProcessorConfig) (*EventProcessor, error) {\n\tconfig.setDefaults()\n\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config EventProcessor\")\n\t}\n\tif router == nil && !config.disableRouterAutoAddHandlers {\n\t\treturn nil, errors.New(\"missing router\")\n\t}\n\n\treturn &EventProcessor{\n\t\trouter: router,\n\t\tconfig: config,\n\t}, nil\n}\n\n// NewEventProcessor creates a new EventProcessor.\n// Deprecated. Use NewEventProcessorWithConfig instead.\nfunc NewEventProcessor(\n\tindividualHandlers []EventHandler,\n\tgenerateTopic func(eventName string) string,\n\tsubscriberConstructor EventsSubscriberConstructor,\n\tmarshaler CommandEventMarshaler,\n\tlogger watermill.LoggerAdapter,\n) (*EventProcessor, error) {\n\tif len(individualHandlers) == 0 {\n\t\treturn nil, errors.New(\"missing handlers\")\n\t}\n\tif generateTopic == nil {\n\t\treturn nil, errors.New(\"nil generateTopic\")\n\t}\n\tif subscriberConstructor == nil {\n\t\treturn nil, errors.New(\"missing subscriberConstructor\")\n\t}\n\tif marshaler == nil {\n\t\treturn nil, errors.New(\"missing marshaler\")\n\t}\n\tif logger == nil {\n\t\tlogger = watermill.NopLogger{}\n\t}\n\n\teventProcessorConfig := EventProcessorConfig{\n\t\tAckOnUnknownEvent: true, // this is the previous default behaviour - keeping backwards compatibility\n\t\tGenerateSubscribeTopic: func(params EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn generateTopic(params.EventName), nil\n\t\t},\n\t\tSubscriberConstructor: func(params EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn subscriberConstructor(params.HandlerName)\n\t\t},\n\t\tMarshaler:                    marshaler,\n\t\tLogger:                       logger,\n\t\tdisableRouterAutoAddHandlers: true,\n\t}\n\teventProcessorConfig.setDefaults()\n\n\tep, err := NewEventProcessorWithConfig(nil, eventProcessorConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, handler := range individualHandlers {\n\t\tif err := ep.AddHandlers(handler); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn ep, nil\n}\n\n// EventsSubscriberConstructor creates a subscriber for EventHandler.\n// It allows you to create separated customized Subscriber for every command handler.\n//\n// When handler groups are used, handler group is passed as handlerName.\n// Deprecated: please use EventProcessorSubscriberConstructorFn instead.\ntype EventsSubscriberConstructor func(handlerName string) (message.Subscriber, error)\n\n// AddHandlers adds a new EventHandler to the EventProcessor and adds it to the router.\nfunc (p *EventProcessor) AddHandlers(handlers ...EventHandler) error {\n\tif p.config.disableRouterAutoAddHandlers {\n\t\tp.handlers = append(p.handlers, handlers...)\n\t\treturn nil\n\t}\n\n\tfor _, handler := range handlers {\n\t\tif _, err := p.addHandlerToRouter(p.router, handler); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tp.handlers = append(p.handlers, handler)\n\t}\n\n\treturn nil\n}\n\n// AddHandler adds a new EventHandler to the EventProcessor and adds it to the router.\nfunc (p *EventProcessor) AddHandler(handler EventHandler) (*message.Handler, error) {\n\tif p.config.disableRouterAutoAddHandlers {\n\t\tp.handlers = append(p.handlers, handler)\n\n\t\treturn nil, nil\n\t}\n\n\th, err := p.addHandlerToRouter(p.router, handler)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.handlers = append(p.handlers, handler)\n\n\treturn h, nil\n}\n\n// AddHandlersToRouter adds the EventProcessor's handlers to the given router.\n// It should be called only once per EventProcessor instance.\n//\n// It is required to call AddHandlersToRouter only if command processor is created with NewEventProcessor (disableRouterAutoAddHandlers is set to true).\n// Deprecated: please migrate to event processor created by NewEventProcessorWithConfig.\nfunc (p EventProcessor) AddHandlersToRouter(r *message.Router) error {\n\tif !p.config.disableRouterAutoAddHandlers {\n\t\treturn errors.New(\"AddHandlersToRouter should be called only when using deprecated NewEventProcessor\")\n\t}\n\n\tfor i := range p.handlers {\n\t\thandler := p.handlers[i]\n\n\t\tif _, err := p.addHandlerToRouter(r, handler); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p EventProcessor) addHandlerToRouter(r *message.Router, handler EventHandler) (*message.Handler, error) {\n\tif err := validateEvent(handler.NewEvent()); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"invalid event for handler %s\", handler.HandlerName())\n\t}\n\n\thandlerName := handler.HandlerName()\n\teventName := p.config.Marshaler.Name(handler.NewEvent())\n\n\ttopicName, err := p.config.GenerateSubscribeTopic(EventProcessorGenerateSubscribeTopicParams{\n\t\tEventName:    eventName,\n\t\tEventHandler: handler,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"cannot generate topic name for handler %s\", handlerName)\n\t}\n\n\tlogger := p.config.Logger.With(watermill.LogFields{\n\t\t\"event_handler_name\": handlerName,\n\t\t\"topic\":              topicName,\n\t})\n\n\thandlerFunc, err := p.routerHandlerFunc(handler, logger)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif p.config.SubscriberConstructor == nil {\n\t\treturn nil, errors.New(\"missing SubscriberConstructor config option\")\n\t}\n\n\tsubscriber, err := p.config.SubscriberConstructor(EventProcessorSubscriberConstructorParams{\n\t\tEventName:    eventName,\n\t\tHandlerName:  handlerName,\n\t\tEventHandler: handler,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot create subscriber for event processor\")\n\t}\n\n\treturn addHandlerToRouter(p.config.Logger, r, handlerName, topicName, handlerFunc, subscriber), nil\n}\n\nfunc (p EventProcessor) Handlers() []EventHandler {\n\treturn p.handlers\n}\n\nfunc addHandlerToRouter(logger watermill.LoggerAdapter, r *message.Router, handlerName string, topicName string, handlerFunc message.NoPublishHandlerFunc, subscriber message.Subscriber) *message.Handler {\n\tlogger = logger.With(watermill.LogFields{\n\t\t\"event_handler_name\": handlerName,\n\t\t\"topic\":              topicName,\n\t})\n\n\tlogger.Debug(\"Adding CQRS event handler to router\", nil)\n\n\treturn r.AddConsumerHandler(\n\t\thandlerName,\n\t\ttopicName,\n\t\tsubscriber,\n\t\thandlerFunc,\n\t)\n}\n\nfunc (p EventProcessor) routerHandlerFunc(handler EventHandler, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) {\n\tinitEvent := handler.NewEvent()\n\texpectedEventName := p.config.Marshaler.Name(initEvent)\n\n\tif err := validateEvent(initEvent); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn func(msg *message.Message) error {\n\t\tevent := handler.NewEvent()\n\t\tmessageEventName := p.config.Marshaler.NameFromMessage(msg)\n\n\t\tif messageEventName != expectedEventName {\n\t\t\tif !p.config.AckOnUnknownEvent {\n\t\t\t\treturn fmt.Errorf(\"received unexpected event type %s, expected %s\", messageEventName, expectedEventName)\n\t\t\t} else {\n\t\t\t\tlogger.Trace(\"Received different event type than expected, ignoring\", watermill.LogFields{\n\t\t\t\t\t\"message_uuid\":        msg.UUID,\n\t\t\t\t\t\"expected_event_type\": expectedEventName,\n\t\t\t\t\t\"received_event_type\": messageEventName,\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tlogger.Debug(\"Handling event\", watermill.LogFields{\n\t\t\t\"message_uuid\":        msg.UUID,\n\t\t\t\"received_event_type\": messageEventName,\n\t\t})\n\n\t\tctx := CtxWithOriginalMessage(msg.Context(), msg)\n\t\tmsg.SetContext(ctx)\n\n\t\tif err := p.config.Marshaler.Unmarshal(msg, event); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thandle := func(params EventProcessorOnHandleParams) error {\n\t\t\treturn params.Handler.Handle(ctx, params.Event)\n\t\t}\n\t\tif p.config.OnHandle != nil {\n\t\t\thandle = p.config.OnHandle\n\t\t}\n\n\t\terr := handle(EventProcessorOnHandleParams{\n\t\t\tHandler:   handler,\n\t\t\tEvent:     event,\n\t\t\tEventName: messageEventName,\n\t\t\tMessage:   msg,\n\t\t})\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"Error when handling event\", watermill.LogFields{\"err\": err})\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}, nil\n}\n\nfunc validateEvent(event interface{}) error {\n\t// EventHandler's NewEvent must return a pointer, because it is used to unmarshal\n\tif err := isPointer(event); err != nil {\n\t\treturn errors.Wrap(err, \"command must be a non-nil pointer\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "components/cqrs/event_processor_group.go",
    "content": "package cqrs\n\nimport (\n\tstdErrors \"errors\"\n\t\"fmt\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\ntype EventGroupProcessorConfig struct {\n\t// GenerateSubscribeTopic is used to generate topic for subscribing to events for handler groups.\n\t// This option is required for EventProcessor if handler groups are used.\n\tGenerateSubscribeTopic EventGroupProcessorGenerateSubscribeTopicFn\n\n\t// SubscriberConstructor is used to create subscriber for GroupEventHandler.\n\t// This function is called for every events group once - thanks to that it's possible to have one subscription per group.\n\t// It's useful, when we are processing events from one stream and we want to do it in order.\n\tSubscriberConstructor EventGroupProcessorSubscriberConstructorFn\n\n\t// OnHandle is called before handling event.\n\t// OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling an event.\n\t//\n\t// Because of that, you need to explicitly call params.Handler.Handle() to handle the event.\n\t//\n\t//  func(params EventGroupProcessorOnHandleParams) (err error) {\n\t//      // logic before handle\n\t//      //  (...)\n\t//\n\t//      err := params.Handler.Handle(params.Message.Context(), params.Event)\n\t//\n\t//      // logic after handle\n\t//      //  (...)\n\t//\n\t//      return err\n\t//  }\n\t//\n\t// This option is not required.\n\tOnHandle EventGroupProcessorOnHandleFn\n\n\t// AckOnUnknownEvent is used to decide if message should be acked if event has no handler defined.\n\tAckOnUnknownEvent bool\n\n\t// Marshaler is used to marshal and unmarshal events.\n\t// It is required.\n\tMarshaler CommandEventMarshaler\n\n\t// Logger instance used to log.\n\t// If not provided, watermill.NopLogger is used.\n\tLogger watermill.LoggerAdapter\n}\n\nfunc (c *EventGroupProcessorConfig) setDefaults() {\n\tif c.Logger == nil {\n\t\tc.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (c EventGroupProcessorConfig) Validate() error {\n\tvar err error\n\n\tif c.Marshaler == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing Marshaler\"))\n\t}\n\n\tif c.GenerateSubscribeTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing GenerateHandlerGroupTopic\"))\n\t}\n\tif c.SubscriberConstructor == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"missing SubscriberConstructor\"))\n\t}\n\n\treturn err\n}\n\ntype EventGroupProcessorGenerateSubscribeTopicFn func(EventGroupProcessorGenerateSubscribeTopicParams) (string, error)\n\ntype EventGroupProcessorGenerateSubscribeTopicParams struct {\n\tEventGroupName     string\n\tEventGroupHandlers []GroupEventHandler\n}\n\ntype EventGroupProcessorSubscriberConstructorFn func(EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error)\n\ntype EventGroupProcessorSubscriberConstructorParams struct {\n\tEventGroupName     string\n\tEventGroupHandlers []GroupEventHandler\n}\n\ntype EventGroupProcessorOnHandleFn func(params EventGroupProcessorOnHandleParams) error\n\ntype EventGroupProcessorOnHandleParams struct {\n\tGroupName string\n\tHandler   GroupEventHandler\n\n\tEvent     any\n\tEventName string\n\n\t// Message is never nil and can be modified.\n\tMessage *message.Message\n}\n\n// EventGroupProcessor determines which EventHandler should handle event received from event bus.\n// Compared to EventProcessor, EventGroupProcessor allows to have multiple handlers that share the same subscriber instance.\ntype EventGroupProcessor struct {\n\trouter *message.Router\n\n\tgroupEventHandlers map[string][]GroupEventHandler\n\n\tconfig EventGroupProcessorConfig\n}\n\n// NewEventGroupProcessorWithConfig creates a new EventGroupProcessor.\nfunc NewEventGroupProcessorWithConfig(router *message.Router, config EventGroupProcessorConfig) (*EventGroupProcessor, error) {\n\tconfig.setDefaults()\n\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config EventProcessor\")\n\t}\n\tif router == nil {\n\t\treturn nil, errors.New(\"missing router\")\n\t}\n\n\treturn &EventGroupProcessor{\n\t\trouter:             router,\n\t\tgroupEventHandlers: map[string][]GroupEventHandler{},\n\t\tconfig:             config,\n\t}, nil\n}\n\n// AddHandlersGroup adds a new list of GroupEventHandler to the EventGroupProcessor and adds it to the router.\n//\n// Compared to AddHandlers, AddHandlersGroup allows to have multiple handlers that share the same subscriber instance.\n//\n// It's allowed to have multiple handlers for the same event type in one group, but we recommend to not do that.\n// Please keep in mind that those handlers will be processed within the same message.\n// If first handler succeeds and the second fails, the message will be re-delivered and the first will be re-executed.\n//\n// Handlers group needs to be unique within the EventProcessor instance.\n//\n// Handler group name is used as handler's name in router.\nfunc (p *EventGroupProcessor) AddHandlersGroup(groupName string, handlers ...GroupEventHandler) error {\n\tif len(handlers) == 0 {\n\t\treturn errors.New(\"no handlers provided\")\n\t}\n\tif _, ok := p.groupEventHandlers[groupName]; ok {\n\t\treturn fmt.Errorf(\"event handler group '%s' already exists\", groupName)\n\t}\n\n\tif err := p.addHandlerToRouter(p.router, groupName, handlers); err != nil {\n\t\treturn err\n\t}\n\n\tp.groupEventHandlers[groupName] = handlers\n\n\treturn nil\n}\n\nfunc (p EventGroupProcessor) addHandlerToRouter(r *message.Router, groupName string, handlersGroup []GroupEventHandler) error {\n\tfor i, handler := range handlersGroup {\n\t\tif err := validateEvent(handler.NewEvent()); err != nil {\n\t\t\treturn errors.Wrapf(\n\t\t\t\terr,\n\t\t\t\t\"invalid event for handler %T (num %d) in group %s\",\n\t\t\t\thandler,\n\t\t\t\ti,\n\t\t\t\tgroupName,\n\t\t\t)\n\t\t}\n\t}\n\n\ttopicName, err := p.config.GenerateSubscribeTopic(EventGroupProcessorGenerateSubscribeTopicParams{\n\t\tEventGroupName:     groupName,\n\t\tEventGroupHandlers: handlersGroup,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"cannot generate topic name for handler group %s\", groupName)\n\t}\n\n\tlogger := p.config.Logger.With(watermill.LogFields{\n\t\t\"event_handler_group_name\": groupName,\n\t\t\"topic\":                    topicName,\n\t})\n\n\thandlerFunc, err := p.routerHandlerGroupFunc(handlersGroup, groupName, logger)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsubscriber, err := p.config.SubscriberConstructor(EventGroupProcessorSubscriberConstructorParams{\n\t\tEventGroupName:     groupName,\n\t\tEventGroupHandlers: handlersGroup,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"cannot create subscriber for event processor\")\n\t}\n\n\t_ = addHandlerToRouter(p.config.Logger, r, groupName, topicName, handlerFunc, subscriber)\n\n\treturn nil\n}\n\nfunc (p EventGroupProcessor) routerHandlerGroupFunc(handlers []GroupEventHandler, groupName string, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) {\n\treturn func(msg *message.Message) error {\n\t\tmessageEventName := p.config.Marshaler.NameFromMessage(msg)\n\n\t\thandledAnyEvent := false\n\n\t\tfor _, handler := range handlers {\n\t\t\tinitEvent := handler.NewEvent()\n\t\t\texpectedEventName := p.config.Marshaler.Name(initEvent)\n\n\t\t\tevent := handler.NewEvent()\n\n\t\t\tif messageEventName != expectedEventName {\n\t\t\t\tlogger.Trace(\"Received different event type than expected, ignoring\", watermill.LogFields{\n\t\t\t\t\t\"message_uuid\":        msg.UUID,\n\t\t\t\t\t\"expected_event_type\": expectedEventName,\n\t\t\t\t\t\"received_event_type\": messageEventName,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlogger.Debug(\"Handling event\", watermill.LogFields{\n\t\t\t\t\"message_uuid\":        msg.UUID,\n\t\t\t\t\"received_event_type\": messageEventName,\n\t\t\t})\n\n\t\t\tctx := CtxWithOriginalMessage(msg.Context(), msg)\n\t\t\tmsg.SetContext(ctx)\n\n\t\t\tif err := p.config.Marshaler.Unmarshal(msg, event); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\thandle := func(params EventGroupProcessorOnHandleParams) error {\n\t\t\t\treturn params.Handler.Handle(ctx, params.Event)\n\t\t\t}\n\t\t\tif p.config.OnHandle != nil {\n\t\t\t\thandle = p.config.OnHandle\n\t\t\t}\n\n\t\t\terr := handle(EventGroupProcessorOnHandleParams{\n\t\t\t\tGroupName: groupName,\n\t\t\t\tHandler:   handler,\n\t\t\t\tEventName: messageEventName,\n\t\t\t\tEvent:     event,\n\t\t\t\tMessage:   msg,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlogger.Debug(\"Error when handling event\", watermill.LogFields{\"err\": err})\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\thandledAnyEvent = true\n\t\t}\n\t\tif handledAnyEvent {\n\t\t\treturn nil\n\t\t}\n\n\t\tif !p.config.AckOnUnknownEvent {\n\t\t\treturn fmt.Errorf(\"no handler found for event %s\", p.config.Marshaler.NameFromMessage(msg))\n\t\t} else {\n\t\t\tlogger.Trace(\"Received event can't be handled by any handler in handler group\", watermill.LogFields{\n\t\t\t\t\"message_uuid\":        msg.UUID,\n\t\t\t\t\"received_event_type\": messageEventName,\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "components/cqrs/event_processor_group_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEventGroupProcessorConfig_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tName              string\n\t\tModifyValidConfig func(*cqrs.EventGroupProcessorConfig)\n\t\tExpectedErr       error\n\t}{\n\t\t{\n\t\t\tName:              \"valid_config\",\n\t\t\tModifyValidConfig: nil,\n\t\t\tExpectedErr:       nil,\n\t\t},\n\t\t{\n\t\t\tName:        \"valid_with_group_handlers\",\n\t\t\tExpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GroupSubscriberConstructor\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventGroupProcessorConfig) {\n\t\t\t\tconfig.SubscriberConstructor = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing SubscriberConstructor\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateHandlerGroupSubscribeTopic\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventGroupProcessorConfig) {\n\t\t\t\tconfig.GenerateSubscribeTopic = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing GenerateHandlerGroupTopic\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_marshaler\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventGroupProcessorConfig) {\n\t\t\t\tconfig.Marshaler = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing Marshaler\"),\n\t\t},\n\t}\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tvalidConfig := cqrs.EventGroupProcessorConfig{\n\t\t\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t},\n\t\t\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\t\treturn nil, nil\n\t\t\t\t},\n\t\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\t}\n\n\t\t\tif tc.ModifyValidConfig != nil {\n\t\t\t\ttc.ModifyValidConfig(&validConfig)\n\t\t\t}\n\n\t\t\terr := validConfig.Validate()\n\t\t\tif tc.ExpectedErr == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, tc.ExpectedErr.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewEventProcessor_OnGroupHandle(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg1, err := ts.Marshaler.Marshal(&TestEvent{ID: \"1\"})\n\trequire.NoError(t, err)\n\n\tmsg2, err := ts.Marshaler.Marshal(&TestEvent{ID: \"2\"})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg1,\n\t\t\tmsg2,\n\t\t},\n\t}\n\n\thandlerCalled := 0\n\n\tdefer func() {\n\t\t// for msg 1 we are not calling handler - but returning before\n\t\tassert.Equal(t, 1, handlerCalled)\n\t}()\n\n\thandler := cqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\thandlerCalled++\n\t\treturn nil\n\t})\n\n\tonHandleCalled := int64(0)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tconfig := cqrs.EventGroupProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn \"events\", nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn mockSub, nil\n\t\t},\n\t\tOnHandle: func(params cqrs.EventGroupProcessorOnHandleParams) error {\n\t\t\tatomic.AddInt64(&onHandleCalled, 1)\n\n\t\t\tassert.Equal(t, \"some_group\", params.GroupName)\n\n\t\t\tassert.IsType(t, &TestEvent{}, params.Event)\n\t\t\tassert.Equal(t, \"cqrs_test.TestEvent\", params.EventName)\n\t\t\tassert.Equal(t, handler, params.Handler)\n\n\t\t\tif params.Event.(*TestEvent).ID == \"1\" {\n\t\t\t\tassert.Equal(t, msg1, params.Message)\n\t\t\t\treturn errors.New(\"test error\")\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, msg2, params.Message)\n\t\t\t}\n\n\t\t\treturn params.Handler.Handle(params.Message.Context(), params.Event)\n\t\t},\n\t\tMarshaler: ts.Marshaler,\n\t\tLogger:    ts.Logger,\n\t}\n\tcp, err := cqrs.NewEventGroupProcessorWithConfig(router, config)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlersGroup(\"some_group\", handler)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg1.Nacked():\n\t\t// ok\n\tcase <-msg1.Acked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n\n\tselect {\n\tcase <-msg2.Acked():\n\t\t// ok\n\tcase <-msg2.Nacked():\n\t\t// nack received\n\t}\n\n\tassert.EqualValues(t, 2, onHandleCalled)\n}\n\nfunc TestNewEventProcessor_AckOnUnknownEvent_handler_group(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&UnknownEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventGroupProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventGroupProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: true,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlersGroup(\n\t\t\"foo\",\n\t\tcqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\t\treturn nil\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tcase <-msg.Nacked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n}\n\nfunc TestNewEventProcessor_AckOnUnknownEvent_disabled_handler_group(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&UnknownEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventGroupProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventGroupProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: false,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlersGroup(\n\t\t\"foo\",\n\t\tcqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\t\treturn nil\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Nacked():\n\t\t// ok\n\tcase <-msg.Acked():\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n}\n\nfunc TestEventProcessor_handler_group(t *testing.T) {\n\tts := NewTestServices()\n\n\tevent1 := &TestEvent{ID: \"1\"}\n\n\tmsg1, err := ts.Marshaler.Marshal(event1)\n\trequire.NoError(t, err)\n\n\tevent2 := &AnotherTestEvent{ID: \"2\"}\n\n\tmsg2, err := ts.Marshaler.Marshal(event2)\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg1,\n\t\t\tmsg2,\n\t\t},\n\t\tWaitForAckBeforeSendingNext: true,\n\t}\n\n\tvar handlersCalls []int\n\n\thandlers := []cqrs.GroupEventHandler{\n\t\tcqrs.NewGroupEventHandler(func(ctx context.Context, event *TestEvent) error {\n\t\t\tassert.EqualValues(t, event1, event)\n\n\t\t\thandlersCalls = append(handlersCalls, 1)\n\n\t\t\treturn nil\n\t\t}),\n\t\tcqrs.NewGroupEventHandler(func(ctx context.Context, event *AnotherTestEvent) error {\n\t\t\tassert.EqualValues(t, event2, event)\n\n\t\t\thandlersCalls = append(handlersCalls, 2)\n\n\t\t\treturn nil\n\t\t}),\n\t\tcqrs.NewGroupEventHandler(func(ctx context.Context, event *AnotherTestEvent) error {\n\t\t\tassert.EqualValues(t, event2, event)\n\n\t\t\thandlersCalls = append(handlersCalls, 3)\n\n\t\t\treturn nil\n\t\t}),\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\teventProcessor, err := cqrs.NewEventGroupProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventGroupProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\tassert.Equal(t, \"some_group\", params.EventGroupName)\n\t\t\t\tassert.Equal(t, handlers, params.EventGroupHandlers)\n\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\tassert.Equal(t, \"some_group\", params.EventGroupName)\n\t\t\t\tassert.Equal(t, handlers, params.EventGroupHandlers)\n\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = eventProcessor.AddHandlersGroup(\n\t\t\"some_group\",\n\t\thandlers...,\n\t)\n\trequire.NoError(t, err)\n\n\terr = eventProcessor.AddHandlersGroup(\n\t\t\"some_group\",\n\t\thandlers...,\n\t)\n\trequire.ErrorContains(t, err, \"event handler group 'some_group' already exists\")\n\n\terr = eventProcessor.AddHandlersGroup(\n\t\t\"some_group_2\",\n\t)\n\trequire.ErrorContains(t, err, \"no handlers provided\")\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg1.Acked():\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"message 1 not acked\")\n\t}\n\n\tselect {\n\tcase <-msg2.Acked():\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"message 2 not acked\")\n\t}\n\n\tassert.Equal(t, []int{1, 2, 3}, handlersCalls)\n}\n\nfunc TestEventGroupProcessor_original_msg_set_to_ctx(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&TestEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventGroupProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventGroupProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: true,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\tvar msgFromCtx *message.Message\n\n\terr = cp.AddHandlersGroup(\n\t\t\"some_group\",\n\t\tcqrs.NewGroupEventHandler(\n\t\t\tfunc(ctx context.Context, cmd *TestEvent) error {\n\t\t\t\tmsgFromCtx = cqrs.OriginalMessageFromCtx(ctx)\n\t\t\t\treturn nil\n\t\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tcase <-msg.Nacked():\n\t\t// nack received\n\t\tt.Fatal(\"nack received, message should be acked\")\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"timeout waiting for ack\")\n\t}\n\n\trequire.NotNil(t, msgFromCtx)\n\tassert.Equal(t, msg, msgFromCtx)\n}\n"
  },
  {
    "path": "components/cqrs/event_processor_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEventProcessorConfig_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tName              string\n\t\tModifyValidConfig func(*cqrs.EventProcessorConfig)\n\t\tExpectedErr       error\n\t}{\n\t\t{\n\t\t\tName:              \"valid_config\",\n\t\t\tModifyValidConfig: nil,\n\t\t\tExpectedErr:       nil,\n\t\t},\n\t\t{\n\t\t\tName: \"missing_GenerateHandlerSubscribeTopic\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventProcessorConfig) {\n\t\t\t\tconfig.GenerateSubscribeTopic = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing GenerateHandlerTopic\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_marshaler\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventProcessorConfig) {\n\t\t\t\tconfig.Marshaler = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing Marshaler\"),\n\t\t},\n\t\t{\n\t\t\tName: \"missing_subscriber_constructor\",\n\t\t\tModifyValidConfig: func(config *cqrs.EventProcessorConfig) {\n\t\t\t\tconfig.SubscriberConstructor = nil\n\t\t\t},\n\t\t\tExpectedErr: fmt.Errorf(\"missing SubscriberConstructor\"),\n\t\t},\n\t}\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tvalidConfig := cqrs.EventProcessorConfig{\n\t\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t},\n\t\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\t\treturn nil, nil\n\t\t\t\t},\n\t\t\t\tMarshaler: cqrs.JSONMarshaler{},\n\t\t\t}\n\n\t\t\tif tc.ModifyValidConfig != nil {\n\t\t\t\ttc.ModifyValidConfig(&validConfig)\n\t\t\t}\n\n\t\t\terr := validConfig.Validate()\n\t\t\tif tc.ExpectedErr == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, tc.ExpectedErr.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewEventProcessor(t *testing.T) {\n\teventConfig := cqrs.EventProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn \"\", nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t\tMarshaler: cqrs.JSONMarshaler{},\n\t}\n\trequire.NoError(t, eventConfig.Validate())\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, nil)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventProcessorWithConfig(router, eventConfig)\n\tassert.NotNil(t, cp)\n\tassert.NoError(t, err)\n\n\teventConfig.SubscriberConstructor = nil\n\trequire.Error(t, eventConfig.Validate())\n\n\tcp, err = cqrs.NewEventProcessorWithConfig(router, eventConfig)\n\tassert.Nil(t, cp)\n\tassert.Error(t, err)\n}\n\ntype nonPointerEventProcessor struct {\n}\n\nfunc (nonPointerEventProcessor) HandlerName() string {\n\treturn \"nonPointerEventProcessor\"\n}\n\nfunc (nonPointerEventProcessor) NewEvent() interface{} {\n\treturn TestEvent{}\n}\n\nfunc (nonPointerEventProcessor) Handle(ctx context.Context, cmd interface{}) error {\n\tpanic(\"not implemented\")\n}\n\nfunc TestEventProcessor_non_pointer_event(t *testing.T) {\n\tts := NewTestServices()\n\n\thandler := nonPointerEventProcessor{}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = eventProcessor.AddHandlers(handler)\n\tassert.IsType(t, cqrs.NonPointerError{}, errors.Cause(err))\n}\n\ntype duplicateTestEventHandler1 struct{}\n\nfunc (h duplicateTestEventHandler1) HandlerName() string {\n\treturn \"duplicateTestEventHandler1\"\n}\n\nfunc (duplicateTestEventHandler1) NewEvent() interface{} {\n\treturn &TestEvent{}\n}\n\nfunc (h *duplicateTestEventHandler1) Handle(ctx context.Context, event interface{}) error { return nil }\n\ntype duplicateTestEventHandler2 struct{}\n\nfunc (h duplicateTestEventHandler2) HandlerName() string {\n\treturn \"duplicateTestEventHandler2\"\n}\n\nfunc (duplicateTestEventHandler2) NewEvent() interface{} {\n\treturn &TestEvent{}\n}\n\nfunc (h *duplicateTestEventHandler2) Handle(ctx context.Context, event interface{}) error { return nil }\n\nfunc TestEventProcessor_multiple_same_event_handlers(t *testing.T) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\teventProcessor, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\tMarshaler: ts.Marshaler,\n\t\t\tLogger:    ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = eventProcessor.AddHandlers(\n\t\t&duplicateTestEventHandler1{},\n\t\t&duplicateTestEventHandler2{},\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestNewEventProcessor_OnHandle(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg1, err := ts.Marshaler.Marshal(&TestEvent{ID: \"1\"})\n\trequire.NoError(t, err)\n\n\tmsg2, err := ts.Marshaler.Marshal(&TestEvent{ID: \"2\"})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg1,\n\t\t\tmsg2,\n\t\t},\n\t}\n\n\thandlerCalled := 0\n\n\tdefer func() {\n\t\t// for msg 1 we are not calling handler - but returning before\n\t\tassert.Equal(t, 1, handlerCalled)\n\t}()\n\n\thandler := cqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\thandlerCalled++\n\t\treturn nil\n\t})\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tonHandleCalled := int64(0)\n\n\tconfig := cqrs.EventProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn \"events\", nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn mockSub, nil\n\t\t},\n\t\tOnHandle: func(params cqrs.EventProcessorOnHandleParams) error {\n\t\t\tatomic.AddInt64(&onHandleCalled, 1)\n\n\t\t\tassert.IsType(t, &TestEvent{}, params.Event)\n\t\t\tassert.Equal(t, \"cqrs_test.TestEvent\", params.EventName)\n\t\t\tassert.Equal(t, handler, params.Handler)\n\n\t\t\tif params.Event.(*TestEvent).ID == \"1\" {\n\t\t\t\tassert.Equal(t, msg1, params.Message)\n\t\t\t\treturn errors.New(\"test error\")\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, msg2, params.Message)\n\t\t\t}\n\n\t\t\treturn params.Handler.Handle(params.Message.Context(), params.Event)\n\t\t},\n\t\tMarshaler: ts.Marshaler,\n\t\tLogger:    ts.Logger,\n\t}\n\tcp, err := cqrs.NewEventProcessorWithConfig(router, config)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlers(handler)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg1.Nacked():\n\t\t// ok\n\tcase <-msg1.Acked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n\n\tselect {\n\tcase <-msg2.Acked():\n\t\t// ok\n\tcase <-msg2.Nacked():\n\t\t// nack received\n\t}\n\n\tassert.EqualValues(t, 2, onHandleCalled)\n}\n\ntype UnknownEvent struct {\n}\n\nfunc TestNewEventProcessor_AckOnUnknownEvent(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&UnknownEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: true,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlers(\n\t\tcqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\t\treturn nil\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tcase <-msg.Nacked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n}\n\nfunc TestNewEventProcessor_AckOnUnknownEvent_disabled(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&UnknownEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: false,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlers(\n\t\tcqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\t\treturn nil\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Nacked():\n\t\t// ok\n\tcase <-msg.Acked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n}\n\nfunc TestNewEventProcessor_backward_compatibility_of_AckOnUnknownEvent(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&UnknownEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\tcp, err := cqrs.NewEventProcessor(\n\t\t[]cqrs.EventHandler{\n\t\t\tcqrs.NewEventHandler(\"test\", func(ctx context.Context, cmd *TestEvent) error {\n\t\t\t\treturn nil\n\t\t\t}),\n\t\t},\n\t\tfunc(eventName string) string {\n\t\t\treturn \"events\"\n\t\t},\n\t\tfunc(handlerName string) (message.Subscriber, error) {\n\t\t\treturn mockSub, nil\n\t\t},\n\t\tts.Marshaler,\n\t\tts.Logger,\n\t)\n\trequire.NoError(t, err)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlersToRouter(router)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tcase <-msg.Nacked():\n\t\t// ack received\n\t\tt.Fatal(\"ack received, message should be nacked\")\n\t}\n}\n\nfunc TestEventProcessor_AddHandlersToRouter_without_disableRouterAutoAddHandlers(t *testing.T) {\n\tts := NewTestServices()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn ts.EventsPubSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: false,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\terr = cp.AddHandlersToRouter(router)\n\tassert.ErrorContains(t, err, \"AddHandlersToRouter should be called only when using deprecated NewEventProcessor\")\n}\n\nfunc TestEventProcessor_original_msg_set_to_ctx(t *testing.T) {\n\tts := NewTestServices()\n\n\tmsg, err := ts.Marshaler.Marshal(&TestEvent{})\n\trequire.NoError(t, err)\n\n\tmockSub := &mockSubscriber{\n\t\tMessagesToSend: []*message.Message{\n\t\t\tmsg,\n\t\t},\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, ts.Logger)\n\trequire.NoError(t, err)\n\n\tcp, err := cqrs.NewEventProcessorWithConfig(\n\t\trouter,\n\t\tcqrs.EventProcessorConfig{\n\t\t\tGenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\t\treturn \"events\", nil\n\t\t\t},\n\t\t\tSubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\t\treturn mockSub, nil\n\t\t\t},\n\t\t\tAckOnUnknownEvent: true,\n\t\t\tMarshaler:         ts.Marshaler,\n\t\t\tLogger:            ts.Logger,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\tvar msgFromCtx *message.Message\n\n\terr = cp.AddHandlers(cqrs.NewEventHandler(\n\t\t\"handler\", func(ctx context.Context, cmd *TestEvent) error {\n\t\t\tmsgFromCtx = cqrs.OriginalMessageFromCtx(ctx)\n\t\t\treturn nil\n\t\t}),\n\t)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tcase <-msg.Nacked():\n\t\t// nack received\n\t\tt.Fatal(\"nack received, message should be acked\")\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"timeout waiting for ack\")\n\t}\n\n\trequire.NotNil(t, msgFromCtx)\n\tassert.Equal(t, msg, msgFromCtx)\n}\n"
  },
  {
    "path": "components/cqrs/marshaler.go",
    "content": "package cqrs\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// CommandEventMarshaler marshals Commands and Events to Watermill's messages and vice versa.\n// Payload of the command needs to be marshaled to []bytes.\ntype CommandEventMarshaler interface {\n\t// Marshal marshals Command or Event to Watermill's message.\n\tMarshal(v interface{}) (*message.Message, error)\n\n\t// Unmarshal unmarshals watermill's message to v Command or Event.\n\tUnmarshal(msg *message.Message, v interface{}) (err error)\n\n\t// Name returns the name of Command or Event.\n\t// Name is used to determine, that received command or event is event which we want to handle.\n\tName(v interface{}) string\n\n\t// NameFromMessage returns the name of Command or Event from Watermill's message (generated by Marshal).\n\t//\n\t// When we have Command or Event marshaled to Watermill's message,\n\t// we should use NameFromMessage instead of Name to avoid unnecessary unmarshaling.\n\tNameFromMessage(msg *message.Message) string\n}\n\n// CommandEventMarshalerDecorator decorates CommandEventMarshaler with additional functionality.\n// It can be used to add additional metadata to the message.\ntype CommandEventMarshalerDecorator struct {\n\tCommandEventMarshaler\n\n\t// DecorateFunc is called after marshaling the message.\n\tDecorateFunc func(v any, msg *message.Message) error\n}\n\n// Marshal marshals Command or Event to Watermill's message and decorates it.\nfunc (c CommandEventMarshalerDecorator) Marshal(v any) (*message.Message, error) {\n\tmsg, err := c.CommandEventMarshaler.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.DecorateFunc == nil {\n\t\treturn nil, errors.New(\"DecorateFunc is nil\")\n\t}\n\n\tif err := c.DecorateFunc(v, msg); err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot decorate message: %w\", err)\n\t}\n\n\treturn msg, nil\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_json.go",
    "content": "package cqrs\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype JSONMarshaler struct {\n\tNewUUID      func() string\n\tGenerateName func(v interface{}) string\n}\n\nfunc (m JSONMarshaler) Marshal(v interface{}) (*message.Message, error) {\n\tb, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmsg := message.NewMessage(\n\t\tm.newUUID(),\n\t\tb,\n\t)\n\tmsg.Metadata.Set(\"name\", m.Name(v))\n\n\treturn msg, nil\n}\n\nfunc (m JSONMarshaler) newUUID() string {\n\tif m.NewUUID != nil {\n\t\treturn m.NewUUID()\n\t}\n\n\t// default\n\treturn watermill.NewUUID()\n}\n\nfunc (JSONMarshaler) Unmarshal(msg *message.Message, v interface{}) (err error) {\n\treturn json.Unmarshal(msg.Payload, v)\n}\n\nfunc (m JSONMarshaler) Name(cmdOrEvent interface{}) string {\n\tif m.GenerateName != nil {\n\t\treturn m.GenerateName(cmdOrEvent)\n\t}\n\n\treturn FullyQualifiedStructName(cmdOrEvent)\n}\n\nfunc (m JSONMarshaler) NameFromMessage(msg *message.Message) string {\n\treturn msg.Metadata.Get(\"name\")\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_json_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n)\n\nvar jsonEventToMarshal = TestEvent{\n\tID:   watermill.NewULID(),\n\tWhen: time.Date(2016, time.August, 15, 14, 13, 12, 0, time.UTC),\n}\n\nfunc TestJsonMarshaler(t *testing.T) {\n\tmarshaler := cqrs.JSONMarshaler{}\n\n\tmsg, err := marshaler.Marshal(jsonEventToMarshal)\n\trequire.NoError(t, err)\n\n\teventToUnmarshal := TestEvent{}\n\terr = marshaler.Unmarshal(msg, &eventToUnmarshal)\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, jsonEventToMarshal, eventToUnmarshal)\n}\n\nfunc TestJSONMarshaler_Marshal_new_uuid_set(t *testing.T) {\n\tmarshaler := cqrs.JSONMarshaler{\n\t\tNewUUID: func() string {\n\t\t\treturn \"foo\"\n\t\t},\n\t}\n\n\tmsg, err := marshaler.Marshal(jsonEventToMarshal)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, msg.UUID, \"foo\")\n}\n\nfunc TestJSONMarshaler_Marshal_generate_name(t *testing.T) {\n\tmarshaler := cqrs.JSONMarshaler{\n\t\tGenerateName: func(v interface{}) string {\n\t\t\treturn \"foo\"\n\t\t},\n\t}\n\n\tmsg, err := marshaler.Marshal(jsonEventToMarshal)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, msg.Metadata.Get(\"name\"), \"foo\")\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_protobuf.go",
    "content": "package cqrs\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// ProtoMarshaler is the default Protocol Buffers marshaler.\ntype ProtoMarshaler struct {\n\tNewUUID      func() string\n\tGenerateName func(v interface{}) string\n}\n\n// NoProtoMessageError is returned when the given value does not implement proto.Message.\ntype NoProtoMessageError struct {\n\tv interface{}\n}\n\nfunc (e NoProtoMessageError) Error() string {\n\trv := reflect.ValueOf(e.v)\n\tif rv.Kind() != reflect.Ptr {\n\t\treturn \"v is not proto.Message, you must pass pointer value to implement proto.Message\"\n\t}\n\n\treturn \"v is not proto.Message\"\n}\n\n// Marshal marshals the given protobuf's message into watermill's Message.\nfunc (m ProtoMarshaler) Marshal(v interface{}) (*message.Message, error) {\n\tprotoMsg, ok := v.(proto.Message)\n\tif !ok {\n\t\treturn nil, errors.WithStack(NoProtoMessageError{v})\n\t}\n\n\tb, err := proto.Marshal(protoMsg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmsg := message.NewMessage(\n\t\tm.newUUID(),\n\t\tb,\n\t)\n\tmsg.Metadata.Set(\"name\", m.Name(v))\n\n\treturn msg, nil\n}\n\nfunc (m ProtoMarshaler) newUUID() string {\n\tif m.NewUUID != nil {\n\t\treturn m.NewUUID()\n\t}\n\n\t// default\n\treturn watermill.NewUUID()\n}\n\n// Unmarshal unmarshals given watermill's Message into protobuf's message.\nfunc (ProtoMarshaler) Unmarshal(msg *message.Message, v interface{}) (err error) {\n\tprotoV, ok := v.(proto.Message)\n\tif !ok {\n\t\treturn errors.WithStack(NoProtoMessageError{v})\n\t}\n\n\treturn proto.Unmarshal(msg.Payload, protoV)\n}\n\n// Name returns the command or event's name.\nfunc (m ProtoMarshaler) Name(cmdOrEvent interface{}) string {\n\tif m.GenerateName != nil {\n\t\treturn m.GenerateName(cmdOrEvent)\n\t}\n\n\treturn FullyQualifiedStructName(cmdOrEvent)\n}\n\n// NameFromMessage returns the metadata name value for a given Message.\nfunc (m ProtoMarshaler) NameFromMessage(msg *message.Message) string {\n\treturn msg.Metadata.Get(\"name\")\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_protobuf_events_new_test.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.31.0\n// \tprotoc        v4.24.4\n// source: events.proto\n\npackage cqrs_test\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Status int32\n\nconst (\n\tStatus_STATUS_UNSPECIFIED Status = 0\n\tStatus_ACTIVE             Status = 1\n\tStatus_DELETED            Status = 2\n)\n\n// Enum value maps for Status.\nvar (\n\tStatus_name = map[int32]string{\n\t\t0: \"STATUS_UNSPECIFIED\",\n\t\t1: \"ACTIVE\",\n\t\t2: \"DELETED\",\n\t}\n\tStatus_value = map[string]int32{\n\t\t\"STATUS_UNSPECIFIED\": 0,\n\t\t\"ACTIVE\":             1,\n\t\t\"DELETED\":            2,\n\t}\n)\n\nfunc (x Status) Enum() *Status {\n\tp := new(Status)\n\t*p = x\n\treturn p\n}\n\nfunc (x Status) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Status) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_events_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Status) Type() protoreflect.EnumType {\n\treturn &file_events_proto_enumTypes[0]\n}\n\nfunc (x Status) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Status.Descriptor instead.\nfunc (Status) EnumDescriptor() ([]byte, []int) {\n\treturn file_events_proto_rawDescGZIP(), []int{0}\n}\n\ntype TestProtobufLegacyEvent struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tId   string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tWhen *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=when,proto3\" json:\"when,omitempty\"`\n}\n\nfunc (x *TestProtobufLegacyEvent) Reset() {\n\t*x = TestProtobufLegacyEvent{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_events_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *TestProtobufLegacyEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestProtobufLegacyEvent) ProtoMessage() {}\n\nfunc (x *TestProtobufLegacyEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_events_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestProtobufLegacyEvent.ProtoReflect.Descriptor instead.\nfunc (*TestProtobufLegacyEvent) Descriptor() ([]byte, []int) {\n\treturn file_events_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *TestProtobufLegacyEvent) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestProtobufLegacyEvent) GetWhen() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.When\n\t}\n\treturn nil\n}\n\ntype SubEvent struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tTags  []string        `protobuf:\"bytes,1,rep,name=tags,proto3\" json:\"tags,omitempty\"`\n\tFlags map[string]bool `protobuf:\"bytes,2,rep,name=flags,proto3\" json:\"flags,omitempty\" protobuf_key:\"bytes,1,opt,name=key,proto3\" protobuf_val:\"varint,2,opt,name=value,proto3\"`\n}\n\nfunc (x *SubEvent) Reset() {\n\t*x = SubEvent{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_events_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *SubEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SubEvent) ProtoMessage() {}\n\nfunc (x *SubEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_events_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SubEvent.ProtoReflect.Descriptor instead.\nfunc (*SubEvent) Descriptor() ([]byte, []int) {\n\treturn file_events_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *SubEvent) GetTags() []string {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\nfunc (x *SubEvent) GetFlags() map[string]bool {\n\tif x != nil {\n\t\treturn x.Flags\n\t}\n\treturn nil\n}\n\ntype TestComplexProtobufEvent struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tId   string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tData []byte                 `protobuf:\"bytes,2,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tWhen *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=when,proto3\" json:\"when,omitempty\"`\n\t// Complex fields to test edge cases\n\tNestedMap map[string]*SubEvent `protobuf:\"bytes,4,rep,name=nested_map,json=nestedMap,proto3\" json:\"nested_map,omitempty\" protobuf_key:\"bytes,1,opt,name=key,proto3\" protobuf_val:\"bytes,2,opt,name=value,proto3\"`\n\tEvents    []*SubEvent          `protobuf:\"bytes,5,rep,name=events,proto3\" json:\"events,omitempty\"`\n\t// Types that are assignable to Result:\n\t//\n\t//\t*TestComplexProtobufEvent_Success\n\t//\t*TestComplexProtobufEvent_Error\n\t//\t*TestComplexProtobufEvent_Fallback\n\tResult isTestComplexProtobufEvent_Result `protobuf_oneof:\"result\"`\n}\n\nfunc (x *TestComplexProtobufEvent) Reset() {\n\t*x = TestComplexProtobufEvent{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_events_proto_msgTypes[2]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *TestComplexProtobufEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestComplexProtobufEvent) ProtoMessage() {}\n\nfunc (x *TestComplexProtobufEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_events_proto_msgTypes[2]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestComplexProtobufEvent.ProtoReflect.Descriptor instead.\nfunc (*TestComplexProtobufEvent) Descriptor() ([]byte, []int) {\n\treturn file_events_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *TestComplexProtobufEvent) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestComplexProtobufEvent) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *TestComplexProtobufEvent) GetWhen() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.When\n\t}\n\treturn nil\n}\n\nfunc (x *TestComplexProtobufEvent) GetNestedMap() map[string]*SubEvent {\n\tif x != nil {\n\t\treturn x.NestedMap\n\t}\n\treturn nil\n}\n\nfunc (x *TestComplexProtobufEvent) GetEvents() []*SubEvent {\n\tif x != nil {\n\t\treturn x.Events\n\t}\n\treturn nil\n}\n\nfunc (m *TestComplexProtobufEvent) GetResult() isTestComplexProtobufEvent_Result {\n\tif m != nil {\n\t\treturn m.Result\n\t}\n\treturn nil\n}\n\nfunc (x *TestComplexProtobufEvent) GetSuccess() *SubEvent {\n\tif x, ok := x.GetResult().(*TestComplexProtobufEvent_Success); ok {\n\t\treturn x.Success\n\t}\n\treturn nil\n}\n\nfunc (x *TestComplexProtobufEvent) GetError() string {\n\tif x, ok := x.GetResult().(*TestComplexProtobufEvent_Error); ok {\n\t\treturn x.Error\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestComplexProtobufEvent) GetFallback() Status {\n\tif x, ok := x.GetResult().(*TestComplexProtobufEvent_Fallback); ok {\n\t\treturn x.Fallback\n\t}\n\treturn Status_STATUS_UNSPECIFIED\n}\n\ntype isTestComplexProtobufEvent_Result interface {\n\tisTestComplexProtobufEvent_Result()\n}\n\ntype TestComplexProtobufEvent_Success struct {\n\tSuccess *SubEvent `protobuf:\"bytes,6,opt,name=success,proto3,oneof\"`\n}\n\ntype TestComplexProtobufEvent_Error struct {\n\tError string `protobuf:\"bytes,7,opt,name=error,proto3,oneof\"`\n}\n\ntype TestComplexProtobufEvent_Fallback struct {\n\tFallback Status `protobuf:\"varint,8,opt,name=fallback,proto3,enum=cqrs_test.Status,oneof\"`\n}\n\nfunc (*TestComplexProtobufEvent_Success) isTestComplexProtobufEvent_Result() {}\n\nfunc (*TestComplexProtobufEvent_Error) isTestComplexProtobufEvent_Result() {}\n\nfunc (*TestComplexProtobufEvent_Fallback) isTestComplexProtobufEvent_Result() {}\n\nvar File_events_proto protoreflect.FileDescriptor\n\nvar file_events_proto_rawDesc = []byte{\n\t0x0a, 0x0c, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09,\n\t0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,\n\t0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,\n\t0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x59, 0x0a, 0x17, 0x54, 0x65,\n\t0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79,\n\t0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,\n\t0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x77, 0x68, 0x65, 0x6e, 0x18, 0x03, 0x20,\n\t0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,\n\t0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,\n\t0x04, 0x77, 0x68, 0x65, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65,\n\t0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,\n\t0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18,\n\t0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73,\n\t0x74, 0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73,\n\t0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x1a, 0x38, 0x0a, 0x0a,\n\t0x46, 0x6c, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,\n\t0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,\n\t0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c,\n\t0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xcb, 0x03, 0x0a, 0x18, 0x54, 0x65, 0x73, 0x74, 0x43,\n\t0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x45, 0x76,\n\t0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,\n\t0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28,\n\t0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x0a, 0x04, 0x77, 0x68, 0x65, 0x6e, 0x18,\n\t0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,\n\t0x70, 0x52, 0x04, 0x77, 0x68, 0x65, 0x6e, 0x12, 0x51, 0x0a, 0x0a, 0x6e, 0x65, 0x73, 0x74, 0x65,\n\t0x64, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x71,\n\t0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x70,\n\t0x6c, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x45, 0x76, 0x65, 0x6e, 0x74,\n\t0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,\n\t0x09, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4d, 0x61, 0x70, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76,\n\t0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x71, 0x72,\n\t0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,\n\t0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65,\n\t0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f,\n\t0x74, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52,\n\t0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f,\n\t0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,\n\t0x12, 0x2f, 0x0a, 0x08, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x08, 0x20, 0x01,\n\t0x28, 0x0e, 0x32, 0x11, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x53,\n\t0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x08, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63,\n\t0x6b, 0x1a, 0x51, 0x0a, 0x0e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e,\n\t0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,\n\t0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,\n\t0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74,\n\t0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,\n\t0x3a, 0x02, 0x38, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4a, 0x04,\n\t0x08, 0x17, 0x10, 0x1f, 0x2a, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16,\n\t0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,\n\t0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45,\n\t0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02, 0x42,\n\t0x0d, 0x5a, 0x0b, 0x2e, 0x2f, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06,\n\t0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_events_proto_rawDescOnce sync.Once\n\tfile_events_proto_rawDescData = file_events_proto_rawDesc\n)\n\nfunc file_events_proto_rawDescGZIP() []byte {\n\tfile_events_proto_rawDescOnce.Do(func() {\n\t\tfile_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_events_proto_rawDescData)\n\t})\n\treturn file_events_proto_rawDescData\n}\n\nvar file_events_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_events_proto_msgTypes = make([]protoimpl.MessageInfo, 5)\nvar file_events_proto_goTypes = []interface{}{\n\t(Status)(0),                      // 0: cqrs_test.Status\n\t(*TestProtobufLegacyEvent)(nil),  // 1: cqrs_test.TestProtobufLegacyEvent\n\t(*SubEvent)(nil),                 // 2: cqrs_test.SubEvent\n\t(*TestComplexProtobufEvent)(nil), // 3: cqrs_test.TestComplexProtobufEvent\n\tnil,                              // 4: cqrs_test.SubEvent.FlagsEntry\n\tnil,                              // 5: cqrs_test.TestComplexProtobufEvent.NestedMapEntry\n\t(*timestamppb.Timestamp)(nil),    // 6: google.protobuf.Timestamp\n}\nvar file_events_proto_depIdxs = []int32{\n\t6, // 0: cqrs_test.TestProtobufLegacyEvent.when:type_name -> google.protobuf.Timestamp\n\t4, // 1: cqrs_test.SubEvent.flags:type_name -> cqrs_test.SubEvent.FlagsEntry\n\t6, // 2: cqrs_test.TestComplexProtobufEvent.when:type_name -> google.protobuf.Timestamp\n\t5, // 3: cqrs_test.TestComplexProtobufEvent.nested_map:type_name -> cqrs_test.TestComplexProtobufEvent.NestedMapEntry\n\t2, // 4: cqrs_test.TestComplexProtobufEvent.events:type_name -> cqrs_test.SubEvent\n\t2, // 5: cqrs_test.TestComplexProtobufEvent.success:type_name -> cqrs_test.SubEvent\n\t0, // 6: cqrs_test.TestComplexProtobufEvent.fallback:type_name -> cqrs_test.Status\n\t2, // 7: cqrs_test.TestComplexProtobufEvent.NestedMapEntry.value:type_name -> cqrs_test.SubEvent\n\t8, // [8:8] is the sub-list for method output_type\n\t8, // [8:8] is the sub-list for method input_type\n\t8, // [8:8] is the sub-list for extension type_name\n\t8, // [8:8] is the sub-list for extension extendee\n\t0, // [0:8] is the sub-list for field type_name\n}\n\nfunc init() { file_events_proto_init() }\nfunc file_events_proto_init() {\n\tif File_events_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_events_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*TestProtobufLegacyEvent); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_events_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*SubEvent); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_events_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*TestComplexProtobufEvent); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tfile_events_proto_msgTypes[2].OneofWrappers = []interface{}{\n\t\t(*TestComplexProtobufEvent_Success)(nil),\n\t\t(*TestComplexProtobufEvent_Error)(nil),\n\t\t(*TestComplexProtobufEvent_Fallback)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_events_proto_rawDesc,\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   5,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_events_proto_goTypes,\n\t\tDependencyIndexes: file_events_proto_depIdxs,\n\t\tEnumInfos:         file_events_proto_enumTypes,\n\t\tMessageInfos:      file_events_proto_msgTypes,\n\t}.Build()\n\tFile_events_proto = out.File\n\tfile_events_proto_rawDesc = nil\n\tfile_events_proto_goTypes = nil\n\tfile_events_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_protobuf_events_test.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// source: testdata/events.proto\n\npackage cqrs_test\n\nimport (\n\tfmt \"fmt\"\n\tmath \"math\"\n\n\tproto \"github.com/golang/protobuf/proto\"\n\ttimestamp \"github.com/golang/protobuf/ptypes/timestamp\"\n)\n\n// Reference imports to suppress errors if they are not otherwise used.\nvar _ = proto.Marshal\nvar _ = fmt.Errorf\nvar _ = math.Inf\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the proto package it is being compiled against.\n// A compilation error at this line likely means your copy of the\n// proto package needs to be updated.\nconst _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package\n\ntype TestProtobufEvent struct {\n\tId                   string               `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tWhen                 *timestamp.Timestamp `protobuf:\"bytes,3,opt,name=when,proto3\" json:\"when,omitempty\"`\n\tXXX_NoUnkeyedLiteral struct{}             `json:\"-\"`\n\tXXX_unrecognized     []byte               `json:\"-\"`\n\tXXX_sizecache        int32                `json:\"-\"`\n}\n\nfunc (m *TestProtobufEvent) Reset()         { *m = TestProtobufEvent{} }\nfunc (m *TestProtobufEvent) String() string { return proto.CompactTextString(m) }\nfunc (*TestProtobufEvent) ProtoMessage()    {}\nfunc (*TestProtobufEvent) Descriptor() ([]byte, []int) {\n\treturn fileDescriptor_37faf0ac8d97ee4c, []int{0}\n}\n\nfunc (m *TestProtobufEvent) XXX_Unmarshal(b []byte) error {\n\treturn xxx_messageInfo_TestProtobufEvent.Unmarshal(m, b)\n}\nfunc (m *TestProtobufEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {\n\treturn xxx_messageInfo_TestProtobufEvent.Marshal(b, m, deterministic)\n}\nfunc (m *TestProtobufEvent) XXX_Merge(src proto.Message) {\n\txxx_messageInfo_TestProtobufEvent.Merge(m, src)\n}\nfunc (m *TestProtobufEvent) XXX_Size() int {\n\treturn xxx_messageInfo_TestProtobufEvent.Size(m)\n}\nfunc (m *TestProtobufEvent) XXX_DiscardUnknown() {\n\txxx_messageInfo_TestProtobufEvent.DiscardUnknown(m)\n}\n\nvar xxx_messageInfo_TestProtobufEvent proto.InternalMessageInfo\n\nfunc (m *TestProtobufEvent) GetId() string {\n\tif m != nil {\n\t\treturn m.Id\n\t}\n\treturn \"\"\n}\n\nfunc (m *TestProtobufEvent) GetWhen() *timestamp.Timestamp {\n\tif m != nil {\n\t\treturn m.When\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tproto.RegisterType((*TestProtobufEvent)(nil), \"cqrs_test.TestProtobufEvent\")\n}\n\nfunc init() { proto.RegisterFile(\"testdata/events.proto\", fileDescriptor_37faf0ac8d97ee4c) }\n\nvar fileDescriptor_37faf0ac8d97ee4c = []byte{\n\t// 146 bytes of a gzipped FileDescriptorProto\n\t0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2d, 0x49, 0x2d, 0x2e,\n\t0x49, 0x49, 0x2c, 0x49, 0xd4, 0x4f, 0x2d, 0x4b, 0xcd, 0x2b, 0x29, 0xd6, 0x2b, 0x28, 0xca, 0x2f,\n\t0xc9, 0x17, 0xe2, 0x4c, 0x2e, 0x2c, 0x2a, 0x8e, 0x07, 0xc9, 0x49, 0xc9, 0xa7, 0xe7, 0xe7, 0xa7,\n\t0xe7, 0xa4, 0xea, 0x83, 0x25, 0x92, 0x4a, 0xd3, 0xf4, 0x4b, 0x32, 0x73, 0x53, 0x8b, 0x4b, 0x12,\n\t0x73, 0x0b, 0x20, 0x6a, 0x95, 0x82, 0xb9, 0x04, 0x43, 0x52, 0x8b, 0x4b, 0x02, 0xa0, 0xf2, 0xae,\n\t0x20, 0x73, 0x84, 0xf8, 0xb8, 0x98, 0x32, 0x53, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x98,\n\t0x32, 0x53, 0x84, 0xf4, 0xb8, 0x58, 0xca, 0x33, 0x52, 0xf3, 0x24, 0x98, 0x15, 0x18, 0x35, 0xb8,\n\t0x8d, 0xa4, 0xf4, 0x20, 0x86, 0xea, 0xc1, 0x0c, 0xd5, 0x0b, 0x81, 0x19, 0x1a, 0x04, 0x56, 0x97,\n\t0xc4, 0x06, 0x96, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xff, 0x4e, 0x39, 0x0e, 0xa0, 0x00,\n\t0x00, 0x00,\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_protobuf_gogo.go",
    "content": "package cqrs\n\nimport (\n\t\"fmt\"\n\t\"runtime/debug\"\n\n\tstderrors \"errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/gogo/protobuf/proto\"\n\t\"github.com/pkg/errors\"\n\tstdproto \"google.golang.org/protobuf/proto\"\n)\n\n// ProtobufMarshaler a protobuf marshaler using github.com/gogo/protobuf/proto (deprecated).\n//\n// DEPRECATED: Use ProtoMarshaler instead. This marshaler will not work with newer protobuf files.\n// IMPORTANT: This marshaler is backward and forward compatible with ProtoMarshaler.\n// ProtobufMarshaler from Watermill versions until v1.4.3 are not forward compatible with ProtoMarshaler.\n// Suggested migration steps:\n//  1. Update Watermill to v1.4.4 or newer, so all publishers and subscribers will be forward and backward compatible.\n//  2. Change all usages of ProtobufMarshaler to ProtoMarshaler.\ntype ProtobufMarshaler struct {\n\tNewUUID      func() string\n\tGenerateName func(v interface{}) string\n\n\t// DisableStdProtoFallback disables fallback to github.com/golang/protobuf/proto when github.com/gogo/protobuf/proto\n\t// because receiving a message that was marshaled with github.com/golang/protobuf/proto.\n\t// Fallback is enabled by default to enable migration to ProtoMarshaler.\n\tDisableStdProtoFallback bool\n}\n\n// Marshal marshals the given protobuf's message into watermill's Message.\nfunc (m ProtobufMarshaler) Marshal(v interface{}) (msg *message.Message, err error) {\n\tdefer func() {\n\t\t// gogo proto can panic on unmarshal (for example, because it received a message from ProtoMarshaler with oneof)\n\t\tif r := recover(); r != nil {\n\t\t\terr = stderrors.Join(err, fmt.Errorf(\n\t\t\t\t\"github.com/gogo/protobuf/proto panic (we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that): %v\\n%s\",\n\t\t\t\tr,\n\t\t\t\tstring(debug.Stack()),\n\t\t\t))\n\t\t}\n\n\t\tif err != nil && !m.DisableStdProtoFallback {\n\t\t\t_, isStdProtoMsg := v.(stdproto.Message)\n\n\t\t\tif isStdProtoMsg {\n\t\t\t\tmsg, err = m.ToProtoMarshaler().Marshal(v)\n\t\t\t}\n\t\t}\n\t}()\n\n\tprotoMsg, ok := v.(proto.Message)\n\tif !ok {\n\t\treturn nil, errors.WithStack(NoProtoMessageError{v})\n\t}\n\n\tb, err := proto.Marshal(protoMsg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmsg = message.NewMessage(\n\t\tm.newUUID(),\n\t\tb,\n\t)\n\tmsg.Metadata.Set(\"name\", m.Name(v))\n\n\treturn msg, nil\n}\n\nfunc (m ProtobufMarshaler) newUUID() string {\n\tif m.NewUUID != nil {\n\t\treturn m.NewUUID()\n\t}\n\n\t// default\n\treturn watermill.NewUUID()\n}\n\n// Unmarshal unmarshals given watermill's Message into protobuf's message.\nfunc (m ProtobufMarshaler) Unmarshal(msg *message.Message, v interface{}) (err error) {\n\tprotoV, ok := v.(proto.Message)\n\tif !ok {\n\t\treturn errors.WithStack(NoProtoMessageError{v})\n\t}\n\n\tdefer func() {\n\t\t// gogo proto can panic on unmarshal (for example, because it received a message from ProtoMarshaler with oneof)\n\t\tif r := recover(); r != nil {\n\t\t\terr = stderrors.Join(err, fmt.Errorf(\n\t\t\t\t\"github.com/gogo/protobuf/proto panic (we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that): %v\\n%s\",\n\t\t\t\tr,\n\t\t\t\tstring(debug.Stack()),\n\t\t\t))\n\t\t}\n\n\t\tif err != nil && !m.DisableStdProtoFallback {\n\t\t\terr = m.ToProtoMarshaler().Unmarshal(msg, v)\n\t\t}\n\t}()\n\n\treturn proto.Unmarshal(msg.Payload, protoV)\n}\n\nfunc (m ProtobufMarshaler) ToProtoMarshaler() ProtoMarshaler {\n\treturn ProtoMarshaler{\n\t\tNewUUID:      m.NewUUID,\n\t\tGenerateName: m.GenerateName,\n\t}\n}\n\n// Name returns the command or event's name.\nfunc (m ProtobufMarshaler) Name(cmdOrEvent interface{}) string {\n\tif m.GenerateName != nil {\n\t\treturn m.GenerateName(cmdOrEvent)\n\t}\n\n\treturn FullyQualifiedStructName(cmdOrEvent)\n}\n\n// NameFromMessage returns the metadata name value for a given Message.\nfunc (m ProtobufMarshaler) NameFromMessage(msg *message.Message) string {\n\treturn msg.Metadata.Get(\"name\")\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_protobuf_gogo_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProtobufMarshaler_with_fallback(t *testing.T) {\n\tmarshaler := cqrs.ProtobufMarshaler{}\n\n\tassertProtoMarshalUnmarshal(\n\t\tt,\n\t\tmarshaler,\n\t\tmarshaler,\n\t\tnewProtoTestComplexEvent(),\n\t\t&TestComplexProtobufEvent{},\n\t\t\"cqrs_test.TestComplexProtobufEvent\",\n\t)\n\n\tlegacyEvent, _ := newProtoLegacyTestEvent()\n\tassertProtoMarshalUnmarshal(\n\t\tt,\n\t\tmarshaler,\n\t\tmarshaler,\n\t\tlegacyEvent,\n\t\t&TestProtobufEvent{},\n\t\t\"cqrs_test.TestProtobufEvent\",\n\t)\n}\n\nfunc TestProtobufMarshaler_without_fallback_legacy_event(t *testing.T) {\n\tlegacyEvent, _ := newProtoLegacyTestEvent()\n\n\tmarshaler := cqrs.ProtobufMarshaler{\n\t\tDisableStdProtoFallback: true,\n\t}\n\n\tassertProtoMarshalUnmarshal(\n\t\tt,\n\t\tmarshaler,\n\t\tmarshaler,\n\t\tlegacyEvent,\n\t\t&TestProtobufEvent{},\n\t\t\"cqrs_test.TestProtobufEvent\",\n\t)\n}\n\nfunc TestProtobufMarshaler_Marshal_generated_name(t *testing.T) {\n\tmarshaler := cqrs.ProtobufMarshaler{\n\t\tNewUUID: func() string {\n\t\t\treturn \"foo\"\n\t\t},\n\t}\n\n\tmsg, err := marshaler.Marshal(newProtoTestComplexEvent())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, msg.UUID, \"foo\")\n}\n\nfunc TestProtobufMarshaler_catch_panic(t *testing.T) {\n\tmarshalerNoFallback := cqrs.ProtobufMarshaler{\n\t\tDisableStdProtoFallback: true,\n\t}\n\tmarshalerWithFallback := cqrs.ProtobufMarshaler{\n\t\tDisableStdProtoFallback: false,\n\t}\n\n\tcomplexEvent := newProtoTestComplexEvent()\n\n\tmsg, err := marshalerNoFallback.Marshal(complexEvent)\n\tassert.Nil(t, msg)\n\tassert.ErrorContains(t, err, \"(we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that)\")\n\tassert.ErrorContains(t, err, \"invalid memory address or nil pointer dereference\")\n\tassert.ErrorContains(t, err, \"github.com/gogo/protobuf/proto panic\")\n\tassert.ErrorContains(t, err, \"runtime/debug.Stack()\", \"error should contain stack trace\")\n\n\t// let's simulate situation when publishing service uses fallback and consuming service does not\n\tmsg, err = marshalerWithFallback.Marshal(complexEvent)\n\trequire.NoError(t, err)\n\n\terr = marshalerNoFallback.Unmarshal(msg, &TestComplexProtobufEvent{})\n\tassert.ErrorContains(t, err, \"(we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that)\")\n\tassert.ErrorContains(t, err, \"protobuf tag not enough fields in TestComplexProtobufEvent.state\")\n\tassert.ErrorContains(t, err, \"github.com/gogo/protobuf/proto panic\")\n\tassert.ErrorContains(t, err, \"runtime/debug.Stack()\", \"error should contain stack trace\")\n\n\t// marshaler with fallback should handle this message\n\terr = marshalerWithFallback.Unmarshal(msg, &TestComplexProtobufEvent{})\n\trequire.NoError(t, err)\n}\n\nfunc TestProtobufMarshaler_compatible_with_ProtoMarshaler(t *testing.T) {\n\tlegacyEvent, legacyEventRegenerated := newProtoLegacyTestEvent()\n\tcomplexEvent := newProtoTestComplexEvent()\n\n\tdeprecatedMarshaler := cqrs.ProtobufMarshaler{}\n\tnewMarshaler := cqrs.ProtoMarshaler{}\n\n\tt.Run(\"from_deprecated_to_new\", func(t *testing.T) {\n\t\tassertProtoMarshalUnmarshal(\n\t\t\tt,\n\t\t\tdeprecatedMarshaler,\n\t\t\tnewMarshaler,\n\t\t\tcomplexEvent,\n\t\t\t&TestComplexProtobufEvent{},\n\t\t\t\"cqrs_test.TestComplexProtobufEvent\",\n\t\t)\n\n\t\tassertProtoMarshalUnmarshal(\n\t\t\tt,\n\t\t\tdeprecatedMarshaler,\n\t\t\tnewMarshaler,\n\t\t\tlegacyEvent,\n\t\t\t&TestProtobufLegacyEvent{},\n\t\t\t\"cqrs_test.TestProtobufEvent\",\n\t\t)\n\t})\n\n\tt.Run(\"from_new_to_deprecated\", func(t *testing.T) {\n\t\tassertProtoMarshalUnmarshal(\n\t\t\tt,\n\t\t\tnewMarshaler,\n\t\t\tdeprecatedMarshaler,\n\t\t\tcomplexEvent,\n\t\t\t&TestComplexProtobufEvent{},\n\t\t\t\"cqrs_test.TestComplexProtobufEvent\",\n\t\t)\n\n\t\tassertProtoMarshalUnmarshal(\n\t\t\tt,\n\t\t\tnewMarshaler,\n\t\t\tdeprecatedMarshaler,\n\t\t\tlegacyEventRegenerated,\n\t\t\t&TestProtobufEvent{},\n\t\t\t\"cqrs_test.TestProtobufLegacyEvent\",\n\t\t)\n\t})\n}\n"
  },
  {
    "path": "components/cqrs/marshaler_protobuf_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n)\n\nfunc TestProtoMarshaler(t *testing.T) {\n\tassertProtoMarshalUnmarshal(\n\t\tt,\n\t\tcqrs.ProtoMarshaler{},\n\t\tcqrs.ProtoMarshaler{},\n\t\tnewProtoTestComplexEvent(),\n\t\t&TestComplexProtobufEvent{},\n\t\t\"cqrs_test.TestComplexProtobufEvent\",\n\t)\n}\n\nfunc TestProtoMarshaler_Marshal_generated_name(t *testing.T) {\n\tmarshaler := cqrs.ProtoMarshaler{\n\t\tNewUUID: func() string {\n\t\t\treturn \"foo\"\n\t\t},\n\t}\n\n\tmsg, err := marshaler.Marshal(newProtoTestComplexEvent())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, msg.UUID, \"foo\")\n}\n\n// newProtoLegacyTestEvent returns the same event in two different protobuf versions\nfunc newProtoLegacyTestEvent() (*TestProtobufEvent, *TestProtobufLegacyEvent) {\n\twhen := timestamppb.New(time.Now())\n\tid := watermill.NewULID()\n\n\tlegacy := &TestProtobufEvent{\n\t\tId:   id,\n\t\tWhen: when,\n\t}\n\n\tregenerated := &TestProtobufLegacyEvent{\n\t\tId:   id,\n\t\tWhen: when,\n\t}\n\n\treturn legacy, regenerated\n}\n\nfunc newProtoTestComplexEvent() *TestComplexProtobufEvent {\n\twhen := timestamppb.New(time.Now())\n\n\teventToMarshal := &TestComplexProtobufEvent{\n\t\tId:   watermill.NewULID(),\n\t\tData: []byte(\"data\"),\n\t\tWhen: when,\n\t\tNestedMap: map[string]*SubEvent{\n\t\t\t\"foo\": {\n\t\t\t\tTags:  []string{\"tag1\", \"tag2\"},\n\t\t\t\tFlags: map[string]bool{\"flag1\": true, \"flag2\": false},\n\t\t\t},\n\t\t},\n\t\tEvents: []*SubEvent{\n\t\t\t{\n\t\t\t\tTags: []string{\"tag1\", \"tag2\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tTags: []string{\"tag3\", \"tag4\"},\n\t\t\t},\n\t\t},\n\t\tResult: &TestComplexProtobufEvent_Success{\n\t\t\tSuccess: &SubEvent{\n\t\t\t\tTags:  []string{\"tag10\"},\n\t\t\t\tFlags: map[string]bool{\"flag10\": true},\n\t\t\t},\n\t\t},\n\t}\n\treturn eventToMarshal\n}\n\nfunc assertProtoMarshalUnmarshal[T1, T2 fmt.Stringer](\n\tt *testing.T,\n\tmarshaler cqrs.CommandEventMarshaler,\n\tunmarshaler cqrs.CommandEventMarshaler,\n\teventToMarshal T1,\n\teventToUnmarshal T2,\n\texpectedEventName string,\n) {\n\tt.Helper()\n\n\tmsg, err := marshaler.Marshal(eventToMarshal)\n\trequire.NoError(t, err)\n\n\terr = unmarshaler.Unmarshal(msg, eventToUnmarshal)\n\trequire.NoError(t, err)\n\n\teventToMarshalJson, err := json.Marshal(eventToMarshal)\n\trequire.NoError(t, err)\n\n\teventToUnmarshalJson, err := json.Marshal(eventToUnmarshal)\n\trequire.NoError(t, err)\n\n\tassert.JSONEq(t, string(eventToMarshalJson), string(eventToUnmarshalJson))\n\tassert.Equal(t, expectedEventName, msg.Metadata.Get(\"name\"))\n}\n"
  },
  {
    "path": "components/cqrs/name.go",
    "content": "package cqrs\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// FullyQualifiedStructName returns object name in format [package].[type name].\n// For example, for the struct:\n//\n//\tpackage events\n//\ttype UserCreated struct {}\n//\n// it will return \"events.UserCreated\".\n//\n// It ignores if the value is a pointer or not.\nfunc FullyQualifiedStructName(v interface{}) string {\n\ts := fmt.Sprintf(\"%T\", v)\n\ts = strings.TrimLeft(s, \"*\")\n\n\treturn s\n}\n\n// StructName returns struct name in format [type name].\n// For example, for the struct:\n//\n//\tpackage events\n//\ttype UserCreated struct {}\n//\n// it will return \"UserCreated\".\n//\n// It ignores if the value is a pointer or not.\nfunc StructName(v interface{}) string {\n\tsegments := strings.Split(fmt.Sprintf(\"%T\", v), \".\")\n\n\treturn segments[len(segments)-1]\n}\n\ntype namedStruct interface {\n\tName() string\n}\n\n// NamedStruct returns the name from a message implementing the following interface:\n//\n//\ttype namedStruct interface {\n//\t\tName() string\n//\t}\n//\n// It ignores if the value is a pointer or not.\nfunc NamedStruct(fallback func(v interface{}) string) func(v interface{}) string {\n\treturn func(v interface{}) string {\n\t\tif v, ok := v.(namedStruct); ok {\n\t\t\treturn v.Name()\n\t\t}\n\n\t\treturn fallback(v)\n\t}\n}\n"
  },
  {
    "path": "components/cqrs/name_test.go",
    "content": "package cqrs_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n)\n\nfunc TestFullyQualifiedStructName(t *testing.T) {\n\ttype Object struct{}\n\n\tassert.Equal(t, \"cqrs_test.Object\", cqrs.FullyQualifiedStructName(Object{}))\n\tassert.Equal(t, \"cqrs_test.Object\", cqrs.FullyQualifiedStructName(&Object{}))\n}\n\nfunc BenchmarkFullyQualifiedStructName(b *testing.B) {\n\ttype Object struct{}\n\to := Object{}\n\n\tfor i := 0; i < b.N; i++ {\n\t\tcqrs.FullyQualifiedStructName(o)\n\t}\n}\n\nfunc TestStructName(t *testing.T) {\n\ttype Object struct{}\n\n\tassert.Equal(t, \"Object\", cqrs.StructName(Object{}))\n\tassert.Equal(t, \"Object\", cqrs.StructName(&Object{}))\n}\n\nfunc TestNamedStruct(t *testing.T) {\n\tassert.Equal(t, \"named object\", cqrs.NamedStruct(cqrs.StructName)(namedObject{}))\n\tassert.Equal(t, \"named object\", cqrs.NamedStruct(cqrs.StructName)(&namedObject{}))\n\n\t// Test fallback\n\ttype Object struct{}\n\n\tassert.Equal(t, \"Object\", cqrs.NamedStruct(cqrs.StructName)(Object{}))\n\tassert.Equal(t, \"Object\", cqrs.NamedStruct(cqrs.StructName)(&Object{}))\n}\n\ntype namedObject struct{}\n\nfunc (namedObject) Name() string {\n\treturn \"named object\"\n}\n"
  },
  {
    "path": "components/cqrs/object.go",
    "content": "package cqrs\n\nimport (\n\t\"reflect\"\n)\n\nfunc isPointer(v interface{}) error {\n\trv := reflect.ValueOf(v)\n\n\tif rv.Kind() != reflect.Ptr || rv.IsNil() {\n\t\treturn NonPointerError{rv.Type()}\n\t}\n\n\treturn nil\n}\n\ntype NonPointerError struct {\n\tType reflect.Type\n}\n\nfunc (e NonPointerError) Error() string {\n\treturn \"non-pointer command: \" + e.Type.String() + \", handler.NewCommand() should return pointer to the command\"\n}\n"
  },
  {
    "path": "components/cqrs/testdata/events.proto",
    "content": "syntax = \"proto3\";\npackage cqrs_test;\noption go_package = \"./cqrs_test\";\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage TestProtobufLegacyEvent {\n    string id = 1;\n    google.protobuf.Timestamp when = 3;\n}\n\nenum Status {\n    STATUS_UNSPECIFIED = 0;\n    ACTIVE = 1;\n    DELETED = 2;\n}\n\nmessage SubEvent {\n    repeated string tags = 1;\n    map<string, bool> flags = 2;\n}\n\nmessage TestComplexProtobufEvent {\n    string id = 1;\n    bytes data = 2;\n    google.protobuf.Timestamp when = 3;\n\n    map<string, SubEvent> nested_map = 4;\n    repeated SubEvent events = 5;\n\n    oneof result {\n        SubEvent success = 6;\n        string error = 7;\n        Status fallback = 8;\n    }\n\n    reserved 23 to 30;\n}\n"
  },
  {
    "path": "components/delay/delay.go",
    "content": "package delay\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// Delay represents a message's delay.\n// It can be either a delay until a specific time or a delay for a specific duration.\n// The zero value of Delay is a zero delay.\n//\n// IMPORTANT: Delay doesn't work with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.\n// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/\ntype Delay struct {\n\ttime     time.Time\n\tduration time.Duration\n}\n\nfunc (d Delay) IsZero() bool {\n\treturn d.time.IsZero()\n}\n\n// Until returns a delay of the given time.\nfunc Until(delayedUntil time.Time) Delay {\n\treturn Delay{\n\t\ttime:     delayedUntil,\n\t\tduration: delayedUntil.Sub(time.Now().UTC()),\n\t}\n}\n\n// For returns a delay of now plus the given duration.\nfunc For(delayedFor time.Duration) Delay {\n\treturn Delay{\n\t\ttime:     time.Now().UTC().Add(delayedFor),\n\t\tduration: delayedFor,\n\t}\n}\n\ntype contextKey string\n\nvar (\n\tdelayContextKey = contextKey(\"delay\")\n)\n\n// WithContext returns a new context with the given delay.\n// If used together with a publisher wrapped with NewPublisher, the delay will be applied to the message.\n//\n// IMPORTANT: Delay doesn't work with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.\n// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/\nfunc WithContext(ctx context.Context, delay Delay) context.Context {\n\treturn context.WithValue(ctx, delayContextKey, delay)\n}\n\nconst (\n\tDelayedUntilKey = \"_watermill_delayed_until\"\n\tDelayedForKey   = \"_watermill_delayed_for\"\n)\n\n// Message sets the delay metadata on the message.\n//\n// IMPORTANT: Delay doesn't work with all Pub/Subs! Using it won't have any effect on Pub/Subs that don't support it.\n// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/\nfunc Message(msg *message.Message, delay Delay) {\n\tmsg.Metadata.Set(DelayedUntilKey, delay.time.Format(time.RFC3339))\n\tmsg.Metadata.Set(DelayedForKey, delay.duration.String())\n}\n"
  },
  {
    "path": "components/delay/publisher.go",
    "content": "package delay\n\nimport (\n\t\"errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype DefaultDelayGeneratorParams struct {\n\tTopic   string\n\tMessage *message.Message\n}\n\n// PublisherConfig is a configuration for the delay publisher.\ntype PublisherConfig struct {\n\t// DefaultDelayGenerator is a function that generates the default delay for a message.\n\t// If the message doesn't have the delay metadata set, the default delay will be applied.\n\tDefaultDelayGenerator func(params DefaultDelayGeneratorParams) (Delay, error)\n\n\t// AllowNoDelay allows publishing messages without a delay set.\n\t// By default, the publisher returns an error when a message is published without a delay and no default delay generator is provided.\n\tAllowNoDelay bool\n}\n\n// NewPublisher wraps a publisher with a delay mechanism.\n// A message can be published with delay metadata set in the context by using the WithContext function.\n// If the message doesn't have the delay metadata set, the default delay will be applied, if provided.\nfunc NewPublisher(pub message.Publisher, config PublisherConfig) (message.Publisher, error) {\n\treturn &publisher{\n\t\tpub:    pub,\n\t\tconfig: config,\n\t}, nil\n}\n\ntype publisher struct {\n\tpub    message.Publisher\n\tconfig PublisherConfig\n}\n\nfunc (p *publisher) Publish(topic string, messages ...*message.Message) error {\n\tfor i := range messages {\n\t\terr := p.applyDelay(topic, messages[i])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn p.pub.Publish(topic, messages...)\n}\n\nfunc (p *publisher) Close() error {\n\treturn p.pub.Close()\n}\n\nfunc (p *publisher) applyDelay(topic string, msg *message.Message) error {\n\tif msg.Metadata.Get(DelayedForKey) != \"\" {\n\t\treturn nil\n\t}\n\n\tif msg.Context().Value(delayContextKey) != nil {\n\t\tdelay := msg.Context().Value(delayContextKey).(Delay)\n\t\tMessage(msg, delay)\n\t\treturn nil\n\t}\n\n\tif p.config.DefaultDelayGenerator != nil {\n\t\tdelay, err := p.config.DefaultDelayGenerator(DefaultDelayGeneratorParams{\n\t\t\tTopic:   topic,\n\t\t\tMessage: msg,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tMessage(msg, delay)\n\n\t\treturn nil\n\t}\n\n\tif !p.config.AllowNoDelay {\n\t\treturn errors.New(\"message doesn't have a delay set\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "components/delay/publisher_test.go",
    "content": "package delay_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/delay\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nfunc TestPublisher(t *testing.T) {\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, nil)\n\n\tmessages, err := pubSub.Subscribe(context.Background(), \"test\")\n\trequire.NoError(t, err)\n\n\tpub, err := delay.NewPublisher(pubSub, delay.PublisherConfig{})\n\trequire.NoError(t, err)\n\n\tpubAllowNoDelay, err := delay.NewPublisher(pubSub, delay.PublisherConfig{\n\t\tAllowNoDelay: true,\n\t})\n\trequire.NoError(t, err)\n\n\tdefaultDelayPub, err := delay.NewPublisher(pubSub, delay.PublisherConfig{\n\t\tDefaultDelayGenerator: func(params delay.DefaultDelayGeneratorParams) (delay.Delay, error) {\n\t\t\treturn delay.For(1 * time.Second), nil\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tpublisher          message.Publisher\n\t\tmessageConstructor func(id string) *message.Message\n\t\texpectedError      bool\n\t\texpectedDelay      time.Duration\n\t}{\n\t\t{\n\t\t\tname:      \"no delay\",\n\t\t\tpublisher: pub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\treturn message.NewMessage(id, nil)\n\t\t\t},\n\t\t\texpectedError: true,\n\t\t\texpectedDelay: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"no delay but allowed\",\n\t\t\tpublisher: pubAllowNoDelay,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\treturn message.NewMessage(id, nil)\n\t\t\t},\n\t\t\texpectedDelay: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"default delay\",\n\t\t\tpublisher: defaultDelayPub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\treturn message.NewMessage(id, nil)\n\t\t\t},\n\t\t\texpectedDelay: 1 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:      \"delay from metadata\",\n\t\t\tpublisher: pub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\tmsg := message.NewMessage(id, nil)\n\t\t\t\tdelay.Message(msg, delay.For(2*time.Second))\n\t\t\t\treturn msg\n\t\t\t},\n\t\t\texpectedDelay: 2 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:      \"default delay override with metadata\",\n\t\t\tpublisher: defaultDelayPub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\tmsg := message.NewMessage(id, nil)\n\t\t\t\tdelay.Message(msg, delay.For(2*time.Second))\n\t\t\t\treturn msg\n\t\t\t},\n\t\t\texpectedDelay: 2 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:      \"delay from context\",\n\t\t\tpublisher: pub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\tmsg := message.NewMessage(id, nil)\n\t\t\t\tctx := delay.WithContext(context.Background(), delay.For(3*time.Second))\n\t\t\t\tmsg.SetContext(ctx)\n\t\t\t\treturn msg\n\t\t\t},\n\t\t\texpectedDelay: 3 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:      \"default delay override with context\",\n\t\t\tpublisher: defaultDelayPub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\tmsg := message.NewMessage(id, nil)\n\t\t\t\tctx := delay.WithContext(context.Background(), delay.For(3*time.Second))\n\t\t\t\tmsg.SetContext(ctx)\n\t\t\t\treturn msg\n\t\t\t},\n\t\t\texpectedDelay: 3 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:      \"delay with until\",\n\t\t\tpublisher: pub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\tmsg := message.NewMessage(id, nil)\n\t\t\t\tdelay.Message(msg, delay.Until(time.Now().UTC().Add(4*time.Second)))\n\t\t\t\treturn msg\n\t\t\t},\n\t\t\texpectedDelay: 4 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:      \"both metadata and context set\",\n\t\t\tpublisher: defaultDelayPub,\n\t\t\tmessageConstructor: func(id string) *message.Message {\n\t\t\t\tmsg := message.NewMessage(id, nil)\n\t\t\t\tdelay.Message(msg, delay.For(5*time.Second))\n\t\t\t\tctx := delay.WithContext(context.Background(), delay.For(6*time.Second))\n\t\t\t\tmsg.SetContext(ctx)\n\t\t\t\treturn msg\n\t\t\t},\n\t\t\texpectedDelay: 5 * time.Second,\n\t\t},\n\t}\n\n\tfor i, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tid := fmt.Sprint(i)\n\n\t\t\tmsg := testCase.messageConstructor(id)\n\t\t\terr = testCase.publisher.Publish(\"test\", msg)\n\n\t\t\tif testCase.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassertMessage(t, messages, id, testCase.expectedDelay)\n\t\t})\n\t}\n}\n\nfunc assertMessage(t *testing.T, messages <-chan *message.Message, expectedID string, expectedDelay time.Duration) {\n\tt.Helper()\n\tselect {\n\tcase msg := <-messages:\n\t\tassert.Equal(t, expectedID, msg.UUID)\n\n\t\tif expectedDelay == 0 {\n\t\t\tassert.Empty(t, msg.Metadata.Get(delay.DelayedUntilKey))\n\t\t\tassert.Empty(t, msg.Metadata.Get(delay.DelayedForKey))\n\t\t} else {\n\t\t\tdelayedFor, err := time.ParseDuration(msg.Metadata.Get(delay.DelayedForKey))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, expectedDelay, delayedFor.Round(time.Second))\n\n\t\t\tdelayedUntil, err := time.Parse(time.RFC3339, msg.Metadata.Get(delay.DelayedUntilKey))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.WithinDuration(t, time.Now().UTC().Add(expectedDelay), delayedUntil, 1*time.Second)\n\t\t}\n\n\t\tmsg.Ack()\n\tcase <-time.After(100 * time.Millisecond):\n\t\trequire.Fail(t, \"timeout\")\n\t}\n}\n"
  },
  {
    "path": "components/fanin/fanin.go",
    "content": "package fanin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype Config struct {\n\t// SourceTopics contains topics on which FanIn subscribes.\n\tSourceTopics []string\n\n\t// TargetTopic determines the topic on which messages from SourceTopics are published.\n\tTargetTopic string\n\n\t// CloseTimeout determines how long router should work for handlers when closing.\n\tCloseTimeout time.Duration\n}\n\n// FanIn is a component that receives messages from 1..N topics from a subscriber and publishes them\n// on a specified topic in the publisher. In effect, messages are \"multiplexed\".\ntype FanIn struct {\n\trouter *message.Router\n\tconfig Config\n\tlogger watermill.LoggerAdapter\n}\n\nfunc (c *Config) setDefaults() {\n\tif c.CloseTimeout == 0 {\n\t\tc.CloseTimeout = time.Second * 30\n\t}\n}\n\nfunc (c *Config) Validate() error {\n\tif len(c.SourceTopics) == 0 {\n\t\treturn errors.New(\"sourceTopics must not be empty\")\n\t}\n\n\tif slices.Contains(c.SourceTopics, \"\") {\n\t\treturn errors.New(\"sourceTopics must not be empty\")\n\t}\n\n\tif c.TargetTopic == \"\" {\n\t\treturn errors.New(\"targetTopic must not be empty\")\n\t}\n\n\tif slices.Contains(c.SourceTopics, c.TargetTopic) {\n\t\treturn errors.New(\"sourceTopics must not contain targetTopic\")\n\t}\n\n\treturn nil\n}\n\n// NewFanIn creates a new FanIn.\nfunc NewFanIn(\n\tsubscriber message.Subscriber,\n\tpublisher message.Publisher,\n\tconfig Config,\n\tlogger watermill.LoggerAdapter,\n) (*FanIn, error) {\n\tif subscriber == nil {\n\t\treturn nil, errors.New(\"missing subscriber\")\n\t}\n\tif publisher == nil {\n\t\treturn nil, errors.New(\"missing publisher\")\n\t}\n\n\tconfig.setDefaults()\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tif logger == nil {\n\t\tlogger = watermill.NopLogger{}\n\t}\n\n\trouterConfig := message.RouterConfig{CloseTimeout: config.CloseTimeout}\n\tif err := routerConfig.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid router config\")\n\t}\n\n\trouter, err := message.NewRouter(routerConfig, logger)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot create a router\")\n\t}\n\n\tfor _, topic := range config.SourceTopics {\n\t\trouter.AddHandler(\n\t\t\tfmt.Sprintf(\"fan_in_%s\", topic),\n\t\t\ttopic,\n\t\t\tsubscriber,\n\t\t\tconfig.TargetTopic,\n\t\t\tpublisher,\n\t\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\t\treturn []*message.Message{msg}, nil\n\t\t\t},\n\t\t)\n\t}\n\n\treturn &FanIn{\n\t\trouter: router,\n\t\tconfig: config,\n\t\tlogger: logger,\n\t}, nil\n}\n\n// Run runs the FanIn.\nfunc (f *FanIn) Run(ctx context.Context) error {\n\treturn f.router.Run(ctx)\n}\n\n// Running is closed when FanIn is running.\nfunc (f *FanIn) Running() chan struct{} {\n\treturn f.router.Running()\n}\n\n// Close gracefully closes the FanIn\nfunc (f *FanIn) Close() error {\n\treturn f.router.Close()\n}\n"
  },
  {
    "path": "components/fanin/fanin_test.go",
    "content": "package fanin_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/fanin\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nfunc TestFanIn(t *testing.T) {\n\tconst (\n\t\tupstreamTopicPattern = \"upstream-topic-%d\"\n\t\tdownstreamTopic      = \"downstream-topic\"\n\n\t\tcancelAfter = time.Millisecond * 100\n\n\t\tworkersCount        = 3\n\t\tmessagesCount       = 10\n\t\tupstreamTopicsCount = 5\n\t)\n\n\tvar upstreamTopics []string\n\tfor i := 1; i <= upstreamTopicsCount; i++ {\n\t\ttopic := fmt.Sprintf(upstreamTopicPattern, i)\n\t\tupstreamTopics = append(upstreamTopics, topic)\n\t}\n\n\tlogger := watermill.NopLogger{}\n\n\tpubsub := gochannel.NewGoChannel(gochannel.Config{}, watermill.NopLogger{})\n\n\tfi, err := fanin.NewFanIn(\n\t\tpubsub,\n\t\tpubsub,\n\t\tfanin.Config{\n\t\t\tSourceTopics: upstreamTopics,\n\t\t\tTargetTopic:  downstreamTopic,\n\t\t},\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\texpectedNumberOfMessages := workersCount * messagesCount * upstreamTopicsCount\n\n\treceivedMessages := make(chan string, expectedNumberOfMessages)\n\n\tfor i := 0; i < workersCount; i++ {\n\t\trouter.AddConsumerHandler(\n\t\t\tfmt.Sprintf(\"worker-%v\", i),\n\t\t\tdownstreamTopic,\n\t\t\tpubsub,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\tpayload := string(msg.Payload)\n\t\t\t\treceivedMessages <- payload\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), cancelAfter)\n\tdefer cancel()\n\n\tgo func() {\n\t\terr := router.Run(ctx)\n\t\trequire.NoError(t, err)\n\t}()\n\n\tgo func() {\n\t\terr := fi.Run(ctx)\n\t\trequire.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\t<-fi.Running()\n\n\tvar wg sync.WaitGroup\n\twg.Add(len(upstreamTopics) * messagesCount)\n\tfor _, topic := range upstreamTopics {\n\t\tgo func(topic string) {\n\t\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(topic))\n\t\t\t\terr := pubsub.Publish(topic, msg)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\twg.Done()\n\t\t\t}\n\t\t}(topic)\n\t}\n\twg.Wait()\n\n\tcounts := map[string]int{}\nloop:\n\tfor {\n\t\tselect {\n\t\tcase msg := <-receivedMessages:\n\t\t\tcounts[msg]++\n\t\tcase <-time.After(cancelAfter):\n\t\t\tclose(receivedMessages)\n\t\t\tbreak loop\n\t\t}\n\t}\n\n\tsum := 0\n\trequire.Len(t, counts, upstreamTopicsCount)\n\tfor _, count := range counts {\n\t\trequire.Equal(t, workersCount*messagesCount, count)\n\t\tsum += count\n\t}\n\trequire.Equal(t, expectedNumberOfMessages, sum)\n}\n\nfunc TestNewFanIn(t *testing.T) {\n\tpubsub := gochannel.NewGoChannel(gochannel.Config{}, nil)\n\n\tt.Run(\"error when subscriber nil\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tnil,\n\t\t\tnil,\n\t\t\tfanin.Config{},\n\t\t\tnil,\n\t\t)\n\t\trequire.EqualError(t, err, \"missing subscriber\")\n\t})\n\n\tt.Run(\"error when publisher nil\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tpubsub,\n\t\t\tnil,\n\t\t\tfanin.Config{},\n\t\t\tnil,\n\t\t)\n\t\trequire.EqualError(t, err, \"missing publisher\")\n\t})\n\n\tt.Run(\"error when sourceTopics empty\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tpubsub,\n\t\t\tpubsub,\n\t\t\tfanin.Config{},\n\t\t\tnil,\n\t\t)\n\t\trequire.EqualError(t, err, \"sourceTopics must not be empty\")\n\t})\n\n\tt.Run(\"error when sourceTopics empty\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tpubsub,\n\t\t\tpubsub,\n\t\t\tfanin.Config{\n\t\t\t\tSourceTopics: []string{\"\"},\n\t\t\t},\n\t\t\tnil,\n\t\t)\n\t\trequire.EqualError(t, err, \"sourceTopics must not be empty\")\n\t})\n\n\tt.Run(\"error when targetTopic empty\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tpubsub,\n\t\t\tpubsub,\n\t\t\tfanin.Config{\n\t\t\t\tSourceTopics: []string{\"topic\"},\n\t\t\t\tTargetTopic:  \"\",\n\t\t\t},\n\t\t\tnil,\n\t\t)\n\t\trequire.EqualError(t, err, \"targetTopic must not be empty\")\n\t})\n\n\tt.Run(\"error when sourceTopics contains targetTopic\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tpubsub,\n\t\t\tpubsub,\n\t\t\tfanin.Config{\n\t\t\t\tSourceTopics: []string{\"topic\"},\n\t\t\t\tTargetTopic:  \"topic\",\n\t\t\t},\n\t\t\tnil,\n\t\t)\n\t\trequire.EqualError(t, err, \"sourceTopics must not contain targetTopic\")\n\t})\n\n\tt.Run(\"correct\", func(t *testing.T) {\n\t\t_, err := fanin.NewFanIn(\n\t\t\tpubsub,\n\t\t\tpubsub,\n\t\t\tfanin.Config{\n\t\t\t\tSourceTopics: []string{\"topic\"},\n\t\t\t\tTargetTopic:  \"targetTopic\",\n\t\t\t},\n\t\t\tnil,\n\t\t)\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "components/forwarder/envelope.go",
    "content": "package forwarder\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// messageEnvelope wraps Watermill message and contains destination topic.\ntype messageEnvelope struct {\n\tDestinationTopic string `json:\"destination_topic\"`\n\n\tUUID     string            `json:\"uuid\"`\n\tPayload  []byte            `json:\"payload\"`\n\tMetadata map[string]string `json:\"metadata\"`\n}\n\nfunc newMessageEnvelope(destTopic string, msg *message.Message) (*messageEnvelope, error) {\n\te := &messageEnvelope{\n\t\tDestinationTopic: destTopic,\n\t\tUUID:             msg.UUID,\n\t\tPayload:          msg.Payload,\n\t\tMetadata:         msg.Metadata,\n\t}\n\n\tif err := e.validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot create a message envelope\")\n\t}\n\n\treturn e, nil\n}\n\nfunc (e *messageEnvelope) validate() error {\n\tif e.DestinationTopic == \"\" {\n\t\treturn errors.New(\"unknown destination topic\")\n\t}\n\n\treturn nil\n}\n\nfunc wrapMessageInEnvelope(destinationTopic string, msg *message.Message) (*message.Message, error) {\n\tenvelope, err := newMessageEnvelope(destinationTopic, msg)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot envelope a message\")\n\t}\n\n\tenvelopedMessage, err := json.Marshal(envelope)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot marshal a message\")\n\t}\n\n\twrappedMsg := message.NewMessage(watermill.NewUUID(), envelopedMessage)\n\twrappedMsg.SetContext(msg.Context())\n\n\treturn wrappedMsg, nil\n}\n\nfunc unwrapMessageFromEnvelope(msg *message.Message) (destinationTopic string, unwrappedMsg *message.Message, err error) {\n\tenvelopedMsg := messageEnvelope{}\n\tif err := json.Unmarshal(msg.Payload, &envelopedMsg); err != nil {\n\t\treturn \"\", nil, errors.Wrap(err, \"cannot unmarshal message wrapped in an envelope\")\n\t}\n\n\tif err := envelopedMsg.validate(); err != nil {\n\t\treturn \"\", nil, errors.Wrap(err, \"an unmarshalled message envelope is invalid\")\n\t}\n\n\twatermillMessage := message.NewMessage(envelopedMsg.UUID, envelopedMsg.Payload)\n\twatermillMessage.Metadata = envelopedMsg.Metadata\n\twatermillMessage.SetContext(msg.Context())\n\n\treturn envelopedMsg.DestinationTopic, watermillMessage, nil\n}\n"
  },
  {
    "path": "components/forwarder/envelope_test.go",
    "content": "package forwarder\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype contextKey string\n\nfunc TestEnvelope(t *testing.T) {\n\texpectedUUID := watermill.NewUUID()\n\texpectedPayload := message.Payload(\"msg content\")\n\texpectedMetadata := message.Metadata{\"key\": \"value\"}\n\texpectedDestinationTopic := \"dest_topic\"\n\n\tctx := context.WithValue(context.Background(), contextKey(\"key\"), \"value\")\n\n\tmsg := message.NewMessage(expectedUUID, expectedPayload)\n\tmsg.Metadata = expectedMetadata\n\tmsg.SetContext(ctx)\n\n\twrappedMsg, err := wrapMessageInEnvelope(expectedDestinationTopic, msg)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, wrappedMsg)\n\tv, ok := wrappedMsg.Context().Value(contextKey(\"key\")).(string)\n\trequire.True(t, ok)\n\trequire.Equal(t, \"value\", v)\n\n\tdestinationTopic, unwrappedMsg, err := unwrapMessageFromEnvelope(wrappedMsg)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, unwrappedMsg)\n\tassert.Equal(t, expectedUUID, unwrappedMsg.UUID)\n\tassert.Equal(t, expectedPayload, unwrappedMsg.Payload)\n\tassert.Equal(t, expectedMetadata, unwrappedMsg.Metadata)\n\tassert.Equal(t, expectedDestinationTopic, destinationTopic)\n\n\tv, ok = unwrappedMsg.Context().Value(contextKey(\"key\")).(string)\n\trequire.True(t, ok)\n\trequire.Equal(t, \"value\", v)\n}\n"
  },
  {
    "path": "components/forwarder/forwarder.go",
    "content": "package forwarder\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\nconst defaultForwarderTopic = \"forwarder_topic\"\n\ntype Config struct {\n\t// ForwarderTopic is a topic on which the forwarder will be listening to enveloped messages to forward.\n\t// Defaults to `forwarder_topic`.\n\tForwarderTopic string\n\n\t// Middlewares are used to decorate forwarder's handler function.\n\tMiddlewares []message.HandlerMiddleware\n\n\t// CloseTimeout determines how long router should work for handlers when closing.\n\tCloseTimeout time.Duration\n\n\t// AckWhenCannotUnwrap enables acking of messages which cannot be unwrapped from an envelope.\n\tAckWhenCannotUnwrap bool\n\n\t// Router is a router used by the forwarder.\n\t// If not provided, a new router will be created.\n\t//\n\t// If router is provided, it's not necessary to call `Forwarder.Run()` if the router is started with `router.Run()`.\n\tRouter *message.Router\n}\n\nfunc (c *Config) setDefaults() {\n\tif c.CloseTimeout == 0 {\n\t\tc.CloseTimeout = time.Second * 30\n\t}\n\tif c.ForwarderTopic == \"\" {\n\t\tc.ForwarderTopic = defaultForwarderTopic\n\t}\n}\n\nfunc (c *Config) Validate() error {\n\tif c.ForwarderTopic == \"\" {\n\t\treturn errors.New(\"empty forwarder topic\")\n\t}\n\n\treturn nil\n}\n\n// Forwarder subscribes to the topic provided in the config and publishes them to the destination topic embedded in the enveloped message.\ntype Forwarder struct {\n\trouter    *message.Router\n\tpublisher message.Publisher\n\tlogger    watermill.LoggerAdapter\n\tconfig    Config\n}\n\n// NewForwarder creates a forwarder which will subscribe to the topic provided in the config using the provided subscriber.\n// It will publish messages received on this subscription to the destination topic embedded in the enveloped message using the provided publisher.\n//\n// Provided subscriber and publisher can be from different Watermill Pub/Sub implementations, i.e. MySQL subscriber and Google Pub/Sub publisher.\n//\n// Note: Keep in mind that by default the forwarder will nack all messages which weren't sent using a decorated publisher.\n// You can change this behavior by passing a middleware which will ack them instead.\nfunc NewForwarder(subscriberIn message.Subscriber, publisherOut message.Publisher, logger watermill.LoggerAdapter, config Config) (*Forwarder, error) {\n\tconfig.setDefaults()\n\n\trouterConfig := message.RouterConfig{CloseTimeout: config.CloseTimeout}\n\tif err := routerConfig.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid router config\")\n\t}\n\n\tvar router *message.Router\n\tif config.Router != nil {\n\t\trouter = config.Router\n\t} else {\n\t\tvar err error\n\t\trouter, err = message.NewRouter(routerConfig, logger)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot create a router\")\n\t\t}\n\t}\n\n\tf := &Forwarder{router, publisherOut, logger, config}\n\n\thandler := router.AddConsumerHandler(\n\t\t\"events_forwarder\",\n\t\tconfig.ForwarderTopic,\n\t\tsubscriberIn,\n\t\tf.forwardMessage,\n\t)\n\n\thandler.AddMiddleware(config.Middlewares...)\n\n\treturn f, nil\n}\n\n// Run runs forwarder's handler responsible for forwarding messages.\n// This call is blocking while the forwarder is running.\n// ctx will be propagated to the forwarder's subscription.\n//\n// To stop Run() you should call Close() on the forwarder.\nfunc (f *Forwarder) Run(ctx context.Context) error {\n\treturn f.router.Run(ctx)\n}\n\n// Close stops forwarder's handler.\nfunc (f *Forwarder) Close() error {\n\treturn f.router.Close()\n}\n\n// Running returns channel which is closed when the forwarder is running.\nfunc (f *Forwarder) Running() chan struct{} {\n\treturn f.router.Running()\n}\n\nfunc (f *Forwarder) forwardMessage(msg *message.Message) error {\n\tdestTopic, unwrappedMsg, err := unwrapMessageFromEnvelope(msg)\n\tif err != nil {\n\t\tf.logger.Error(\"Could not unwrap a message from an envelope\", err, watermill.LogFields{\n\t\t\t\"uuid\":     msg.UUID,\n\t\t\t\"payload\":  msg.Payload,\n\t\t\t\"metadata\": msg.Metadata,\n\t\t\t\"acked\":    f.config.AckWhenCannotUnwrap,\n\t\t})\n\n\t\tif f.config.AckWhenCannotUnwrap {\n\t\t\treturn nil\n\t\t}\n\t\treturn errors.Wrap(err, \"cannot unwrap message from an envelope\")\n\t}\n\n\tif err := f.publisher.Publish(destTopic, unwrappedMsg); err != nil {\n\t\treturn errors.Wrap(err, \"cannot publish a message\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "components/forwarder/forwarder_test.go",
    "content": "package forwarder_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/forwarder\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tlogger = watermill.NewStdLogger(true, true)\n\n\tforwarderTopic = \"forwarder_topic\"\n\toutTopic       = \"out_topic\"\n)\n\nfunc TestForwarder(t *testing.T) {\n\tsuite.Run(t, new(ForwarderSuite))\n}\n\n// ForwarderSuite tests forwarding messages from PubSubIn to PubSubOut (which are GoChannel implementation underneath).\ntype ForwarderSuite struct {\n\tsuite.Suite\n\tctx       context.Context\n\tcancelCtx func()\n\n\tpublisherIn  PubSubInPublisher\n\tsubscriberIn PubSubInSubscriber\n\n\tpublisherOut  PubSubOutPublisher\n\tsubscriberOut PubSubOutSubscriber\n\n\tdecoratedPublisherIn *forwarder.Publisher\n\n\toutMessagesCh <-chan *message.Message\n}\n\nfunc (s *ForwarderSuite) SetupTest() {\n\t// Create test context with a 5 seconds timeout so it will close any subscriptions/handlers running in the background\n\t// in case of too long test execution.\n\ts.ctx, s.cancelCtx = context.WithTimeout(context.Background(), time.Second*5)\n\n\t// Create a set of publisher and subscribers for both In and Out Pub/Subs.\n\ts.publisherIn, s.subscriberIn = newPubSubIn()\n\ts.publisherOut, s.subscriberOut = newPubSubOut()\n\n\ts.decoratedPublisherIn = forwarder.NewPublisher(s.publisherIn, forwarder.PublisherConfig{ForwarderTopic: forwarderTopic})\n\ts.listenOnOutTopic()\n}\n\nfunc (s *ForwarderSuite) TearDownTest() {\n\ts.NoError(s.publisherIn.Close())\n\ts.NoError(s.subscriberIn.Close())\n\ts.NoError(s.publisherOut.Close())\n\ts.NoError(s.subscriberOut.Close())\n\ts.cancelCtx()\n}\n\nfunc (s *ForwarderSuite) TestForwarder_publish_using_decorated_publisher() {\n\tfwd := s.setupForwarder(forwarder.Config{ForwarderTopic: forwarderTopic})\n\tdefer func() {\n\t\ts.NoError(fwd.Close())\n\t}()\n\n\tsentMsg := s.sampleMessage()\n\terr := s.decoratedPublisherIn.Publish(outTopic, sentMsg)\n\ts.Require().NoError(err)\n\n\ts.requireFirstMessage(sentMsg)\n}\n\nfunc (s *ForwarderSuite) TestForwarder_publish_using_non_decorated_publisher() {\n\tmsgAckedDetectorMiddleware, msgAckedCh := s.setupMessageAckedDetectorMiddleware()\n\tfwd := s.setupForwarder(forwarder.Config{\n\t\tForwarderTopic: forwarderTopic,\n\t\tMiddlewares:    []message.HandlerMiddleware{msgAckedDetectorMiddleware},\n\t})\n\tdefer func() {\n\t\ts.NoError(fwd.Close())\n\t}()\n\n\tsentMsg := s.sampleMessage()\n\terr := s.publisherIn.Publish(forwarderTopic, sentMsg)\n\ts.Require().NoError(err)\n\n\ts.requireFirstAckingResult(msgAckedCh, false)\n}\n\nfunc (s *ForwarderSuite) TestForwarder_publish_using_non_decorated_publisher_acking_enabled() {\n\tmsgAckedDetectorMiddleware, msgAckedCh := s.setupMessageAckedDetectorMiddleware()\n\tfwd := s.setupForwarder(forwarder.Config{\n\t\tForwarderTopic:      forwarderTopic,\n\t\tMiddlewares:         []message.HandlerMiddleware{msgAckedDetectorMiddleware},\n\t\tAckWhenCannotUnwrap: true,\n\t})\n\tdefer func() {\n\t\ts.NoError(fwd.Close())\n\t}()\n\n\tsentMsg := s.sampleMessage()\n\terr := s.publisherIn.Publish(forwarderTopic, sentMsg)\n\ts.Require().NoError(err)\n\n\ts.requireFirstAckingResult(msgAckedCh, true)\n}\n\ntype PubSubInPublisher struct {\n\tmessage.Publisher\n}\ntype PubSubInSubscriber struct {\n\tmessage.Subscriber\n}\n\ntype PubSubOutPublisher struct {\n\tmessage.Publisher\n}\ntype PubSubOutSubscriber struct {\n\tmessage.Subscriber\n}\n\nfunc newPubSubIn() (PubSubInPublisher, PubSubInSubscriber) {\n\tchannelPubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\treturn PubSubInPublisher{channelPubSub}, PubSubInSubscriber{channelPubSub}\n}\n\nfunc newPubSubOut() (PubSubOutPublisher, PubSubOutSubscriber) {\n\tchannelPubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\treturn PubSubOutPublisher{channelPubSub}, PubSubOutSubscriber{channelPubSub}\n}\n\nfunc (s *ForwarderSuite) setupForwarder(config forwarder.Config) *forwarder.Forwarder {\n\tf, err := forwarder.NewForwarder(s.subscriberIn, s.publisherOut, logger, config)\n\ts.Require().NoError(err)\n\n\tgo func() {\n\t\ts.Require().NoError(f.Run(s.ctx))\n\t}()\n\n\tselect {\n\tcase <-f.Running():\n\tcase <-s.ctx.Done():\n\t\ts.T().Fatal(\"forwarder not running\")\n\t}\n\n\treturn f\n}\n\nfunc (s *ForwarderSuite) listenOnOutTopic() {\n\tvar err error\n\ts.outMessagesCh, err = s.subscriberOut.Subscribe(s.ctx, outTopic)\n\ts.Require().NoError(err)\n}\n\nfunc (s *ForwarderSuite) requireFirstMessage(expectedMessage *message.Message) {\n\tselect {\n\tcase receivedMessage := <-s.outMessagesCh:\n\t\ts.Require().NotNil(receivedMessage)\n\t\ts.Require().Truef(receivedMessage.Equals(expectedMessage), \"received message: '%s', expected: '%s'\", receivedMessage, expectedMessage)\n\t\treceivedMessage.Ack()\n\tcase <-time.After(time.Second):\n\t\ts.T().Fatal(\"didn't receive any message after 1 sec\")\n\t}\n}\n\nfunc (s *ForwarderSuite) setupMessageAckedDetectorMiddleware() (message.HandlerMiddleware, <-chan bool) {\n\tmessageAckedCh := make(chan bool, 1)\n\tmessageAckedDetector := func(handlerFunc message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\t\tmsgs, err := handlerFunc(msg)\n\t\t\tmessageAckedCh <- err == nil\n\n\t\t\t// Always return nil as we don't want to nack the message in tests.\n\t\t\treturn msgs, nil\n\t\t}\n\t}\n\n\treturn messageAckedDetector, messageAckedCh\n}\n\nfunc (s *ForwarderSuite) requireFirstAckingResult(msgAckedCh <-chan bool, expected bool) {\n\tselect {\n\tcase msgAcked := <-msgAckedCh:\n\t\ts.Require().Equal(expected, msgAcked)\n\tcase <-time.After(time.Second):\n\t\ts.T().Fatal(\"acking result not received after 1 sec\")\n\t}\n}\n\nfunc (s *ForwarderSuite) sampleMessage() *message.Message {\n\tmsg := message.NewMessage(watermill.NewUUID(), message.Payload(\"message payload\"))\n\tmsg.Metadata = message.Metadata{\"key\": \"value\"}\n\treturn msg\n}\n"
  },
  {
    "path": "components/forwarder/publisher.go",
    "content": "package forwarder\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\ntype PublisherConfig struct {\n\t// ForwarderTopic is a topic which the forwarder is listening to. Publisher will send enveloped messages to this topic.\n\t// Defaults to `forwarder_topic`.\n\tForwarderTopic string\n}\n\nfunc (c *PublisherConfig) setDefaults() {\n\tif c.ForwarderTopic == \"\" {\n\t\tc.ForwarderTopic = defaultForwarderTopic\n\t}\n}\n\nfunc (c *PublisherConfig) Validate() error {\n\tif c.ForwarderTopic == \"\" {\n\t\treturn errors.New(\"empty forwarder topic\")\n\t}\n\n\treturn nil\n}\n\n// Publisher changes `Publish` method behavior so it wraps a sent message in an envelope\n// and sends it to the forwarder topic provided in the config.\ntype Publisher struct {\n\twrappedPublisher message.Publisher\n\tconfig           PublisherConfig\n}\n\nfunc NewPublisher(publisher message.Publisher, config PublisherConfig) *Publisher {\n\tconfig.setDefaults()\n\n\treturn &Publisher{\n\t\twrappedPublisher: publisher,\n\t\tconfig:           config,\n\t}\n}\n\nfunc (p *Publisher) Publish(topic string, messages ...*message.Message) error {\n\tenvelopedMessages := make([]*message.Message, 0, len(messages))\n\tfor _, msg := range messages {\n\t\tenvelopedMsg, err := wrapMessageInEnvelope(topic, msg)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"cannot wrap message, target topic: '%s', uuid: '%s'\", topic, msg.UUID)\n\t\t}\n\n\t\tenvelopedMessages = append(envelopedMessages, envelopedMsg)\n\t}\n\n\tif err := p.wrappedPublisher.Publish(p.config.ForwarderTopic, envelopedMessages...); err != nil {\n\t\treturn errors.Wrapf(err, \"cannot publish messages to forwarder topic: '%s'\", p.config.ForwarderTopic)\n\t}\n\n\treturn nil\n}\n\nfunc (p *Publisher) Close() error {\n\treturn p.wrappedPublisher.Close()\n}\n"
  },
  {
    "path": "components/metrics/builder.go",
    "content": "package metrics\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/internal\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\ntype PrometheusMetricsBuilderConfig struct {\n\tNamespace        string\n\tSubsystem        string\n\tAdditionalLabels []MetricLabel\n}\n\nfunc NewPrometheusMetricsBuilderWithConfig(prometheusRegistry prometheus.Registerer, config PrometheusMetricsBuilderConfig) PrometheusMetricsBuilder {\n\tbuilder := PrometheusMetricsBuilder{\n\t\tNamespace:          config.Namespace,\n\t\tSubsystem:          config.Subsystem,\n\t\tPrometheusRegistry: prometheusRegistry,\n\t\tadditionalLabels:   config.AdditionalLabels,\n\t}\n\treturn builder\n}\n\nfunc NewPrometheusMetricsBuilder(prometheusRegistry prometheus.Registerer, namespace string, subsystem string) PrometheusMetricsBuilder {\n\treturn NewPrometheusMetricsBuilderWithConfig(prometheusRegistry, PrometheusMetricsBuilderConfig{\n\t\tNamespace: namespace,\n\t\tSubsystem: subsystem,\n\t})\n}\n\n// PrometheusMetricsBuilder provides methods to decorate publishers, subscribers and handlers.\ntype PrometheusMetricsBuilder struct {\n\t// PrometheusRegistry may be filled with a pre-existing Prometheus registry, or left empty for the default registry.\n\tPrometheusRegistry prometheus.Registerer\n\n\tNamespace string\n\tSubsystem string\n\t// PublishBuckets defines the histogram buckets for publish time histogram, defaulted if nil.\n\tPublishBuckets []float64\n\t// HandlerBuckets defines the histogram buckets for handle execution time histogram, defaulted to watermill's default.\n\tHandlerBuckets []float64\n\n\tadditionalLabels []MetricLabel\n}\n\n// AddPrometheusRouterMetrics is a convenience function that acts on the message router to add the metrics middleware\n// to all its handlers. The handlers' publishers and subscribers are also decorated.\n// The default buckets are used for the handler execution time histogram (use your own provisioning\n// with NewRouterMiddlewareWithConfig if needed).\nfunc (b PrometheusMetricsBuilder) AddPrometheusRouterMetrics(r *message.Router) {\n\tr.AddPublisherDecorators(b.DecoratePublisher)\n\tr.AddSubscriberDecorators(b.DecorateSubscriber)\n\tr.AddMiddleware(b.NewRouterMiddleware().Middleware)\n}\n\n// DecoratePublisher wraps the underlying publisher with Prometheus metrics.\nfunc (b PrometheusMetricsBuilder) DecoratePublisher(pub message.Publisher) (message.Publisher, error) {\n\tvar err error\n\td := PublisherPrometheusMetricsDecorator{\n\t\tpub:              pub,\n\t\tpublisherName:    internal.StructName(pub),\n\t\tadditionalLabels: b.additionalLabels,\n\t}\n\n\td.publishTimeSeconds, err = b.registerHistogramVec(prometheus.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: b.Namespace,\n\t\t\tSubsystem: b.Subsystem,\n\t\t\tName:      \"publish_time_seconds\",\n\t\t\tHelp:      \"The time that a publishing attempt (success or not) took in seconds\",\n\t\t\tBuckets:   b.PublishBuckets,\n\t\t},\n\t\ttoLabelsSlice(publisherLabelKeys, b.additionalLabels),\n\t))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"could not register publish time metric\")\n\t}\n\treturn d, nil\n}\n\n// DecorateSubscriber wraps the underlying subscriber with Prometheus metrics.\nfunc (b PrometheusMetricsBuilder) DecorateSubscriber(sub message.Subscriber) (message.Subscriber, error) {\n\tvar err error\n\td := &SubscriberPrometheusMetricsDecorator{\n\t\tclosing:          make(chan struct{}),\n\t\tsubscriberName:   internal.StructName(sub),\n\t\tadditionalLabels: b.additionalLabels,\n\t}\n\n\td.subscriberMessagesReceivedTotal, err = b.registerCounterVec(prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: b.Namespace,\n\t\t\tSubsystem: b.Subsystem,\n\t\t\tName:      \"subscriber_messages_received_total\",\n\t\t\tHelp:      \"The total number of messages received by the subscriber\",\n\t\t},\n\t\ttoLabelsSlice(append(subscriberLabelKeys, labelAcked), b.additionalLabels),\n\t))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"could not register time to ack metric\")\n\t}\n\n\td.Subscriber, err = message.MessageTransformSubscriberDecorator(d.recordMetrics)(sub)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"could not decorate subscriber with metrics decorator\")\n\t}\n\n\treturn d, nil\n}\n\nfunc (b PrometheusMetricsBuilder) register(c prometheus.Collector) (prometheus.Collector, error) {\n\terr := b.PrometheusRegistry.Register(c)\n\tif err == nil {\n\t\treturn c, nil\n\t}\n\n\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\treturn are.ExistingCollector, nil\n\t}\n\n\treturn nil, err\n}\n\nfunc (b PrometheusMetricsBuilder) registerCounterVec(c *prometheus.CounterVec) (*prometheus.CounterVec, error) {\n\tcol, err := b.register(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn col.(*prometheus.CounterVec), nil\n}\n\nfunc (b PrometheusMetricsBuilder) registerHistogramVec(h *prometheus.HistogramVec) (*prometheus.HistogramVec, error) {\n\tcol, err := b.register(h)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn col.(*prometheus.HistogramVec), nil\n}\n"
  },
  {
    "path": "components/metrics/ctx.go",
    "content": "package metrics\n\nimport \"context\"\n\ntype contextValue int\n\nconst (\n\tpublishObserved contextValue = iota\n\tsubscribeObserved\n)\n\n// setPublishObservedToCtx is used to achieve metrics idempotency in case of double applied middleware\nfunc setPublishObservedToCtx(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, publishObserved, true)\n}\n\nfunc publishAlreadyObserved(ctx context.Context) bool {\n\treturn ctx.Value(publishObserved) != nil\n}\n\n// setSubscribeObservedToCtx is used to achieve metrics idempotency in case of double applied middleware\nfunc setSubscribeObservedToCtx(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, subscribeObserved, true)\n}\n\nfunc subscribeAlreadyObserved(ctx context.Context) bool {\n\treturn ctx.Value(subscribeObserved) != nil\n}\n"
  },
  {
    "path": "components/metrics/handler.go",
    "content": "package metrics\n\nimport (\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nvar (\n\thandlerLabelKeys = []string{\n\t\tlabelKeyHandlerName,\n\t\tlabelSuccess,\n\t}\n\n\t// defaultHandlerExecutionTimeBuckets are one order of magnitude smaller than default buckets (5ms~10s),\n\t// because the handler execution times are typically shorter (µs~ms range).\n\tdefaultHandlerExecutionTimeBuckets = []float64{\n\t\t0.0005,\n\t\t0.001,\n\t\t0.0025,\n\t\t0.005,\n\t\t0.01,\n\t\t0.025,\n\t\t0.05,\n\t\t0.1,\n\t\t0.25,\n\t\t0.5,\n\t\t1,\n\t}\n)\n\n// HandlerPrometheusMetricsMiddleware is a middleware that captures Prometheus metrics.\ntype HandlerPrometheusMetricsMiddleware struct {\n\thandlerExecutionTimeSeconds *prometheus.HistogramVec\n\tadditionalLabels            []MetricLabel\n}\n\n// Middleware returns the middleware ready to be used with watermill's Router.\nfunc (m HandlerPrometheusMetricsMiddleware) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) (msgs []*message.Message, err error) {\n\t\tnow := time.Now()\n\t\tctx := msg.Context()\n\t\tlabels := prometheus.Labels{\n\t\t\tlabelKeyHandlerName: message.HandlerNameFromCtx(ctx),\n\t\t}\n\t\tfor _, lb := range m.additionalLabels {\n\t\t\tlabels[lb.Label] = lb.ComputeValueFn(ctx)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tif err != nil {\n\t\t\t\tlabels[labelSuccess] = \"false\"\n\t\t\t} else {\n\t\t\t\tlabels[labelSuccess] = \"true\"\n\t\t\t}\n\t\t\tm.handlerExecutionTimeSeconds.With(labels).Observe(time.Since(now).Seconds())\n\t\t}()\n\n\t\treturn h(msg)\n\t}\n}\n\n// NewRouterMiddleware returns new middleware.\nfunc (b PrometheusMetricsBuilder) NewRouterMiddleware() HandlerPrometheusMetricsMiddleware {\n\tvar err error\n\tm := HandlerPrometheusMetricsMiddleware{\n\t\tadditionalLabels: b.additionalLabels,\n\t}\n\n\tif b.HandlerBuckets == nil {\n\t\tb.HandlerBuckets = defaultHandlerExecutionTimeBuckets\n\t}\n\n\tm.handlerExecutionTimeSeconds, err = b.registerHistogramVec(prometheus.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: b.Namespace,\n\t\t\tSubsystem: b.Subsystem,\n\t\t\tName:      \"handler_execution_time_seconds\",\n\t\t\tHelp:      \"The total time elapsed while executing the handler function in seconds\",\n\t\t\tBuckets:   b.HandlerBuckets,\n\t\t},\n\t\ttoLabelsSlice(handlerLabelKeys, b.additionalLabels),\n\t))\n\tif err != nil {\n\t\tpanic(errors.Wrap(err, \"could not register handler execution time metric\"))\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "components/metrics/http.go",
    "content": "package metrics\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\n// CreateRegistryAndServeHTTP establishes an HTTP server that exposes the /metrics endpoint for Prometheus at the given address.\n// It returns a new prometheus registry (to register the metrics on) and a canceling function that ends the server.\nfunc CreateRegistryAndServeHTTP(addr string) (registry *prometheus.Registry, cancel func()) {\n\tregistry = prometheus.NewRegistry()\n\treturn registry, ServeHTTP(addr, registry)\n}\n\n// ServeHTTP establishes an HTTP server that exposes the /metrics endpoint for Prometheus at the given address.\n// It takes an existing Prometheus registry and returns a canceling function that ends the server.\nfunc ServeHTTP(addr string, registry *prometheus.Registry) (cancel func()) {\n\trouter := chi.NewRouter()\n\n\thandler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})\n\trouter.Get(\"/metrics\", func(w http.ResponseWriter, r *http.Request) {\n\t\thandler.ServeHTTP(w, r)\n\t})\n\tserver := http.Server{\n\t\tAddr:    addr,\n\t\tHandler: router,\n\t}\n\n\tgo func() {\n\t\terr := server.ListenAndServe()\n\t\tif !errors.Is(err, http.ErrServerClosed) {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\treturn func() { _ = server.Close() }\n}\n"
  },
  {
    "path": "components/metrics/http_test.go",
    "content": "package metrics_test\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/prometheus/client_golang/prometheus/collectors\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/metrics\"\n)\n\nfunc TestCreateRegistryAndServeHTTP_metrics_endpoint(t *testing.T) {\n\treg, cancel := metrics.CreateRegistryAndServeHTTP(\":8090\")\n\tdefer cancel()\n\terr := reg.Register(collectors.NewBuildInfoCollector())\n\tif err != nil {\n\t\tt.Fatal(errors.Wrap(err, \"registration of prometheus build info collector failed\"))\n\t}\n\twaitServerReady(t, \"http://localhost:8090\")\n\tresp, err := http.DefaultClient.Get(\"http://localhost:8090/metrics\")\n\tif resp != nil {\n\t\tdefer resp.Body.Close()\n\t}\n\n\tif err != nil {\n\t\tt.Fatal(errors.Wrap(err, \"call to metrics endpoint failed\"))\n\t}\n\tassert.NotNil(t, resp)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n}\n\nfunc TestCreateRegistryAndServeHTTP_unknown_endpoint(t *testing.T) {\n\treg, cancel := metrics.CreateRegistryAndServeHTTP(\":8091\")\n\tdefer cancel()\n\terr := reg.Register(collectors.NewBuildInfoCollector())\n\tif err != nil {\n\t\tt.Error(errors.Wrap(err, \"registration of prometheus build info collector failed\"))\n\t}\n\twaitServerReady(t, \"http://localhost:8091\")\n\tresp, err := http.DefaultClient.Get(\"http://localhost:8091/unknown\")\n\tif resp != nil {\n\t\tdefer resp.Body.Close()\n\t}\n\n\tif err != nil {\n\t\tt.Fatal(errors.Wrap(err, \"call to unknown endpoint failed\"))\n\t}\n\tassert.NotNil(t, resp)\n\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n}\n\n// server might have small delay before being able to server traffic\nfunc waitServerReady(t *testing.T, addr string) {\n\tfor i := 0; i < 50; i++ {\n\t\t_, err := http.DefaultClient.Get(addr)\n\t\t// assume server ready when no err anymore\n\t\tif err == nil {\n\t\t\treturn\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tcontinue\n\t}\n}\n"
  },
  {
    "path": "components/metrics/labels.go",
    "content": "package metrics\n\nimport (\n\t\"context\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nconst (\n\tlabelKeyHandlerName    = \"handler_name\"\n\tlabelKeyPublisherName  = \"publisher_name\"\n\tlabelKeySubscriberName = \"subscriber_name\"\n\tlabelSuccess           = \"success\"\n\tlabelAcked             = \"acked\"\n\n\tlabelValueNoHandler = \"<no handler>\"\n)\n\nvar (\n\tlabelGetters = map[string]func(context.Context) string{\n\t\tlabelKeyHandlerName:    message.HandlerNameFromCtx,\n\t\tlabelKeyPublisherName:  message.PublisherNameFromCtx,\n\t\tlabelKeySubscriberName: message.SubscriberNameFromCtx,\n\t}\n)\n\nfunc labelsFromCtx(ctx context.Context, labels ...string) prometheus.Labels {\n\tctxLabels := map[string]string{}\n\n\tfor _, l := range labels {\n\t\tk := l\n\t\tctxLabels[l] = \"\"\n\n\t\tgetter, ok := labelGetters[k]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tv := getter(ctx)\n\t\tif v != \"\" {\n\t\t\tctxLabels[l] = v\n\t\t}\n\t}\n\n\treturn ctxLabels\n}\n\ntype LabelComputeValueFn func(msgCtx context.Context) string\n\ntype MetricLabel struct {\n\tLabel          string\n\tComputeValueFn LabelComputeValueFn\n}\n\nfunc toLabelsSlice(baseLabels []string, customs []MetricLabel) []string {\n\tlabels := make([]string, len(baseLabels), len(baseLabels)+len(customs))\n\tcopy(labels, baseLabels)\n\tfor _, label := range customs {\n\t\t//Check if the additional label is already in the base labels. We cannot have duplicate labels\n\t\t//If it's in the base, just skip it as the compute function is going to overwrite the default value\n\t\tcontains := false\n\t\tfor _, baseLabel := range baseLabels {\n\t\t\tif baseLabel == label.Label {\n\t\t\t\tcontains = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !contains {\n\t\t\tlabels = append(labels, label.Label)\n\t\t}\n\t}\n\treturn labels\n}\n"
  },
  {
    "path": "components/metrics/publisher.go",
    "content": "package metrics\n\nimport (\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tpublisherLabelKeys = []string{\n\t\tlabelKeyHandlerName,\n\t\tlabelKeyPublisherName,\n\t\tlabelSuccess,\n\t}\n)\n\n// PublisherPrometheusMetricsDecorator decorates a publisher to capture Prometheus metrics.\ntype PublisherPrometheusMetricsDecorator struct {\n\tpub                message.Publisher\n\tpublisherName      string\n\tpublishTimeSeconds *prometheus.HistogramVec\n\tadditionalLabels   []MetricLabel\n}\n\n// Publish updates the relevant publisher metrics and calls the wrapped publisher's Publish.\nfunc (m PublisherPrometheusMetricsDecorator) Publish(topic string, messages ...*message.Message) (err error) {\n\tif len(messages) == 0 {\n\t\treturn m.pub.Publish(topic)\n\t}\n\n\t// TODO: take ctx not only from first msg. Might require changing the signature of Publish, which is planned anyway.\n\tctx := messages[0].Context()\n\tlabels := labelsFromCtx(ctx, publisherLabelKeys...)\n\tif labels[labelKeyPublisherName] == \"\" {\n\t\tlabels[labelKeyPublisherName] = m.publisherName\n\t}\n\tif labels[labelKeyHandlerName] == \"\" {\n\t\tlabels[labelKeyHandlerName] = labelValueNoHandler\n\t}\n\tfor _, lb := range m.additionalLabels {\n\t\tlabels[lb.Label] = lb.ComputeValueFn(ctx)\n\t}\n\tstart := time.Now()\n\n\tdefer func() {\n\t\tif publishAlreadyObserved(ctx) {\n\t\t\t// decorator idempotency when applied decorator multiple times\n\t\t\treturn\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlabels[labelSuccess] = \"false\"\n\t\t} else {\n\t\t\tlabels[labelSuccess] = \"true\"\n\t\t}\n\t\tm.publishTimeSeconds.With(labels).Observe(time.Since(start).Seconds())\n\t}()\n\n\tfor _, msg := range messages {\n\t\tmsg.SetContext(setPublishObservedToCtx(msg.Context()))\n\t}\n\n\treturn m.pub.Publish(topic, messages...)\n}\n\n// Close decreases the total publisher count, closes the Prometheus HTTP server and calls wrapped Close.\nfunc (m PublisherPrometheusMetricsDecorator) Close() error {\n\treturn m.pub.Close()\n}\n"
  },
  {
    "path": "components/metrics/subscriber.go",
    "content": "package metrics\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tsubscriberLabelKeys = []string{\n\t\tlabelKeyHandlerName,\n\t\tlabelKeySubscriberName,\n\t}\n)\n\n// SubscriberPrometheusMetricsDecorator decorates a subscriber to capture Prometheus metrics.\ntype SubscriberPrometheusMetricsDecorator struct {\n\tmessage.Subscriber\n\tsubscriberName                  string\n\tsubscriberMessagesReceivedTotal *prometheus.CounterVec\n\tclosing                         chan struct{}\n\tadditionalLabels                []MetricLabel\n}\n\nfunc (s SubscriberPrometheusMetricsDecorator) recordMetrics(msg *message.Message) {\n\tif msg == nil {\n\t\treturn\n\t}\n\n\tctx := msg.Context()\n\tlabels := labelsFromCtx(ctx, subscriberLabelKeys...)\n\tif labels[labelKeySubscriberName] == \"\" {\n\t\tlabels[labelKeySubscriberName] = s.subscriberName\n\t}\n\tif labels[labelKeyHandlerName] == \"\" {\n\t\tlabels[labelKeyHandlerName] = labelValueNoHandler\n\t}\n\tfor _, lb := range s.additionalLabels {\n\t\tlabels[lb.Label] = lb.ComputeValueFn(ctx)\n\t}\n\n\tgo func() {\n\t\tif subscribeAlreadyObserved(ctx) {\n\t\t\t// decorator idempotency when applied decorator multiple times\n\t\t\treturn\n\t\t}\n\n\t\tselect {\n\t\tcase <-msg.Acked():\n\t\t\tlabels[labelAcked] = \"acked\"\n\t\tcase <-msg.Nacked():\n\t\t\tlabels[labelAcked] = \"nacked\"\n\t\t}\n\t\ts.subscriberMessagesReceivedTotal.With(labels).Inc()\n\t}()\n\n\tmsg.SetContext(setSubscribeObservedToCtx(msg.Context()))\n}\n"
  },
  {
    "path": "components/requestreply/backend_pubsub.go",
    "content": "package requestreply\n\nimport (\n\t\"context\"\n\tstdErrors \"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// PubSubBackend is a Backend that uses Pub/Sub to transport commands and replies.\ntype PubSubBackend[Result any] struct {\n\tconfig    PubSubBackendConfig\n\tmarshaler BackendPubsubMarshaler[Result]\n}\n\n// NewPubSubBackend creates a new PubSubBackend.\n//\n// If you want to use backend together with `NewCommandHandler` (without result), you should pass `NoResult` or `struct{}` as Result type.\nfunc NewPubSubBackend[Result any](\n\tconfig PubSubBackendConfig,\n\tmarshaler BackendPubsubMarshaler[Result],\n) (*PubSubBackend[Result], error) {\n\tconfig.setDefaults()\n\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config\")\n\t}\n\tif marshaler == nil {\n\t\treturn nil, errors.New(\"marshaler cannot be nil\")\n\t}\n\n\treturn &PubSubBackend[Result]{\n\t\tconfig:    config,\n\t\tmarshaler: marshaler,\n\t}, nil\n}\n\ntype PubSubBackendSubscribeParams struct {\n\tCommand any\n\n\tOperationID OperationID\n}\n\ntype PubSubBackendSubscriberConstructorFn func(PubSubBackendSubscribeParams) (message.Subscriber, error)\n\ntype PubSubBackendGenerateSubscribeTopicFn func(PubSubBackendSubscribeParams) (string, error)\n\ntype PubSubBackendPublishParams struct {\n\tCommand any\n\n\tCommandMessage *message.Message\n\n\tOperationID OperationID\n}\n\ntype PubSubBackendGeneratePublishTopicFn func(PubSubBackendPublishParams) (string, error)\n\ntype PubSubBackendOnCommandProcessedParams struct {\n\tHandleErr error\n\n\tPubSubBackendPublishParams\n}\n\ntype PubSubBackendModifyNotificationMessageFn func(msg *message.Message, params PubSubBackendOnCommandProcessedParams) error\n\ntype PubSubBackendOnListenForReplyFinishedFn func(ctx context.Context, params PubSubBackendSubscribeParams)\n\ntype ReplyPublishErrorHandler func(replyTopic string, notificationMsg *message.Message, err error) error\n\ntype PubSubBackendConfig struct {\n\tPublisher             message.Publisher\n\tSubscriberConstructor PubSubBackendSubscriberConstructorFn\n\n\tGeneratePublishTopic   PubSubBackendGeneratePublishTopicFn\n\tGenerateSubscribeTopic PubSubBackendGenerateSubscribeTopicFn\n\n\tLogger watermill.LoggerAdapter\n\n\tListenForReplyTimeout *time.Duration\n\n\tModifyNotificationMessage PubSubBackendModifyNotificationMessageFn\n\n\tOnListenForReplyFinished PubSubBackendOnListenForReplyFinishedFn\n\n\t// AckCommandErrors determines if the command should be acked or nacked when handler returns an error.\n\t// Command will be nacked by default when sending reply fails, you can control this behaviour with the\n\t// ReplyPublishErrorHandler config option.\n\t// You should use this option instead of cqrs.CommandProcessorConfig.AckCommandHandlingErrors, as it's aware\n\t// if error was returned by handler or sending reply failed.\n\tAckCommandErrors bool\n\n\t// ReplyPublishErrorHandler if not nil will be invoked when sending the reply fails. If it returns an error\n\t// the command will be nacked.\n\tReplyPublishErrorHandler ReplyPublishErrorHandler\n}\n\nfunc (p *PubSubBackendConfig) setDefaults() {\n\tif p.Logger == nil {\n\t\tp.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (p *PubSubBackendConfig) Validate() error {\n\tvar err error\n\n\tif p.Publisher == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"publisher cannot be nil\"))\n\t}\n\tif p.SubscriberConstructor == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"subscriber constructor cannot be nil\"))\n\t}\n\tif p.GeneratePublishTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"GeneratePublishTopic cannot be nil\"))\n\t}\n\tif p.GenerateSubscribeTopic == nil {\n\t\terr = stdErrors.Join(err, errors.New(\"GenerateSubscribeTopic cannot be nil\"))\n\t}\n\n\treturn err\n}\n\nfunc (p PubSubBackend[Result]) ListenForNotifications(\n\tctx context.Context,\n\tparams BackendListenForNotificationsParams,\n) (<-chan Reply[Result], error) {\n\tstart := time.Now()\n\n\treplyContext := PubSubBackendSubscribeParams(params)\n\n\t// this needs to be done before publishing the message to avoid race condition\n\tnotificationsSubscriber, err := p.config.SubscriberConstructor(replyContext)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot create request/reply notifications subscriber\")\n\t}\n\n\treplyNotificationTopic, err := p.config.GenerateSubscribeTopic(replyContext)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot generate request/reply notifications topic\")\n\t}\n\n\tvar cancel context.CancelFunc\n\tif p.config.ListenForReplyTimeout != nil {\n\t\tctx, cancel = context.WithTimeout(ctx, *p.config.ListenForReplyTimeout)\n\t} else {\n\t\tctx, cancel = context.WithCancel(ctx)\n\t}\n\n\tnotifyMsgs, err := notificationsSubscriber.Subscribe(ctx, replyNotificationTopic)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, errors.Wrap(err, \"cannot subscribe to request/reply notifications topic\")\n\t}\n\n\tp.config.Logger.Debug(\n\t\t\"Subscribed to request/reply notifications topic\",\n\t\twatermill.LogFields{\n\t\t\t\"request_reply_topic\": replyNotificationTopic,\n\t\t},\n\t)\n\n\treplyChan := make(chan Reply[Result], 1)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif p.config.OnListenForReplyFinished == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp.config.OnListenForReplyFinished(ctx, replyContext)\n\t\t}()\n\t\tdefer close(replyChan)\n\t\tdefer cancel()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treplyChan <- Reply[Result]{\n\t\t\t\t\tError: ReplyTimeoutError{time.Since(start), ctx.Err()},\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase notifyMsg, ok := <-notifyMsgs:\n\t\t\t\tif !ok {\n\t\t\t\t\t// subscriber is closed\n\t\t\t\t\treplyChan <- Reply[Result]{\n\t\t\t\t\t\tError: ReplyTimeoutError{time.Since(start), fmt.Errorf(\"subscriber closed\")},\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tresp, ok, unmarshalErr := p.handleNotifyMsg(notifyMsg, string(params.OperationID), p.marshaler)\n\t\t\t\tif unmarshalErr != nil {\n\t\t\t\t\treplyChan <- Reply[Result]{\n\t\t\t\t\t\tError: ReplyUnmarshalError{unmarshalErr},\n\t\t\t\t\t}\n\t\t\t\t} else if ok {\n\t\t\t\t\treplyChan <- Reply[Result]{\n\t\t\t\t\t\tHandlerResult:       resp.HandlerResult,\n\t\t\t\t\t\tError:               resp.Error,\n\t\t\t\t\t\tNotificationMessage: notifyMsg,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// we assume that more messages may arrive (in case of fan-out commands handling) - we don't exit yet\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn replyChan, nil\n}\n\nconst OperationIDMetadataKey = \"_watermill_requestreply_op_id\"\n\nfunc (p PubSubBackend[Result]) OnCommandProcessed(ctx context.Context, params BackendOnCommandProcessedParams[Result]) error {\n\tp.config.Logger.Debug(\"Sending request reply\", nil)\n\n\tnotificationMsg, err := p.marshaler.MarshalReply(params)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"cannot marshal request reply notification\")\n\t}\n\tnotificationMsg.SetContext(ctx)\n\n\toperationID, err := operationIDFromMetadata(params.CommandMessage)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnotificationMsg.Metadata.Set(OperationIDMetadataKey, string(operationID))\n\n\tif p.config.ModifyNotificationMessage != nil {\n\t\tprocessedContext := PubSubBackendOnCommandProcessedParams{\n\t\t\tHandleErr: params.HandleErr,\n\t\t\tPubSubBackendPublishParams: PubSubBackendPublishParams{\n\t\t\t\tCommand:        params.Command,\n\t\t\t\tCommandMessage: params.CommandMessage,\n\t\t\t\tOperationID:    operationID,\n\t\t\t},\n\t\t}\n\t\tif err := p.config.ModifyNotificationMessage(notificationMsg, processedContext); err != nil {\n\t\t\treturn errors.Wrap(err, \"cannot modify notification message\")\n\t\t}\n\t}\n\n\treplyTopic, err := p.config.GeneratePublishTopic(PubSubBackendPublishParams{\n\t\tCommand:        params.Command,\n\t\tCommandMessage: params.CommandMessage,\n\t\tOperationID:    operationID,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"cannot generate request/reply notify topic\")\n\t}\n\n\terr = p.config.Publisher.Publish(replyTopic, notificationMsg)\n\tif err != nil {\n\t\tif p.config.ReplyPublishErrorHandler != nil {\n\t\t\terr = p.config.ReplyPublishErrorHandler(replyTopic, notificationMsg, err)\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"cannot publish command executed message\")\n\t}\n\n\tif p.config.AckCommandErrors {\n\t\t// we are ignoring handler error - message will be acked\n\t\treturn nil\n\t} else {\n\t\t// if handler returned error, it will nack the message\n\t\t// if params.HandleErr is nil, message will be acked\n\t\treturn params.HandleErr\n\t}\n}\n\nfunc operationIDFromMetadata(msg *message.Message) (OperationID, error) {\n\toperationID := msg.Metadata.Get(OperationIDMetadataKey)\n\tif operationID == \"\" {\n\t\treturn \"\", errors.Errorf(\"cannot get notification ID from command message metadata, key: %s\", OperationIDMetadataKey)\n\t}\n\n\treturn OperationID(operationID), nil\n}\n\nfunc (p PubSubBackend[Result]) handleNotifyMsg(\n\tmsg *message.Message,\n\texpectedCommandUuid string,\n\tmarshaler BackendPubsubMarshaler[Result],\n) (Reply[Result], bool, error) {\n\tdefer msg.Ack()\n\n\tif msg.Metadata.Get(OperationIDMetadataKey) != expectedCommandUuid {\n\t\tp.config.Logger.Debug(\"Received notify message with different command UUID\", nil)\n\t\treturn Reply[Result]{}, false, nil\n\t}\n\n\tres, unmarshalErr := marshaler.UnmarshalReply(msg)\n\treturn res, true, unmarshalErr\n}\n"
  },
  {
    "path": "components/requestreply/backend_pubsub_marshaler.go",
    "content": "package requestreply\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\ntype BackendPubsubMarshaler[Result any] interface {\n\tMarshalReply(params BackendOnCommandProcessedParams[Result]) (*message.Message, error)\n\tUnmarshalReply(msg *message.Message) (reply Reply[Result], err error)\n}\n\nconst (\n\tErrorMetadataKey    = \"_watermill_requestreply_error\"\n\tHasErrorMetadataKey = \"_watermill_requestreply_has_error\"\n)\n\ntype BackendPubsubJSONMarshaler[Result any] struct{}\n\nfunc (m BackendPubsubJSONMarshaler[Result]) MarshalReply(\n\tparams BackendOnCommandProcessedParams[Result],\n) (*message.Message, error) {\n\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\n\tif params.HandleErr != nil {\n\t\tmsg.Metadata.Set(ErrorMetadataKey, params.HandleErr.Error())\n\t\tmsg.Metadata.Set(HasErrorMetadataKey, \"1\")\n\t} else {\n\t\tmsg.Metadata.Set(HasErrorMetadataKey, \"0\")\n\t}\n\n\tb, err := json.Marshal(params.HandlerResult)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot marshal reply\")\n\t}\n\tmsg.Payload = b\n\n\treturn msg, nil\n}\n\nfunc (m BackendPubsubJSONMarshaler[Result]) UnmarshalReply(msg *message.Message) (Reply[Result], error) {\n\treply := Reply[Result]{}\n\n\tif msg.Metadata.Get(HasErrorMetadataKey) == \"1\" {\n\t\treply.Error = errors.New(msg.Metadata.Get(ErrorMetadataKey))\n\t}\n\n\tvar result Result\n\tif err := json.Unmarshal(msg.Payload, &result); err != nil {\n\t\treturn Reply[Result]{}, errors.Wrap(err, \"cannot unmarshal result\")\n\t}\n\treply.HandlerResult = result\n\n\treturn reply, nil\n}\n"
  },
  {
    "path": "components/requestreply/command_bus.go",
    "content": "package requestreply\n\nimport (\n\t\"context\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\ntype CommandBus interface {\n\tSendWithModifiedMessage(ctx context.Context, cmd any, modify func(*message.Message) error) error\n}\n\n// SendWithReply sends command to the command bus and receives a replies of the command handler.\n// It returns a channel with replies, cancel function and error.\n// If more than one replies are sent, only the first which is received is returned.\n//\n// If you expect multiple replies, please use SendWithReplies instead.\n//\n// SendWithReply is blocking until the first reply is received or the context is canceled.\n// SendWithReply can be cancelled by cancelling context or\n// by exceeding the timeout set in the backend (if set).\n//\n// SendWithReply can listen for handlers with results (NewCommandHandlerWithResult) and without results (NewCommandHandler).\n// If you are listening for handlers without results, you should pass `NoResult` or `struct{}` as `Result` generic type:\n//\n//\t reply, err := requestreply.SendWithReply[requestreply.NoResult](\n//\t\t\tcontext.Background(),\n//\t\t\tts.CommandBus,\n//\t\t\tts.RequestReplyBackend,\n//\t\t\t&TestCommand{ID: \"1\"},\n//\t\t)\n//\n// If `NewCommandHandlerWithResult` handler returns a specific type, you should pass it as `Result` generic type:\n//\n//\t reply, err := requestreply.SendWithReply[SomeTypeReturnedByHandler](\n//\t\t\tcontext.Background(),\n//\t\t\tts.CommandBus,\n//\t\t\tts.RequestReplyBackend,\n//\t\t\t&TestCommand{ID: \"1\"},\n//\t\t)\nfunc SendWithReply[Result any](\n\tctx context.Context,\n\tc CommandBus,\n\tbackend Backend[Result],\n\tcmd any,\n) (Reply[Result], error) {\n\treplyCh, cancel, err := SendWithReplies[Result](ctx, c, backend, cmd)\n\tif err != nil {\n\t\treturn Reply[Result]{}, errors.Wrap(err, \"SendWithReplies failed\")\n\t}\n\tdefer cancel()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn Reply[Result]{}, errors.Wrap(ctx.Err(), \"context closed\")\n\tcase reply := <-replyCh:\n\t\treturn reply, nil\n\t}\n}\n\n// SendWithReplies sends command to the command bus and receives a replies of the command handler.\n// It returns a channel with replies, cancel function and error.\n//\n// SendWithReplies can be cancelled by calling cancel function or by cancelling context or\n// When SendWithReplies is canceled, the returned channel is closed as well.\n// by exceeding the timeout set in the backend (if set).\n// Warning: It's important to cancel the function, because it's listening for the replies in the background.\n// Lack of cancelling the function can lead to subscriber leak.\n//\n// SendWithReplies can listen for handlers with results (NewCommandHandlerWithResult) and without results (NewCommandHandler).\n// If you are listening for handlers without results, you should pass `NoResult` or `struct{}` as `Result` generic type:\n//\n//\t replyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult](\n//\t\t\tcontext.Background(),\n//\t\t\tts.CommandBus,\n//\t\t\tts.RequestReplyBackend,\n//\t\t\t&TestCommand{ID: \"1\"},\n//\t\t)\n//\n// If `NewCommandHandlerWithResult` handler returns a specific type, you should pass it as `Result` generic type:\n//\n//\t replyCh, cancel, err := requestreply.SendWithReplies[SomeTypeReturnedByHandler](\n//\t\t\tcontext.Background(),\n//\t\t\tts.CommandBus,\n//\t\t\tts.RequestReplyBackend,\n//\t\t\t&TestCommand{ID: \"1\"},\n//\t\t)\n//\n// SendWithReplies will send the replies to the channel until the context is cancelled or the timeout is exceeded.\n// They are multiple cases when more than one reply can be sent:\n//   - when the handler returns an error, and backend is configured to nack the message on error\n//     (for the PubSubBackend, it depends on `PubSubBackendConfig.AckCommandErrors` option.),\n//   - when you are using fan-out mechanism and commands are handled multiple times,\nfunc SendWithReplies[Result any](\n\tctx context.Context,\n\tc CommandBus,\n\tbackend Backend[Result],\n\tcmd any,\n) (replCh <-chan Reply[Result], cancel func(), err error) {\n\tctx, cancel = context.WithCancel(ctx)\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tcancel()\n\t\t}\n\t}()\n\n\toperationID := watermill.NewUUID()\n\n\treplyChan, err := backend.ListenForNotifications(ctx, BackendListenForNotificationsParams{\n\t\tCommand:     cmd,\n\t\tOperationID: OperationID(operationID),\n\t})\n\tif err != nil {\n\t\treturn nil, cancel, errors.Wrap(err, \"cannot listen for reply\")\n\t}\n\n\tif err := c.SendWithModifiedMessage(ctx, cmd, func(m *message.Message) error {\n\t\tm.Metadata.Set(OperationIDMetadataKey, operationID)\n\t\treturn nil\n\t}); err != nil {\n\t\treturn nil, cancel, errors.Wrap(err, \"cannot send command\")\n\t}\n\n\treturn replyChan, cancel, nil\n}\n"
  },
  {
    "path": "components/requestreply/handler.go",
    "content": "package requestreply\n\nimport (\n\t\"context\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// NewCommandHandler creates a new CommandHandler which supports the request-reply pattern.\n// The result handler is handler compatible with cqrs.CommandHandler.\n//\n// The logic if a command should be acked or not is based on the logic of the Backend.\n// For example, for the PubSubBackend, it depends on the `PubSubBackendConfig.AckCommandErrors` option.\nfunc NewCommandHandler[Command any](\n\thandlerName string,\n\tbackend Backend[struct{}],\n\thandleFunc func(ctx context.Context, cmd *Command) error,\n) cqrs.CommandHandler {\n\treturn cqrs.NewCommandHandler(handlerName, func(ctx context.Context, cmd *Command) error {\n\t\thandlerErr := handleFunc(ctx, cmd)\n\n\t\toriginalMessage, err := originalCommandMsgFromCtx(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn backend.OnCommandProcessed(ctx, BackendOnCommandProcessedParams[struct{}]{\n\t\t\tCommand:        cmd,\n\t\t\tCommandMessage: originalMessage,\n\t\t\tHandleErr:      handlerErr,\n\t\t})\n\t})\n}\n\n// NewCommandHandlerWithResult creates a new CommandHandler which supports the request-reply pattern with a result.\n// The result handler is handler compatible with cqrs.CommandHandler.\n//\n// In addition to cqrs.CommandHandler, it also allows returning a result from the handler.\n// The result is passed to the Backend implementation and sent to the caller.\n//\n// The logic if a command should be acked or not is based on the logic of the Backend.\n// For example, for the PubSubBackend, it depends on the `PubSubBackendConfig.AckCommandErrors` option.\n//\n// The reply is sent to the caller, even if the handler returns an error.\nfunc NewCommandHandlerWithResult[Command any, Result any](\n\thandlerName string,\n\tbackend Backend[Result],\n\thandleFunc func(ctx context.Context, cmd *Command) (Result, error),\n) cqrs.CommandHandler {\n\treturn cqrs.NewCommandHandler(handlerName, func(ctx context.Context, cmd *Command) error {\n\t\tresp, handlerErr := handleFunc(ctx, cmd)\n\n\t\toriginalMessage, err := originalCommandMsgFromCtx(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn backend.OnCommandProcessed(ctx, BackendOnCommandProcessedParams[Result]{\n\t\t\tCommand:        cmd,\n\t\t\tCommandMessage: originalMessage,\n\t\t\tHandlerResult:  resp,\n\t\t\tHandleErr:      handlerErr,\n\t\t})\n\t})\n}\n\nfunc originalCommandMsgFromCtx(ctx context.Context) (*message.Message, error) {\n\toriginalMessage := cqrs.OriginalMessageFromCtx(ctx)\n\tif originalMessage == nil {\n\t\t// This should not happen, as long as cqrs.CommandProcessor is used - but it's not mandatory.\n\t\t// In this case, it's enough to use cqrs.CtxWithOriginalMessage\n\t\treturn nil, errors.New(\n\t\t\t\"original message not found in context, did you pass context correctly everywhere? \" +\n\t\t\t\t\"did you use cqrs.CommandProcessor? \" +\n\t\t\t\t\"if you are using custom implementation, please call cqrs.CtxWithOriginalMessage on the context passed to the handler\",\n\t\t)\n\t}\n\treturn originalMessage, nil\n}\n"
  },
  {
    "path": "components/requestreply/requestreply.go",
    "content": "package requestreply\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// NoResult is a result type for commands that don't have result.\ntype NoResult = struct{}\n\ntype Reply[Result any] struct {\n\t// HandlerResult contains the handler result.\n\t// It's preset only when NewCommandHandlerWithResult is used. If NewCommandHandler is used, HandlerResult is empty.\n\t//\n\t// Result is sent even if the handler returns an error.\n\tHandlerResult Result\n\n\t// Error contains the error returned by the command handler or the Backend when handling notification fails.\n\t// Handling the notification can fail, for example, when unmarshaling the message or if there's a timeout.\n\t// If listening for a reply times out or the context is canceled, the Error is ReplyTimeoutError.\n\t//\n\t// If an error from the handler is returned, CommandHandlerError is returned.\n\t// If processing was successful, Error is nil.\n\tError error\n\n\t// NotificationMessage contains the notification message sent after the command is handled.\n\t// It's present only if the request/reply backend uses a Pub/Sub for notifications (for example, PubSubBackend).\n\t//\n\t// Warning: NotificationMessage is nil if a timeout occurs.\n\tNotificationMessage *message.Message\n}\n\ntype Backend[Result any] interface {\n\tListenForNotifications(ctx context.Context, params BackendListenForNotificationsParams) (<-chan Reply[Result], error)\n\tOnCommandProcessed(ctx context.Context, params BackendOnCommandProcessedParams[Result]) error\n}\n\ntype BackendListenForNotificationsParams struct {\n\tCommand     any\n\tOperationID OperationID\n}\n\ntype BackendOnCommandProcessedParams[Result any] struct {\n\tCommand        any\n\tCommandMessage *message.Message\n\n\tHandlerResult Result\n\tHandleErr     error\n}\n\n// OperationID is a unique identifier of a command.\n// It correlates commands with replies between the bus and the handler.\ntype OperationID string\n\n// ReplyTimeoutError is returned when the reply timeout is exceeded.\ntype ReplyTimeoutError struct {\n\tDuration time.Duration\n\tErr      error\n}\n\nfunc (e ReplyTimeoutError) Error() string {\n\treturn fmt.Sprintf(\"reply timeout after %s: %s\", e.Duration, e.Err)\n}\n\ntype ReplyUnmarshalError struct {\n\tErr error\n}\n\nfunc (r ReplyUnmarshalError) Error() string {\n\treturn fmt.Sprintf(\"cannot unmarshal reply: %s\", r.Err)\n}\n\nfunc (r ReplyUnmarshalError) Unwrap() error {\n\treturn r.Err\n}\n\n// CommandHandlerError is returned when the command handler returns an error.\ntype CommandHandlerError struct {\n\tErr error\n}\n\nfunc (e CommandHandlerError) Error() string {\n\treturn e.Err.Error()\n}\n\nfunc (e CommandHandlerError) Unwrap() error {\n\treturn e.Err\n}\n"
  },
  {
    "path": "components/requestreply/requestreply_test.go",
    "content": "package requestreply_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/cqrs\"\n\t\"github.com/ThreeDotsLabs/watermill/components/requestreply\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype TestServices[Result any] struct {\n\tLogger    watermill.LoggerAdapter\n\tMarshaler cqrs.CommandEventMarshaler\n\tPubSub    *gochannel.GoChannel\n\tRouter    *message.Router\n\n\tCommandBus       *cqrs.CommandBus\n\tCommandProcessor *cqrs.CommandProcessor\n\n\tRequestReplyBackend *requestreply.PubSubBackend[Result]\n\tBackendConfig       requestreply.PubSubBackendConfig\n}\n\ntype TestServicesConfig struct {\n\tDoNotAckOnCommandErrors bool\n\tListenForReplyTimeout   *time.Duration\n\n\tAssertNotificationMessage func(t *testing.T, msg *message.Message)\n\n\tDoNotBlockPublishUntilSubscriberAck bool\n}\n\nfunc NewTestServices[Result any](t *testing.T, c TestServicesConfig) TestServices[Result] {\n\tt.Helper()\n\n\tlogger := watermill.NewStdLogger(true, true)\n\tmarshaler := cqrs.JSONMarshaler{}\n\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{BlockPublishUntilSubscriberAck: false},\n\t\tlogger,\n\t)\n\n\tbackendConfig := requestreply.PubSubBackendConfig{\n\t\tPublisher: pubSub,\n\t\tSubscriberConstructor: func(subscriberContext requestreply.PubSubBackendSubscribeParams) (message.Subscriber, error) {\n\t\t\tassert.NotEmpty(t, subscriberContext.OperationID)\n\t\t\tassert.NotEmpty(t, subscriberContext.Command)\n\n\t\t\treturn pubSub, nil\n\t\t},\n\t\tGenerateSubscribeTopic: func(subscriberContext requestreply.PubSubBackendSubscribeParams) (string, error) {\n\t\t\tassert.NotEmpty(t, subscriberContext.OperationID)\n\t\t\tassert.NotEmpty(t, subscriberContext.Command)\n\n\t\t\treturn \"reply\", nil\n\t\t},\n\t\tGeneratePublishTopic: func(subscriberContext requestreply.PubSubBackendPublishParams) (string, error) {\n\t\t\tassert.NotEmpty(t, subscriberContext.OperationID)\n\t\t\tassert.NotEmpty(t, subscriberContext.Command)\n\t\t\tassert.NotEmpty(t, subscriberContext.CommandMessage)\n\n\t\t\treturn \"reply\", nil\n\t\t},\n\t\tLogger: logger,\n\t\tModifyNotificationMessage: func(msg *message.Message, params requestreply.PubSubBackendOnCommandProcessedParams) error {\n\t\t\t// to make it deterministic\n\t\t\tmsg.UUID = \"1\"\n\n\t\t\tassert.NotEmpty(t, params.OperationID)\n\t\t\tassert.NotEmpty(t, params.Command)\n\t\t\tassert.NotEmpty(t, params.CommandMessage)\n\n\t\t\t// to ensure backward compatibility\n\t\t\tif c.AssertNotificationMessage != nil {\n\t\t\t\tc.AssertNotificationMessage(t, msg)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tAckCommandErrors:      !c.DoNotAckOnCommandErrors,\n\t\tListenForReplyTimeout: c.ListenForReplyTimeout,\n\t}\n\tbackend, err := requestreply.NewPubSubBackend[Result](\n\t\tbackendConfig,\n\t\trequestreply.BackendPubsubJSONMarshaler[Result]{},\n\t)\n\trequire.NoError(t, err)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tcommandBus, err := cqrs.NewCommandBusWithConfig(pubSub, cqrs.CommandBusConfig{\n\t\tGeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {\n\t\t\treturn \"commands\", nil\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\trequire.NoError(t, err)\n\n\tcommandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cqrs.CommandProcessorConfig{\n\t\tGenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {\n\t\t\treturn \"commands\", nil\n\t\t},\n\t\tSubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {\n\t\t\treturn pubSub, nil\n\t\t},\n\t\tMarshaler: marshaler,\n\t\tLogger:    logger,\n\t})\n\trequire.NoError(t, err)\n\n\treturn TestServices[Result]{\n\t\tLogger: logger,\n\t\tPubSub: gochannel.NewGoChannel(\n\t\t\tgochannel.Config{BlockPublishUntilSubscriberAck: !c.DoNotBlockPublishUntilSubscriberAck},\n\t\t\tlogger,\n\t\t),\n\t\tRouter:              router,\n\t\tRequestReplyBackend: backend,\n\t\tCommandBus:          commandBus,\n\t\tCommandProcessor:    commandProcessor,\n\t\tMarshaler:           marshaler,\n\t\tBackendConfig:       backendConfig,\n\t}\n}\n\nfunc (ts TestServices[Result]) RunRouter() {\n\tgo func() {\n\t\terr := ts.Router.Run(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\t<-ts.Router.Running()\n}\n\ntype TestCommand struct {\n\tID string `json:\"id\"`\n}\n\ntype TestCommand2 struct {\n\tID string `json:\"id\"`\n}\n\ntype TestCommandResult struct {\n\tID string `json:\"id\"`\n}\n\nfunc TestRequestReply_without_result_no_error(t *testing.T) {\n\tts := NewTestServices[requestreply.NoResult](t, TestServicesConfig{\n\t\tAssertNotificationMessage: func(t *testing.T, msg *message.Message) {\n\t\t\tassert.NotEmpty(t, msg.Metadata.Get(requestreply.HasErrorMetadataKey))\n\t\t},\n\t})\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandler(\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\tdefer cancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.Empty(t, reply.HandlerResult)\n\t\tassert.NoError(t, reply.Error)\n\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_without_result_with_error(t *testing.T) {\n\tts := NewTestServices[requestreply.NoResult](t, TestServicesConfig{\n\t\tAssertNotificationMessage: func(t *testing.T, msg *message.Message) {\n\t\t\tassert.NotEmpty(t, msg.Metadata.Get(requestreply.HasErrorMetadataKey))\n\t\t\tassert.NotEmpty(t, msg.Metadata.Get(requestreply.ErrorMetadataKey))\n\t\t},\n\t})\n\n\texpectedErr := errors.New(\"some error\")\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandler(\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) error {\n\t\t\t\treturn expectedErr\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\tdefer cancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.Empty(t, reply.HandlerResult)\n\n\t\trequire.Error(t, reply.Error)\n\t\tassert.Equal(t, expectedErr.Error(), reply.Error.Error())\n\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_with_result_no_error(t *testing.T) {\n\tts := NewTestServices[TestCommandResult](t, TestServicesConfig{\n\t\tAssertNotificationMessage: func(t *testing.T, msg *message.Message) {\n\t\t\tassert.NotEmpty(t, msg.Metadata.Get(requestreply.HasErrorMetadataKey))\n\t\t},\n\t})\n\n\texpectedResult := TestCommandResult{ID: \"123\"}\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand, TestCommandResult](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) (TestCommandResult, error) {\n\t\t\t\treturn expectedResult, nil\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[TestCommandResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\tdefer cancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.EqualValues(t, expectedResult, reply.HandlerResult)\n\t\tassert.NoError(t, reply.Error)\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_with_result_with_error(t *testing.T) {\n\tts := NewTestServices[TestCommandResult](t, TestServicesConfig{})\n\n\texpectedResult := TestCommandResult{ID: \"123\"}\n\texpectedErr := errors.New(\"some error\")\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand, TestCommandResult](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) (TestCommandResult, error) {\n\t\t\t\treturn expectedResult, expectedErr\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[TestCommandResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\tdefer cancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.EqualValues(t, TestCommandResult{ID: \"123\"}, reply.HandlerResult)\n\n\t\trequire.Error(t, reply.Error)\n\t\tassert.Equal(t, expectedErr.Error(), reply.Error.Error())\n\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestSendWithReply(t *testing.T) {\n\tts := NewTestServices[TestCommandResult](t, TestServicesConfig{})\n\n\texpectedResult := TestCommandResult{ID: \"123\"}\n\texpectedErr := errors.New(\"some error\")\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand, TestCommandResult](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) (TestCommandResult, error) {\n\t\t\t\treturn expectedResult, expectedErr\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treply, err := requestreply.SendWithReply[TestCommandResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, TestCommandResult{ID: \"123\"}, reply.HandlerResult)\n\n\trequire.Error(t, reply.Error)\n\tassert.Equal(t, expectedErr.Error(), reply.Error.Error())\n\n\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n}\n\nfunc TestRequestReply_without_result_multiple_replies(t *testing.T) {\n\tts := NewTestServices[TestCommandResult](t, TestServicesConfig{\n\t\tDoNotAckOnCommandErrors: true,\n\t})\n\n\ttype toSend struct {\n\t\tID  string\n\t\tErr error\n\t}\n\n\ttoSendCh := make(chan toSend, 1)\n\ttoSendCh <- toSend{ID: \"1\", Err: fmt.Errorf(\"error 1\")}\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand, TestCommandResult](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) (TestCommandResult, error) {\n\t\t\t\ttoSend := <-toSendCh\n\n\t\t\t\treturn TestCommandResult{ID: toSend.ID}, toSend.Err\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[TestCommandResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\tdefer cancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.EqualValues(t, TestCommandResult{ID: \"1\"}, reply.HandlerResult)\n\n\t\trequire.Error(t, reply.Error)\n\t\tassert.Equal(t, \"error 1\", reply.Error.Error())\n\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n\n\ttoSendCh <- toSend{ID: \"2\", Err: fmt.Errorf(\"error 2\")}\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.EqualValues(t, TestCommandResult{ID: \"2\"}, reply.HandlerResult)\n\n\t\trequire.Error(t, reply.Error)\n\t\tassert.Equal(t, \"error 2\", reply.Error.Error())\n\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n\n\ttoSendCh <- toSend{ID: \"3\", Err: nil}\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.EqualValues(t, TestCommandResult{ID: \"3\"}, reply.HandlerResult)\n\n\t\trequire.NoError(t, reply.Error)\n\n\t\tassert.NotEmpty(t, reply.NotificationMessage.Metadata.Get(requestreply.OperationIDMetadataKey))\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_timeout(t *testing.T) {\n\ttimeout := time.Millisecond * 10\n\n\tts := NewTestServices[requestreply.NoResult](t, TestServicesConfig{\n\t\tListenForReplyTimeout: &timeout,\n\t})\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandler[TestCommand](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) error {\n\t\t\t\ttime.Sleep(time.Second)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\tdefer cancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.Empty(t, reply.HandlerResult)\n\t\trequire.Error(t, reply.Error)\n\t\trequire.IsType(t, requestreply.ReplyTimeoutError{}, reply.Error)\n\n\t\treplyTimeoutError := reply.Error.(requestreply.ReplyTimeoutError)\n\t\tassert.Equal(t, context.DeadlineExceeded, replyTimeoutError.Err)\n\t\tassert.NotEmpty(t, replyTimeoutError.Duration)\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_context_cancellation(t *testing.T) {\n\tts := NewTestServices[struct{}](t, TestServicesConfig{})\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandler[TestCommand](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) error {\n\t\t\t\ttime.Sleep(time.Second)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\treplyCh, _, err := requestreply.SendWithReplies[struct{}](\n\t\tctx,\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\n\tcancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.Empty(t, reply.HandlerResult)\n\t\trequire.Error(t, reply.Error)\n\t\trequire.IsType(t, requestreply.ReplyTimeoutError{}, reply.Error)\n\n\t\treplyTimeoutError := reply.Error.(requestreply.ReplyTimeoutError)\n\t\tassert.Contains(\n\t\t\tt,\n\t\t\t// it depends on which switch will be executed first\n\t\t\t[]string{\"subscriber closed\", context.Canceled.Error()},\n\t\t\treplyTimeoutError.Err.Error(),\n\t\t)\n\t\tassert.NotEmpty(t, replyTimeoutError.Duration)\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_fn_cancellation(t *testing.T) {\n\tts := NewTestServices[struct{}](t, TestServicesConfig{})\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandler[TestCommand](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) error {\n\t\t\t\ttime.Sleep(time.Second)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\treplyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult](\n\t\tcontext.Background(),\n\t\tts.CommandBus,\n\t\tts.RequestReplyBackend,\n\t\t&TestCommand{ID: \"1\"},\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, replyCh)\n\n\tcancel()\n\n\tselect {\n\tcase reply := <-replyCh:\n\t\tassert.Empty(t, reply.HandlerResult)\n\t\trequire.Error(t, reply.Error)\n\t\trequire.IsType(t, requestreply.ReplyTimeoutError{}, reply.Error)\n\n\t\treplyTimeoutError := reply.Error.(requestreply.ReplyTimeoutError)\n\t\tassert.Contains(\n\t\t\tt,\n\t\t\t// it depends on which switch will be executed first\n\t\t\t[]string{\"subscriber closed\", context.Canceled.Error()},\n\t\t\treplyTimeoutError.Err.Error(),\n\t\t)\n\t\tassert.NotEmpty(t, replyTimeoutError.Duration)\n\tcase <-time.After(time.Millisecond * 100):\n\t\tt.Fatal(\"timeout\")\n\t}\n}\n\nfunc TestRequestReply_parallel_different_handlers(t *testing.T) {\n\tts := NewTestServices[TestCommandResult](t, TestServicesConfig{\n\t\tDoNotAckOnCommandErrors: true,\n\t})\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand, TestCommandResult](\n\t\t\t\"test_handler_1\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) (TestCommandResult, error) {\n\n\t\t\t\treturn TestCommandResult{ID: cmd.ID}, fmt.Errorf(\"error 1 %s\", cmd.ID)\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\terr = ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand2, TestCommandResult](\n\t\t\t\"test_handler_2\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand2) (TestCommandResult, error) {\n\n\t\t\t\treturn TestCommandResult{ID: cmd.ID}, fmt.Errorf(\"error 2 %s\", cmd.ID)\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\tstart := make(chan struct{})\n\twg := sync.WaitGroup{}\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\t<-start\n\n\t\tcmd := TestCommand{ID: watermill.NewUUID()}\n\n\t\treplyCh, cancel, err := requestreply.SendWithReplies[TestCommandResult](\n\t\t\tcontext.Background(),\n\t\t\tts.CommandBus,\n\t\t\tts.RequestReplyBackend,\n\t\t\t&cmd,\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, replyCh)\n\t\tdefer cancel()\n\n\t\ti := 0\n\n\t\tfor reply := range replyCh {\n\t\t\tassert.EqualValues(t, TestCommandResult(cmd), reply.HandlerResult)\n\t\t\trequire.Error(t, reply.Error)\n\t\t\tassert.Equal(t, fmt.Sprintf(\"error 1 %s\", cmd.ID), reply.Error.Error())\n\t\t\ti++\n\n\t\t\tif i > 100 {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\t<-start\n\n\t\tcmd := TestCommand2{ID: watermill.NewUUID()}\n\n\t\treplyCh, cancel, err := requestreply.SendWithReplies[TestCommandResult](\n\t\t\tcontext.Background(),\n\t\t\tts.CommandBus,\n\t\t\tts.RequestReplyBackend,\n\t\t\t&cmd,\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, replyCh)\n\t\tdefer cancel()\n\n\t\ti := 0\n\n\t\tfor reply := range replyCh {\n\t\t\tassert.EqualValues(t, TestCommandResult(cmd), reply.HandlerResult)\n\t\t\trequire.Error(t, reply.Error)\n\t\t\tassert.Equal(t, fmt.Sprintf(\"error 2 %s\", cmd.ID), reply.Error.Error())\n\t\t\ti++\n\n\t\t\tif i > 100 {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// sync workers\n\tclose(start)\n\twg.Wait()\n}\n\nfunc TestRequestReply_parallel_same_handler(t *testing.T) {\n\tts := NewTestServices[TestCommandResult](t, TestServicesConfig{\n\t\tDoNotBlockPublishUntilSubscriberAck: true,\n\t})\n\n\terr := ts.CommandProcessor.AddHandlers(\n\t\trequestreply.NewCommandHandlerWithResult[TestCommand, TestCommandResult](\n\t\t\t\"test_handler\",\n\t\t\tts.RequestReplyBackend,\n\t\t\tfunc(ctx context.Context, cmd *TestCommand) (TestCommandResult, error) {\n\t\t\t\treturn TestCommandResult{ID: cmd.ID}, nil\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tts.RunRouter()\n\n\tcount := 20\n\twg := sync.WaitGroup{}\n\twg.Add(count)\n\n\tstart := make(chan struct{})\n\n\tfor i := 0; i < count; i++ {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\t<-start\n\n\t\t\tcmd := TestCommand{ID: uuid.NewString()}\n\t\t\treplyCh, cancel, err := requestreply.SendWithReplies[TestCommandResult](\n\t\t\t\tcontext.Background(),\n\t\t\t\tts.CommandBus,\n\t\t\t\tts.RequestReplyBackend,\n\t\t\t\t&cmd,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, replyCh)\n\t\t\tdefer cancel()\n\n\t\t\tselect {\n\t\t\tcase reply := <-replyCh:\n\t\t\t\tassert.EqualValues(t, TestCommandResult(cmd), reply.HandlerResult)\n\t\t\t\tassert.NoError(t, reply.Error)\n\t\t\tcase <-time.After(time.Millisecond * 100):\n\t\t\t\tt.Error(\"timeout\")\n\t\t\t}\n\t\t}()\n\t}\n\n\t// sync workers\n\tclose(start)\n\twg.Wait()\n}\n\nfunc TestNewPubSubBackend_missing_values(t *testing.T) {\n\tt.Run(\"invalid_config\", func(t *testing.T) {\n\t\tinvalidConfig := requestreply.PubSubBackendConfig{}\n\t\trequire.Error(t, invalidConfig.Validate())\n\n\t\tbackend, err := requestreply.NewPubSubBackend[requestreply.NoResult](\n\t\t\tinvalidConfig,\n\t\t\trequestreply.BackendPubsubJSONMarshaler[requestreply.NoResult]{},\n\t\t)\n\t\tassert.Error(t, err)\n\t\tassert.ErrorContains(t, err, \"invalid config\")\n\t\tassert.Nil(t, backend)\n\t})\n\n\tt.Run(\"missing_marshaler\", func(t *testing.T) {\n\t\tts := NewTestServices[struct{}](t, TestServicesConfig{})\n\t\trequire.NoError(t, ts.BackendConfig.Validate())\n\n\t\tbackend, err := requestreply.NewPubSubBackend[requestreply.NoResult](\n\t\t\tts.BackendConfig,\n\t\t\tnil,\n\t\t)\n\t\tassert.Error(t, err)\n\t\tassert.ErrorContains(t, err, \"marshaler cannot be nil\")\n\t\tassert.Nil(t, backend)\n\t})\n}\n"
  },
  {
    "path": "components/requeuer/requeuer.go",
    "content": "package requeuer\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nconst RetriesKey = \"_watermill_requeuer_retries\"\n\n// Requeuer is a component that moves messages from one topic to another.\n// It can be used to requeue messages that failed to process.\ntype Requeuer struct {\n\tconfig Config\n}\n\n// GeneratePublishTopicParams are the parameters passed to the GeneratePublishTopic function.\ntype GeneratePublishTopicParams struct {\n\tMessage *message.Message\n}\n\n// Config is the configuration for the Requeuer.\ntype Config struct {\n\t// Subscriber is the subscriber to consume messages from. Required.\n\tSubscriber message.Subscriber\n\n\t// SubscribeTopic is the topic related to the Subscriber to consume messages from. Required.\n\tSubscribeTopic string\n\n\t// Publisher is the publisher to publish requeued messages to. Required.\n\tPublisher message.Publisher\n\n\t// GeneratePublishTopic is the topic related to the Publisher to publish the requeued message to.\n\t// For example, it could be a constant, or taken from the message's metadata.\n\t// Required.\n\tGeneratePublishTopic func(params GeneratePublishTopicParams) (string, error)\n\n\t// Delay is the duration to wait before requeuing the message. Optional.\n\t// The default is no delay.\n\t//\n\t// This can be useful to avoid requeuing messages too quickly, for example, to avoid\n\t// requeuing a message that failed to process due to a temporary issue.\n\t//\n\t// Avoid setting this to a very high value, as it will block the message processing.\n\tDelay time.Duration\n\n\t// Router is the custom router to run the requeue handler on. Optional.\n\tRouter *message.Router\n}\n\nfunc (c *Config) setDefaults(logger watermill.LoggerAdapter) error {\n\tif c.Router == nil {\n\t\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not create router: %w\", err)\n\t\t}\n\n\t\tc.Router = router\n\t}\n\n\treturn nil\n}\n\nfunc (c *Config) validate() error {\n\tif c.Subscriber == nil {\n\t\treturn errors.New(\"subscriber is required\")\n\t}\n\n\tif c.SubscribeTopic == \"\" {\n\t\treturn errors.New(\"subscribe topic is required\")\n\t}\n\n\tif c.Publisher == nil {\n\t\treturn errors.New(\"publisher is required\")\n\t}\n\n\tif c.GeneratePublishTopic == nil {\n\t\treturn errors.New(\"generate publish topic is required\")\n\t}\n\n\treturn nil\n}\n\n// NewRequeuer creates a new Requeuer with the provided Config.\n// It's not started automatically. You need to call Run on the returned Requeuer.\nfunc NewRequeuer(\n\tconfig Config,\n\tlogger watermill.LoggerAdapter,\n) (*Requeuer, error) {\n\tif logger == nil {\n\t\tlogger = watermill.NewStdLogger(false, false)\n\t}\n\n\terr := config.setDefaults(logger)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = config.validate()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid config: %w\", err)\n\t}\n\n\tr := &Requeuer{\n\t\tconfig: config,\n\t}\n\n\tconfig.Router.AddConsumerHandler(\n\t\t\"requeuer\",\n\t\tconfig.SubscribeTopic,\n\t\tconfig.Subscriber,\n\t\tr.handler,\n\t)\n\n\treturn r, nil\n}\n\nfunc (r *Requeuer) handler(msg *message.Message) error {\n\tif r.config.Delay > 0 {\n\t\tselect {\n\t\tcase <-msg.Context().Done():\n\t\t\treturn msg.Context().Err()\n\t\tcase <-time.After(r.config.Delay):\n\t\t}\n\t}\n\n\ttopic, err := r.config.GeneratePublishTopic(GeneratePublishTopicParams{Message: msg})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tretriesStr := msg.Metadata.Get(RetriesKey)\n\tretries, err := strconv.Atoi(retriesStr)\n\tif err != nil {\n\t\tretries = 0\n\t}\n\n\tretries++\n\n\tmsg.Metadata.Set(RetriesKey, strconv.Itoa(retries))\n\n\terr = r.config.Publisher.Publish(topic, msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Run runs the Requeuer.\nfunc (r *Requeuer) Run(ctx context.Context) error {\n\treturn r.config.Router.Run(ctx)\n}\n"
  },
  {
    "path": "components/requeuer/requeuer_test.go",
    "content": "package requeuer_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/components/requeuer\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nfunc TestRequeue(t *testing.T) {\n\tlogger := watermill.NewStdLogger(false, false)\n\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\n\trequeue, err := requeuer.NewRequeuer(requeuer.Config{\n\t\tSubscriber:     pubSub,\n\t\tSubscribeTopic: \"requeue\",\n\t\tPublisher:      pubSub,\n\t\tGeneratePublishTopic: func(params requeuer.GeneratePublishTopicParams) (string, error) {\n\t\t\treturn \"test\", nil\n\t\t},\n\t\tDelay: time.Millisecond * 200,\n\t}, logger)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := requeue.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tpq, err := middleware.PoisonQueue(pubSub, \"requeue\")\n\trequire.NoError(t, err)\n\n\trouter.AddMiddleware(pq)\n\n\treceivedMessages := make(chan int, 10)\n\n\tlock := sync.Mutex{}\n\tcounter := 0\n\n\trouter.AddConsumerHandler(\n\t\t\"test\",\n\t\t\"test\",\n\t\tpubSub,\n\t\tfunc(msg *message.Message) error {\n\t\t\ti, err := strconv.Atoi(string(msg.Payload))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tlock.Lock()\n\t\t\tdefer lock.Unlock()\n\t\t\tcounter++\n\n\t\t\tif counter < 10 && i%2 == 0 {\n\t\t\t\treturn errors.New(\"error\")\n\t\t\t}\n\n\t\t\treceivedMessages <- i\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\n\ttime.Sleep(time.Second)\n\n\tfor i := 0; i < 10; i++ {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), fmt.Append(nil, i))\n\t\terr := pubSub.Publish(\"test\", msg)\n\t\trequire.NoError(t, err)\n\t}\n\n\tvar received []int\n\n\ttimeout := false\n\tfor !timeout {\n\t\tselect {\n\t\tcase i := <-receivedMessages:\n\t\t\treceived = append(received, i)\n\t\tcase <-time.After(5 * time.Second):\n\t\t\ttimeout = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, received)\n}\n"
  },
  {
    "path": "dev/consolidate-gomods/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// simple script to consolidate all gomods to one gomod\n// required for GolangCI linter\nfunc main() {\n\tbigFatGomod := \"\"\n\n\tfor _, fileName := range getGomods() {\n\t\tdir := filepath.Dir(fileName)\n\t\tif dir == \".\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfile, err := os.Open(fileName)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tfileMod := \"\"\n\n\t\tscanner := bufio.NewScanner(file)\n\t\tfor scanner.Scan() {\n\t\t\ttxt := scanner.Text()\n\t\t\tif strings.HasPrefix(txt, \"go \") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(txt, \"module \") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfileMod += txt + \"\\n\"\n\t\t}\n\n\t\tif err := scanner.Err(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif fileMod != \"\" {\n\t\t\tbigFatGomod += \"// \" + fileName + \"\\n\"\n\t\t\tbigFatGomod += fileMod + \"\\n\"\n\t\t}\n\n\t\t_ = file.Close()\n\n\t\t// gomod is stupid, and go vendor removes all deps that are not needed\n\t\t// (and they are not needed if they are already meet in sub go.mods)\n\t\tif err := os.Remove(fileName); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tfmt.Println(bigFatGomod)\n}\n\nfunc getGomods() []string {\n\tvar fileList []string\n\n\terr := filepath.Walk(\".\", func(path string, f os.FileInfo, err error) error {\n\t\tif strings.Contains(path, \"/vendor/\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tif strings.Contains(path, \"go.mod\") {\n\t\t\tfileList = append(fileList, path)\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn fileList\n}\n"
  },
  {
    "path": "dev/coverage.sh",
    "content": "#!/bin/sh\n########\n# Source: https://gist.github.com/lwolf/3764a3b6cd08387e80aa6ca3b9534b8a\n# originally from https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage\n#######\n# Generate test coverage statistics for Go packages.\n#\n# Works around the fact that `go test -coverprofile` currently does not work\n# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909\n#\n# Usage: script/coverage [--html|--coveralls]\n#\n#     --html      Additionally create HTML report and open it in browser\n#     --coveralls Push coverage statistics to coveralls.io\n#\n\nset -e\n\nworkdir=.cover\nprofile=\"$workdir/cover.out\"\nmode=count\n\ngenerate_cover_data() {\n    rm -rf \"$workdir\"\n    mkdir \"$workdir\"\n\n    for pkg in \"$@\"; do\n        f=\"$workdir/$(echo $pkg | tr / -).cover\"\n        go test -covermode=\"$mode\" -coverprofile=\"$f\" \"$pkg\"\n    done\n\n    echo \"mode: $mode\" >\"$profile\"\n    grep -h -v \"^mode:\" \"$workdir\"/*.cover >>\"$profile\"\n}\n\nshow_cover_report() {\n    go tool cover -${1}=\"$profile\"\n}\n\npush_to_coveralls() {\n    echo \"Pushing coverage statistics to coveralls.io\"\n    goveralls -coverprofile=\"$profile\"\n}\n\ngenerate_cover_data $(go list ./... | grep -v /vendor/)\nshow_cover_report func\ncase \"$1\" in\n\"\")\n    ;;\n--html)\n    show_cover_report html ;;\n--coveralls)\n    push_to_coveralls ;;\n*)\n    echo >&2 \"error: invalid option: $1\"; exit 1 ;;\nesac"
  },
  {
    "path": "dev/prometheus.yml",
    "content": "# for Watermill development purposes.\n# there is one sample scrape target; add your own if needed.\nglobal:\n  scrape_interval:     15s\n  evaluation_interval: 15s\n\nscrape_configs:\n  - job_name: 'prometheus'\n    static_configs:\n    - targets: ['localhost:9090']\n\n  - job_name: 'some_metrics_endpoint'\n    static_configs:\n    - targets: ['localhost:8080']\n"
  },
  {
    "path": "dev/update-examples-deps/go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill/dev/update-examples-deps\n\ngo 1.25\n\ntoolchain go1.23.4\n"
  },
  {
    "path": "dev/update-examples-deps/go.sum",
    "content": ""
  },
  {
    "path": "dev/update-examples-deps/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n)\n\nvar latestGoVersion string\n\nfunc main() {\n\tlatestGoVersion = getLatestGoVersionFromWebsite()\n\n\tconst workers = 5\n\n\twg := sync.WaitGroup{}\n\twg.Add(workers)\n\n\tfiles := make(chan string)\n\n\tfor i := 0; i < workers; i++ {\n\t\tgo func() {\n\t\t\tfor file := range files {\n\t\t\t\tdir := filepath.Dir(file)\n\t\t\t\tif dir == \".\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"update of\", file, \"@\", dir)\n\n\t\t\t\tif err := replaceGoInDockerCompose(dir); err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\n\t\t\t\tif err := updateDeps(dir, file); err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\n\t\t\t\tif err := updateWatermill(dir, file); err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"failed to update %s: %s\", file, err))\n\t\t\t\t}\n\n\t\t\t\tif err := goModTidy(dir, file); err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\twg.Done()\n\t\t}()\n\t}\n\n\tfor _, file := range getGomods() {\n\t\tfiles <- file\n\t}\n\tclose(files)\n\n\twg.Wait()\n}\n\nfunc getGomods() []string {\n\tvar fileList []string\n\n\terr := filepath.Walk(\".\", func(path string, f os.FileInfo, err error) error {\n\t\tif strings.Contains(path, \"go.mod\") {\n\t\t\tfileList = append(fileList, path)\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn fileList\n}\n\nfunc getLatestGoVersionFromWebsite() string {\n\tresp, err := http.Get(\"https://go.dev/VERSION?m=text\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer resp.Body.Close()\n\n\tout, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tversion := strings.Split(string(out), \"\\n\")[0]\n\tversion = strings.TrimPrefix(version, \"go\")\n\n\t// we only want the major.minor version\n\tversion = strings.Split(version, \".\")[0] + \".\" + strings.Split(version, \".\")[1]\n\n\treturn version\n}\n\nfunc goModTidy(dir string, file string) error {\n\tcmd := []string{\"go\", \"mod\", \"tidy\", \"-go=\" + latestGoVersion}\n\n\tfmt.Println(\"\\nrunning\", cmd, \"in\", dir)\n\n\tcmd2 := exec.Command(cmd[0], cmd[1:]...)\n\tcmd2.Dir = dir\n\tcmd2.Stderr = os.Stderr\n\tcmd2.Stdout = os.Stdout\n\n\treturn cmd2.Run()\n}\n\n// replaceGoInDockerCompose replaces the go version in the Dockerfile\n// using Go (not sed)\nfunc replaceGoInDockerCompose(dir string) error {\n\tdockerComposeFile := filepath.Join(dir, \"docker-compose.yml\")\n\n\tb, err := os.ReadFile(dockerComposeFile)\n\t// return if not exist\n\tif os.IsNotExist(err) {\n\t\treturn nil\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpattern := `golang:1\\.[0-9]+(?:\\.[0-9]+)?`\n\tre, err := regexp.Compile(pattern)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to compile regex: %w\", err)\n\t}\n\n\tnewContent := re.ReplaceAllString(string(b), \"golang:\"+latestGoVersion)\n\n\terr = os.WriteFile(dockerComposeFile, []byte(newContent), 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write updated docker-compose.yml: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc updateWatermill(dir string, file string) error {\n\tc := []string{\"go\", \"get\", \"-u\", \"github.com/ThreeDotsLabs/watermill@latest\"}\n\n\tfmt.Println(\"\\nrunning\", c, \"in\", dir)\n\n\tcmd := exec.Command(c[0], c[1:]...)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdout = os.Stdout\n\terr := cmd.Run()\n\n\treturn err\n}\n\nfunc updateDeps(dir string, file string) error {\n\tc := []string{\"go\", \"get\", \"-u\", \"./...\"}\n\n\tfmt.Println(\"\\nrunning\", c, \"in\", dir)\n\n\tcmd := exec.Command(c[0], c[1:]...)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdout = os.Stdout\n\terr := cmd.Run()\n\n\treturn err\n}\n"
  },
  {
    "path": "dev/validate-examples/go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill/dev/validate-examples\n\ngo 1.25\n\nrequire (\n\tgithub.com/fatih/color v1.18.0\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n)\n"
  },
  {
    "path": "dev/validate-examples/go.sum",
    "content": "github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\n"
  },
  {
    "path": "dev/validate-examples/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\tyaml \"gopkg.in/yaml.v2\"\n)\n\ntype Config struct {\n\tValidationCmd   string   `yaml:\"validation_cmd\"`\n\tTeardownCmd     string   `yaml:\"teardown_cmd\"`\n\tTimeout         int      `yaml:\"timeout\"`\n\tExpectedOutput  string   `yaml:\"expected_output\"`\n\tExpectedOutputs []string `yaml:\"expected_outputs\"`\n}\n\nfunc (c *Config) LoadFrom(path string) error {\n\tfile, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = yaml.Unmarshal(file, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc main() {\n\tpath := \"../../_examples/\"\n\n\tif len(os.Args) > 1 {\n\t\tpath = filepath.Join(path, os.Args[1])\n\t}\n\n\twalkErr := filepath.Walk(path, func(exampleConfig string, f os.FileInfo, _ error) error {\n\t\tif f == nil {\n\t\t\treturn nil\n\t\t}\n\t\tmatches, err := filepath.Match(\".validate_example*.yml\", f.Name())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not match file, err: %w\", err)\n\t\t}\n\t\tif !matches {\n\t\t\treturn nil\n\t\t}\n\n\t\texampleDirectory := filepath.Dir(exampleConfig)\n\n\t\tfmt.Printf(\"validating %s\\n\", exampleDirectory)\n\n\t\terr = validate(exampleConfig)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"validation for %s failed, err: %v\", exampleDirectory, err)\n\t\t}\n\n\t\treturn nil\n\t})\n\tif walkErr != nil {\n\t\tpanic(walkErr)\n\t}\n\n}\n\nfunc validate(path string) error {\n\tconfig := &Config{}\n\terr := config.LoadFrom(path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not load config, err: %v\", err)\n\t}\n\n\tdirName := filepath.Base(filepath.Dir(path))\n\n\texpectedOutputs := config.ExpectedOutputs\n\tif config.ExpectedOutput != \"\" {\n\t\texpectedOutputs = append(expectedOutputs, config.ExpectedOutput)\n\t}\n\n\tfmt.Print(\"\\n\\n\")\n\tfmt.Println(\"Validating example:\", dirName)\n\tfmt.Println(\"Waiting for output: \", color.GreenString(fmt.Sprintf(\"%+q\", expectedOutputs)))\n\n\tcmdAndArgs := strings.Fields(config.ValidationCmd)\n\tvalidationCmd := exec.Command(cmdAndArgs[0], cmdAndArgs[1:]...)\n\tvalidationCmd.Dir = filepath.Dir(path)\n\tdefer func() {\n\t\tif config.TeardownCmd == \"\" {\n\t\t\treturn\n\t\t}\n\t\tcmdAndArgs := strings.Fields(config.TeardownCmd)\n\t\tteardownCmd := exec.Command(cmdAndArgs[0], cmdAndArgs[1:]...)\n\t\tteardownCmd.Dir = filepath.Dir(path)\n\t\t_ = teardownCmd.Run()\n\t}()\n\n\tstdout, err := validationCmd.StdoutPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not attach to stdout, err: %v\", err)\n\t}\n\n\tstderr, err := validationCmd.StderrPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not attach to stderr, err: %v\", err)\n\t}\n\n\tfmt.Printf(\"running: %v\\n\", validationCmd.Args)\n\n\terr = validationCmd.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not start validation, err: %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr := validationCmd.Process.Kill()\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"could not kill process in %s, err: %v\\n\", dirName, err)\n\t\t}\n\t}()\n\n\tsuccess := make(chan bool)\n\tlines := make(chan string)\n\n\tgo readLines(stdout, lines)\n\tgo readLines(stderr, lines)\n\n\toutputsFound := map[int]struct{}{}\n\n\tgo func() {\n\t\tfor line := range lines {\n\t\t\tfmt.Printf(\"[%s] > %s\\n\", color.CyanString(dirName), line)\n\n\t\t\tfor num, output := range expectedOutputs {\n\t\t\t\tok, _ := regexp.MatchString(output, line)\n\t\t\t\tif ok {\n\t\t\t\t\toutputsFound[num] = struct{}{}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(outputsFound) == len(expectedOutputs) {\n\t\t\t\tsuccess <- true\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-success:\n\t\treturn nil\n\tcase <-time.After(time.Duration(config.Timeout) * time.Second):\n\t\treturn fmt.Errorf(\"validation command timed out\")\n\t}\n}\n\nfunc readLines(reader io.Reader, output chan<- string) {\n\tscanner := bufio.NewScanner(reader)\n\tfor scanner.Scan() {\n\t\tif scanner.Err() != nil {\n\t\t\tif scanner.Err() == io.EOF {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tline := scanner.Text()\n\t\toutput <- line\n\t}\n}\n"
  },
  {
    "path": "doc.go",
    "content": "// Watermill is a Golang library for working efficiently with message streams.\n//\n// It is intended for building event driven applications,\n// enabling event sourcing, RPC over messages, sagas\n// and basically whatever else comes to your mind.\n//\n// You can use conventional pub/sub implementations\n// like Kafka or RabbitMQ, but also HTTP or MySQL binlog if that fits your use case.\n//\n// Website with detailed documentation: https://watermill.io/\n//\n// Getting started guide: https://watermill.io/learn/getting-started/\npackage watermill\n"
  },
  {
    "path": "docs/.npmignore",
    "content": ".env\r\n.netlify\r\n.hugo_build.lock\r\nnode_modules\r\npublic\r\nresources\r\n"
  },
  {
    "path": "docs/.npmrc",
    "content": "enable-pre-post-scripts=true\r\nauto-install-peers=true\r\nnode-linker=hoisted\r\nprefer-symlinked-executables=false\r\n"
  },
  {
    "path": "docs/.prettierignore",
    "content": "*.html\r\n*.ico\r\n*.png\r\n*.jp*g\r\n*.toml\r\n*.*ignore\r\n*.svg\r\n*.xml\r\nLICENSE\r\n.npmrc\r\n.gitkeep\r\n*.woff*\r\n"
  },
  {
    "path": "docs/.prettierrc.yaml",
    "content": "# Default config\r\ntabWidth: 4\r\nendOfLine: crlf\r\nsingleQuote: true\r\nprintWidth: 100000\r\ntrailingComma: none\r\nbracketSameLine: true\r\nquoteProps: consistent\r\nexperimentalTernaries: true\r\n\r\n# Overridden config\r\noverrides:\r\n  - files: [\"*.md\", \"*.json\", \"*.yaml\"]\r\n    options:\r\n      tabWidth: 2\r\n      singleQuote: false\r\n  - files: [\"*.scss\"]\r\n    options:\r\n      singleQuote: false\r\n"
  },
  {
    "path": "docs/DEVELOP.md",
    "content": "## How to Develop watermill.io docs?\n\n### Building & running\n\n```bash\n./build.sh\nnpm run dev\n```\n\n### Useful resources\n\n- [Available shortcodes](https://getdoks.org/docs/basics/shortcodes/)\n- [Diagrams](https://getdoks.org/docs/built-ins/diagrams/) (we recommend [Mermaid](https://getdoks.org/docs/built-ins/diagrams/#mermaid))\n- [Codeglocks](https://getdoks.org/docs/built-ins/code-blocks/)\n"
  },
  {
    "path": "docs/assets/images/.gitkeep",
    "content": ""
  },
  {
    "path": "docs/assets/js/custom.js",
    "content": "// Put your custom JS code here\r\n\r\n// a bit hacky way to force dark mode by default\r\n// it sets local storage item used by docs/node_modules/@thulite/doks-core/assets/js/color-mode.js\r\nif (!localStorage.getItem('theme')) {\r\n  localStorage.setItem('theme', 'dark');\r\n}\r\n\r\nimport { render } from 'github-buttons';\r\n\r\nlet renderGitHubButton= () => {\r\n  let oldButton = document.getElementById(\"github-button\");\r\n  if (oldButton) {\r\n    oldButton.remove();\r\n  }\r\n\r\n  let options = {\r\n    \"href\": \"https://github.com/ThreeDotsLabs/watermill\",\r\n    \"data-show-count\": true,\r\n    \"data-size\": \"large\",\r\n    \"data-color-scheme\": localStorage.getItem('theme'),\r\n  }\r\n\r\n  render(options, function (el) {\r\n    let menu = document.getElementById(\"offcanvasNavMain\").querySelector(\".offcanvas-body\");\r\n    let searchToggle = document.getElementById(\"searchToggleDesktop\");\r\n\r\n    el.setAttribute(\"id\", \"github-button\");\r\n    el.classList.add(\"nav-link\", \"px-2\", \"mx-auto\");\r\n    el.setAttribute(\"style\", \"margin-top: 12px;\");\r\n\r\n\r\n    menu.insertBefore(el, searchToggle);\r\n  })\r\n}\r\nrenderGitHubButton()\r\n"
  },
  {
    "path": "docs/assets/jsconfig.json",
    "content": "{\r\n  \"compilerOptions\": {\r\n    \"baseUrl\": \".\",\r\n    \"paths\": {\r\n      \"*\": [\"*\", \"..\\\\node_modules\\\\@thulite\\\\doks-core\\\\assets\\\\*\"]\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "docs/assets/scss/common/_custom.scss",
    "content": "// Put your custom SCSS code here\r\n* {\r\n  -webkit-font-smoothing: antialiased;\r\n}\r\n\r\nh1, h2, h3, h4, h5, .navbar-brand {\r\n  font-family: Quicksand, sans-serif;\r\n  font-weight: 700;\r\n}\r\n\r\n[data-bs-theme=\"dark\"] .only-light {\r\n    display: none;\r\n}\r\n\r\n[data-bs-theme=\"light\"] .only-dark {\r\n    display: none;\r\n}\r\n\r\n\r\n.homepage-features {\r\n    .row {\r\n        max-width: 1400px;\r\n        margin: 0 auto;\r\n    }\r\n}\r\n\r\n// Improved text alignment and spacing\r\n.text-center.text-lg-start {\r\n    @media (min-width: 992px) {\r\n        padding-right: 2rem;\r\n    }\r\n}\r\n\r\n\r\n// Learn page styling\r\n.learn-container {\r\n    max-width: 1200px;\r\n    margin: 0 auto;\r\n    padding: 3rem 1rem;\r\n}\r\n\r\n.learn-header {\r\n    text-align: center;\r\n    margin-bottom: 4rem;\r\n}\r\n\r\n.learn-main-title {\r\n    font-size: 3rem;\r\n    font-weight: 700;\r\n    margin-bottom: 1rem;\r\n    color: var(--bs-body-color);\r\n}\r\n\r\n.learn-subtitle {\r\n    font-size: 1.25rem;\r\n    color: var(--bs-body-color-secondary);\r\n    max-width: 600px;\r\n    margin: 0 auto;\r\n    line-height: 1.6;\r\n}\r\n\r\n.learn-section {\r\n    margin-bottom: 4rem;\r\n}\r\n\r\n.learn-section:last-child {\r\n    margin-bottom: 0;\r\n}\r\n\r\n.learn-section-title {\r\n    text-align: center;\r\n    font-size: 2rem;\r\n    font-weight: 600;\r\n    margin-bottom: 3rem;\r\n    color: var(--bs-body-color);\r\n}\r\n\r\n.learn-row {\r\n    display: grid;\r\n    grid-template-columns: repeat(3, 1fr);\r\n    gap: 2rem;\r\n    justify-items: center;\r\n}\r\n\r\n.learn-card {\r\n    display: flex;\r\n    flex-direction: column;\r\n    padding: 2rem;\r\n    border-radius: 0.75rem;\r\n    transition: all 0.3s ease;\r\n    text-decoration: none;\r\n    background: var(--bs-body-bg);\r\n    border: 1px solid var(--bs-border-color);\r\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\r\n    min-height: 280px;\r\n    width: 100%;\r\n    max-width: 350px;\r\n    text-align: center;\r\n    color: inherit;\r\n    cursor: pointer;\r\n    position: relative;\r\n    overflow: hidden;\r\n}\r\n\r\n.learn-card:hover {\r\n    transform: translateY(-2px);\r\n    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);\r\n    text-decoration: none;\r\n    color: inherit;\r\n    border-color: var(--bs-primary);\r\n}\r\n\r\n.learn-card:focus {\r\n    outline: 2px solid var(--bs-primary);\r\n    outline-offset: 2px;\r\n}\r\n\r\n.learn-card-icon {\r\n    margin: 0 auto 0;\r\n    color: var(--bs-primary);\r\n    display: flex;\r\n    justify-content: center;\r\n    align-items: center;\r\n    width: 100px;\r\n    height: 100px;\r\n}\r\n\r\n.learn-card h3 {\r\n    font-size: 1.25rem;\r\n    font-weight: 600;\r\n    margin-bottom: 0.5rem;\r\n    color: var(--bs-body-color);\r\n    margin-top: 0;\r\n}\r\n\r\n.learn-card-secondary {\r\n    display: block;\r\n    font-size: 0.9rem;\r\n    font-weight: 600;\r\n    color: var(--bs-gray-600);\r\n    margin-bottom: 1rem;\r\n}\r\n\r\n\r\n\r\n.learn-card p {\r\n    font-size: 0.95rem;\r\n    line-height: 1.6;\r\n    margin-bottom: 2rem;\r\n    flex-grow: 1;\r\n    color: var(--bs-body-color-secondary);\r\n}\r\n\r\n\r\n// Dark theme support for learn cards\r\n[data-bs-theme=\"dark\"] .learn-card {\r\n    background: var(--bs-body-bg);\r\n    border-color: var(--bs-border-color);\r\n}\r\n\r\n[data-bs-theme=\"dark\"] .learn-card:hover {\r\n    border-color: var(--bs-primary);\r\n}\r\n\r\n\r\n// Responsive grid for learn page\r\n@media (max-width: 767.98px) {\r\n    .learn-container {\r\n        padding: 2rem 1rem;\r\n    }\r\n\r\n    .learn-header {\r\n        margin-bottom: 3rem;\r\n    }\r\n\r\n    .learn-main-title {\r\n        font-size: 2.5rem;\r\n    }\r\n\r\n    .learn-subtitle {\r\n        font-size: 1.1rem;\r\n    }\r\n\r\n    .learn-section-title {\r\n        font-size: 1.5rem;\r\n        margin-bottom: 2rem;\r\n    }\r\n\r\n    .learn-row {\r\n        grid-template-columns: 1fr;\r\n        gap: 1.5rem;\r\n    }\r\n\r\n    .learn-card {\r\n        padding: 1.5rem;\r\n        min-height: 240px;\r\n        max-width: none;\r\n    }\r\n}\r\n\r\n@media (min-width: 768px) and (max-width: 1199.98px) {\r\n    .learn-row {\r\n        grid-template-columns: repeat(2, 1fr);\r\n    }\r\n\r\n    .learn-section:first-child .learn-row {\r\n        grid-template-columns: repeat(2, 1fr);\r\n    }\r\n\r\n    .learn-section:first-child .learn-row .learn-card:nth-child(3) {\r\n        grid-column: 1 / -1;\r\n        justify-self: center;\r\n    }\r\n}\r\n\r\n// Quickstart page styling\r\n.quickstart-container {\r\n    max-width: 800px;\r\n    margin: 0 auto;\r\n    padding: 3rem 1rem;\r\n}\r\n\r\n.quickstart-header {\r\n    text-align: center;\r\n    margin-bottom: 3rem;\r\n}\r\n\r\n.quickstart-main-title {\r\n    font-size: 3rem;\r\n    font-weight: 700;\r\n    margin-bottom: 1rem;\r\n    color: var(--bs-body-color);\r\n}\r\n\r\n.quickstart-subtitle {\r\n    font-size: 1.25rem;\r\n    color: var(--bs-body-color-secondary);\r\n    max-width: 600px;\r\n    margin: 0 auto;\r\n    line-height: 1.6;\r\n}\r\n\r\n.quickstart-content {\r\n    font-size: 1.1rem;\r\n    line-height: 1.8;\r\n}\r\n\r\n// Hide footer banner on quickstart page too\r\nbody.quickstart .event-driven-banner {\r\n    display: none;\r\n}\r\n\r\n// Responsive styling for quickstart\r\n@media (max-width: 767.98px) {\r\n    .quickstart-container {\r\n        padding: 2rem 1rem;\r\n    }\r\n\r\n    .quickstart-header {\r\n        margin-bottom: 2rem;\r\n    }\r\n\r\n    .quickstart-main-title {\r\n        font-size: 2.5rem;\r\n    }\r\n\r\n    .quickstart-subtitle {\r\n        font-size: 1.1rem;\r\n    }\r\n}\r\n\r\n// Responsive improvements\r\n@media (max-width: 991.98px) {\r\n    .display-5 {\r\n        font-size: 2rem;\r\n    }\r\n\r\n    .text-center.text-lg-start {\r\n        text-align: center !important;\r\n        padding-right: 0;\r\n    }\r\n}\r\n\r\n// Pub/Sub Logos Collage Styling\r\n.pubsub-logos-container {\r\n    padding: 2rem 0;\r\n}\r\n\r\n.pubsub-logos-grid {\r\n    display: flex;\r\n    flex-wrap: wrap;\r\n    gap: 2rem;\r\n    max-width: 900px;\r\n    margin: 0 auto;\r\n    align-items: center;\r\n    justify-content: center;\r\n}\r\n\r\n.pubsub-logo-item {\r\n    display: flex;\r\n    flex-direction: column;\r\n    align-items: center;\r\n    text-align: center;\r\n    transition: all 0.3s ease;\r\n    padding: 1rem;\r\n    border-radius: 0.75rem;\r\n    text-decoration: none;\r\n    color: inherit;\r\n    flex: 0 0 120px;\r\n\r\n    img {\r\n        width: 60px;\r\n        height: 60px;\r\n        margin-bottom: 0.75rem;\r\n        transition: all 0.3s ease;\r\n    }\r\n\r\n    span {\r\n        font-size: 0.9rem;\r\n        font-weight: 600;\r\n        color: var(--bs-body-color-secondary);\r\n        transition: all 0.3s ease;\r\n    }\r\n\r\n    &:hover {\r\n        background: rgba(var(--bs-primary-rgb), 0.05);\r\n        text-decoration: none;\r\n        color: inherit;\r\n\r\n        img {\r\n        }\r\n\r\n        span {\r\n            color: var(--bs-primary);\r\n        }\r\n    }\r\n\r\n    &:focus {\r\n        outline: 2px solid var(--bs-primary);\r\n        outline-offset: 2px;\r\n        text-decoration: none;\r\n        color: inherit;\r\n    }\r\n}\r\n\r\n// Dark theme support for pub/sub logos\r\n[data-bs-theme=\"dark\"] .pubsub-logo-item {\r\n    &:hover {\r\n        background: rgba(var(--bs-primary-rgb), 0.1);\r\n    }\r\n}\r\n\r\n// Responsive adjustments for pub/sub logos\r\n@media (max-width: 767.98px) {\r\n    .pubsub-logos-grid {\r\n        grid-template-columns: repeat(3, 1fr);\r\n        gap: 1.5rem;\r\n        max-width: 100%;\r\n    }\r\n\r\n    .pubsub-logo-item {\r\n        padding: 0.75rem;\r\n\r\n        img {\r\n            width: 50px;\r\n            height: 50px;\r\n            margin-bottom: 0.5rem;\r\n        }\r\n\r\n        span {\r\n            font-size: 0.8rem;\r\n        }\r\n    }\r\n}\r\n\r\n@media (max-width: 575.98px) {\r\n    .pubsub-logos-grid {\r\n        grid-template-columns: repeat(2, 1fr);\r\n        gap: 1rem;\r\n    }\r\n\r\n    .pubsub-logo-item {\r\n        img {\r\n            width: 45px;\r\n            height: 45px;\r\n        }\r\n\r\n        span {\r\n            font-size: 0.75rem;\r\n        }\r\n    }\r\n}\r\n\r\n// Homepage code blocks styling\r\n.homepage-code-block {\r\n    .frame {\r\n        border-radius: 12px !important;\r\n        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06) !important;\r\n        border: 1px solid var(--bs-border-color) !important;\r\n        overflow: hidden;\r\n        overflow-x: auto; // Allow horizontal scrolling when needed\r\n\r\n        // Ensure child elements respect the border radius\r\n        * {\r\n            border-radius: inherit !important;\r\n        }\r\n\r\n        // Target the actual code container\r\n        pre, code, .expressive-code-block {\r\n            border-radius: 12px !important;\r\n            border: none !important;\r\n        }\r\n\r\n        // Target any highlighted code elements\r\n        .highlight {\r\n            border-radius: 12px !important;\r\n            border: none !important;\r\n        }\r\n    }\r\n}\r\n\r\n[data-bs-theme=\"light\"] {\r\n    .homepage-code-block .frame pre,\r\n    .homepage-code-block .frame code,\r\n    .homepage-code-block .frame .expressive-code-block {\r\n        background: white !important;\r\n    }\r\n}\r\n\r\n// Responsive design for homepage code blocks\r\n@media (max-width: 767.98px) {\r\n    .homepage-code-block {\r\n      margin-left: -45px;\r\n      margin-right: -45px;\r\n    }\r\n}\r\n\r\n// Dark theme support for homepage code blocks\r\n[data-bs-theme=\"dark\"] .homepage-code-block {\r\n    .frame {\r\n        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2) !important;\r\n        border: 1px solid var(--bs-border-color) !important;\r\n    }\r\n}\r\n\r\n// Modern gradient text styles\r\n.gradient-text-1 {\r\n    background: linear-gradient(to right, #6366f1, #8b5cf6, #3b82f6);\r\n    -webkit-background-clip: text;\r\n    background-clip: text;\r\n    -webkit-text-fill-color: transparent;\r\n    color: transparent;\r\n}\r\n\r\n// Homepage gradient section backgrounds\r\n.homepage-section-gradient {\r\n    position: relative;\r\n\r\n    &::before {\r\n        content: '';\r\n        position: absolute;\r\n        top: 0;\r\n        left: 50%;\r\n        transform: translateX(-50%);\r\n        width: 100vw;\r\n        height: 100%;\r\n        border-top: 1px solid rgba(0, 0, 0, 0.08);\r\n        pointer-events: none;\r\n        z-index: -1;\r\n    }\r\n}\r\n\r\n// Single gradient class for all sections\r\n.homepage-section-gradient {\r\n    &::before {\r\n        background: linear-gradient(180deg, \r\n            rgba(79, 70, 229, 0.03) 0%, \r\n            transparent 100%\r\n        );\r\n    }\r\n}\r\n\r\n// Dark theme adjustments for gradients\r\n[data-bs-theme=\"dark\"] {\r\n    .homepage-section-gradient::before {\r\n        background: linear-gradient(180deg, \r\n            rgba(79, 70, 229, 0.06) 0%, \r\n            transparent 100%\r\n        );\r\n        border-top: 1px solid rgba(255, 255, 255, 0.08);\r\n    }\r\n}\r\n\r\n[data-bs-theme=\"dark\"], [data-bs-theme=\"light\"] {\r\n  /* Background */ .bg { color: #c9c9c9; background-color: #282c34; }\r\n  /* PreWrapper */ .chroma { color: #c9c9c9; background-color: #282c34; }\r\n  /* Other */ .chroma .x {  }\r\n  /* Error */ .chroma .err { color: #cf5967 }\r\n  /* CodeLine */ .chroma .cl {  }\r\n  /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }\r\n  /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }\r\n  /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }\r\n  /* LineHighlight */ .chroma .hl { background-color: #3d4148 }\r\n  /* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }\r\n  /* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }\r\n  /* Line */ .chroma .line { display: flex; }\r\n  /* Keyword */ .chroma .k { color: #7fbaf5 }\r\n  /* KeywordConstant */ .chroma .kc { color: #cf5967; }\r\n  /* KeywordDeclaration */ .chroma .kd { color: #7fbaf5 }\r\n  /* KeywordNamespace */ .chroma .kn { color: #bc74c4 }\r\n  /* KeywordPseudo */ .chroma .kp { color: #bc74c4 }\r\n  /* KeywordReserved */ .chroma .kr { color: #7fbaf5 }\r\n  /* KeywordType */ .chroma .kt { color: #57c7ff; font-weight: bold }\r\n  /* Name */ .chroma .n {  }\r\n  /* NameAttribute */ .chroma .na { color: #bc74c4 }\r\n  /* NameBuiltin */ .chroma .nb { color: #7fbaf5 }\r\n  /* NameBuiltinPseudo */ .chroma .bp { color: #7fbaf5 }\r\n  /* NameClass */ .chroma .nc { color: #ecbe7b }\r\n  /* NameConstant */ .chroma .no { color: #ecbe7b }\r\n  /* NameDecorator */ .chroma .nd { color: #ecbe7b }\r\n  /* NameEntity */ .chroma .ni {  }\r\n  /* NameException */ .chroma .ne { color: #cf5967 }\r\n  /* NameFunction */ .chroma .nf { color: #57c7ff }\r\n  /* NameFunctionMagic */ .chroma .fm {  }\r\n  /* NameLabel */ .chroma .nl { color: #cf5967 }\r\n  /* NameNamespace */ .chroma .nn {  }\r\n  /* NameOther */ .chroma .nx {  }\r\n  /* NameProperty */ .chroma .py {  }\r\n  /* NameTag */ .chroma .nt { color: #bc74c4 }\r\n  /* NameVariable */ .chroma .nv { color: #bc74c4; font-style: italic }\r\n  /* NameVariableClass */ .chroma .vc { color: #57c7ff; font-weight: bold }\r\n  /* NameVariableGlobal */ .chroma .vg { color: #ecbe7b }\r\n  /* NameVariableInstance */ .chroma .vi { color: #57c7ff }\r\n  /* NameVariableMagic */ .chroma .vm {  }\r\n  /* Literal */ .chroma .l {  }\r\n  /* LiteralDate */ .chroma .ld { color: #57c7ff }\r\n  /* LiteralString */ .chroma .s { color: #82cc6a }\r\n  /* LiteralStringAffix */ .chroma .sa { color: #82cc6a }\r\n  /* LiteralStringBacktick */ .chroma .sb { color: #57c7ff }\r\n  /* LiteralStringChar */ .chroma .sc { color: #57c7ff }\r\n  /* LiteralStringDelimiter */ .chroma .dl { color: #82cc6a }\r\n  /* LiteralStringDoc */ .chroma .sd { color: #82cc6a }\r\n  /* LiteralStringDouble */ .chroma .s2 { color: #82cc6a }\r\n  /* LiteralStringEscape */ .chroma .se { color: #56b6c2 }\r\n  /* LiteralStringHeredoc */ .chroma .sh { color: #56b6c2 }\r\n  /* LiteralStringInterpol */ .chroma .si { color: #82cc6a }\r\n  /* LiteralStringOther */ .chroma .sx { color: #82cc6a }\r\n  /* LiteralStringRegex */ .chroma .sr { color: #57c7ff }\r\n  /* LiteralStringSingle */ .chroma .s1 { color: #82cc6a }\r\n  /* LiteralStringSymbol */ .chroma .ss { color: #82cc6a }\r\n  /* LiteralNumber */ .chroma .m { color: #56b6c2 }\r\n  /* LiteralNumberBin */ .chroma .mb { color: #57c7ff }\r\n  /* LiteralNumberFloat */ .chroma .mf { color: #56b6c2 }\r\n  /* LiteralNumberHex */ .chroma .mh { color: #57c7ff }\r\n  /* LiteralNumberInteger */ .chroma .mi { color: #56b6c2 }\r\n  /* LiteralNumberIntegerLong */ .chroma .il { color: #56b6c2 }\r\n  /* LiteralNumberOct */ .chroma .mo { color: #57c7ff }\r\n  /* Operator */ .chroma .o { color: #bc74c4 }\r\n  /* OperatorWord */ .chroma .ow { color: #bc74c4 }\r\n  /* Punctuation */ .chroma .p { color: #56b6c2 }\r\n  /* Comment */ .chroma .c { color: #3e4460 }\r\n  /* CommentHashbang */ .chroma .ch { color: #3e4460; font-style: italic }\r\n  /* CommentMultiline */ .chroma .cm { color: #3e4460 }\r\n  /* CommentSingle */ .chroma .c1 { color: #3e4460 }\r\n  /* CommentSpecial */ .chroma .cs { color: #bc74c4; font-style: italic }\r\n  /* CommentPreproc */ .chroma .cp { color: #7fbaf5 }\r\n  /* CommentPreprocFile */ .chroma .cpf { color: #7fbaf5 }\r\n  /* Generic */ .chroma .g {  }\r\n  /* GenericDeleted */ .chroma .gd { color: #cf5967 }\r\n  /* GenericEmph */ .chroma .ge { text-decoration: underline }\r\n  /* GenericError */ .chroma .gr { color: #cf5967; font-weight: bold }\r\n  /* GenericHeading */ .chroma .gh { color: #ecbe7b; font-weight: bold }\r\n  /* GenericInserted */ .chroma .gi { color: #ecbe7b }\r\n  /* GenericOutput */ .chroma .go { color: #43454f }\r\n  /* GenericPrompt */ .chroma .gp {  }\r\n  /* GenericStrong */ .chroma .gs { color: #cf5967; font-weight: bold }\r\n  /* GenericSubheading */ .chroma .gu { color: #cf5967; font-style: italic }\r\n  /* GenericTraceback */ .chroma .gt {  }\r\n  /* GenericUnderline */ .chroma .gl { text-decoration: underline }\r\n  /* TextWhitespace */ .chroma .w {  }\r\n\r\n}\r\n"
  },
  {
    "path": "docs/assets/scss/common/_variables-custom.scss",
    "content": "// Put your custom SCSS variables here\r\n$font-family-sans-serif:\r\n  \"Heebo\",\r\n  \"sans-serif\",\r\n  system-ui,\r\n  -apple-system,\r\n  \"Segoe UI\",\r\n  Roboto,\r\n  \"Helvetica Neue\",\r\n  \"Noto Sans\",\r\n  \"Liberation Sans\",\r\n  Arial,\r\n  sans-serif,\r\n  \"Apple Color Emoji\",\r\n  \"Segoe UI Emoji\",\r\n  \"Segoe UI Symbol\",\r\n  \"Noto Color Emoji\";\r\n\r\n$container-max-widths: (\r\n    sm: 540px,\r\n    md: 720px,\r\n    lg: 960px,\r\n    xl: 1240px,\r\n    xxl: 1820px\r\n);\r\n\r\n\r\n$primary: #4f46e5;\r\n"
  },
  {
    "path": "docs/assets/svgs/.gitkeep",
    "content": ""
  },
  {
    "path": "docs/build.sh",
    "content": "#!/bin/bash\nset -e -x\n\ncd \"$(dirname \"$0\")\"\n\nfunction cloneOrPull() {\n    if [[ -d \"$2\" ]]\n    then\n        pushd $2\n        git pull\n        popd\n    else\n        git clone --single-branch $1 $2\n    fi\n}\n\nif [[ \"$1\" == \"--copy\" ]]; then\n    rm -rf content/src-link || true\n    mkdir content/src-link/\n    cp -r ../message content/src-link/\n    cp -r ../pubsub content/src-link/\n    cp -r ../_examples content/src-link/\n    cp -r ../components content/src-link/\nelse\n    declare -a files_to_link=(\n        \"_examples\"\n\n        \"message/decorator.go\"\n        \"message/message.go\"\n        \"message/pubsub.go\"\n        \"message/router.go\"\n        \"message/router_context.go\"\n        \"pubsub/gochannel/pubsub.go\"\n        \"pubsub/gochannel/fanout.go\"\n\n        \"components/cqrs/command_bus.go\"\n        \"components/cqrs/command_processor.go\"\n        \"components/cqrs/command_handler.go\"\n\n        \"components/cqrs/event_bus.go\"\n        \"components/cqrs/event_processor.go\"\n        \"components/cqrs/event_processor_group.go\"\n        \"components/cqrs/event_handler.go\"\n\n        \"components/cqrs/marshaler.go\"\n        \"components/cqrs/cqrs.go\"\n        \"components/cqrs/marshaler.go\"\n\n        \"components/delay/delay.go\"\n        \"components/delay/publisher.go\"\n\n        \"components/requeuer/requeuer.go\"\n\n        \"components/metrics/builder.go\"\n        \"components/metrics/http.go\"\n\n        \"components/fanin/fanin.go\"\n    )\n\n    pushd ../\n    for i in \"${files_to_link[@]}\"\n    do\n        DIR=$(dirname \"${i}\")\n        DEST_DIR=\"docs/content/src-link/${DIR}\"\n\n        mkdir -p \"${DEST_DIR}\"\n        ln -sf \"$PWD/${i}\" \"$PWD/${DEST_DIR}\"\n    done\n    popd\nfi\n\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-amqp.git\" content/src-link/watermill-amqp\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-googlecloud.git\" content/src-link/watermill-googlecloud\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-http.git\" content/src-link/watermill-http\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-io.git\" content/src-link/watermill-io\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-kafka.git\" content/src-link/watermill-kafka\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-nats.git\" content/src-link/watermill-nats\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-sql.git\" content/src-link/watermill-sql\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-firestore.git\" content/src-link/watermill-firestore\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-bolt.git\" content/src-link/watermill-bolt\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-redisstream.git\" content/src-link/watermill-redisstream\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-aws.git\" content/src-link/watermill-aws\ncloneOrPull \"https://github.com/ThreeDotsLabs/watermill-sqlite.git\" content/src-link/watermill-sqlite\n\nfind content/src-link -name '*.md' -delete\nfind content/src-link -name '*.html' -delete\n\npython3 ./extract_middleware_godocs.py > content/src-link/middleware-defs.md\n\nhugo --gc --minify\n"
  },
  {
    "path": "docs/config/_default/hugo.toml",
    "content": "title = \"Watermill\"\r\nbaseurl = \"http://localhost/\"\r\ncanonifyURLs = false\r\ndisableAliases = true\r\ndisableHugoGeneratorInject = true\r\n# disableKinds = [\"taxonomy\", \"term\"]\r\nenableEmoji = true\r\nenableGitInfo = false\r\nenableRobotsTXT = true\r\nlanguageCode = \"en-US\"\r\npaginate = 10\r\nrssLimit = 10\r\nsummarylength = 20 # 70 (default)\r\n\r\n# Multilingual\r\ndefaultContentLanguage = \"en\"\r\ndisableLanguages = []\r\ndefaultContentLanguageInSubdir = false\r\n\r\ncopyRight = \"Three Dots Labs\"\r\n\r\n[build.buildStats]\r\n  enable = true\r\n\r\n[outputs]\r\n  home = [\"HTML\", \"RSS\", \"searchIndex\"]\r\n  section = [\"HTML\", \"RSS\", \"SITEMAP\"]\r\n\r\n[outputFormats.searchIndex]\r\n  mediaType = \"application/json\"\r\n  baseName = \"search-index\"\r\n  isPlainText = true\r\n  notAlternative = true\r\n\r\n# Add output format for section sitemap.xml\r\n[outputFormats.SITEMAP]\r\n  mediaType = \"application/xml\"\r\n  baseName = \"sitemap\"\r\n  isHTML = false\r\n  isPlainText = true\r\n  noUgly = true\r\n  rel  = \"sitemap\"\r\n\r\n[sitemap]\r\n  changefreq = \"monthly\"\r\n  filename = \"sitemap.xml\"\r\n  priority = 0.5\r\n\r\n[caches]\r\n  [caches.getjson]\r\n    dir = \":cacheDir/:project\"\r\n    maxAge = -1 # \"30m\"\r\n\r\n[taxonomies]\r\n  contributor = \"contributors\"\r\n  category = \"categories\"\r\n  tag = \"tags\"\r\n\r\n[minify.tdewolff.html]\r\n  keepWhitespace = false\r\n\r\n[related]\r\n  threshold = 80\r\n  includeNewer = true\r\n  toLower = false\r\n    [[related.indices]]\r\n      name = \"categories\"\r\n      weight = 100\r\n    [[related.indices]]\r\n      name = \"tags\"\r\n      weight = 80\r\n    [[related.indices]]\r\n      name = \"date\"\r\n      weight = 10\r\n\r\n[imaging]\r\n  anchor = \"Center\"\r\n  bgColor = \"#ffffff\"\r\n  hint = \"photo\"\r\n  quality = 85\r\n  resampleFilter = \"Lanczos\"\r\n"
  },
  {
    "path": "docs/config/_default/languages.toml",
    "content": "[en]\r\n  languageName = \"English\"\r\n  contentDir = \"content/en\"\r\n  weight = 10\r\n  [en.params]\r\n    languageISO = \"EN\"\r\n    languageTag = \"en-US\"\r\n    footer = ''\r\n    alertText = ''\r\n"
  },
  {
    "path": "docs/config/_default/markup.toml",
    "content": "defaultMarkdownHandler = \"goldmark\"\r\n\r\n[goldmark]\r\n  [goldmark.extensions]\r\n    linkify = true\r\n  [goldmark.parser]\r\n    autoHeadingID = true\r\n    autoHeadingIDType = \"github\"\r\n    [goldmark.parser.attribute]\r\n      block = true\r\n      title = true\r\n  [goldmark.renderer]\r\n    unsafe = true\r\n\r\n[highlight]\r\n  anchorLineNos = false\r\n  codeFences = true\r\n  guessSyntax = false\r\n  hl_Lines = ''\r\n  hl_inline = false\r\n  lineAnchors = ''\r\n  lineNoStart = 1\r\n  lineNos = false\r\n  lineNumbersInTable = false\r\n  noClasses = false\r\n  noHl = false\r\n  style = 'vulcan'\r\n  tabWidth = 2\r\n\r\n[tableOfContents]\r\n  endLevel = 3\r\n  ordered = false\r\n  startLevel = 2\r\n"
  },
  {
    "path": "docs/config/_default/menus/menus.en.toml",
    "content": "[[main]]\r\n  name = \"Learn\"\r\n  url = \"/learn/\"\r\n  weight = 5\r\n\r\n[[main]]\r\n  name = \"Docs\"\r\n  url = \"/learn/getting-started/\"\r\n  weight = 10\r\n\r\n[[main]]\r\n  name = \"Support\"\r\n  url = \"/support\"\r\n  weight = 40\r\n\r\n[[social]]\r\n  name = \"GitHub\"\r\n  pre = '<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-brand-github\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5\"></path></svg>'\r\n  url = \"https://github.com/ThreeDotsLabs/watermill\"\r\n  post = \"v0.1.0\"\r\n  weight = 30\r\n\r\n[[sidebar]]\r\n  name = \"Learn\"\r\n  pageRef = \"/learn\"\r\n  weight = 5\r\n\r\n[[sidebar]]\r\n  name = \"Basics\"\r\n  pageRef = \"/docs\"\r\n  weight = 10\r\n\r\n[[sidebar]]\r\n  name = \"Advanced Topics\"\r\n  pageRef = \"/advanced\"\r\n  weight = 20\r\n\r\n[[sidebar]]\r\n  name = \"Supported Pub/Subs\"\r\n  pageRef = \"/pubsubs\"\r\n  weight = 30\r\n\r\n[[sidebar]]\r\n  name = \"Development\"\r\n  pageRef = \"/development\"\r\n  weight = 40\r\n"
  },
  {
    "path": "docs/config/_default/module.toml",
    "content": "# mounts\r\n## archetypes\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/doks-core/archetypes\"\r\n  target = \"archetypes\"\r\n\r\n[[mounts]]\r\n  source = \"archetypes\"\r\n  target = \"archetypes\"\r\n\r\n## assets\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/core/assets\"\r\n  target = \"assets\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/images/assets\"\r\n  target = \"assets\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/doks-core/assets\"\r\n  target = \"assets\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@tabler/icons/icons\"\r\n  target = \"assets/svgs/tabler-icons\"\r\n\r\n[[mounts]]\r\n  source = \"assets\"\r\n  target = \"assets\"\r\n\r\n## content\r\n[[mounts]]\r\n  source = \"content\"\r\n  target = \"content\"\r\n\r\n## data\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/doks-core/data\"\r\n  target = \"data\"\r\n\r\n[[mounts]]\r\n  source = \"data\"\r\n  target = \"data\"\r\n\r\n## i18n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/doks-core/i18n\"\r\n  target = \"i18n\"\r\n\r\n[[mounts]]\r\n  source = \"i18n\"\r\n  target = \"i18n\"\r\n\r\n## layouts\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/core/layouts\"\r\n  target = \"layouts\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/seo/layouts\"\r\n  target = \"layouts\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/images/layouts\"\r\n  target = \"layouts\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/doks-core/layouts\"\r\n  target = \"layouts\"\r\n\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/inline-svg/layouts\"\r\n  target = \"layouts\"\r\n\r\n[[mounts]]\r\n  source = \"layouts\"\r\n  target = \"layouts\"\r\n\r\n## static\r\n[[mounts]]\r\n  source = \"node_modules/@thulite/doks-core/static\"\r\n  target = \"static\"\r\n\r\n[[mounts]]\r\n  source = \"static\"\r\n  target = \"static\"\r\n"
  },
  {
    "path": "docs/config/_default/params.toml",
    "content": "# Hugo\r\ntitle = \"Watermill\"\r\ndescription = \"Building event-driven applications the easy way in Go.\"\r\nimages = [\"cover.png\"]\r\n\r\n# mainSections\r\nmainSections = [\"docs\"]\r\n\r\n[social]\r\n  twitter = \"ThreeDotsLabs\"\r\n\r\n# Doks (@thulite/doks-core)\r\n[doks]\r\n  # Color mode - it should stay auto to render toggle button,\r\n  # in practice we are using dark by default (see docs/assets/js/custom.js)\r\n  colorMode = \"auto\" # auto (default), light or dark\r\n  colorModeToggler = true # true (default) or false (this setting is only relevant when colorMode = auto)\r\n\r\n  # Navbar\r\n  navbarSticky = true # true (default) or false\r\n  containerBreakpoint = \"lg\" # \"\", \"sm\", \"md\", \"lg\" (default), \"xl\", \"xxl\", or \"fluid\"\r\n\r\n  ## Button\r\n  navBarButton = false # false (default) or true\r\n  navBarButtonUrl = \"/docs/prologue/introduction/\"\r\n  navBarButtonText = \"Get started\"\r\n\r\n  # FlexSearch\r\n  flexSearch = true # true (default) or false\r\n  searchExclKinds = [] # list of page kinds to exclude from search indexing (e.g. [\"home\", \"taxonomy\", \"term\"] )\r\n  searchExclTypes = [] # list of content types to exclude from search indexing (e.g. [\"blog\", \"docs\", \"legal\", \"contributors\", \"categories\"])\r\n  showSearch = [] # [] (all pages, default) or homepage (optionally) and list of sections (e.g. [\"homepage\", \"blog\", \"guides\"])\r\n  indexSummary = false # true or false (default); whether to index only the `.Summary` instead of the full `.Content`; limits the respective JSON field size and thus increases loading time\r\n\r\n  ## Search results\r\n  showDate = false # false (default) or true\r\n  showSummary = true # true (default) or false\r\n  searchLimit = 99 # 0 (no limit, default) or natural number\r\n\r\n  # Global alert\r\n  alert = false # false (default) or true\r\n  alertDismissable = true # true (default) or false\r\n\r\n  # Bootstrap\r\n  bootstrapJavascript = false # false (default) or true\r\n\r\n  # Nav\r\n  sectionNav = [\"learn\", \"docs\", \"advanced\", \"pubsubs\", \"development\"] # [\"docs\"] (default) or list of sections (e.g. [\"docs\", \"guides\"])\r\n  toTopButton = false # false (default) or true\r\n  breadcrumbTrail = false # false (default) or true\r\n  headlineHash = true # true (default) or false\r\n  scrollSpy = true # true (default) or false\r\n\r\n  # Multilingual\r\n  multilingualMode = false # false (default) or true\r\n  showMissingLanguages = true # whether or not to show untranslated languages in the language menu; true (default) or false\r\n\r\n  # Versioning\r\n  docsVersioning = false # false (default) or true\r\n  docsVersion = \"1.0\"\r\n\r\n  # UX\r\n  headerBar = false # true (default) or false\r\n  backgroundDots = false # true (default) or false\r\n\r\n  # Homepage\r\n  sectionFooter = false # false (default) or true\r\n\r\n  # Blog\r\n  relatedPosts = false # false (default) or true\r\n  imageList = true # true (default) or false\r\n  imageSingle = true # true (default) or false\r\n\r\n  # Repository\r\n  editPage = true # false (default) or true\r\n  lastMod = false # false (default) or true\r\n  repoHost = \"GitHub\" # GitHub (default), Gitea, GitLab, Bitbucket, or BitbucketServer\r\n  docsRepo = \"https://github.com/ThreeDotsLabs/watermill\"\r\n  docsRepoBranch = \"master\" # main (default), master, or <branch name>\r\n  docsRepoSubPath = \"/docs\" # \"\" (none, default) or <sub path>\r\n\r\n  # SCSS colors\r\n  # backGround = \"yellowgreen\"\r\n  ## Dark theme\r\n  # textDark = \"#dee2e6\" # \"#dee2e6\" (default), \"#dee2e6\" (original), or custom color\r\n  # accentDark = \"#5d2f86\" # \"#5d2f86\" (default), \"#5d2f86\" (original), or custom color\r\n  ## Light theme\r\n  # textLight = \"#1d2d35\" # \"#1d2d35\" (default), \"#1d2d35\" (original), or custom color\r\n  # accentLight = \"#8ed6fb\" # \"#8ed6fb\" (default), \"#8ed6fb\" (original), or custom color\r\n\r\n  # [doks.menu]\r\n  #   [doks.menu.section]\r\n  #     auto = true # true (default) or false\r\n  #     collapsibleSidebar = true # true (default) or false\r\n\r\n# Debug\r\n[render_hooks.image]\r\n  errorLevel = 'ignore' # ignore (default), warning, or error (fails the build)\r\n\r\n[render_hooks.link]\r\n  errorLevel = 'ignore' # ignore (default), warning, or error (fails the build)\r\n  highlightBroken = false # true or false (default)\r\n\r\n# Images (@thulite/images)\r\n[thulite_images]\r\n  [thulite_images.defaults]\r\n    decoding = \"async\" # sync, async, or auto (default)\r\n    fetchpriority = \"auto\" # high, low, or auto (default)\r\n    loading = \"lazy\" # eager or lazy (default)\r\n    widths = [480, 576, 768, 1025, 1200, 1440] # [640, 768, 1024, 1366, 1600, 1920] for example\r\n    sizes = \"auto\" # 100vw (default), 75vw, or auto for example\r\n    process = \"\" # \"fill 1600x900\" or \"fill 2100x900\" for example\r\n    lqip = \"16x webp q20\" # \"16x webp q20\" or \"21x webp q20\" for example\r\n\r\n# Inline SVG (@thulite/inline-svg)\r\n[inline_svg]\r\n  iconSetDir = \"tabler-icons\" # \"tabler-icons\" (default)\r\n\r\n# SEO (@thulite/seo)\r\n[seo]\r\n  [seo.title]\r\n    separator = \" | \"\r\n    suffix = \"Watermill | Event-Driven in Go\"\r\n  [seo.favicons]\r\n    sizes = []\r\n    icon = \"favicon.png\" # favicon.png (default)\r\n    svgIcon = \"favicon.svg\" # favicon.svg (default)\r\n    maskIcon = \"mask-icon.svg\" # mask-icon.svg (default)\r\n    maskIconColor = \"white\" # white (default)\r\n  [seo.schemas]\r\n    type = \"Organization\" # Organization (default) or Person\r\n    logo = \"favicon-512x512.png\" # Logo of Organization — favicon-512x512.png (default)\r\n    name = \"Three Dots Labs\" # Name of Organization or Person\r\n    sameAs = [] # E.g. [\"https://github.com/thuliteio/thulite\", \"https://fosstodon.org/@thulite\"]\r\n    images = [\"cover.png\"] # [\"cover.png\"] (default)\r\n    article = [] # Article sections\r\n    newsArticle = [] # NewsArticle sections\r\n    blogPosting = [\"blog\"] # BlogPosting sections\r\n    product = [] # Product sections\r\n"
  },
  {
    "path": "docs/config/babel.config.js",
    "content": "module.exports = {\r\n    presets: [\r\n        [\r\n            '@babel/preset-env',\r\n            {\r\n                targets: {\r\n                    browsers: [\r\n                        // Best practice: https://github.com/babel/babel/issues/7789\r\n                        '>=1%',\r\n                        'not ie 11',\r\n                        'not op_mini all'\r\n                    ]\r\n                }\r\n            }\r\n        ]\r\n    ]\r\n};\r\n"
  },
  {
    "path": "docs/config/next/hugo.toml",
    "content": "# Overrides for next environment\r\nbaseurl = \"/\"\r\n"
  },
  {
    "path": "docs/config/postcss.config.js",
    "content": "const autoprefixer = require('autoprefixer');\r\nconst purgecss = require('@fullhuman/postcss-purgecss');\r\nconst whitelister = require('purgecss-whitelister');\r\n\r\nmodule.exports = {\r\n    plugins: [\r\n        autoprefixer(),\r\n        purgecss({\r\n            content: ['./hugo_stats.json'],\r\n            extractors: [\r\n                {\r\n                    extractor: (content) => {\r\n                        const els = JSON.parse(content).htmlElements;\r\n                        return els.tags.concat(els.classes, els.ids);\r\n                    },\r\n                    extensions: ['json']\r\n                }\r\n            ],\r\n            dynamicAttributes: [\r\n                'aria-expanded',\r\n                'data-bs-popper',\r\n                'data-bs-target',\r\n                'data-bs-theme',\r\n                'data-dark-mode',\r\n                'data-global-alert',\r\n                'data-pane', // tabs.js\r\n                'data-popper-placement',\r\n                'data-sizes',\r\n                'data-toggle-tab', // tabs.js\r\n                'id',\r\n                'size',\r\n                'type'\r\n            ],\r\n            safelist: [\r\n                'active',\r\n                'btn-clipboard', // clipboards.js\r\n                'clipboard', // clipboards.js\r\n                'disabled',\r\n                'hidden',\r\n                'modal-backdrop', // search-modal.js\r\n                'selected', // search-modal.js\r\n                'show',\r\n                'img-fluid',\r\n                'blur-up',\r\n                'lazyload',\r\n                'lazyloaded',\r\n                'alert-link',\r\n                'container-fw ',\r\n                'container-lg',\r\n                'container-fluid',\r\n                'offcanvas-backdrop',\r\n                'figcaption',\r\n                'dt',\r\n                'dd',\r\n                'showing',\r\n                'hiding',\r\n                'page-item',\r\n                'page-link',\r\n                'not-content',\r\n                ...whitelister(['./assets/scss/**/*.scss', './node_modules/@thulite/doks-core/assets/scss/components/_code.scss', './node_modules/@thulite/doks-core/assets/scss/components/_expressive-code.scss', './node_modules/@thulite/doks-core/assets/scss/common/_syntax.scss'])\r\n            ]\r\n        })\r\n    ]\r\n};\r\n"
  },
  {
    "path": "docs/config/production/hugo.toml",
    "content": "# Overrides for production environment\r\n"
  },
  {
    "path": "docs/content/_index.md",
    "content": "---\r\ntitle: \"Watermill\"\r\ndescription: \"Building event-driven applications the easy way in Go.\"\r\nlead: \"Building event-driven applications the easy way in Go.\"\r\ndate: 2023-09-07T16:33:54+02:00\r\nlastmod: 2023-09-07T16:33:54+02:00\r\ndraft: false\r\nseo:\r\n  title: \"Watermill\" # custom title (optional)\r\n  description: \"\" # custom description (recommended)\r\n  canonical: \"\" # custom canonical URL (optional)\r\n  noindex: false # false (default) or true\r\n---\r\n"
  },
  {
    "path": "docs/content/advanced/delayed-messages.md",
    "content": "+++\ntitle = \"Delayed Messages\"\ndescription = \"Receive messages with a delay\"\nweight = -40\ndraft = false\nbref = \"Receive messages with a delay\"\n+++\n\nDelaying events or commands is a common use case in many applications.\nFor example, you may want to send the user a reminder after a few days of signing up.\nIt's not a complex logic to implement, but you can leverage messages to use it out of the box.\n\n## Delay Metadata\n\nWatermill's [`delay`](https://github.com/ThreeDotsLabs/watermill/tree/master/components/delay) package allows you to \n*add delay metadata* to messages.\n\n{{< callout \"danger\" >}}\n**The delay metadata does nothing by itself. You need to use a Pub/Sub implementation that supports it to make it work.**\n\nSee below for supported Pub/Subs.\n{{< /callout >}}\n\nThere are two APIs you can use. If you work with raw messages, use `delay.Message`:\n\n```go\nmsg := message.NewMessage(watermill.NewUUID(), []byte(\"hello\"))\ndelay.Message(msg, delay.For(time.Second * 10))\n```\n\nIf you use the CQRS component, use `delay.WithContext` instead (since you can't access the message directly):\n\n{{% 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\" %}}\n\nYou can also use `delay.Until` instead of `delay.For` to specify `time.Time` instead of `time.Duration`.\n\n## Supported Pub/Subs\n\n* [PostgreSQL](/pubsubs/sql/)\n* [MySQL](/pubsubs/sql/)\n\n## Full Example\n\nSee the [full example](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/real-world-examples/delayed-messages) in the Watermill repository.\n"
  },
  {
    "path": "docs/content/advanced/fanin.md",
    "content": "+++\ntitle = \"FanIn (merging topics)\"\ndescription = \"Merging two topics into one with the FanIn component\"\ndate = 2023-01-21T12:47:30+01:00\nweight = -100\ndraft = false\nbref = \"Merging two topics into one with the FanIn component\"\n+++\n\n## FanIn component\n\nThe FanIn component merges two topics into one.\n\n### Configuring\n\n{{% load-snippet-partial file=\"src-link/components/fanin/fanin.go\" first_line_contains=\"type Config struct {\" last_line_contains=\"CloseTimeout time.Duration\" padding_after=\"1\" %}}\n\n### Running\n\nYou need to provide a Publisher and a Subscriber implementation for the FanIn component.\n\nYou can find the list of supported Pub/Subs on [Supported Pub/Subs page](/pubsubs/).\nThe Publisher and subscriber can be implemented by different message brokers (for example, you can merge a Kafka topic with a RabbitMQ topic).\n\n```go\n\nlogger := watermill.NewStdLogger(false, false)\n\n// create Publisher and Subscriber\npub, err := // ...\nsub, err := // ...\n\nfi, err := fanin.NewFanIn(\n    sub,\n    pub,\n    fanin.Config{\n        SourceTopics: upstreamTopics,\n        TargetTopic:  downstreamTopic,\n    },\n    logger,\n)\nif err != nil {\n    panic(err)\n}\n\nif err := fi.Run(context.Background()); err != nil {\n    panic(err)\n}\n```\n\n### Controlling FanIn component\n\nThe FanIn component can be stopped by cancelling the context passed to the `Run` method or by calling the `Close` method.\n\n{{% load-snippet-partial file=\"src-link/components/fanin/fanin.go\" first_line_contains=\"func (f *FanIn) Run\" last_line_contains=\" Close() error\" padding_after=\"2\" %}}\n"
  },
  {
    "path": "docs/content/advanced/fanout.md",
    "content": "+++\ntitle = \"FanOut (multiplying messages)\"\ndescription = \"FanOut is a component that receives messages from the subscriber and passes them to all publishers.\"\ndate = 2024-10-09T02:47:30+01:00\nweight = -50\ndraft = false\nbref = \"FanOut is a component that receives messages from the subscriber and passes them to all publishers.\"\n+++\n\n## FanOut component\n\nFanOut is a component that receives messages from a topic and passes them to all subscribers. In effect, messages are \"multiplied\".\n\nA typical use case for using FanOut is having one external subscription and multiple workers\ninside the process.\n\n### Configuring\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/fanout.go\" first_line_contains=\"// NewFanOut\" last_line_contains=\")\" padding_after=\"0\" %}}\n\nYou need to call AddSubscription method for all topics that you want to listen to.\nThis needs to be done *before* starting the FanOut.\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/fanout.go\" first_line_contains=\"// AddSubscription\" last_line_contains=\")\" padding_after=\"0\" %}}\n\n### Running\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/fanout.go\" first_line_contains=\"// Run\" last_line_contains=\")\" padding_after=\"0\" %}}\n\nThen, use it as any other `message.Subscriber`.\n"
  },
  {
    "path": "docs/content/advanced/forwarder.md",
    "content": "+++\ntitle = \"Forwarder (the outbox pattern)\"\ndescription = \"Implement outbox pattern by publishing messages in transactional way\"\ndate = 2021-01-13T12:47:30+01:00\nweight = -300\ndraft = false\nbref = \"Emitting events along with storing data in a database in one transaction\"\n+++\n\n## Publishing messages in transactions (and why we should care) \nWhile working with an event-driven application, you may in some point need to store an application state and publish a message \ntelling the rest of the system about what's just happened. In a perfect scenario, you'd want to persist the application state \nand publish the message **in a transaction**, as not doing so might get you easily into troubles with data consistency. In \norder to commit both storing data and emitting an event in one transaction, you'd have to be able to publish \nmessages to the same database you use for the data storage, \nor implement [2PC](https://martinfowler.com/articles/patterns-of-distributed-systems/two-phase-commit.html) \non your own. If you don't want to change your message broker to a database, nor invent the wheel once again,\nyou can make your life easier by using Watermill's [Forwarder component](https://github.com/ThreeDotsLabs/watermill/blob/master/components/forwarder/forwarder.go)! \n\n## Forwarder component \nYou can think of the Forwarder as a background running daemon which awaits messages that are published to a database, and makes sure they eventually reach a message broker.  \n\n<img src=\"/img/publishing-with-forwarder.svg\" alt=\"Watermill Forwarder component\" style=\"width:100%;\" />\n\nIn order to make the Forwarder universal and usable transparently, it listens to a single topic on an intermediate \ndatabase based Pub/Sub, where enveloped messages are sent with help of a decorated [Forwarder Publisher](https://github.com/ThreeDotsLabs/watermill/blob/9e04bfefbd6fef9f9ffa59956654277005fa2e8a/components/forwarder/publisher.go#L30). \nThe Forwarder unwraps them, and sends to a specified destined topic on the message broker.  \n\n<img src=\"/img/forwarder-envelope.svg\" alt=\"Forwarder envelope\" style=\"width:100%; background-color: white; padding: 2rem;\" />\n\n## Example\n\nLet's consider a following example: there's a command which responsibility is to run a lottery. It has to pick \na random user that's registered in the system as a winner. While it does so, it should also persist the decision it made by \nstoring a database entry associating a unique lottery ID with a picked user's ID. Additionally, as it's an \nevent-driven system, it should emit a `LotteryConcluded` event, so that other components could react to that appropriately. \nTo be precise - there will be component responsible for sending prizes to lottery winners. It will receive `LotteryConcluded`\nevents, and using the lottery ID embedded in the event, verify who was the winner, checking with the database entry. \n\nIn our case, the database is MySQL and the message broker is Google Pub/Sub, but it could be any two other technologies.  \n\nApproaching to implementation of such a command, we could go various ways. Below we're going to cover three possible \nattempts, pointing their vulnerabilities. \n\n### Publishing an event first, storing data next\nIn this approach, the command is going to publish an event first and store data just after that. While in most of the \ncases that approach will probably work just fine, let's try to find out what could possibly go wrong. \n\nThere are three basic actions that the command has to do:\n\n1. Pick a random user `A` as a lottery winner.\n2. Publish a `LotteryConcluded` event telling that lottery `B` has been concluded. \n3. Store in the database that the lottery `B` has been won by the user `A`. \n\nEvery of these steps could potentially fail, breaking the flow of our command. The first point wouldn't have huge \nrepercussions in case of its failure - we would just return an error and consider the whole command failed. No data would\nbe stored, no event would be emitted. We can simply rerun the command. \n\nIn case the second point fails, we'll still have no event emitted and no data stored in the database. We can rerun the \ncommand and try once again. \n\nWhat's most interesting is what could happen in case the third point fails. We'd already have the event emitted after \nthe second point, but no data would be stored eventually in the database. Other components would get a signal \nthat the lottery had been concluded, but no winner would be associated to the lottery ID sent in the event. They wouldn't \nbe able to verify who's the winner, so their action would have to be considered failed as well. \n\nWe still can get out of this situation, but most probably it will require some manual action, i.e., rerunning the command \nwith the lottery ID that the emitted event has.\n\n{{% load-snippet-partial file=\"src-link/_examples/real-world-examples/transactional-events-forwarder/main.go\" first_line_contains=\"// 1. Publishes event\" last_line_contains=\"// In case this fails\" padding_after=\"9\" %}}\n\n### Storing data first, publishing an event next\nIn the second approach, we're going to try address first approach's drawbacks. We won't leak our failure to outer \ncomponents by not emitting an event in case we don't have the state persisted properly in database. That means \nwe'll change the order of our actions to following:\n\n1. Pick a random user `A` as a lottery winner.\n2. Store in the database that the lottery `B` has been won by the user `A`.\n3. Publish a `LotteryConcluded` event telling that lottery `B` has been concluded.\n\nHaving two first actions failed, we have no repercussions, just as in the first approach. In case of failure of the 3rd \npoint, we'd have data persisted in the database, but no event emitted. In this case, we wouldn't leak our failure \noutside the lottery component. Although, considering the expected system behavior, we'd have no prize sent to our winner,\nbecause no event would be delivered to the component responsible for this action. \n\nThat probably can be fixed by some manual action as well, i.e., emitting the event manually. We still can do better. \n\n{{% load-snippet-partial file=\"src-link/_examples/real-world-examples/transactional-events-forwarder/main.go\" first_line_contains=\"// 2. Persists data\" last_line_contains=\"// In case this fails\" padding_after=\"9\" %}}\n\n### Storing data and publishing an event in one transaction\nLet's imagine our command could do the 2nd, and the 3rd point at the same time. They would be committed atomically, \nmeaning that any of them can't succeed having the other failed. This can be achieved by leveraging a transaction\nmechanism which happens to be implemented by most of the databases in today's world. One of them is MySQL used in our \nexample. \n\nIn order to commit both storing data and emitting an event in one transaction, we'd have to be able to publish our \nmessages to MySQL. Because we don't want to change our message broker to be backed by MySQL in the whole system, \nwe have to find a way to do that differently. \n\nThere's a good news: Watermill provides all the tools straight away! In case the database you're using is one among MySQL,\nPostgreSQL (or any other SQL), Firestore or Bolt, you can publish messages to them. **Forwarder** component will help \nyou with picking all the messages you publish to the database and forwarding them to a message broker of yours. \n\nEverything you have to do is to make sure that:\n\n1. Your command uses a publisher working in a context of a database transaction (i.e. [SQL](https://github.com/ThreeDotsLabs/watermill-sql/blob/4f39bf82b6180ca2191c791e7cb220fff22b9255/pkg/sql/publisher.go#L53), \n[Firestore](https://github.com/ThreeDotsLabs/watermill-firestore/blob/b7bd31b3458884dc76076196cdc8942d18b5ab61/pkg/firestore/transactional.go#L14), [Bolt](https://github.com/ThreeDotsLabs/watermill-bolt/blob/0652f3602f6adbe4e3e39b97308fbed16dcbe29e/pkg/bolt/tx_publisher.go#L24)).\n2. **Forwarder** component is running, using a database subscriber, and a message broker publisher.  \n\nThe command could look like following in this case:\n\n{{% load-snippet-partial file=\"src-link/_examples/real-world-examples/transactional-events-forwarder/main.go\" first_line_contains=\"// 3. Persists data\" last_line_contains=\"err = publisher.Publish(googleCloudEventTopic\" padding_after=\"5\" %}}\n\nIn order to make the **Forwarder** component work in background for you and forward messages from MySQL to Google Pub/Sub,\nyou'd have to set it up as follows:\n\n{{% load-snippet-partial file=\"src-link/_examples/real-world-examples/transactional-events-forwarder/main.go\" first_line_contains=\"// Setup the Forwarder \" last_line_contains=\"err := fwd.Run\" padding_after=\"3\" %}}\n\nIf you wish to explore the example more, you can find it implemented [here](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/real-world-examples/transactional-events-forwarder/main.go).\n"
  },
  {
    "path": "docs/content/advanced/metrics.md",
    "content": "+++\ntitle = \"Metrics\"\ndescription = \"Monitor Watermill in realtime\"\ndate = 2019-02-12T21:00:00+01:00\nweight = -400\ndraft = false\nbref = \"Monitor Watermill in realtime using Prometheus\"\n+++\n\nMonitoring of Watermill may be performed by using decorators for publishers/subscribers and middlewares for handlers. \nWe provide a default implementation using Prometheus, based on the official [Prometheus client](https://github.com/prometheus/client_golang) for Go.\n\nThe `components/metrics` package exports `PrometheusMetricsBuilder`, which provides convenience functions to wrap publishers, subscribers and handlers so that they update the relevant Prometheus registry:\n\n{{% load-snippet-partial file=\"src-link/components/metrics/builder.go\" first_line_contains=\"// PrometheusMetricsBuilder\" last_line_contains=\"func (b PrometheusMetricsBuilder)\" %}}\n\n## Wrapping publishers, subscribers and handlers\n\nIf you are using Watermill's [router](/docs/messages-router) (which is recommended in most cases), you can use a single convenience function `AddPrometheusRouterMetrics` to ensure that all the handlers added to this router are wrapped to update the Prometheus registry, together with their publishers and subscribers:\n\n{{% load-snippet-partial file=\"src-link/components/metrics/builder.go\" first_line_contains=\"// AddPrometheusRouterMetrics\" last_line_contains=\"AddMiddleware\" padding_after=\"1\" %}}\n\nExample use of `AddPrometheusRouterMetrics`:\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/4-metrics/main.go\" first_line_contains=\"// we leave the namespace\" last_line_contains=\"metricsBuilder.AddPrometheusRouterMetrics\" %}}\n\nIn the snippet above, we have left the `namespace` and `subsystem` arguments empty. The Prometheus client library [uses these](https://godoc.org/github.com/prometheus/client_golang/prometheus#BuildFQName) to prefix the metric names. You may want to use namespace or subsystem, but be aware that this will impact the metric names and you will have to adjust the Grafana dashboard accordingly.\n\nThe `PrometheusMetricsBuilder` allows for custom configuration of histogram buckets by setting the `PublishBuckets` or `HandlerBuckets` field.\nIf `HandlerBuckets` is not provided, default watermill's values will be used, which are one order of magnitude smaller than default buckets (5ms~10s), because the handler execution times are typically shorter (µs~ms range).\nFor `PublishBuckets`, the default values are the same as the default Prometheus buckets (5ms~10s).\n\nStandalone publishers and subscribers may also be decorated through the use of dedicated methods of `PrometheusMetricBuilder`:\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/4-metrics/main.go\" first_line_contains=\"subWithMetrics, err := \" last_line_contains=\"pubWithMetrics, err := \" padding_after=\"3\" %}}\n\n## Exposing the /metrics endpoint\n\nIn accordance with how Prometheus works, the service needs to expose a HTTP endpoint for scraping. By convention, it is a GET endpoint, and its path is usually `/metrics`.\n\nTo serve this endpoint, there are two convenience functions, one using a previously created Prometheus Registry, while the other also creates a new registry:\n\n{{% load-snippet-partial file=\"src-link/components/metrics/http.go\" first_line_contains=\"// CreateRegistryAndServeHTTP\" last_line_contains=\"func ServeHTTP(\" %}}\n\nHere is an example of its use in practice:\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/4-metrics/main.go\" first_line_contains=\"prometheusRegistry, closeMetricsServer :=\" last_line_contains=\"metricsBuilder.AddPrometheusRouterMetrics\" %}}\n\n## Example application\n\nTo see how the metrics dashboard works in practice, you can check out the [metrics example](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/basic/4-metrics). \n\nFollow the instructions in the example's [README](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/4-metrics/README.md) to make it run and add the Prometheus data source to Grafana.\n\n## Grafana dashboard\n\nWe have prepared a [Grafana dashboard](https://grafana.com/grafana/dashboards/9777-watermill/) to use with the metrics implementation described above. It provides basic information about the throughput, failure rates and publish/handler durations.\n\nIf you want to check out the dashboard on your machine, you can use the [Example application](#example-application).\n\nTo find out more about the metrics that are exported to Prometheus, see [Exported metrics](#exported-metrics).\n\n<a target=\"_blank\" href=\"https://threedots.tech/watermill-io/grafana_dashboard.png\"><img src=\"https://threedots.tech/watermill-io/grafana_dashboard_small.png\" /></a>\n\n### Importing the dashboard\n\nTo import the Grafana dashboard, select Dashboard/Manage from the left menu, and then click on `+Import`.\n\nEnter the dashboard URL https://grafana.com/dashboards/9777 (or just the ID, 9777), and click on Load.\n\n![Importing the dashboard](https://threedots.tech/watermill-io/grafana_import_dashboard.png)\n\nThen select your Prometheus data source that scrapes the `/metrics` endpoint. Click on `Import`, and you're done!\n\n## Exported metrics\n\nListed below are all the metrics that are registered on the Prometheus Registry by `PrometheusMetricsBuilder`.\n \nFor more information on Prometheus metric types, please refer to [Prometheus docs](https://prometheus.io/docs/concepts/metric_types).\n \n<table>\n  <tr>\n    <th>Object</th>\n    <th>Metric</th>\n    <th>Description</th>\n    <th>Labels/Values</th>\n  </tr>\n  <tr>\n    <td rowspan=\"3\">Subscriber</td>\n    <td rowspan=\"3\"><code>subscriber_messages_received_total</code></td>\n    <td rowspan=\"3\">A Prometheus Counter.<br>Counts the number of messages obtained by the subscriber.</td>\n    <td><code>acked</code> is either \"acked\" or \"nacked\".</td>\n  </tr>\n  <tr>\n    <td><code>handler_name</code> is set if the subscriber operates within a handler; \"&lt;no handler&gt;\" otherwise.</td>\n  </tr>\n  <tr>\n    <td><code>subscriber_name</code> identifies the subscriber. If it implements <code>fmt.Stringer</code>, it is the result of `String()`, <code>package.structName</code> otherwise.</td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">Handler</td>\n    <td rowspan=\"2\"><code>handler_execution_time_seconds</code></td>\n    <td rowspan=\"2\">A Prometheus Histogram. <br>Registers the execution time of the handler function wrapped by the middleware.</td>\n    <td><code>handler_name</code> is the name of the handler.</td>\n  </tr>\n  <tr>\n    <td><code>success</code> is either \"true\" or \"false\", depending on whether the wrapped handler function returned an error or not.</td>\n  </tr>\n  <tr>\n    <td rowspan=\"3\">Publisher</td>\n    <td rowspan=\"3\"><code>publish_time_seconds</code></td>\n    <td rowspan=\"3\">A Prometheus Histogram.<br>Registers the time of execution of the Publish function of the decorated publisher.</td>\n    <td><code>success</code> is either \"true\" or \"false\", depending on whether the decorated publisher returned an error or not.</td>\n  </tr>\n  <tr>\n    <td><code>handler_name</code> is set if the publisher operates within a handler; \"&lt;no handler&gt;\" otherwise.</td>\n  </tr>\n  <tr>\n    <td><code>publisher_name</code> identifies the publisher. If it implements <code>fmt.Stringer</code>, it is the result of `String()`, <code>package.structName</code> otherwise.</td>\n  </tr>\n</table>\n\nAdditionally, every metric has the `node` label, provided by Prometheus, with value corresponding to the instance that the metric comes from, and `job`, which is the job name specified in the [Prometheus configuration file](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/4-metrics/prometheus.yml).\n\n**NOTE**: As described [above](#wrapping-publishers-subscribers-and-handlers), using non-empty `namespace` or `subsystem` will result in prefixed metric names. You might need to adjust for it, for example in the definitions of panels in the Grafana dashboard.\n\n## Customization\n\nIf you feel like some metric is missing, you can easily expand this basic implementation. The best way to do so is to use the prometheus registry that is used with the [ServeHTTP method](#exposing-the-metrics-endpoint) and register a metric according to [the documentation](https://godoc.org/github.com/prometheus/client_golang/prometheus) of the Prometheus client.\n\nAn elegant way to update these metrics would be through the use of decorators:\n\n{{% load-snippet-partial file=\"src-link/message/decorator.go\" first_line_contains=\"// MessageTransformSubscriberDecorator\" last_line_contains=\"type messageTransformSubscriberDecorator\" %}}\n\nand/or [router middlewares](/docs/messages-router/#middleware). \n\nA more simplistic approach would be to just update the metric that you want in the handler function.\n\n"
  },
  {
    "path": "docs/content/advanced/requeuing-after-error.md",
    "content": "+++\ntitle = \"Requeuing After Error\"\ndescription = \"How to requeue a message after it fails to process\"\nweight = -20\ndraft = false\nbref = \"How to requeue a message after it fails to process\"\n+++\n\nWhen 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).\n\nDepending on your setup, it may be useful to requeue the failed message back to the tail of the queue.\n\nConsider this if:\n* You don't care about the order of messages.\n* Your system isn't resilient to blocked messages.\n\n## Requeuer\n\nThe `Requeuer` component is a wrapper on the `Router` that moves messages from one topic to another.\n\n{{% load-snippet-partial file=\"src-link/components/requeuer/requeuer.go\" first_line_contains=\"type Config\" last_line_contains=\"}\" %}}\n\nA trivial usage can look like this. It requeues messages from one topic to the same topic after a delay.\n\n{{< callout \"danger\" >}}\nUsing the delay this way is not recommended, as it blocks the entire requeue process for the given time.\n{{< /callout >}}\n\n```go\nreq, err := requeuer.NewRequeuer(requeuer.Config{\n    Subscriber:     sub,\n    SubscribeTopic: \"topic\",\n    Publisher:      pub,\n    GeneratePublishTopic: func(params requeuer.GeneratePublishTopicParams) (string, error) {\n        return \"topic\", nil\n    },\n    Delay: time.Millisecond * 200,\n}, logger)\nif err != nil {\n\treturn err\n}\n\nerr := req.Run(context.Background())\nif err != nil {\n    return err\n}\n```\n\nA better way to use the `Requeuer` is to combine it with the `Poison` middleware.\nThe middleware moves messages to a separate \"poison\" topic.\nThen, the requeuer moves them back to the original topic based on the metadata.\n\nYou combine this with a Pub/Sub that [supports delayed messages](/advanced/delayed-messages/#supported-pubsubs).\nSee the [full example based on PostgreSQL](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/real-world-examples/delayed-requeue/main.go).\n\n"
  },
  {
    "path": "docs/content/development/benchmark.md",
    "content": "+++\ntitle = \"Benchmark\"\ndescription = \"Watermill Benchmark\"\nweight = 250\ndraft = false\nbref = \"Watermill Benchmark\"\n+++\n\nYou can find benchmarking tools and results in the [github.com/ThreeDotsLabs/watermill-benchmark](https://github.com/ThreeDotsLabs/watermill-benchmark) repository.\n\n**Note they are meant as rough estimations and should not be used to decide which Pub/Sub is the best pick.**\nPerformance depends on many factors and configurations, and it's always best to test it in your environment.\n"
  },
  {
    "path": "docs/content/development/contributing.md",
    "content": "+++\ntitle = \"Contributing Guide\"\ndescription = \"Contribute to Watermill\"\nweight = 100\nbref = \"Contribute to Watermill\"\n+++\n\n## How can I help?\n\nWe are always happy to help you in contributing to Watermill. If you have any ideas, please let us know on our [Discord server](https://watermill.io/support/).\n\nThere are multiple ways in which you can help us.\n\n### Existing issues\n\nYou can pick one of the existing issues. Most of the issues should have an estimation (S - small, M - medium, L - large).\n\n- [Good first issues list](https://github.com/ThreeDotsLabs/watermill/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - simple issues to begin with\n- [Help wanted issues list](https://github.com/ThreeDotsLabs/watermill/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) - tasks that are already more or less clear, and you can start to implement them pretty quickly\n\n### New Pub/Sub implementations\n\nIf you have an idea to create a Pub/Sub based on some technology and it is not listed yet in our issues (because we don't know it, or it is just some crazy idea, like physical mail based Pub/Sub), feel free to add your own implementation.\n\nYou can do it in your private repository and later, if you want, we can move it to `ThreeDotsLabs/watermill-[name]`.\nYou will keep the maintainer permissions to the repository, and we'll invite you to a maintainers-only discord channel.\n\nWhen adding a new Pub/Sub implementation, you should start with [Implementing a new Pub/Sub]({{< ref \"pub-sub-implementing\" >}}).\n\n### New ideas\n\nIf you have any idea that is not covered in the issues list, please post a new issue describing it. \nIt's recommended to discuss your idea on [Discord](https://discord.gg/QV6VFg4YQE)/GitHub before creating production-ready implementation - in some situations, it may save a lot of your time before implementing something that can be simplified or done more easily. :)\n\nIn general, it's helpful to discuss a Proof of Concept to align with the idea.\n\n## Local development\n\nMakefile and docker-compose (for Pub/Subs) are your friends. You can run all tests locally (they are running in CI in the same way).\n\nUseful commands:\n- `make up` - docker-compose up\n- `make test` - tests\n- `make test_short` - run short tests (useful to perform a very fast check after changes)\n- `make fmt` - do goimports\n\n## Code standards\n\n- you should run `make fmt`\n- [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)\n- [Effective Go](https://golang.org/doc/effective_go.html)\n- SOLID\n- code should be open for configuration and not coupled to any serialization method (for example: [AMQP marshaler](https://github.com/ThreeDotsLabs/watermill-amqp/blob/master/pkg/amqp/marshaler.go), [AMQP Config](https://github.com/ThreeDotsLabs/watermill-amqp/blob/master/pkg/amqp/config.go)\n"
  },
  {
    "path": "docs/content/development/pub-sub-implementing.md",
    "content": "+++\ntitle = \"Implementing a new Pub/Sub\"\ndescription = \"Bring Your Own Pub/Sub\"\ndate = 2018-12-05T12:48:34+01:00\nweight = 200\ndraft = false\nbref = \"Bring Your Own Pub/Sub\"\n+++\n\n## The Pub/Sub interface\n\nTo add support for a custom Pub/Sub, you have to implement both `message.Publisher` and `message.Subscriber` interfaces.\n\n{{% load-snippet-partial file=\"src-link/message/pubsub.go\" first_line_contains=\"type Publisher interface\" last_line_contains=\"type SubscribeInitializer\" padding_after=\"0\" %}}\n\n## Testing\n\nWatermill provides [a set of test scenarios](https://github.com/ThreeDotsLabs/watermill/blob/master/pubsub/tests/test_pubsub.go)\nthat any Pub/Sub implementation can use. Each test suite needs to declare what features it supports and how to construct a new Pub/Sub.\nThese scenarios check both basic usage and more uncommon use cases. Stress tests are also included.\n\n## TODO list\n\nHere are a few things you shouldn't forget about:\n\n1. Logging (good messages and proper levels).\n2. Replaceable and configurable messages marshaller.\n3. `Close()` implementation for the publisher and subscriber that is:\n    - idempotent\n    - working correctly even when the publisher or the subscriber is blocked (for example, waiting for an Ack).\n    - working correctly when the subscriber output channel is blocked (because nothing is listening on it).\n4. `Ack()` **and** `Nack()` support for consumed messages.\n5. Redelivery on `Nack()` for a consumed message.\n6. Use [Universal Pub/Sub tests]({{< ref \"/docs/pub-sub#universal-tests\" >}}). For debugging tips, you should check [tests troubleshooting guide](/docs/troubleshooting/#debugging-pubsub-tests).\n7. Performance optimizations.\n8. GoDocs, [Markdown docs]({{< ref \"/pubsubs\" >}}) and [Getting Started examples](/learn/getting-started).\n\nWe will also be thankful for submitting a [pull requests](https://github.com/ThreeDotsLabs/watermill/pulls) with the new Pub/Sub implementation.\n\nIf anything is not clear, feel free to use any of our [support channels]({{< ref \"/support\" >}}) to reach us, we will be glad to help.\n"
  },
  {
    "path": "docs/content/development/releases.md",
    "content": "+++\ntitle = \"Releases\"\ndescription = \"Watermill Releases\"\nweight = 300\nbref = \"Watermill Releases\"\n+++\n\nYou can read about the historical Watermill releases in [the posts on the Three Dots Labs blog](https://threedots.tech/series/watermill-release-post/).\n\n<iframe src=\"https://releases.threedots.tech\" frameborder=\"0\" style=\"border: 0; width: 100%; height: 1000px\"></iframe>\n"
  },
  {
    "path": "docs/content/docs/_index.md",
    "content": "---\r\ntitle: \"Docs\"\r\ndescription: \"\"\r\nsummary: \"\"\r\ndate: 2023-09-07T16:12:03+02:00\r\nlastmod: 2023-09-07T16:12:03+02:00\r\ndraft: false\r\nweight: 999\r\ntoc: true\r\nseo:\r\n  title: \"\" # custom title (optional)\r\n  description: \"\" # custom description (recommended)\r\n  canonical: \"\" # custom canonical URL (optional)\r\n  noindex: false # false (default) or true\r\n---\r\n"
  },
  {
    "path": "docs/content/docs/articles.md",
    "content": "+++\ntitle = \"Articles\"\ndescription = \"In-depth articles mentioning Watermill\"\nweight = 100\ndraft = false\nbref = \"In-depth articles mentioning Watermill\"\n+++\n\nYou can find more in-depth tips on Watermill in these articles:\n\n* [Distributed Transactions in Go: Read Before You Try](https://threedots.tech/post/distributed-transactions-in-go/)\n* [Live website updates with Go, SSE, and htmx](https://threedots.tech/post/live-website-updates-go-sse-htmx/)\n* [Using MySQL as a Pub/Sub](https://threedots.tech/post/when-sql-database-makes-great-pub-sub/)\n* [Creating local Go dev environment with Docker and live code reloading](https://threedots.tech/post/go-docker-dev-environment-with-go-modules-and-live-code-reloading/)\n\n"
  },
  {
    "path": "docs/content/docs/awesome.md",
    "content": "+++\ntitle = \"Awesome Watermill\"\ndescription = \"Selected unofficial libraries\"\nweight = 200\ndraft = false\nbref = \"Selected unofficial libraries\"\n+++\n\nBelow is a list of libraries that are not maintained by Three Dots Labs, but you may find them useful.\n\n**Please note we can't provide support or guarantee they work correctly**. Do your own research.\n\nIf you know another library or are an author of one, please [add it to the list](https://github.com/ThreeDotsLabs/watermill/edit/master/docs/content/docs/awesome.md).\n\n## Examples\n\n* https://github.com/minghsu0107/golang-taipei-watermill-example\n* https://github.com/minghsu0107/Kafka-PubSub\n* https://github.com/pperaltaisern/go-example-financing\n\n## Pub/Subs\n\n* AMQP 1.0 https://github.com/kahowell/watermill-amqp10\n* Apache Pulsar https://github.com/AlexCuse/watermill-pulsar\n* Apache RocketMQ https://github.com/yflau/watermill-rocketmq\n* CockroachDB https://github.com/cockroachdb/watermill-crdb\n* Ensign https://github.com/rotationalio/watermill-ensign\n* GoogleCloud Pub/Sub HTTP Push https://github.com/dentech-floss/watermill-googlecloud-http\n* MongoDB https://github.com/cunyat/watermill-mongodb\n* MQTT https://github.com/perfect13/watermill-mqtt\n* NSQ https://github.com/chennqqi/watermill-nsq\n* Redis Zset https://github.com/stong1994/watermill-rediszset\n* SQLite https://github.com/davidroman0O/watermill-comfymill\n\nIf you want to find out how to implement your own Pub/Sub adapter, \ncheck out [Implementing custom Pub/Sub](/development/pub-sub-implementing).\n\n## Logging\n\n* logrus\n  * https://github.com/ma-hartma/watermill-logrus-adapter\n  * https://github.com/UNIwise/walrus\n* logur https://github.com/logur/integration-watermill\n* zap\n  * https://github.com/garsue/watermillzap\n  * https://github.com/pperaltaisern/watermillzap\n* zerolog\n  * https://github.com/alexdrl/zerowater\n  * https://github.com/bogatyr285/watermillzlog\n  * https://github.com/vsvp21/zerolog-watermill-adapter\n\n## Observability\n\n* OpenCensus\n  * https://github.com/czeslavo/watermill-opencensus\n  * https://github.com/sagikazarmark/ocwatermill\n* OpenTelemetry\n  * https://github.com/voi-oss/watermill-opentelemetry\n  * https://github.com/dentech-floss/watermill-opentelemetry-go-extra\n  * https://github.com/nkonev/watermill-opentelemetry\n  * AMQP https://github.com/hpcslag/otel-watermill-amqp\n  * GoChannel https://github.com/hpcslag/watermill-otel-tracable-gochannel\n\n## Other\n\n* https://github.com/asyncapi/go-watermill-template\n* https://github.com/goph/watermillx\n* https://github.com/voi-oss/protoc-gen-event\n"
  },
  {
    "path": "docs/content/docs/cqrs.md",
    "content": "+++\ntitle = \"CQRS Component\"\ndescription = \"Build CQRS and Event-Driven applications\"\ndate = 2019-02-12T12:47:30+01:00\nweight = -400\ndraft = false\nbref = \"Go CQRS implementation in Watermill\"\n+++\n\nThe CQRS component is a high-level API that lets you work with Go structs instead of messages.\n\nOnce you configure the EventBus and EventProcessor (or the command equivalents), publishing and handling events becomes very straightforward.\n\n```go\nevent := UserRegistered{\n    UserID:   id,\n    Email:    email,\n    JoinedAt: time.Now(),\n}\n\nerr := eventBus.Publish(ctx, event)\n```\n\n```go\neventProcessor.AddHandlers(\n    cqrs.NewEventHandler(\"SendWelcomeEmail\", sendWelcomeEmail),\n)\n\nfunc sendWelcomeEmail(ctx context.Context, event *UserRegistered) error {\n    return emailService.Send(event.Email, \"Welcome!\")\n}\n```\n\n## CQRS\n\n> CQRS means \"Command-query responsibility segregation\". We segregate the responsibility between commands (write requests) and queries (read requests). The write requests and the read requests are handled by different objects.\n>\n> That's it. We can further split up the data storage, having separate read and write stores. Once that happens, there may be many read stores, optimized for handling different types of queries or spanning many bounded contexts. Though separate read/write stores are often discussed in relation with CQRS, this is not CQRS itself. CQRS is just the first split of commands and queries.\n>\n> Source: [www.cqrs.nu FAQ](http://www.cqrs.nu/Faq/command-query-responsibility-segregation)\n\n<img src=\"https://threedots.tech/watermill-io/cqrs-big-picture.svg\" alt=\"CQRS Schema\" style=\"width:100%; margin-bottom: 3rem; background-color: white; padding: 2rem;\" />\n\nThe `cqrs` component provides some useful abstractions built on top of Pub/Sub and Router that help to implement the CQRS pattern.\n\nYou don't need to implement the entire CQRS. It's very common to use just the event part of this component to build event-driven applications.\n\n### Building blocks\n\n#### Event\n\nThe event represents something that already took place. Events are immutable.\n\n#### Event Bus\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_bus.go\" first_line_contains=\"// EventBus\" last_line_contains=\"type EventBus\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_bus.go\" first_line_contains=\"type EventBusConfig\" last_line_contains=\"func (c *EventBusConfig) setDefaults()\" padding_after=\"4\" %}}\n\n#### Event Processor\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_processor.go\" first_line_contains=\"// EventProcessor\" last_line_contains=\"type EventProcessor\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_processor.go\" first_line_contains=\"type EventProcessorConfig\" last_line_contains=\"func (c *EventProcessorConfig) setDefaults()\" padding_after=\"4\" %}}\n\n#### Event Group Processor\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_processor_group.go\" first_line_contains=\"// EventGroupProcessor\" last_line_contains=\"type EventGroupProcessor\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_processor_group.go\" first_line_contains=\"type EventGroupProcessorConfig\" last_line_contains=\"func (c *EventGroupProcessorConfig) setDefaults()\" padding_after=\"4\" %}}\n\nLearn more in [Event Group Processor](#event-handler-groups).\n\n#### Event Handler\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_handler.go\" first_line_contains=\"// EventHandler\" last_line_contains=\"type EventHandler\" padding_after=\"0\" %}}\n\n#### Command\n\nThe command is a simple data structure, representing the request for executing some operation.\n\n#### Command Bus\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/command_bus.go\" first_line_contains=\"// CommandBus\" last_line_contains=\"type CommandBus\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/command_bus.go\" first_line_contains=\"type CommandBusConfig\" last_line_contains=\"func (c *CommandBusConfig) setDefaults()\" padding_after=\"4\" %}}\n\n#### Command Processor\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/command_processor.go\" first_line_contains=\"// CommandProcessor\" last_line_contains=\"type CommandProcessor\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/command_processor.go\" first_line_contains=\"type CommandProcessorConfig\" last_line_contains=\"func (c *CommandProcessorConfig) setDefaults()\" padding_after=\"4\" %}}\n\n#### Command Handler\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/command_handler.go\" first_line_contains=\"// CommandHandler\" last_line_contains=\"type CommandHandler\" padding_after=\"0\" %}}\n\n#### Command and Event Marshaler\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/marshaler.go\" first_line_contains=\"// CommandEventMarshaler\" last_line_contains=\"NameFromMessage(\" padding_after=\"1\" %}}\n\n#### Command and Event Marshaler Decorator\n\nSometimes it's useful to add extra metadata to each command or event after marshaling it to a message. For example, you may want to add a partition key to each message using Kafka.\n\nYou can use `CommandEventMarshalerDecorator` to extend a marshaler with an extra step.\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/marshaler.go\" first_line_contains=\"// CommandEventMarshalerDecorator\" last_line_contains=\"}\" padding_after=\"0\" %}}\n\n```go\ntype Event interface {\n\tPartitionKey() string\n}\n\n// ...\n\ncqrsMarshaler := CommandEventMarshalerDecorator{\n\tCommandEventMarshaler: cqrs.JSONMarshaler{},\n\tDecorateFunc: func(v any, msg *message.Message) error {\n\t\tpm, ok := v.(Event)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"%T does not implement Event and can't be marshaled\", v)\n\t\t}\n\n\t\tpartitionKey := pm.PartitionKey()\n\t\tif partitionKey == \"\" {\n\t\t\treturn fmt.Errorf(\"PartitionKey is empty\")\n\t\t}\n\n\t\tmsg.Metadata.Set(PartitionKeyMetadataField, partitionKey)\n\t\treturn nil\n\t},\n}\n```\n\n## Usage\n\n### Example domain\n\nAs an example, we will use a simple domain, that is responsible for handing room booking in a hotel.\n\nWe will use **Event Storming** notation to show the model of this domain.\n\nLegend:\n\n- **blue** post-its are commands\n- **orange** post-its are events\n- **green** post-its are read models, asynchronously generated from events\n- **violet** post-its are policies, which are triggered by events and produce commands\n- **pink** post its are hot-spots; we mark places where problems often occur\n\n![CQRS Event Storming](https://threedots.tech/watermill-io/cqrs-example-storming.png)\n\nThe domain is simple:\n\n- A Guest is able to **book a room**.\n- **Whenever a room is booked, we order a beer** for the guest (because we love our guests).\n    - We know that sometimes there are **not enough beers**.\n- We generate a **financial report** based on the bookings.\n\n\n### Sending a command\n\nFor the beginning, we need to simulate the guest's action.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/5-cqrs-protobuf/main.go\" first_line_contains=\"bookRoomCmd := &BookRoom{\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n### Command handler\n\n`BookRoomHandler` will handle our command.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/5-cqrs-protobuf/main.go\" first_line_contains=\"// BookRoomHandler is a command handler\" last_line_contains=\"// OrderBeerOnRoomBooked is an event handler\" padding_after=\"0\" %}}\n\n### Event handler\n\nAs mentioned before, we want to order a beer every time when a room is booked (*\"Whenever a Room is booked\"* post-it). We do it by using the `OrderBeer` command.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/5-cqrs-protobuf/main.go\" first_line_contains=\"// OrderBeerOnRoomBooked is an event handler\" last_line_contains=\"// OrderBeerHandler is a command handler\" padding_after=\"0\" %}}\n\n`OrderBeerHandler` is very similar to `BookRoomHandler`. The only difference is, that it sometimes returns an error when there are not enough beers, which causes redelivery of the command.\nYou can find the entire implementation in the [example source code](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/basic/5-cqrs-protobuf/?utm_source=cqrs_doc).\n\n### Event Handler groups\n\nBy default, each event handler has a separate subscriber instance.\nIt works fine, if just one event type is sent to the topic.\n\nIn the scenario, when we have multiple event types on one topic, you have two options:\n\n1. You can set `EventConfig.AckOnUnknownEvent` to true - it will acknowledge all events that are not handled by handler,\n2. You can use Event Handler groups mechanism.\n\n**Key differences between `EventProcessor` and `EventGroupProcessor`:**\n\n1. `EventProcessor`:\n- Each handler has its own subscriber instance\n- One handler per event type\n- Simple one-to-one matching of events to handlers\n\n2. `EventGroupProcessor`:\n- Group of handlers share a single subscriber instance (and one consumer group, if such mechanism is supported -- allows to maintain order of events),\n- One handler group can support multiple event types,\n- When message arrives to the topic, Watermill will match it to the handler in the group based on event type\n\n\n<img src=\"/img/group-handlers.svg\" alt=\"Group Handlers\" style=\"width:100%; background-color: white; padding: ;\">\n\n**Event Handler groups are helpful when you have multiple event types on one topic and you want to maintain order of events.**\nThanks to using one subscriber instance and consumer group, events will be processed in the order they were sent.\n\n{{< callout context=\"note\" title=\"Note\" icon=\"outline/info-circle\" >}}\nIt's supported to have multiple handlers for the same event type in one group, but we recommend to not do that.\n\nPlease keep in mind that those handlers will be processed within the same message.\nIf first handler succeeds and the second fails, the message will be re-delivered and the first will be re-executed.\n{{< /callout >}}\n\nTo use event groups, you need to set `GenerateHandlerGroupSubscribeTopic` and `GroupSubscriberConstructor` options in [`EventConfig`](#event-config).\n\nAfter that, you can use `AddHandlersGroup` on [`EventProcessor`](#event-processor).\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/6-cqrs-ordered-events/main.go\" first_line_contains=\"eventProcessor.AddHandlersGroup(\" last_line_contains=\"if err != nil {\" padding_after=\"0\" %}}\n\nBoth `GenerateHandlerGroupSubscribeTopic` and `GroupSubscriberConstructor` receives information about group name in function arguments.\n\nYou can see a fully working example with event groups in our [examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/basic/6-cqrs-ordered-events/).\n\n### Generic handlers\n\nSince Watermill v1.3 it's possible to use generic handlers for commands and events. It's useful when you have a lot of commands/events and you don't want to create a handler for each of them.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/6-cqrs-ordered-events/main.go\" first_line_contains=\"cqrs.NewGroupEventHandler\" last_line_contains=\"),\" padding_after=\"0\" %}}\n\nUnder the hood, it creates EventHandler or CommandHandler implementation.\nIt's available for all kind of handlers.\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/command_handler.go\" first_line_contains=\"// NewCommandHandler\" last_line_contains=\"func NewCommandHandler\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_handler.go\" first_line_contains=\"// NewEventHandler\" last_line_contains=\"func NewEventHandler\" padding_after=\"0\" %}}\n\n{{% load-snippet-partial file=\"src-link/components/cqrs/event_handler.go\" first_line_contains=\"// NewGroupEventHandler\" last_line_contains=\"func NewGroupEventHandler\" padding_after=\"0\" %}}\n### Building a read model with the event handler\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/5-cqrs-protobuf/main.go\" first_line_contains=\"// BookingsFinancialReport is a read model\" last_line_contains=\"func main() {\" padding_after=\"0\" %}}\n\n### Wiring it up\n\nWe have all the blocks to build our CQRS application.\n\nWe will use the AMQP (RabbitMQ) as our message broker: [AMQP]({{< ref \"/pubsubs/amqp\" >}}).\n\nUnder the hood, CQRS is using Watermill's message router. If you are not familiar with it and want to learn how it works, you should check [Getting Started guide]({{< ref \"getting-started\" >}}).\nIt will also show you how to use some standard messaging patterns, like metrics, poison queue, throttling, correlation and other tools used by every message-driven application. Those come built-in with Watermill.\n\nLet's go back to the CQRS. As you already know, CQRS is built from multiple components, like Command or Event buses, handlers, processors, etc.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/5-cqrs-protobuf/main.go\" first_line_contains=\"main() {\" last_line_contains=\"err := router.Run(\" padding_after=\"3\" %}}\n\nAnd that's all. We have a working CQRS application.\n\n### What's next?\n\nAs mentioned before, if you are not familiar with Watermill, we highly recommend reading [Getting Started guide]({{< ref \"getting-started\" >}}).\n"
  },
  {
    "path": "docs/content/docs/message/.validate_example.yml",
    "content": "validation_cmd: \"go run receiving-ack.go\"\ntimeout: 30\nexpected_output: \"ack received\"\n"
  },
  {
    "path": "docs/content/docs/message/go.mod",
    "content": "module receiving-ack.go\n\nrequire github.com/ThreeDotsLabs/watermill v1.5.1\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "docs/content/docs/message/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "docs/content/docs/message/receiving-ack.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc main() {\n\tmsg := message.NewMessage(\"1\", []byte(\"foo\"))\n\n\tgo func() {\n\t\ttime.Sleep(time.Millisecond * 10)\n\t\tmsg.Ack()\n\t}()\n\n\tselect {\n\tcase <-msg.Acked():\n\t\tlog.Print(\"ack received\")\n\tcase <-msg.Nacked():\n\t\tlog.Print(\"nack received\")\n\t}\n}\n"
  },
  {
    "path": "docs/content/docs/message.md",
    "content": "+++\ntitle = \"Message\"\ndescription = \"Message is one of core parts of Watermill\"\ndate = 2018-12-05T12:42:40+01:00\nweight = -1000\ndraft = false\nbref = \"Message is one of core parts of Watermill\"\n+++\n\nMessage is one of core parts of Watermill. Messages are emitted by [*Publishers*]({{< ref \"/docs/pub-sub#publisher\" >}}) and received by [*Subscribers*]({{< ref \"/docs/pub-sub#subscriber\" >}}).\nWhen a message is processed, you should send an [`Ack()`]({{< ref \"#ack\" >}}) or a [`Nack()`]({{< ref \"#ack\" >}}) when the processing failed.\n\n`Acks` and `Nacks` are processed by Subscribers (in default implementations, the subscribers are waiting for an `Ack` or a `Nack`).\n\n{{% load-snippet-partial file=\"src-link/message/message.go\" first_line_contains=\"type Message struct {\" last_line_contains=\"ctx context.Context\" padding_after=\"2\" %}}\n\n## Ack\n\n#### Sending `Ack`\n\n{{% load-snippet-partial file=\"src-link/message/message.go\" first_line_contains=\"// Ack\" last_line_contains=\"func (m *Message) Ack() bool {\" padding_after=\"0\" %}}\n\n\n## Nack\n\n{{% load-snippet-partial file=\"src-link/message/message.go\" first_line_contains=\"// Nack\" last_line_contains=\"func (m *Message) Nack() bool {\" padding_after=\"0\" %}}\n\n#### Receiving `Ack/Nack`\n\n{{% load-snippet-partial file=\"docs/message/receiving-ack.go\" first_line_contains=\"select {\" last_line_contains=\"}\" padding_after=\"0\" %}}\n\n\n## Context\n\nMessage contains the standard library context, just like an HTTP request.\n\nThis context is used to propagate cancellation and deadlines when publishing and processing messages.\n\nYou can set the context using the `SetContext` method or the `NewMessageWithContext` constructor.\n\n{{% load-snippet-partial file=\"src-link/message/message.go\" first_line_contains=\"// NewMessageWithContext\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/message/message.go\" first_line_contains=\"// Context\" last_line_contains=\"func (m *Message) SetContext\" padding_after=\"2\" %}}\n"
  },
  {
    "path": "docs/content/docs/messages-router.md",
    "content": "+++\ntitle = \"Router\"\ndescription = \"The Magic Glue of Watermill\"\ndate = 2018-12-05T12:48:04+01:00\nweight = -850\ndraft = false\nbref = \"The Magic Glue of Watermill\"\ntoc = true\n+++\n\n[*Publishers and Subscribers*]({{< ref \"/docs/pub-sub\" >}}) are rather low-level parts of Watermill.\nIn production use, you'd usually want to use a high-level interface and features like [correlation, metrics, poison queue, retrying, throttling, etc.]({{< ref \"/docs/messages-router#middleware\" >}}).\n\nYou also might not want to send an Ack when processing was successful. Sometimes, you'd like to send a message after processing of another message finishes.\n\nTo handle these requirements, there is a component named **Router**.\n\n<img src=\"/img/watermill-router.svg\" alt=\"Watermill Router\" style=\"width:100%; background-color: white; padding: ;\">\n\n## Configuration\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"type RouterConfig struct {\" last_line_contains=\"RouterConfig) Validate()\" padding_after=\"2\" %}}\n\n## Handler\n\nAt the beginning you need to implement `HandlerFunc`:\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// HandlerFunc is\" last_line_contains=\"type HandlerFunc func\" padding_after=\"1\" %}}\n\nNext, you have to add a new handler with `Router.AddHandler`:\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// AddHandler\" last_line_contains=\") {\" padding_after=\"0\" %}}\n\nSee an example usage from [Getting Started]({{< ref \"/learn/getting-started#using-messages-router\" >}}):\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"// AddHandler returns a handler\" last_line_contains=\"return h(message)\" padding_after=\"3\" %}}\n\n## Consumer handler\n\nNot every handler will produce new messages. You can add this kind of handler by using `Router.AddConsumerHandler`:\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// AddConsumerHandler\" last_line_contains=\") {\" padding_after=\"0\" %}}\n\n## Ack\n\nBy default, `msg.Ack()` is called when `HandlerFunc` doesn't return an error. If an error is returned, `msg.Nack()` will be called.\nBecause of this, you don't have to call `msg.Ack()` or `msg.Nack()` after a message is processed (you can if you want, of course).\n\n## Producing messages\n\nWhen returning multiple messages from a handler, be aware that most Publisher implementations don't support [atomic publishing of messages]({{< ref \"/docs/pub-sub#publishing-multiple-messages\" >}}). It may end up producing only some messages and sending `msg.Nack()` if the broker or the storage are not available.\n\nIf it is an issue, consider publishing just one message with each handler.\n\n## Running the Router\n\nTo run the Router, you need to call `Run()`.\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// Run\" last_line_contains=\"func (r *Router) Run(ctx context.Context) (err error) {\" padding_after=\"0\" %}}\n\n### Ensuring that the Router is running\n\nIt can be useful to know if the router is running. You can use the `Running()` method for this. \n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// Running\" last_line_contains=\"func (r *Router) Running()\" padding_after=\"0\" %}}\n\nYou can also use `IsRunning` function, that returns bool:\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// IsRunning\" last_line_contains=\"func (r *Router) IsRunning()\" padding_after=\"0\" %}}\n\n### Closing the Router\n\nTo close the Router, you need to call `Close()`.\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// Close gracefully\" last_line_contains=\"func (r *Router) Close()\" padding_after=\"1\" %}}\n\n`Close()` will close all publishers and subscribers, and wait for all handlers to finish.\n\n`Close()` will wait for a timeout configured in `RouterConfig.CloseTimeout`.\nIf the timeout is reached, `Close()` will return an error.\n\n## Adding handler after the router has started\n\nYou can add a new handler while the router is already running.\nTo do that, you need to call `AddConsumerHandler` or `AddHandler` and call `RunHandlers`.\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// RunHandlers\" last_line_contains=\"func (r *Router) RunHandlers\" padding_after=\"0\" %}}\n\n## Stopping running handler\n\nIt is possible to stop **just one running handler** by calling `Stop()`.\n\nPlease keep in mind, that router will be closed when there are no running handlers.\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// Stop\" last_line_contains=\"func (h *Handler) Stop()\" padding_after=\"0\" %}}\n\n## Execution models\n\n*Subscribers* can consume either one message at a time or multiple messages in parallel.\n\n* **Single stream of messages** is the simplest approach and it means that until a `msg.Ack()` is called, the subscriber\n  will not receive any new messages.\n* **Multiple message streams** are supported only by some subscribers. By subscribing to multiple topic partitions at once,\n  several messages can be consumed in parallel, even previous messages that were not acked (for example, the Kafka subscriber\n  works like this). Router handles this model by running concurrent `HandlerFunc`s, one for each partition.\n  \nSee the chosen Pub/Sub documentation for supported execution models.\n\n## Middleware\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// HandlerMiddleware\" last_line_contains=\"type HandlerMiddleware\" padding_after=\"1\" %}}\n\nA full list of standard middleware can be found in [Middleware]({{< ref \"/docs/middlewares\" >}}).\n\n## Plugin\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// RouterPlugin\" last_line_contains=\"type RouterPlugin\" padding_after=\"1\" %}}\n\nA full list of standard plugins can be found in [message/router/plugin](https://github.com/ThreeDotsLabs/watermill/tree/master/message/router/plugin).\n\n## Context\n\nEach message received by handler holds some useful values in the `context`:\n\n{{% load-snippet-partial file=\"src-link/message/router_context.go\" first_line_contains=\"// HandlerNameFromCtx\" last_line_contains=\"func PublishTopicFromCtx\" padding_after=\"2\" %}}\n"
  },
  {
    "path": "docs/content/docs/middlewares.md",
    "content": "+++\ntitle = \"Middleware\"\ndescription = \"Add generic functionalities to your handlers in an unobtrusive way\"\ndate = 2019-06-01T19:00:00+01:00\nweight = -500\ndraft = false \nbref = \"Add functionality to handlers\"\n+++\n\n## Introduction\n\nMiddleware wrap handlers with functionality that is important, but not relevant for the primary handler's logic. \nExamples include retrying the handler after an error was returned, or recovering from panic in the handler\nand capturing the stacktrace.\n\nMiddleware wrap the handler function like this:\n\n{{% load-snippet-partial file=\"src-link/message/router.go\" first_line_contains=\"// HandlerMiddleware\" last_line_contains=\"type HandlerMiddleware\" %}}\n\n## Usage\n\nMiddleware can be executed for all as well as for a specific handler in a router. When middleware is added directly \nto a router it will be executed for all handlers provided for a router. If a middleware should be executed only \nfor a specific handler, it needs to be added to handler in the router.\n\nExample usage is shown below:\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"router, err := message.NewRouter(message.RouterConfig{}, logger)\" last_line_contains=\"// Now that all handlers are registered, we're running the Router.\" padding_after=\"1\" %}}\n\n## Available middleware\n\nBelow are the middleware provided by Watermill and ready to use. You can also easily implement your own.\nFor example, if you'd like to store every received message in some kind of log, it's the best way to do it.\n\n{{% readfile file=\"/content/src-link/middleware-defs.md\" %}}\n\n"
  },
  {
    "path": "docs/content/docs/pub-sub.md",
    "content": "+++\ntitle = \"Publisher & Subscriber\"\ndescription = \"Publishers and Subscribers\"\ndate = 2018-12-05T12:47:30+01:00\nweight = -900\ndraft = false\nbref = \"Publishers and Subscribers\"\n+++\n\n## Publisher\n\n{{% load-snippet-partial file=\"src-link/message/pubsub.go\" first_line_contains=\"Publisher interface {\" last_line_contains=\"Close() error\" padding_after=\"1\" %}}\n\n### Publishing multiple messages\n\nMost publishers implementations don't support atomic publishing of messages.\nThis means that if publishing one of the messages fails, the next messages won't be published.\n\n### Async publish\n\nPublish can be synchronous or asynchronous - it depends on the implementation.\n\n#### `Close()`\n\n`Close` should flush unsent messages if the publisher is asynchronous.\n**It is important to not forget to close the subscriber**. Otherwise you may lose some of the messages.\n\n## Subscriber\n\n{{% load-snippet-partial file=\"src-link/message/pubsub.go\" first_line_contains=\"Subscriber interface {\" last_line_contains=\"Close() error\" padding_after=\"1\" %}}\n\n### Ack/Nack mechanism\n\nIt is the *Subscriber's* responsibility to handle an `Ack` and a `Nack` from a message.\nA proper implementation should wait for an `Ack` or a `Nack` before consuming the next message.\n\n**Important Subscriber's implementation notice**:\nAck/offset to message's storage/broker **must** be sent after Ack from Watermill's message.\nOtherwise there is a chance to lose messages if the process dies before the messages have been processed.\n\n#### `Close()`\n\n`Close` closes all subscriptions with their output channels and flushes offsets, etc. when needed.\n\n## At-least-once delivery\n\nWatermill is built with [at-least-once delivery](http://www.cloudcomputingpatterns.org/at_least_once_delivery/) semantics.\nThat means when some error occurs when processing a message and an Ack cannot be sent, the message will be redelivered.\n\nYou need to keep it in mind and build your application to be [idempotent](http://www.cloudcomputingpatterns.org/idempotent_processor/) or implement a deduplication mechanism.\n\nUnfortunately, it's not possible to create a universal [*middleware*]({{< ref \"/docs/messages-router#middleware\" >}}) for deduplication, so we encourage you to build your own.\n\n## Universal tests\n\nEvery Pub/Sub is similar in most aspects.\nTo avoid implementing separate tests for every Pub/Sub, we've created a test suite which should be passed by any Pub/Sub\nimplementation.\n\nThese tests can be found in `pubsub/tests/test_pubsub.go`.\n\n## Built-in implementations\n\nTo check available Pub/Sub implementations, see [Supported Pub/Subs]({{< ref \"/pubsubs\" >}}).\n\n## Implementing custom Pub/Sub\n\nSee [Implementing custom Pub/Sub]({{< ref \"/development/pub-sub-implementing\" >}}) for instructions on how to introduce support for\na new Pub/Sub.\n\nWe will also be thankful for submitting [pull requests](https://github.com/ThreeDotsLabs/watermill/pulls) with the new Pub/Sub implementations.\n\nYou can also request a new Pub/Sub implementation by submitting a [new issue](https://github.com/ThreeDotsLabs/watermill/issues).\n"
  },
  {
    "path": "docs/content/docs/snippets/amqp-consumer-groups/.validate_example.yml",
    "content": "validation_cmd: \"docker compose up\"\nteardown_cmd: \"docker compose down\"\ntimeout: 120\nexpected_output: \"payload: Hello, world!\"\n"
  },
  {
    "path": "docs/content/docs/snippets/amqp-consumer-groups/docker-compose.yml",
    "content": "services:\n  server:\n    image: golang:1.25\n    restart: unless-stopped\n    depends_on:\n      - rabbitmq\n    volumes:\n      - .:/app\n      - $GOPATH/pkg/mod:/go/pkg/mod\n    working_dir: /app\n    command: go run main.go\n\n  rabbitmq:\n    image: rabbitmq:3.7\n    restart: unless-stopped\n"
  },
  {
    "path": "docs/content/docs/snippets/amqp-consumer-groups/go.mod",
    "content": "module main.go\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.3\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rabbitmq/amqp091-go v1.10.0 // indirect\n)\n\ngo 1.25\n"
  },
  {
    "path": "docs/content/docs/snippets/amqp-consumer-groups/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.3 h1:fkhmiBtaLn+rz5lbkPD1h8tXHfKy3gX0vMtGmxNtAsk=\ngithub.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.3/go.mod h1:xy2qXKcJpgrJURRT6YwgRyGL3qIi6/sOHrDI0MO/r5I=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=\ngithub.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "docs/content/docs/snippets/amqp-consumer-groups/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-amqp/v2/pkg/amqp\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nvar amqpURI = \"amqp://guest:guest@rabbitmq:5672/\"\n\nfunc createSubscriber(queueSuffix string) *amqp.Subscriber {\n\tsubscriber, err := amqp.NewSubscriber(\n\t\t// This config is based on this example: https://www.rabbitmq.com/tutorials/tutorial-three-go.html\n\t\t// to create just a simple queue, you can use NewDurableQueueConfig or create your own config.\n\t\tamqp.NewDurablePubSubConfig(\n\t\t\tamqpURI,\n\t\t\t// Rabbit's queue name in this example is based on Watermill's topic passed to Subscribe\n\t\t\t// plus provided suffix.\n\t\t\t//\n\t\t\t// Exchange is Rabbit's \"fanout\", so when subscribing with suffix other than \"test_consumer_group\",\n\t\t\t// it will also receive all messages. It will work like separate consumer groups in Kafka.\n\t\t\tamqp.GenerateQueueNameTopicNameWithSuffix(queueSuffix),\n\t\t),\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn subscriber\n}\n\nfunc main() {\n\tsubscriber1 := createSubscriber(\"test_consumer_group_1\")\n\tmessages1, err := subscriber1.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgo process(\"subscriber_1\", messages1)\n\n\tsubscriber2 := createSubscriber(\"test_consumer_group_2\")\n\tmessages2, err := subscriber2.Subscribe(context.Background(), \"example.topic\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// subscriber2 will receive all messages independently from subscriber1\n\tgo process(\"subscriber_2\", messages2)\n\n\tpublisher, err := amqp.NewPublisher(\n\t\tamqp.NewDurablePubSubConfig(\n\t\t\tamqpURI,\n\t\t\tnil, // generateQueueName is not used with publisher\n\t\t),\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tpublishMessages(publisher)\n}\n\nfunc publishMessages(publisher message.Publisher) {\n\tfor {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n\n\t\tif err := publisher.Publish(\"example.topic\", msg); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc process(subscriber string, messages <-chan *message.Message) {\n\tfor msg := range messages {\n\t\tlog.Printf(\"[%s] received message: %s, payload: %s\", subscriber, msg.UUID, string(msg.Payload))\n\t\tmsg.Ack()\n\t}\n}\n"
  },
  {
    "path": "docs/content/docs/snippets/tail-log-file/go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill/docs/content/docs/snippets/tail-log-file\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-io v1.1.2\n)\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n)\n"
  },
  {
    "path": "docs/content/docs/snippets/tail-log-file/go.sum",
    "content": "github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-io v1.1.2 h1:t3wismmE++6HbB+fDnSdvLtSKs0yYaSXRtLmyUcRyTk=\ngithub.com/ThreeDotsLabs/watermill-io v1.1.2/go.mod h1:DF6rhoPWBOeWRW/1wWjNfLkke8rZsB5BUzBox/L6fRI=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "docs/content/docs/snippets/tail-log-file/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-io/pkg/io\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// this will `tail -f` a log file and publish an alert if a line fulfils some criterion\n\nfunc main() {\n\t// if an alert is raised, the offending line will be published on this\n\t// this would be set to an actual publisher\n\tvar alertPublisher message.Publisher\n\n\tif len(os.Args) < 2 {\n\t\tpanic(\n\t\t\tfmt.Errorf(\"usage: %s /path/to/file.log\", os.Args[0]),\n\t\t)\n\t}\n\tlogFile, err := os.OpenFile(os.Args[1], os.O_RDONLY, 0444)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsub, err := io.NewSubscriber(logFile, io.SubscriberConfig{\n\t\tUnmarshalFunc: io.PayloadUnmarshalFunc,\n\t}, watermill.NewStdLogger(true, false))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// for io.Subscriber, topic does not matter\n\tlines, err := sub.Subscribe(context.Background(), \"\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor line := range lines {\n\t\tif criterion(string(line.Payload)) {\n\t\t\t_ = alertPublisher.Publish(\"alerts\", line)\n\t\t}\n\t}\n}\n\nfunc criterion(line string) bool {\n\t// decide whether an action needs to be taken\n\treturn false\n}\n"
  },
  {
    "path": "docs/content/docs/troubleshooting.md",
    "content": "+++\ntitle = \"Troubleshooting\"\ndescription = \"When something goes wrong\"\nweight = -90\ndraft = false\nbref = \"When something goes wrong\"\n+++\n\n## Logging\n\nIn most cases, you will find the answer to your problem in the logs.\nWatermill offers a significant amount of logs on different severity levels.\n\nIf you are using `StdLoggerAdapter`, just change `debug`, and `trace` options to true:\n\n```bash\nlogger := watermill.NewStdLogger(true, true)\n````\n\n## Debugging Pub/Sub tests\n\n### Running a single test\n\n```bash\nmake up\ngo test -v ./... -run TestPublishSubscribe/TestContinueAfterSubscribeClose\n```\n\n### grep is your friend\n\nEach executed test case has a unique UUID.\nIt's used in the topic's name.\nThanks to that, you can easily grep the output of the test.\nIt gives you detailed information about the test execution.\n\n```bash\n> go test -v ./... > test.out\n\n> less test.out\n\n// ...\n\n--- PASS: TestPublishSubscribe (0.00s)\n    --- PASS: TestPublishSubscribe/TestPublishSubscribe (2.38s)\n        --- PASS: TestPublishSubscribe/TestPublishSubscribe/81eeb56c-3336-4eb9-a0ac-13abda6f38ff (2.38s)\n```\n\n\n```bash\ncat test.out | grep 81eeb56c-3336-4eb9-a0ac-13abda6f38ff | less\n\n[watermill] 2020/08/18 14:51:46.283366 subscriber.go:300:       level=TRACE msg=\"Msg acked\" message_uuid=5c920330-5075-4870-8d86-9013771eee78 provider=google_cloud_pubsub subscription_name=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff topic=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff\n[watermill] 2020/08/18 14:51:46.283405 subscriber.go:300:       level=TRACE msg=\"Msg acked\" message_uuid=46e04a08-994e-4c04-afff-7fd42fd67f95 provider=google_cloud_pubsub subscription_name=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff topic=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff\n2020/08/18 14:51:46 all messages (100/100) received in bulk read after 110.04155ms of 45s (test ID: 81eeb56c-3336-4eb9-a0ac-13abda6f38ff)\n[watermill] 2020/08/18 14:51:46.284569 subscriber.go:186:       level=DEBUG msg=\"Closing message consumer\" provider=google_cloud_pubsub subscription_name=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff topic=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff\n[watermill] 2020/08/18 14:51:46.284828 subscriber.go:300:       level=TRACE msg=\"Msg acked\" message_uuid=2f409208-d4d2-46f6-b6b9-afb1aea0e59f provider=google_cloud_pubsub subscription_name=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff topic=topic_81eeb56c-3336-4eb9-a0ac-13abda6f38ff\n        --- PASS: TestPublishSubscribe/TestPublishSubscribe/81eeb56c-3336-4eb9-a0ac-13abda6f38ff (2.38s)\n```\n\n## I have a deadlock\n\nWhen running locally, you can send a `SIGQUIT` to the running process:\n\n- `CTRL + \\` on Linux\n- `kill -s SIGQUIT [pid]` on other UNIX systems\n\nThis will kill the process and print all goroutines along with lines on which they have stopped.\n\n```bash\nSIGQUIT: quit\nPC=0x45e7c3 m=0 sigcode=128\n\ngoroutine 1 [runnable]:\ngithub.com/ThreeDotsLabs/watermill/pubsub/gochannel.(*GoChannel).sendMessage(0xc000024100, 0x7c5250, 0xd, 0xc000872d70, 0x0, 0x0)\n\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/pubsub/gochannel/pubsub.go:83 +0x36a\ngithub.com/ThreeDotsLabs/watermill/pubsub/gochannel.(*GoChannel).Publish(0xc000024100, 0x7c5250, 0xd, 0xc000098530, 0x1, 0x1, 0x0, 0x0)\n\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/pubsub/gochannel/pubsub.go:53 +0x6d\nmain.publishMessages(0x7fdf7a317000, 0xc000024100)\n\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/docs/src-link/_examples/pubsubs/go-channel/main.go:43 +0x1ec\nmain.main()\n\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/docs/src-link/_examples/pubsubs/go-channel/main.go:36 +0x20a\n\n// ...\n```\n\nWhen running in production and you don't want to kill the entire process, a better idea is to use [pprof](https://golang.org/pkg/net/http/pprof/).\n\nYou can visit [http://localhost:6060/debug/pprof/goroutine?debug=1](http://localhost:6060/debug/pprof/goroutine?debug=1)\non your local machine to see all goroutines status.\n\n\n```bash\ngoroutine profile: total 5\n1 @ 0x41024c 0x6a8311 0x6a9bcb 0x6a948d 0x7028bc 0x70260a 0x42f187 0x45c971\n#\t0x6a8310\tgithub.com/ThreeDotsLabs/watermill.LogFields.Add+0xd0\t\t\t\t\t\t\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/log.go:15\n#\t0x6a9bca\tgithub.com/ThreeDotsLabs/watermill/pubsub/gochannel.(*GoChannel).sendMessage+0x6fa\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/pubsub/gochannel/pubsub.go:75\n#\t0x6a948c\tgithub.com/ThreeDotsLabs/watermill/pubsub/gochannel.(*GoChannel).Publish+0x6c\t\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/pubsub/gochannel/pubsub.go:53\n#\t0x7028bb\tmain.publishMessages+0x1eb\t\t\t\t\t\t\t\t\t\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/docs/src-link/_examples/pubsubs/go-channel/main.go:43\n#\t0x702609\tmain.main+0x209\t\t\t\t\t\t\t\t\t\t\t\t/home/example/go/src/github.com/ThreeDotsLabs/watermill/docs/src-link/_examples/pubsubs/go-channel/main.go:36\n#\t0x42f186\truntime.main+0x206\t\t\t\t\t\t\t\t\t\t\t/usr/lib/go/src/runtime/proc.go:201\n\n// ...\n```\n"
  },
  {
    "path": "docs/content/learn/_index.md",
    "content": "---\r\ntitle: \"Learn Watermill\"\r\ndescription: \"Resources to help you learn and master Watermill for building event-driven applications in Go.\"\r\ndate: 2024-09-15T10:00:00+00:00\r\nlastmod: 2024-09-15T10:00:00+00:00\r\ndraft: false\r\nlayout: learn\r\nhideBanner: true\r\nseo:\r\n  title: \"Learn Watermill - Go Event-Driven Applications\"\r\n  description: \"Complete learning resources for Watermill - guides, examples, and community resources for building event-driven applications in Go.\"\r\n  canonical: \"\"\r\n  noindex: false\r\n\r\nlearning_options:\r\n  - title: \"Watermill Quickstart\"\r\n    subtitle: \"Code along\"\r\n    description: \"Learn how to build an event-driven application in Go, coding in your own IDE.\"\r\n    link: \"/learn/quickstart/\"\r\n    new: true\r\n    icon: '<svg width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1000 1000\" xmlns=\"https://vecta.io/nano\" fill=\"#4f46e5\"><path d=\"M893.54 441.92c-9.43-29.13-36.33-48.69-66.92-48.69-7.4 0-14.73 1.16-21.8 3.47l-1.94.63c-17.7-27.35-40.36-51.5-66.65-70.87-26.94-19.83-57.49-34.48-89.68-43.12v-2.46c0-18.76-7.29-36.42-20.55-49.72l-.02-.03-.03-.03c-13.29-13.28-30.97-20.6-49.78-20.6-35.39 0-64.76 26.25-69.66 60.31-44.4-1.33-90.31 8.95-133.08 22.87l-10.67 3.58c-66.59 23.06-95.08 10.97-95.08 10.97 33.94 37.66 112.52 24.38 104.24 52.8-7.78 26.67-34.62 13.96-49.94 8.16-31.64-11.97-63.53-23.49-97.94-19.13-22.08 2.8-41.43 13.38-62.09 23.03-22.82 10.65-46.1 20.06-70.05 25.39-9.85 2.18-19.78 3.85-29.72 4.58-2.36.17-8.11 1.68-10.29.34 13.97 8.61 31.16 12.14 46.79 14.74 18.3 3.04 36.83 3.72 55.46 2.7 19.61-1.07 39.69-1.46 57.33 8.62 9.43 5.39 26.36 18.78 17.15 33.8l-5.17 1.45c-16.14 4.54-33.47 3.23-49.46 8.28-116.96 36.95-155.25 97.03-155.84 97.97.69-.4 32.72-18.83 95.33-14.59 50.6 3.42 98.2 26.99 142.35 53.27 61.34 36.51 61.27 60.33 96.42 105.61.83 1.06 3.71 4.1 7.59 8.02-4.56 9.45-6.92 19.78-6.92 30.44 0 22.48 10.86 43.78 29.05 56.98a69.93 69.93 0 0 0 41.32 13.42c22.46 0 43.76-10.85 56.97-29.02l.4-.55c30.54 11.66 63.27 17.73 96.15 17.73 21.99 0 43.87-2.66 65.02-7.91a266.69 266.69 0 0 0 30.72-9.64l.27.38c13.17 18.16 34.47 29.02 56.96 29.02 14.91 0 29.19-4.65 41.3-13.43 31.41-22.81 38.41-66.91 15.63-98.32l-.31-.43c15.41-19.08 28.08-40.13 37.82-62.85 14.36-33.52 21.64-69.13 21.64-105.87a270.59 270.59 0 0 0-.26-11.75l2.77-.9c29.1-9.5 48.63-36.38 48.63-66.92 0-7.33-1.16-14.65-3.46-21.73zm-56.3 54.41l-30.31 9.86a234.23 234.23 0 0 1 2.93 37.03c0 65.1-26.7 123.99-69.77 166.24l17.48 24.05c11.15 15.38 7.74 36.88-7.62 48.03a34.72 34.72 0 0 1-8.48 4.52c-.79.28-1.59.54-2.39.77s-1.62.43-2.43.59a30.94 30.94 0 0 1-1.63.29c-.43.07-.87.12-1.31.18-.87.1-1.75.17-2.62.2-.43.02-.87.03-1.31.03a33.72 33.72 0 0 1-5.94-.52 41.01 41.01 0 0 1-2.92-.64 32.36 32.36 0 0 1-2.85-.89 34.17 34.17 0 0 1-16.13-12.14l-17.45-23.99c-32.19 16.83-68.82 26.33-107.68 26.33a234.53 234.53 0 0 1-38.66-3.19c-24.67-4.12-48.03-12.12-69.44-23.35l-16.48 22.69-1.09 1.51c-6.74 9.27-17.22 14.18-27.85 14.18-.88 0-1.75-.03-2.63-.1-6.14-.47-12.22-2.59-17.56-6.47-9.27-6.72-14.18-17.21-14.18-27.83 0-1.02.05-2.04.14-3.06.53-6 2.64-11.92 6.43-17.14l17.7-24.35c-42.87-42.26-69.43-100.99-69.43-165.94 0-12.66 1.02-25.06 2.95-37.18l-28.68-9.33c-14.54-4.73-23.76-18.21-23.76-32.72 0-3.52.54-7.09 1.68-10.63 1.03-3.18 2.48-6.1 4.27-8.73.51-.75 1.05-1.48 1.61-2.17.85-1.06 1.75-2.06 2.71-2.99l.97-.92c.33-.3.67-.59 1.01-.88.68-.57 1.38-1.11 2.1-1.63 1.8-1.28 3.73-2.4 5.74-3.32a40.61 40.61 0 0 1 2.45-1.01c6.99-2.56 14.85-2.89 22.47-.4l28.29 9.19c33.04-67.19 97.5-116.14 174.22-127.7v-31.92a34.18 34.18 0 0 1 .56-6.19c2.91-16.04 16.96-28.19 33.82-28.19a34.26 34.26 0 0 1 24.32 10.06c6.22 6.24 10.06 14.82 10.06 24.32v31.74c8.32 1.2 16.51 2.85 24.52 4.91 66.44 17.07 121.41 62.73 151.05 123.1l29.85-9.7a33.08 33.08 0 0 1 2.66-.75 35.57 35.57 0 0 1 2-.42 34.47 34.47 0 0 1 2-.3c4.08-.48 8.13-.21 11.99.72.86.2 1.71.44 2.56.72h.01c1.68.54 3.32 1.22 4.89 2.02.4.19.79.4 1.19.62a41.1 41.1 0 0 1 1.43.84 31.49 31.49 0 0 1 2.73 1.88c.44.33.87.68 1.29 1.04h.01c.51.43 1.01.87 1.5 1.33a16.11 16.11 0 0 1 .72.71 28.67 28.67 0 0 1 2.04 2.23c.44.52.85 1.05 1.25 1.59.54.72 1.05 1.48 1.53 2.25 1.45 2.33 2.64 4.87 3.52 7.6 1.15 3.54 1.71 7.13 1.71 10.64 0 14.5-9.25 27.94-23.76 32.68zM448.12 659.39c-28.89-31.94-44.78-73.14-44.78-116.17 0-5.96.3-11.82.88-17.46l92.99 30.22a79.49 79.49 0 0 0 8.17 24.6l-57.26 78.81z\"></path><path d=\"M405.09 527.1l91.23 29.65a80.59 80.59 0 0 0 7.88 23.75l-56.17 77.31c-28.2-31.62-43.7-72.22-43.7-114.58 0-5.49.25-10.9.75-16.12m-1.72-2.68c-.69 6.16-1.02 12.45-1.02 18.79 0 45.34 17.39 86.7 45.87 117.75l58.35-80.31c-4.18-7.79-7.08-16.37-8.45-25.44l-94.75-30.79h0zm173.45 192.26c-24.97 0-49.05-5.21-71.6-15.5l57.16-78.68a80.54 80.54 0 0 0 14.44 1.31c4.82 0 9.38-.41 13.92-1.21l57.26 78.78c-22.4 10.15-46.35 15.29-71.18 15.29z\"></path><path d=\"M562.81 623.6c4.62.81 9.33 1.22 14 1.22s9.07-.37 13.48-1.11l56.17 77.28c-21.95 9.76-45.37 14.7-69.65 14.7s-47.98-5.01-70.07-14.9l56.07-77.18m-.88-2.2l-58.25 80.18c22.26 10.34 47.05 16.1 73.13 16.1s50.58-5.68 72.71-15.89l-58.35-80.28c-4.66.86-9.45 1.3-14.36 1.3s-10.06-.49-14.88-1.41h0zm86-39.97a80.2 80.2 0 0 0 8.14-24.96l92.79-30.98c.62 5.71.96 11.52 1.01 17.3.36 43.22-15.31 84.68-44.13 116.87l-57.81-78.23z\"></path><path d=\"M748 526.84c.54 5.28.83 10.64.87 15.96.35 42.56-14.92 83.4-43.06 115.28l-6.35-8.59-50.36-68.15c3.88-7.6 6.52-15.7 7.86-24.11l71.68-23.93 19.35-6.46m1.73-2.68l-21.7 7.25-72.84 24.32a79.13 79.13 0 0 1-8.42 25.8l51.1 69.15 7.81 10.57c28.42-31.31 45.58-72.97 45.21-118.46-.05-6.29-.44-12.53-1.15-18.63h0zm-113.85-35.78c-6.98-7.51-15.16-13.53-24.32-17.89v-97.23c41.9 8.54 79.05 32.27 104.72 66.94 4.31 5.81 8.28 11.94 11.82 18.22l-5.72 1.86-86.49 28.1z\"></path><path d=\"M612.55 374.49c41.17 8.69 77.64 32.17 102.91 66.3a175.13 175.13 0 0 1 11.15 17.05l-4.56 1.48-85.89 27.91c-6.83-7.23-14.76-13.07-23.62-17.38v-95.37m-1.99-2.44v99.08c9.53 4.45 18.01 10.75 25.02 18.41l87.1-28.3 6.88-2.24c-3.74-6.75-7.91-13.22-12.48-19.39-25.41-34.32-63.1-59.03-106.52-67.56h0zM425.61 458.3c24.57-43.64 66.44-74.46 115.18-84.8v97.61c-8.61 4.29-16.32 10.06-22.94 17.17l-92.24-29.99z\"></path><path d=\"M539.79 374.74v95.75c-8.31 4.22-15.78 9.81-22.23 16.64l-90.47-29.41c11.81-20.61 28.1-38.94 47.22-53.12 19.7-14.61 41.72-24.65 65.49-29.86m1.99-2.47c-50.61 10.36-93.33 42.71-117.65 86.6l94.01 30.56c6.67-7.28 14.67-13.32 23.64-17.7v-99.46h0zm76.22 169.21c0 22.98-18.63 41.62-41.61 41.62s-41.62-18.64-41.62-41.62 18.64-41.61 41.62-41.61 41.61 18.63 41.61 41.61z\"></path></svg>'\r\n\r\n  - title: \"Getting Started Guide\"\r\n    subtitle: \"Read\"\r\n    description: \"Start with a guide that covers core concepts of Watermill in a few minutes.\"\r\n    link: \"/learn/getting-started/\"\r\n    icon: '<svg width=\"48\" height=\"48\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'\r\n    icon: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" fill=\"currentColor\" class=\"bi bi-book\" viewBox=\"0 0 16 16\"><path d=\"M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783\"/></svg>'\r\n\r\n  - title: \"Examples\"\r\n    subtitle: \"Try out\"\r\n    description: \"Real-world examples and patterns for CQRS, the outbox pattern, SSE, and other use cases.\"\r\n    link: \"https://github.com/ThreeDotsLabs/watermill/tree/master/_examples\"\r\n    icon: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" fill=\"currentColor\" class=\"bi bi-terminal\" viewBox=\"0 0 16 16\"><path d=\"M6 9a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3A.5.5 0 0 1 6 9M3.854 4.146a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708z\"/> <path d=\"M2 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm12 1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z\"/></svg>'\r\n\r\ndeeper_options:\r\n  - title: \"Go Event-Driven\"\r\n    description: \"An in-depth online training on Event-Driven Architecture.\"\r\n    link: \"https://threedots.tech/event-driven/?utm_source=watermill-learn\"\r\n    icon: '<svg width=\"76\" height=\"76\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1000 1000\" xmlns=\"https://vecta.io/nano\" fill=\"#4f46e5\"><path d=\"M370.73 308.36l331.77.97c8.39 0 12.26 10.33 6.13 15.81l-156.2 138.78c-8.71 7.75-21.62 7.75-30.34 0L364.92 323.85c-6.45-5.16-2.58-15.49 5.81-15.49zM98.99 455.21c0-10.65 8.71-19.36 19.36-19.36h73.26c10.65 0 19.36 8.71 19.36 19.36h0c0 10.65-8.71 19.36-19.36 19.36h-73.26c-10.65 0-19.36-8.71-19.36-19.36h0zm-4.2 61.32h96.82c10.65 0 19.36 8.71 19.36 19.36s-8.71 19.36-19.36 19.36H94.79c-10.65 0-19.36-8.71-19.36-19.36s8.71-19.36 19.36-19.36zm96.83 119.41H75.11c-10.65 0-19.36-8.71-19.36-19.36h0c0-10.65 8.71-19.36 19.36-19.36h116.51c10.65 0 19.36 8.71 19.36 19.36h0c0 10.65-8.71 19.36-19.36 19.36zM860 617.22c0 50.67-40.99 91.66-91.66 91.66h-538c-10.65 0-19.36-8.71-19.36-19.36v-33.89c0-10.65 8.71-19.36 19.36-19.36h38.08c10.65 0 19.36-8.71 19.36-19.36h0c0-10.65-8.71-19.36-19.36-19.36h-38.08c-10.65 0-19.36-8.71-19.36-19.36v-3.23c0-10.65 8.71-19.36 19.36-19.36h38.08c10.65 0 19.36-8.71 19.36-19.36s-8.71-19.36-19.36-19.36h-38.08c-10.65 0-19.36-8.71-19.36-19.36v-3.23c0-10.65 8.71-19.36 19.36-19.36h38.08c10.65 0 19.36-8.71 19.36-19.36h0c0-10.65-8.71-19.36-19.36-19.36h-38.08c-10.65 0-19.36-8.71-19.36-19.36v-88.11c0-16.78 20.01-25.82 32.6-14.2l271.74 250.44c10.97 10 27.76 10 38.73 0l273.36-250.77c12.26-11.3 32.6-2.58 32.6 14.2v288.85z\"></path></svg>'\r\n\r\n  - title: \"Discord Community\"\r\n    description: \"Join our Discord community to get help and discuss Watermill and related topics.\"\r\n    link: \"https://discord.gg/QV6VFg4YQE\"\r\n    external: true\r\n    icon: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" fill=\"currentColor\" class=\"bi bi-chat-left-dots\" viewBox=\"0 0 16 16\"><path d=\"M14 1a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H4.414A2 2 0 0 0 3 11.586l-2 2V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12.793a.5.5 0 0 0 .854.353l2.853-2.853A1 1 0 0 1 4.414 12H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z\"/><path d=\"M5 6a1 1 0 1 1-2 0 1 1 0 0 1 2 0m4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0\"/></svg>'\r\n\r\n  - title: \"GitHub Repository\"\r\n    description: \"Read the source code, contribute, report issues, and stay up to date.\"\r\n    link: \"https://github.com/ThreeDotsLabs/watermill\"\r\n    external: true\r\n    icon: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" fill=\"currentColor\" class=\"bi bi-github\" viewBox=\"0 0 16 16\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8\"/></svg>'\r\n---\r\n"
  },
  {
    "path": "docs/content/learn/getting-started.md",
    "content": "+++\ntitle = \"Getting started\"\ndescription = \"Watermill up and running\"\ndraft = false\nbref = \"Watermill up and running\"\nweight = 10\n+++\n\n## What is Watermill?\n\nWatermill is a Go library for working with messages the easy way.\n\nYou can use it to build message-driven and event-driven applications with Pub/Subs like Kafka, RabbitMQ, PostgreSQL, and many more.\n\nWatermill comes with batteries included. It gives you tools used by every message-driven application.\n\n## Why use Watermill?\n\nWhen you run an HTTP server, you don't deal directly with TCP sockets, parsing HTTP requests, or managing connections.\nInstead, you use a high-level library like `net/http` that handles all that complexity for you.\n\n**It's what Watermill aims to be for messages**.\nIt provides all you need to build an application based on events or other asynchronous patterns.\n\nThere are many different message queues, each with different features, client libraries, and APIs.\nWatermill hides all that complexity behind an API that is easy to use and understand.\n\n**Watermill is NOT a framework**.\nIt's a lightweight library that's easy to plug in or remove from your project.\n\n## Install\n\n```bash\ngo get -u github.com/ThreeDotsLabs/watermill\n```\n\n{{< callout context=\"note\" title=\"Learn in practice\" icon=\"outline/info-circle\" >}}\n\nDocs too boring? Prefer learning by doing?\n\n[**Try the free hands-on training**]({{< ref \"/learn/quickstart/\" >}}) where you'll solve exercises to learn how to use Watermill in your projects.\n\nIt'll guide you through the basics and a few advanced concepts like message ordering and the Outbox pattern.\n\n{{< /callout >}}\n\n## One-Minute Background\n\nThe idea behind event-driven applications is always the same: one part publishes messages, and another part subscribes to them.\n\nWatermill supports this behavior for multiple [publishers and subscribers]({{< ref \"/pubsubs\" >}}).\n\n### Three APIs\n\nWatermill comes with three APIs for working with messages.\nThey build on top of each other, each step providing a higher-level API.\n\nIn this guide, we're going to start from the bottom and move up.\nIt's good to know the fundamentals, even if you're going to use the high-level APIs.\n\n<div class=\"text-center\">\n    <img src=\"/img/pyramid.png\" alt=\"Watermill components pyramid\" style=\"width:35rem;\" />\n</div>\n\n## Publisher & Subscriber\n\nMost Pub/Sub libraries come with complex features.\n\nWatermill hides this complexity behind two interfaces: the `Publisher` and `Subscriber`.\n\n```go\ntype Publisher interface {\n\tPublish(topic string, messages ...*Message) error\n\tClose() error\n}\n\ntype Subscriber interface {\n\tSubscribe(ctx context.Context, topic string) (<-chan *Message, error)\n\tClose() error\n}\n```\n\n### Creating Messages\n\n**The core part of Watermill is the [Message]({{< ref \"/docs/message\" >}}).**\nIt is what `http.Request` is for the `net/http` package.\nMost Watermill features work with this struct.\n\nWatermill doesn't enforce any message format. `NewMessage` expects a slice of bytes as the payload.\nYou can use strings, JSON, protobuf, Avro, gob, or anything else that serializes to `[]byte`.\n\nThe message UUID is optional but recommended for debugging.\n\n```go\nmsg := message.NewMessage(watermill.NewUUID(), []byte(\"Hello, world!\"))\n```\n\n### Publishing Messages\n\n`Publish` expects a topic and one or more `Message`s to be published.\n\n```go\nerr := publisher.Publish(\"example.topic\", msg)\nif err != nil {\n    panic(err)\n}\n```\n\n{{< tabs \"publishing\" >}}\n\n{{< tab \"Go Channel\" \"go-channel\" >}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/go-channel/main.go\" first_line_contains=\"message.NewMessage\" last_line_contains=\"publisher.Publish\" padding_after=\"2\" %}}\n{{< /tab >}}\n\n{{< tab \"Kafka\" \"kafka\" >}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/kafka/main.go\" first_line_contains=\"message.NewMessage\" last_line_contains=\"publisher.Publish\" padding_after=\"2\" %}}\n{{< /tab >}}\n\n{{< tab \"NATS Streaming\" \"nats\" >}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/nats-streaming/main.go\" first_line_contains=\"message.NewMessage\" last_line_contains=\"publisher.Publish\" padding_after=\"2\" %}}\n{{< /tab >}}\n\n{{< tab \"Google Cloud Pub/Sub\" \"gcp\" >}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/googlecloud/main.go\" first_line_contains=\"message.NewMessage\" last_line_contains=\"publisher.Publish\" padding_after=\"2\" %}}\n{{< /tab >}}\n\n{{< tab \"RabbitMQ (AMQP)\" \"amqp\" >}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/amqp/main.go\" first_line_contains=\"message.NewMessage\" last_line_contains=\"publisher.Publish\" padding_after=\"2\" %}}\n{{< /tab >}}\n\n{{< tab \"SQL\" \"sql\" >}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sql/main.go\" first_line_contains=\"message.NewMessage\" last_line_contains=\"publisher.Publish\" padding_after=\"2\" %}}\n{{< /tab >}}\n\n{{< tab \"AWS SQS\" \"aws-sqs\" >}}\n{{% 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\" %}}\n{{< /tab >}}\n\n{{< tab \"AWS SNS\" \"aws-sns\" >}}\n{{% 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\" %}}\n{{< /tab >}}\n\n{{< /tabs >}}\n\n\n### Subscribing for Messages\n\n`Subscribe` expects a topic name and returns a channel of incoming messages.\n\nWhat _topic_ exactly means depends on the Pub/Sub implementation.\nUsually, it needs to match the topic name used by the publisher.\n\nMessages need to be acknowledged after processing by calling the `Ack()` method.\n\n```go\nmessages, err := subscriber.Subscribe(ctx, \"example.topic\")\nif err != nil {\n\tpanic(err)\n}\n\nfor msg := range messages {\n\tfmt.Printf(\"received message: %s, payload: %s\\n\", msg.UUID, string(msg.Payload))\n\tmsg.Ack()\n}\n```\n\nSee detailed examples below for supported PubSubs.\n\n{{< tabs \"getting-started\" >}}\n\n{{< tab \"Go Channel\" \"go-channel\" >}}\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/go-channel/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/go-channel/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< tab \"Kafka\" \"kafka\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\nThe easiest way to run Watermill locally with Kafka is by using Docker.\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/kafka/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute the `docker-compose up` command.\n\nA more detailed explanation of how it works (and how to add live code reload) can be found in the [*Go Docker dev environment* article](https://threedots.tech/post/go-docker-dev-environment-with-go-modules-and-live-code-reloading/).\n</details>\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/kafka/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/kafka/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< tab \"NATS Streaming\" \"nats\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\nThe easiest way to run Watermill locally with NATS is using Docker.\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/nats-streaming/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute the `docker-compose up` command.\n\nA 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/).\n</details>\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/nats-streaming/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/nats-streaming/main.go\" first_line_contains=\"func process\" %}}\n{{< /tabs >}}\n\n\n{{< tab \"Google Cloud Pub/Sub\" \"gcp\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\nYou can run the Google Cloud Pub/Sub emulator locally for development.\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/googlecloud/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute `docker-compose up`.\n\nA 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/).\n</details>\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/googlecloud/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/googlecloud/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< tab \"RabbitMQ (AMQP)\" \"amqp\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/amqp/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute `docker-compose up`.\n\nA 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/).\n</details>\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/amqp/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/amqp/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< tab \"SQL\" \"sql\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/sql/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute `docker-compose up`.\n\nA 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/).\n</details>\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sql/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sql/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< tab \"AWS SQS\" \"aws-sqs\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/aws-sqs/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute `docker-compose up`.\n\nA 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/).\n</details>\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/aws-sqs/main.go\" first_line_contains=\"package main\" last_line_contains=\"process(messages)\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/aws-sqs/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< tab \"AWS SNS\" \"aws-sns\" >}}\n\n<details>\n<summary><strong>Running in Docker</strong></summary>\n\n{{% load-snippet file=\"src-link/_examples/pubsubs/aws-sns/docker-compose.yml\" type=\"yaml\" %}}\n\nThe source should go to `main.go`.\n\nTo run, execute `docker-compose up`.\n\nA 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/).\n</details>\n\n{{% 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\" %}}\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/aws-sns/main.go\" first_line_contains=\"func process\" %}}\n{{< /tab >}}\n\n{{< /tabs >}}\n\n## Router\n\n[*Publishers and subscribers*]({{< ref \"/docs/pub-sub\" >}}) are the low-level parts of Watermill.\nFor most cases, you want to use a high-level API: the [*Router*]({{< ref \"/docs/messages-router\" >}}) component.\n\n### Router configuration\n\nStart with configuring the router and adding plugins and middlewares.\n\nA middleware is a function executed for each incoming message.\nYou can use one of the existing ones for things like [correlation, metrics, poison queue, retrying, throttling, etc.]({{< ref \"/docs/messages-router#middleware\" >}}).\nYou can also create your own.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"message.NewRouter\" last_line_contains=\"middleware.Recoverer,\" padding_after=\"1\" %}}\n\n### Handlers\n\nSet up handlers that the router uses.\nEach handler independently handles incoming messages.\n\nA handler listens to messages from the given subscriber and topic.\nAny messages returned from the handler function will be published to the given publisher and topic.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"AddHandler returns\" last_line_contains=\")\" padding_after=\"0\" %}}\n\n*Note: the example above uses one `pubSub` argument for both the subscriber and publisher.\nIt's because we use the `GoChannel` implementation, which is a simple in-memory Pub/Sub.*\n\nAlternatively, if you don't plan to publish messages from within the handler, you can use the simpler `AddConsumerHandler` method.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"AddConsumerHandler\" last_line_contains=\")\" padding_after=\"0\" %}}\n\nYou can use two types of *handler functions*:\n\n1. a function `func(msg *message.Message) ([]*message.Message, error)`\n2. a struct method `func (c structHandler) Handler(msg *message.Message) ([]*message.Message, error)`\n\nUse the first one if your handler is a function without any dependencies.\nThe second option is useful when your handler requires dependencies such as a database handle or a logger.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"func printMessages\" last_line_contains=\"return message.Messages{msg}, nil\" padding_after=\"3\" %}}\n\nFinally, run the router.\n\n{{% load-snippet-partial file=\"src-link/_examples/basic/3-router/main.go\" first_line_contains=\"router.Run\" last_line_contains=\"}\" padding_after=\"0\" %}}\n\nThe complete example's source can be found at [/_examples/basic/3-router/main.go](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/3-router/main.go).\n\n## Logging\n\nTo see Watermill's logs, pass any logger that implements the [LoggerAdapter](https://github.com/ThreeDotsLabs/watermill/blob/master/log.go).\nFor experimental development, you can use `NewStdLogger`.\n\nWatermill provides ready-to-use `slog` adapter. You can create it with [`watermill.NewSlogLogger`](https://github.com/ThreeDotsLabs/watermill/blob/master/slog.go).\nYou can also map Watermill's log levels to `slog` levels with [`watermill.NewSlogLoggerWithLevelMapping`](https://github.com/ThreeDotsLabs/watermill/blob/master/slog.go).\n\n## What's next?\n\nSee the [CQRS component](/docs/cqrs) for the generic high-level API.\n\nFor more details, see [documentation topics]({{< ref \"/docs\" >}}).\n\n[The Outbox Pattern](/advanced/forwarder/) is a key pattern to know in event-driven applications.\n\nWe recommend checking the examples below to see how Watermill works in practice.\nYou can also try the [free hands-on training]({{< ref \"/learn/quickstart/\" >}}) to learn how to use Watermill in practice.\n\n## Examples\n\nCheck out the [examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples) that will show you how to start using Watermill.\n\nThe recommended entry point is [Your first Watermill application](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/basic/1-your-first-app).\nIt contains the entire environment in `docker-compose.yml`, including Go and Kafka, which you can run with one command.\n\nAfter that, you can see the [Realtime feed](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/basic/2-realtime-feed) example.\nIt uses more middlewares and contains two handlers.\n\nFor a different subscriber implementation (**HTTP**), see the [receiving-webhooks](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/real-world-examples/receiving-webhooks) example.\nIt is a straightforward application that saves webhooks to Kafka.\n\nYou can find the complete list of examples in the [README](https://github.com/ThreeDotsLabs/watermill#examples).\n\n## Support\n\nIf anything is not clear, feel free to use any of our [support channels]({{< ref \"/support\" >}}); we will be glad to help.\n"
  },
  {
    "path": "docs/content/learn/quickstart.md",
    "content": "---\ntitle: \"Watermill Quickstart\"\ndescription: \"Learn how to use Watermill in your project with a hands-on training.\"\ndraft: false\ntoc: false\nweight: 20\nhideBanner: true\nseo:\n  title: \"Watermill Quickstart - Hands-on Training\"\n  description: \"Learn how to use Watermill in your project with a hands-on training covering Publisher/Subscriber, Router, CQRS, Kafka, and more.\"\n  canonical: \"\"\n  noindex: false\n---\n\n{{< callout context=\"note\" title=\"Just released 🎉\" >}}\n<p>Check our new, interactive quickstart that you can finish in your IDE. \nZero Docker setup, zero coding in browser.</p>\n{{< /callout >}}\n\nWatermill Quickstart is a hands-on training that will teach you how to use Watermill, a Go library for building event-driven applications, in your project.\nBasic Go knowledge is all you need to get started.\n\nYou'll learn the basics of Watermill and a few more advanced concepts:\n\n* The 3 core APIs: Publisher/Subscriber, Router, and CQRS.\n* Working with Kafka.\n* Topic topology, middleware, consumer groups.\n* Message ordering.\n* The Outbox pattern (with PostgreSQL).\n* Switching the Pub/Sub (Redis).\n\n<div class=\"text-center\">\n<a href=\"https://academy.threedots.tech/trainings/watermill-quickstart/start\" class=\"btn btn-primary btn-lg my-3\">Start the Watermill Quickstart</a>\n</div>\n\n<a href=\"https://academy.threedots.tech/trainings/watermill-quickstart/start\" target=\"_blank\">\n<figure class=\"position-relative\" style=\"width: 100%; margin-top:15px;\">\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960.000000 700.000000\" style=\"width: 100%; height: auto;\">\n    <!-- Clip path image -->\n    <defs>\n      <clipPath id=\"svgf77\">\n        <rect y=\"0\" x=\"5\" width=\"735\" height=\"680\"></rect>\n      </clipPath>\n    </defs>\n    <image clip-path=\"url(#svgf77)\" xlink:href=\"/img/exercise.png\" height=\"85%\" width=\"85%\" style=\"transform: translate(104px, 84px); height: 492px;\"></image>\n    <image xlink:href=\"/img/laptop.png\" height=\"95%\" width=\"95%\" style=\"transform: translate(35px, 0px);\"></image>\n    <image clip-path=\"url(#svgf77)\" xlink:href=\"/img/ide.png\" height=\"85%\" width=\"85%\" style=\"transform: translate(-103px, 320px); height: 381px;\"></image>\n  </svg>\n</figure>\n</a>\n\n</div>\n"
  },
  {
    "path": "docs/content/pubsubs/_index.md",
    "content": "+++\ntitle = \"Supported Pub/Subs\"\nbref = \"Watermill supports these Pub/Sub adapters out of the box:\"\n+++\n"
  },
  {
    "path": "docs/content/pubsubs/amqp.md",
    "content": "+++\ntitle = \"RabbitMQ (AMQP)\"\ndescription = \"The most widely deployed open source message broker\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"The most widely deployed open source message broker\"\nweight = 100\n+++\n\n> RabbitMQ is the most widely deployed open source message broker.\n\nWe are providing Pub/Sub implementation based on [github.com/rabbitmq/amqp091-go](https://github.com/rabbitmq/amqp091-go) official library.\n\n{{% load-snippet-partial file=\"src-link/watermill-amqp/pkg/amqp/doc.go\" first_line_contains=\"// AMQP\" last_line_contains=\"package amqp\" padding_after=\"0\" %}}\n\nYou can find a fully functional example with RabbitMQ in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/amqp).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-amqp/v3\n```\n\n### Characteristics\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | yes* | there are no literal consumer groups in AMQP, but we can achieve similar behaviour with `GenerateQueueNameTopicNameWithSuffix`. For more details please check [AMQP \"Consumer Groups\" section](#amqp-consumer-groups) |\n| ExactlyOnceDelivery | no |  |\n| GuaranteedOrder | yes |  yes, please check https://www.rabbitmq.com/semantics.html#ordering |\n| Persistent | yes* | when using `NewDurablePubSubConfig` or `NewDurableQueueConfig`  |\n\n### Configuration\n\nOur AMQP is shipped with some pre-created configurations:\n\n{{% load-snippet-partial file=\"src-link/watermill-amqp/pkg/amqp/config.go\" first_line_contains=\"// NewDurablePubSubConfig\" last_line_contains=\"type Config struct {\" %}}\n\nFor detailed configuration description, please check [watermill-amqp/pkg/amqp/config.go](https://github.com/ThreeDotsLabs/watermill-amqp/tree/master/pkg/amqp/config.go)\n\n#### TLS Config\n\nTLS config can be passed to `Config.TLSConfig`.\n\n#### Connecting\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/amqp/main.go\" first_line_contains=\"publisher, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/amqp/main.go\" first_line_contains=\"subscriber, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-amqp/pkg/amqp/publisher.go\" first_line_contains=\"// Publish\" last_line_contains=\"func (p *Publisher) Publish\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-amqp/pkg/amqp/subscriber.go\" first_line_contains=\"// Subscribe\" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n### Marshaler\n\nMarshaler is responsible for mapping AMQP's messages to Watermill's messages.\n\nMarshaller can be changed via the Configuration.\nIf you need to customize thing in `amqp.Delivery`, you can do it `PostprocessPublishing` function.\n\n{{% load-snippet-partial file=\"src-link/watermill-amqp/pkg/amqp/marshaler.go\" first_line_contains=\"// Marshaler\" last_line_contains=\"func (d DefaultMarshaler)\" padding_after=\"0\" %}}\n\n### AMQP \"Consumer Groups\"\n\nAMQP doesn't provide mechanism like Kafka's \"consumer groups\". You can still achieve similar behaviour with `GenerateQueueNameTopicNameWithSuffix` and `NewDurablePubSubConfig`.\n\n{{% load-snippet-partial file=\"docs/snippets/amqp-consumer-groups/main.go\" first_line_contains=\"func createSubscriber(\" last_line_contains=\"go process(\\\"subscriber_2\\\", messages2)\" %}}\n\nIn this example both `pubSub1` and `pubSub2` will receive some messages independently.\n\n### AMQP `TopologyBuilder`\n\n{{% load-snippet-partial file=\"src-link/watermill-amqp/pkg/amqp/topology_builder.go\" first_line_contains=\"// TopologyBuilder\" last_line_contains=\"}\" padding_after=\"0\" %}}\n"
  },
  {
    "path": "docs/content/pubsubs/aws.md",
    "content": "+++\ntitle = \"Amazon AWS SNS/SQS\"\ndescription = \"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.\"\ndate = 2024-10-19T15:30:00+02:00\nbref = \"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.\"\nweight = 10\n+++\n\nAWS SQS and SNS are fully-managed message queuing and Pub/Sub-like services that make it easy to decouple\nand scale microservices, distributed systems, and serverless applications.\n\nWatermill provides a simple way to use AWS SQS and SNS with Go.\nIt handles all the AWS SDK internals and provides a simple API to publish and subscribe messages.\n\nOfficial Documentation:\n- [SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html)\n- [SNS](https://docs.aws.amazon.com/sns/latest/dg/welcome.html)\n\nYou can find a fully functional example with AWS SNS in the Watermill examples:\n- [SNS](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/aws-sns)\n- [SQS](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/aws-sqs)\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-aws\n```\n\n## SQS vs SNS\n\nWhile 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.\n\n### How SNS is connected with SQS\n\nTo 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.\nWhen a message is published to the SNS topic, it will be delivered to all subscribed SQS queues.\nWe implemented this logic in the `watermill-aws` package out of the box.\n\nWhen you subscribe to an SNS topic, Watermill AWS creates an SQS queue and subscribes to it.\n\n[![](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)\n\nWe can say, that a single SQS queue acts as a consumer group or subscription in other Pub/Sub implementations.\n\nThe mechanism is detailed in [AWS documentation](https://docs.aws.amazon.com/sns/latest/dg/subscribe-sqs-queue-to-sns-topic.html).\n\n### How to choose between SQS and SNS\n\n#### SQS (Simple Queue Service)\n\n- Use when you need a simple message queue with a single consumer.\n- Great for task queues or background job processing.\n- Supports exactly-once processing (with FIFO queues) and guaranteed order (mostly).\n\nExample use case: Processing user uploads in the background.\n\n[![](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)\n\n#### SNS (Simple Notification Service)\n\n- Use when you need to broadcast messages to multiple subscribers.\n- Perfect for implementing pub/sub patterns.\n- Useful for event-driven architectures.\n- Supports multiple types of subscribers (SQS, Lambda, HTTP/S, email, SMS, etc.).\n\nExample use case: Notifying multiple services about a new user registration.\n\nOur SNS implementation in Watermill automatically creates and manages SQS queues for each subscriber, simplifying the process of using SNS with multiple SQS queues.\n\nRemember, 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.\n\nTo learn how SNS and SQS work together, see the [How SNS is connected with SQS](#how-sns-is-connected-with-sqs) section.\n\n## SQS\n\n### Characteristics\n\n| Feature             | Implements | Note                                                                                                                                                                                                                                                                                                                                                                                      |\n|---------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| ConsumerGroups      | no         | it's a queue, for consumer groups-like functionality use [SNS](#sns)                                                                                                                                                                                                                                                                                                                      |\n| ExactlyOnceDelivery | no         | [yes](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html)                                                                                                                                                                                                                                                                |\n| 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.\"_ |\n| Persistent          | yes        |                                                                                                                                                                                                                                                                                                                                                                                           |\n\n### Required permissions\n\n- `\"sqs:ReceiveMessage\"`\n- `\"sqs:DeleteMessage\"`\n- `\"sqs:GetQueueUrl\"`\n- `\"sqs:CreateQueue\"`\n- `\"sqs:GetQueueAttributes\"`\n- `\"sqs:SendMessage\"`\n- `\"sqs:ChangeMessageVisibility\"`\n\n[todo - verify]\n\n### SQS Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sqs/config.go\" first_line_contains=\"type SubscriberConfig struct \" last_line_contains=\"type GenerateCreateQueueInputFunc\" %}}\n\n### Resolving Queue URL\n\nIn the Watermill model, we are normalizing the AWS queue url to `topic` used in the `Publish` and `Subscribe` methods.\n\nTo give you flexibility of what you want to use as a topic in Watermill, you can customize resolving the queue URL.\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sqs/url_resolver.go\" first_line_contains=\"// QueueUrlResolver\" last_line_contains=\"GenerateQueueUrlResolver\" %}}\n\nYou can implement your own `QueueUrlResolver` or use one of the provided resolvers.\n\nBy default, `GetQueueUrlByNameUrlResolver` resolver is used:\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sqs/url_resolver.go\" first_line_contains=\"// GetQueueUrlByNameUrlResolver \" last_line_contains=\"NewGetQueueUrlByNameUrlResolver\" %}}\n\nThere are two more resolvers available:\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sqs/url_resolver.go\" first_line_contains=\"// GenerateQueueUrlResolver\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sqs/url_resolver.go\" first_line_contains=\"// TransparentUrlResolver\" last_line_contains=\"}\" %}}\n\n### Using with SQS emulator\n\nYou 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.\n\nYou can override the endpoint using the `OptFns` option in the `SubscriberConfig` or `PublisherConfig`.\n\n```go\npackage main\n\nimport (\n    amazonsqs \"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\t\"github.com/ThreeDotsLabs/watermill-amazonsqs/sqs\"\n)\n\nfunc main() {\n\t// ...\n\n    sqsOpts := []func(*amazonsqs.Options){\n        amazonsqs.WithEndpointResolverV2(sqs.OverrideEndpointResolver{\n            Endpoint: transport.Endpoint{\n                URI: *lo.Must(url.Parse(\"http://localstack:4566\")),\n            },\n        }),\n    }\n\n\tsqsConfig := sqs.SubscriberConfig{\n\t\tAWSConfig: cfg,\n\t\tOptFns:    sqsOpts,\n\t}\n\n\tsub, err := sqs.NewSubscriber(sqsConfig, logger)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to create new subscriber: %w\", err))\n\t}\n\n\t// ...\n}\n\n```\n\n## SNS\n\n### Characteristics\n\n| Feature             | Implements | Note                                                                                                                                                                                                                                                                                                                                                                                      |\n|---------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| ConsumerGroups      | yes        | yes                                                                                                                                                                                                                                                                                                                                                                                       |\n| ExactlyOnceDelivery | no         | [yes](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html)                                                                                                                                                                                                                                                                |\n| 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.\"_ |\n| Persistent          | yes        |                                                                                                                                                                                                                                                                                                                                                                                           |\n\n\n### Required permissions\n\n- `sns:Subscribe`\n- `sns:ConfirmSubscription`\n- `sns:Receive`\n- `sns:Unsubscribe`\n\nand all permissions required for SQS:\n\n- `sqs:ReceiveMessage`\n- `sqs:DeleteMessage`\n- `sqs:GetQueueUrl`\n- `sqs:CreateQueue`\n- `sqs:GetQueueAttributes`\n- `sqs:SendMessage`\n- `sqs:ChangeMessageVisibility`\n- `sqs:SetQueueAttributes`\n\nAdditionally, if `sns.SubscriberConfig.DoNotSetQueueAccessPolicy` is not enabled, you should have the following:\n\n- `sqs:SetQueueAttributes`\n\n### SNS Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sns/config.go\" first_line_contains=\"type SubscriberConfig struct \" last_line_contains=\"type GenerateSqsQueueNameFn\" %}}\n\nAdditionally, because SNS Subscriber uses SQS queues as \"subscriptions\", you need to pass [SQS configuration](#sqs-configuration) as well.\n\n### Resolving Queue URL\n\nIn the Watermill model, we normalise AWS Topic ARN to the `topic` used in the `Publish` and `Subscribe` methods.\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sns/topic.go\" first_line_contains=\"// TopicResolver\" last_line_contains=\"}\" %}}\n\nWe are providing two out-of-the-box resolvers:\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sns/topic.go\" first_line_contains=\"// TransparentTopicResolver\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-aws/sns/topic.go\" first_line_contains=\"// GenerateArnTopicResolver\" last_line_contains=\"}\" %}}\n\n### Using with SNS emulator\n\nYou 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.\n\nYou can override the endpoint using the `OptFns` option in the `SubscriberConfig` or `PublisherConfig`.\n\n```go\npackage main\n\nimport (\n\tamazonsns \"github.com/aws/aws-sdk-go-v2/service/sns\"\n\t\"github.com/ThreeDotsLabs/watermill-amazonsns/sns\"\n)\n\nfunc main() {\n\t// ...\n\n    snsOpts := []func(*amazonsns.Options){\n        amazonsns.WithEndpointResolverV2(sns.OverrideEndpointResolver{\n            Endpoint: transport.Endpoint{\n                URI: *lo.Must(url.Parse(\"http://localstack:4566\")),\n            },\n        }),\n    }\n\n\tsnsConfig := sns.SubscriberConfig{\n\t\tAWSConfig: cfg,\n\t\tOptFns:    snsOpts,\n\t}\n\n\tsub, err := sns.NewSubscriber(snsConfig, sqsConfig, logger)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to create new subscriber: %w\", err))\n\t}\n\n\t// ...\n}\n```\n"
  },
  {
    "path": "docs/content/pubsubs/bolt.md",
    "content": "+++\ntitle = \"Bolt Pub/Sub\"\ndescription = \"A pure Go key/value store\"\ndate = 2021-11-19T00:00:00+02:00\nbref = \"A pure Go key/value store\"\nweight = 20\n+++\n\nBolt is a pure Go key/value store which provides a simple, fast, and reliable\ndatabase for projects that don't require a full database server such as\nPostgres or MySQL.\n\nBolt backed Pub/Sub is good for simple applications which don't need a more\nadvanced Pub/Sub system with external dependencies or already use Bolt and\nwant to publish messages in transaction when saving other data.\n\nBolt documentation: https://github.com/etcd-io/bbolt\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-bolt\n```\n\n### Characteristics\n\n| Feature             | Implements | Note |\n| ------------------- | ---------- | ---- |\n| ConsumerGroups      | no         |      |\n| ExactlyOnceDelivery | no         |      |\n| GuaranteedOrder     | no         |      |\n| Persistent          | yes        |      |\n\n### Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-bolt/pkg/bolt/bolt.go\" first_line_contains=\"type CommonConfig struct \" last_line_equals=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-bolt/pkg/bolt/bolt.go\" first_line_contains=\"type PublisherConfig struct \" last_line_equals=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-bolt/pkg/bolt/bolt.go\" first_line_contains=\"type SubscriberConfig struct \" last_line_equals=\"}\" %}}\n\n#### Subscription name\n\nTo receive messages published to a topic, you must create a subscription to\nthat topic. Only messages published to the topic after the subscription is\ncreated will be received by the subscriber.\n\nA topic can have multiple subscriptions, but a given subscription belongs to\na single topic.\n\nIn Watermill, the subscription is created automatically during calling\n`Subscribe()`.  Subscription name is generated by calling the function set as\n`SubscriberConfig.GenerateSubscriptionName`.  By default, it is the topic name\nwith the string `_sub` appended to it.\n\n#### Marshaler\n\nWatermill's messages cannot be directly saved in Bolt which operates on byte\nslices. Marshaller converts the messages to and from byte slices. The default\nimplementation marshals messages as JSON, a format which is human-readable\nfor easier debugging. The performance should be enough for most applications\nunless a very large messages are used within your system. If that is the case\nyou may want to consider implementing a more efficient marshaler.\n\n{{% load-snippet-partial file=\"src-link/watermill-bolt/pkg/bolt/marshaler.go\" first_line_contains=\"// Marshaler\" last_line_equals=\"}\" %}}\n"
  },
  {
    "path": "docs/content/pubsubs/firestore.md",
    "content": "+++\ntitle = \"Firestore Pub/Sub\"\ndescription = \"A scalable document database from Google\"\ndate = 2021-07-29T15:30:00+02:00\nbref = \"A scalable document database from Google\"\nweight = 30\n+++\n\nCloud Firestore is a cloud-hosted, NoSQL database from Google.\n\nThis Pub/Sub comes with two publishers. To publish messages in a transaction\nuse the `TransactionalPublisher`. If you do not want to publish messages in\ntransaction use the normal `Publisher`.\n\nUsing Firestore as a Pub/Sub instead of using a dedicated Pub/Sub system can be\nuseful to publish messages in transaction while at the same time saving other\ndata in Firestore. Thanks to that the data and the messages can be consistently\npersisted. If the messages and the data weren't being published transactionally\nyou could end up in situations where messages were emitted even though the data\nwasn't saved or messages weren't emitted even though the data was saved. After\ntransactionally publishing messages in Firestore you can then subscribe to them\nand relay them to a different Pub/Sub system.\n\nGodoc: <https://pkg.go.dev/github.com/ThreeDotsLabs/watermill-firestore>\n\nFirestore documentation: <https://firebase.google.com/docs/firestore/>\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-firestore\n```\n\n### Characteristics\n\n| Feature             | Implements | Note |\n| -------             | ---------- | ---- |\n| ConsumerGroups      | yes        |      |\n| ExactlyOnceDelivery | no         |      |\n| GuaranteedOrder     | no         |      |\n| Persistent          | yes        |      |\n\n### Configuration\n\n#### Publisher configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-firestore/pkg/firestore/publisher.go\" first_line_contains=\"type PublisherConfig struct {\" last_line_equals=\"}\" %}}\n\n#### Subscriber configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-firestore/pkg/firestore/subscriber.go\" first_line_contains=\"type SubscriberConfig struct {\" last_line_equals=\"}\" %}}\n\n#### Subscription name\n\nTo receive messages published to a topic, you must create a subscription to\nthat topic. Only messages published to the topic after the subscription is\ncreated will be received by the subscribers.\n\nA topic can have multiple subscriptions, but a given subscription belongs to a\nsingle topic.\n\nIn Watermill, the subscription is created automatically during calling\n`Subscribe()`. Subscription name is generated by function passed to\n`SubscriberConfig.GenerateSubscriptionName`. By default, it is just the topic\nname with a suffix `_sub` appended to it.\n\nIf you want to consume messages from a topic with multiple subscribers\nprocessing the incoming messages in a different way, you should use a custom\nfunction to generate unique subscription names for each subscriber.\n\n### Marshaler\n\nWatermill's messages cannot be stored directly in Firestore. The marshaler is\nresponsible for converting them to a type which can be stored by Firestore.\nThe default implementation should be enough for most applications so it is\nunlikely that you need to implement your own marshaler.\n\n{{% load-snippet-partial file=\"src-link/watermill-firestore/pkg/firestore/marshaler.go\" first_line_contains=\"// Marshaler\" last_line_equals=\"}\" padding_after=\"0\" %}}\n"
  },
  {
    "path": "docs/content/pubsubs/gochannel.md",
    "content": "+++\ntitle = \"Go Channel\"\ndescription = \"A Pub/Sub implemented on Golang goroutines and channels\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"A Pub/Sub implemented on Golang goroutines and channels\"\nweight = 40\n+++\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/pubsub.go\" first_line_contains=\"// GoChannel\" last_line_contains=\"type GoChannel struct {\" %}}\n\nYou can find a fully functional example with Go Channels in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/go-channel).\n\n### Characteristics\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | no | |\n| ExactlyOnceDelivery | yes |  |\n| GuaranteedOrder | yes |  |\n| Persistent | no| |\n\n### Configuration\n\nYou can inject configuration via the constructor.\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/pubsub.go\" first_line_contains=\"func NewGoChannel\" last_line_contains=\"logger:\" %}}\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/pubsub.go\" first_line_contains=\"// Publish\" last_line_contains=\"func (g *GoChannel) Publish\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/pubsub/gochannel/pubsub.go\" first_line_contains=\"// Subscribe\" last_line_contains=\"func (g *GoChannel) Subscribe\" %}}\n\n### Marshaler\n\nNo marshaling is needed when sending messages within the process.\n"
  },
  {
    "path": "docs/content/pubsubs/googlecloud.md",
    "content": "+++\ntitle = \"Google Cloud Pub/Sub\"\ndescription = \"The fully-managed real-time messaging service from Google\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"The fully-managed real-time messaging service from Google\"\nweight = 50\n+++\n\nCloud Pub/Sub brings the flexibility and reliability of enterprise message-oriented middleware to\nthe cloud.\n\nAt the same time, Cloud Pub/Sub is a scalable, durable event ingestion and delivery\nsystem that serves as a foundation for modern stream analytics pipelines.\nBy providing many-to-many, asynchronous messaging that decouples senders and receivers,\nit allows for secure and highly available communication among independently written applications.\n\nCloud Pub/Sub delivers low-latency, durable messaging that helps developers quickly integrate\nsystems hosted on the Google Cloud Platform and externally.\n\nOfficial Documentation: [https://cloud.google.com/pubsub/docs/](https://cloud.google.com/pubsub/docs/overview)\n\nYou can find a fully functional example with Google Cloud Pub/Sub in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/googlecloud).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-googlecloud/v2\n```\n\n### Characteristics\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | yes | multiple subscribers within the same Subscription name  |\n| ExactlyOnceDelivery | no |  |\n| GuaranteedOrder | no | |\n| Persistent | yes* | maximum retention time is 7 days |\n\n### Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-googlecloud/pkg/googlecloud/publisher.go\" first_line_contains=\"type PublisherConfig struct \" last_line_contains=\"func NewPublisher\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-googlecloud/pkg/googlecloud/subscriber.go\" first_line_contains=\"type SubscriberConfig struct {\" last_line_contains=\"func NewSubscriber(\" %}}\n\n#### Subscription name\n\nTo receive messages published to a topic, you must create a subscription to that topic.\nOnly messages published to the topic after the subscription is created are available to subscriber\napplications.\n\nThe subscription connects the topic to a subscriber application that receives and processes\nmessages published to the topic.\n\nA topic can have multiple subscriptions, but a given subscription belongs to a single topic.\n\nIn Watermill, the subscription is created automatically during calling `Subscribe()`.\nSubscription name is generated by function passed to `SubscriberConfig.GenerateSubscriptionName`.\nBy default, it is just the topic name (`TopicSubscriptionName`).\n\nWhen you want to consume messages from a topic with multiple subscribers, you should use\n`TopicSubscriptionNameWithSuffix` or your custom function to generate the subscription name.\n\n### Connecting\n\nWatermill will connect to the instance of Google Cloud Pub/Sub indicated by the environment variables. For production setup, set the `GOOGLE_APPLICATION_CREDENTIALS` env, as described in [the official Google Cloud Pub/Sub docs](https://cloud.google.com/pubsub/docs/quickstart-client-libraries#pubsub-client-libraries-go). Note that you won't need to install the Cloud SDK, as Watermill will take care of the administrative tasks (creating topics/subscriptions) with the default settings and proper permissions.\n\nFor development, you can use a Docker image with the emulator and the `PUBSUB_EMULATOR_HOST` env ([check out the Getting Started guide]({{< ref \"getting-started#subscribing_gcloud\" >}})).\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/googlecloud/main.go\" first_line_contains=\"publisher, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/googlecloud/main.go\" first_line_contains=\"subscriber, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-googlecloud/pkg/googlecloud/publisher.go\" first_line_contains=\"// Publish\" last_line_contains=\"func (p *Publisher) Publish\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-googlecloud/pkg/googlecloud/subscriber.go\" first_line_contains=\"// Subscribe \" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n### Marshaler\n\nWatermill's messages cannot be directly sent to Google Cloud Pub/Sub - they need to be marshaled. You can implement your marshaler or use the default implementation.\n\n{{% load-snippet-partial file=\"src-link/watermill-googlecloud/pkg/googlecloud/marshaler.go\" first_line_contains=\"// Marshaler\" last_line_contains=\"type DefaultMarshalerUnmarshaler \" padding_after=\"0\" %}}\n"
  },
  {
    "path": "docs/content/pubsubs/http.md",
    "content": "+++\ntitle = \"HTTP\"\ndescription = \"Call and listen to webhooks asynchronously\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"Call and listen to webhooks asynchronously\"\nweight = 60\n+++\n\nThe HTTP subscriber listens to HTTP requests (for example - webhooks) and outputs them as messages.\nYou can then post them to any Publisher. Here is an example with [sending HTTP messages to Kafka](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/real-world-examples/receiving-webhooks/main.go).\n\nThe HTTP publisher sends HTTP requests as specified in its configuration. Here is an example with [transforming Kafka messages into HTTP webhook requests](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/real-world-examples/sending-webhooks).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-http/v2\n```\n\n### Characteristics\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | no | |\n| ExactlyOnceDelivery | yes |  |\n| GuaranteedOrder | yes |  |\n| Persistent | no| |\n\n### Subscriber configuration\n\nSubscriber configuration is done via the config struct passed to the constructor:\n\n{{% load-snippet-partial file=\"src-link/watermill-http/pkg/http/subscriber.go\" first_line_contains=\"type SubscriberConfig struct\" last_line_contains=\"}\" %}}\n\nYou can use the `Router` config option to `SubscriberConfig` to pass your own `chi.Router` (see [chi](https://github.com/go-chi/chi)).\nThis may be helpful if you'd like to add your own HTTP handlers (e.g. a health check endpoint).\n\n### Publisher configuration\n\nPublisher configuration is done via the config struct passed to the constructor:\n\n{{% load-snippet-partial file=\"src-link/watermill-http/pkg/http/publisher.go\" first_line_contains=\"type PublisherConfig struct\" last_line_contains=\"}\" %}}\n\nHow the message topic and body translate into the URL, method, headers, and payload of the HTTP request is highly configurable through the use of `MarshalMessageFunc`.\nUse the provided `DefaultMarshalMessageFunc` to send POST requests to a specific url:\n\n{{% load-snippet-partial file=\"src-link/watermill-http/pkg/http/publisher.go\" first_line_contains=\"// MarshalMessageFunc\" last_line_contains=\"return req, nil\" padding_after=\"2\" %}}\n\nYou can pass your own `http.Client` to execute the requests or use Golang's default client.\n\n### Running\n\nTo run HTTP subscriber you need to run `StartHTTPServer()`. It needs to be run after `Subscribe()`.\n\nWhen using with the router, you should wait for the router to start.\n\n```go\n<-r.Running()\nhttpSubscriber.StartHTTPServer()\n```\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-http/pkg/http/subscriber.go\" first_line_contains=\"// Subscribe adds\" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n#### Custom HTTP status codes\n\nTo specify a custom HTTP status code, which will returned as response, you can use following call during message handling:\n\n```go\n// msg is a *message.Message\nhttp.SetResponseStatusCode(msg, http.StatusForbidden)\nmsg.Nack()\n```\n"
  },
  {
    "path": "docs/content/pubsubs/io.md",
    "content": "+++\ntitle = \"io.Writer/io.Reader\"\ndescription = \"Pub/Sub implemented as Go stdlib's most loved interfaces\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"Pub/Sub implemented as Go stdlib's most loved interfaces\"\nweight = 70\n+++\n\nThis is an experimental Pub/Sub implementation that leverages the [standard library's](https://golang.org/pkg/io/) `io.Writer` and `io.Reader` interfaces as sources of Publisher and Subscriber, respectively.\n\nNote that these aren't full-fledged Pub/Subs like Kafka, RabbitMQ, or the likes, but given the ubiquity of implementations of `Writer` and `Reader` they may come in handy, for uses like:\n\n* Writing messages to file or stdout\n* Subscribing for data on a file or stdin and packaging it as messages\n* Interfacing with third-party libraries that implement `io.Writer` or `io.Reader`, like [github.com/colinmarc/hdfs](https://github.com/colinmarc/hdfs) or [github.com/mholt/archiver](https://github.com/mholt/archiver).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-io\n```\n\n### Characteristics\n\nThis is a very bare-bones implementation for now, so no extra features are supported. However, it is still sufficient for applications like a [CLI producer/consumer](https://github.com/ThreeDotsLabs/watermill/tree/master/tools/mill).\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | no |       |\n| ExactlyOnceDelivery | no |  |\n| GuaranteedOrder | no |  |\n| Persistent | no |   |\n\n### Configuration\n\nThe publisher configuration is relatively simple.\n\n{{% load-snippet-partial file=\"src-link/watermill-io/pkg/io/publisher.go\" first_line_contains=\"type PublisherConfig struct\" last_line_contains=\"// Publisher\" %}}\n\nThe subscriber may work in two modes – either perform buffered reads of constant size from the io.Reader, or split the byte stream into messages using a delimiter byte.\n\nThe reading will continue even if the reads come up empty, but they will not be sent out as messages. The time to wait after an empty read is configured through the `PollInterval` parameter. As soon as a non-empty input is read, it will be packaged as a message and sent out.\n\n{{% load-snippet-partial file=\"src-link/watermill-io/pkg/io/subscriber.go\" first_line_contains=\"type SubscriberConfig struct\" last_line_contains=\"// Subscriber\" %}}\n\nThe continuous reading may be used, for example, to emulate the behaviour of a `tail -f` command, like in this snippet:\n\n{{% load-snippet-partial file=\"docs/snippets/tail-log-file/main.go\" first_line_contains=\"// this will\" last_line_contains=\"return false\" padding_after=\"1\" %}}\n\n### Marshaling/Unmarshaling\n\nThe MarshalFunc is an important part of `io.Publisher`, because it fully controls the format in the underlying `io.Writer` will obtain the messages.\n\nCorrespondingly, the UnmarshalFunc regulates how the bytes read by the `io.Reader` will be interpreted as Watermill messages.\n\n{{% load-snippet-partial file=\"src-link/watermill-io/pkg/io/marshal.go\" first_line_contains=\"// MarshalMessageFunc\" last_line_contains=\"// PayloadMarshalFunc\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-io/pkg/io/marshal.go\" first_line_contains=\"// UnmarshalMessageFunc\" last_line_contains=\"// PayloadUnmarshalFunc\" %}}\n\nThe package comes with some predefined marshal and unmarshal functions, but you might want to write your own marshaler/unmarshaler to work with the specific implementation of `io.Writer/io.Reader` that you are working with.\n\n### Topic\n\nFor the Publisher/Subscriber implementation itself, the topic has no meaning. It is difficult to interpret the meaning of topic in the general context of `io.Writer` and `io.Reader` interfaces.\n\nHowever, the topic is passed as a parameter to the marshal/unmarshal functions, so the adaptations to particular `Writer/Reader` implementation may take it into account.\n"
  },
  {
    "path": "docs/content/pubsubs/kafka.md",
    "content": "+++\ntitle = \"Kafka\"\ndescription = \"A distributed streaming platform from Apache\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"A distributed streaming platform from Apache\"\nweight = 80\n+++\n\nApache Kafka is one of the most popular Pub/Subs. We are providing Pub/Sub implementation based on [IBM Sarama](https://github.com/IBM/sarama).\n\nYou can find a fully functional example with Kafka in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/kafka).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-kafka/v3\n```\n\n### Characteristics\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | yes | |\n| ExactlyOnceDelivery | no | in theory can be achieved with [Transactions](https://www.confluent.io/blog/transactions-apache-kafka/), currently no support for any Golang client  |\n| GuaranteedOrder | yes | require [partition key usage](#partitioning)  |\n| Persistent | yes| |\n\n### Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/subscriber.go\" first_line_contains=\"type SubscriberConfig struct\" last_line_contains=\"// Subscribe\" %}}\n\n#### Passing custom `Sarama` config\n\nYou can pass [custom config](https://github.com/Shopify/sarama/blob/master/config.go#L20) parameters via `overwriteSaramaConfig *sarama.Config` in `NewSubscriber` and `NewPublisher`.\nWhen `nil` is passed, default config is used (`DefaultSaramaSubscriberConfig`).\n\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/subscriber.go\" first_line_contains=\"// DefaultSaramaSubscriberConfig\" last_line_contains=\"return config\" padding_after=\"1\" %}}\n\n### Connecting\n\n#### Publisher\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/publisher.go\" first_line_contains=\"// NewPublisher\" last_line_contains=\"(*Publisher, error)\" padding_after=\"0\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/kafka/main.go\" first_line_contains=\"publisher, err := kafka.NewPublisher\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n\n#### Subscriber\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/subscriber.go\" first_line_contains=\"// NewSubscriber\" last_line_contains=\"(*Subscriber, error)\" padding_after=\"0\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/kafka/main.go\" first_line_contains=\"saramaSubscriberConfig :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/publisher.go\" first_line_contains=\"// Publish\" last_line_contains=\"func (p *Publisher) Publish\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/subscriber.go\" first_line_contains=\"// Subscribe\" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n### Marshaler\n\nWatermill's messages cannot be directly sent to Kafka - they need to be marshaled. You can implement your marshaler or use default implementation.\n\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/marshaler.go\" first_line_contains=\"// Marshaler\" last_line_contains=\"func (DefaultMarshaler)\" padding_after=\"0\" %}}\n\n### Partitioning\n\nOur Publisher has support for the partitioning mechanism.\n\nIt can be done with special Marshaler implementation:\n\n{{% load-snippet-partial file=\"src-link/watermill-kafka/pkg/kafka/marshaler.go\" first_line_contains=\"type kafkaJsonWithPartitioning\" last_line_contains=\"func (j kafkaJsonWithPartitioning) Marshal\" padding_after=\"0\" %}}\n\nWhen using, you need to pass your function to generate partition key.\nIt's a good idea to pass this partition key with metadata to not unmarshal entire message.\n\n```go\nmarshaler := kafka.NewWithPartitioningMarshaler(func(topic string, msg *message.Message) (string, error) {\n    return msg.Metadata.Get(\"partition\"), nil\n})\n```\n\nPlease note that in the example above, if the `partition` key is missing from the message metadata, an empty string `\"\"` will be used as the partitioning key. This will cause all such messages to be routed to the same partition, which may not be the desired behavior and could lead to uneven load distribution."
  },
  {
    "path": "docs/content/pubsubs/nats.md",
    "content": "+++\ntitle = \"NATS Jetstream\"\ndescription = \"A simple, secure and high performance open source messaging system\"\ndate = 2022-02-03T10:30:00+05:00\nbref = \"A simple, secure and high performance open source messaging system\"\nweight = 90\n+++\n\nNATS Jetstream is a data streaming system powered by NATS, and written in the Go programming language.\n\nAs of v2.0.2 this middleware will contain a beta implementation in `pkg/jetstream` based on the\n[nats.go Jetstream package](https://github.com/nats-io/nats.go/tree/main/jetstream). This implementation is\nconsidered experimental tracking with the upstream client though we target a stable watermill API by v2.1.\nFor production use it is recommended to use the pubsub implementations in `pkg/nats` with Jetstream enabled.\n\nYou can find a fully functional example with NATS JetStream in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/nats-jetstream).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-nats/v2\n```\n\n### Characteristics\n\n| Feature             | Implements | Note                                                                                                                  |\n|---------------------|------------|-----------------------------------------------------------------------------------------------------------------------|\n| ConsumerGroups      | yes        | you need to set `QueueGroupPrefix` name or provide an optional calculator                                             |\n| ExactlyOnceDelivery | yes        | you need to ensure 'AckAsync' has default false value and set 'TrackMsgId' to true on the Jetstream configuration     |\n| GuaranteedOrder     | no         | [with the redelivery feature, order can't be guaranteed](https://github.com/nats-io/nats-streaming-server/issues/187) |\n| Persistent          | yes        |                                                                                                                       |\n\n### Configuration\n\nConfiguration is done through PublisherConfig and SubscriberConfig types.  These share a common JetStreamConfig.  To use the experimental nats-core support, set Disabled=true.\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/jetstream.go\" first_line_contains=\"// JetStreamConfig contains\" last_line_contains=\"type DurableCalculator =\" %}}\n\nPublisherConfig:\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/publisher.go\" first_line_contains=\"type PublisherConfig struct\" last_line_contains=\"type Publisher struct {\" %}}\n\nSubscriber Config:\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/subscriber.go\" first_line_contains=\"type SubscriberConfig struct\" last_line_contains=\"type Subscriber struct\" %}}\n\n### Connecting\n\nBy default NATS client will try to connect to `localhost:4222`. If you are using different hostname or port you should specify using the URL property of `SubscriberConfig` and `PublisherConfig`.\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/publisher.go\" first_line_contains=\"// NewPublisher\" last_line_contains=\"func NewPublisher\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/nats-jetstream/main.go\" first_line_contains=\"publisher, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/subscriber.go\" first_line_contains=\"// NewSubscriber\" last_line_contains=\"func NewSubscriber\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/nats-jetstream/main.go\" first_line_contains=\"subscriber, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\nYou can also use `NewSubscriberWithNatsConn` and `NewPublisherWithNatsConn` to use a custom `*nats.Conn`.\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/publisher.go\" first_line_contains=\"// Publish publishes\" last_line_contains=\"func (p *Publisher) Publish\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/subscriber.go\" first_line_contains=\"// Subscribe \" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n### Marshaler\n\nNATS provides a header passing mechanism that allows conveying the watermill message details as metadata. This is done by default with only the binary payload sent in the message body.  The header `_watermill_message_uuid` is reserved.\n\nOther builtin marshalers are based on Golang's [`gob`](https://golang.org/pkg/encoding/gob/) and [`json`](https://golang.org/packages/encoding/json) packages.\n\n{{% load-snippet-partial file=\"src-link/watermill-nats/pkg/nats/marshaler.go\" first_line_contains=\"type Marshaler \" last_line_contains=\"func defaultNatsMsg\" padding_after=\"0\" %}}\n\nWhen you have your own format of the messages, you can implement your own Marshaler, which will serialize messages in your format.  An example protobuf implementation with tests and benchmarks can be found [here](https://github.com/ThreeDotsLabs/watermill-nats/tree/master/_examples/marshalers/protobuf/)\n\nWhen needed, you can bypass both [UUID]({{< ref \"message#message\" >}}) and [Metadata]({{< ref \"message#message\" >}}) and send just a `message.Payload`,\nbut some standard [middlewares]({{< ref \"messages-router#middleware\" >}}) may be not working.\n\n## Core-Nats\n\nThis package also includes limited support for connecting to [core-nats](https://docs.nats.io/nats-concepts/core-nats).  While core-nats does not support many of the streaming features needed for a perfect fit with watermill and most acks end up implemented as no-ops, in environments with a mix of jetstream and core-nats messaging in play it can be nice to use watermill consistently on the application side.\n"
  },
  {
    "path": "docs/content/pubsubs/redisstream.md",
    "content": "+++\ntitle = \"Redis Stream\"\ndescription = \"A fast, open source, in-memory, key-value data store\"\ndate = 2023-02-01T22:30:00+08:00\nbref = \"A fast, open source, in-memory, key-value data store\"\nweight = 110\n+++\n\nRedis is the open source, in-memory data store used by millions of developers. Redis stream is a data structure that acts like an append-only log in Redis. We are providing Pub/Sub implementation based on [redis/go-redis](https://github.com/redis/go-redis).\n\nYou can find a fully functional example with Redis Stream in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/redisstream).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-redisstream\n```\n\n### Characteristics\n\n| Feature | Implements | Note |\n| ------- | ---------- | ---- |\n| ConsumerGroups | yes | |\n| ExactlyOnceDelivery | no | |\n| GuaranteedOrder | no | |\n| Persistent | yes | |\n| FanOut | yes | use XREAD to fan out messages when there is no consumer group |\n\n### Configuration\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/publisher.go\" first_line_contains=\"type PublisherConfig struct\" last_line_contains=\"// Publish publishes message to redis stream\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/subscriber.go\" first_line_contains=\"type SubscriberConfig struct\" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n#### Passing `redis.UniversalClient`\n\nYou need to configure and pass your own go-redis client via `Client redis.UniversalClient` in `NewSubscriber` and `NewPublisher`. The client can be either `redis.Client` or `redis.ClusterClient`.\n\n#### Publisher\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/publisher.go\" first_line_contains=\"// NewPublisher\" last_line_contains=\"(*Publisher, error)\" padding_after=\"0\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/redisstream/main.go\" first_line_contains=\"pubClient := redis.NewClient\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n\n#### Subscriber\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/subscriber.go\" first_line_contains=\"// NewSubscriber\" last_line_contains=\"(*Subscriber, error)\" padding_after=\"0\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/redisstream/main.go\" first_line_contains=\"subClient := redis.NewClient\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/publisher.go\" first_line_contains=\"// Publish\" last_line_contains=\"func (p *Publisher) Publish\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/subscriber.go\" first_line_contains=\"func (s *Subscriber) Subscribe\" last_line_contains=\"func (s *Subscriber) Subscribe\" %}}\n\n### Marshaler\n\nWatermill's messages cannot be directly sent to Redis - they need to be marshaled. You can implement your marshaler or use default implementation. The default implementation uses [MessagePack](https://msgpack.org/index.html) for efficient serialization.\n\n{{% load-snippet-partial file=\"src-link/watermill-redisstream/pkg/redisstream/marshaller.go\" first_line_contains=\"const UUIDHeaderKey\" last_line_contains=\"type DefaultMarshallerUnmarshaller\" padding_after=\"0\" %}}\n"
  },
  {
    "path": "docs/content/pubsubs/sql.md",
    "content": "+++\ntitle = \"SQL (PostgreSQL, MySQL)\"\ndescription = \"Pub/Sub based on MySQL or PostgreSQL.\"\ndate = 2019-07-06T22:30:00+02:00\nbref = \"Pub/Sub based on MySQL or PostgreSQL.\"\nweight = 120\n+++\n\nSQL Pub/Sub executes queries on any SQL database, using it like a messaging system. At the moment, **MySQL** and **PostgreSQL** are supported.\nIt be useful for projects that are not using any specialized message queue at the moment, but have access to a SQL database.\n\nIf you are looking for SQLite Pub/Sub, check out the [SQLite Pub/Sub](/pubsubs/sqlite/) documentation.\n\nThe SQL subscriber runs a `SELECT` query within short periods, remembering the position of the last record. If it finds\nany new records, they are returned. One handy use case is consuming events from a database table, that can be later published\non some kind of message queue.\n\nThe SQL publisher simply inserts consumed messages into the chosen table. A common approach would be to use it as a persistent\nlog of events that were published on a queue with short message expiration time.\n\nSQL Pub/Sub is also a good choice for implementing Outbox pattern with [Forwarder](/docs/forwarder/) component.\n\nSee also the [SQL example](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/sql).\n\n## Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-sql/v4\n```\n\n### Characteristics\n\n| Feature             | Implements | Note                                                                          |\n|---------------------|------------|-------------------------------------------------------------------------------|\n| ConsumerGroups      | yes        | See `ConsumerGroup` in `SubscriberConfig` (not supported by the queue schema) |\n| ExactlyOnceDelivery | yes        |\n| GuaranteedOrder     | yes        |                                                                               |\n| Persistent          | yes        |                                                                               |\n\n### Schema\n\nSQL Pub/Sub uses user-defined schema to handle select and insert queries. You need to implement `SchemaAdapter` and pass\nit to `SubscriberConfig` or `PublisherConfig`.\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/schema_adapter_mysql.go\" first_line_contains=\"// DefaultMySQLSchema\" last_line_contains=\"type DefaultMySQLSchema\" %}}\n\nThere is a default schema provided for each supported engine (`DefaultMySQLSchema` and `DefaultPostgreSQLSchema`).\nIt supports the most common use case (storing events in a table). You can base your schema on one of these, extending only chosen methods.\n\n#### Extending schema\n\nConsider an example project, where you're fine with using the default schema, but would like to use `BINARY(16)` for storing\nthe `uuid` column, instead of `VARCHAR(36)`. In that case, you have to define two methods:\n\n* `SchemaInitializingQueries` that creates the table.\n* `UnmarshalMessage` method that produces a `Message` from the database record.\n\nNote that you don't have to use the initialization queries provided by Watermill. They will be run only if you set the\n`InitializeSchema` field to `true` in the config. Otherwise, you can use your own solution for database migrations.\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/schema_adapter_mysql.go\" first_line_contains=\"// DefaultMySQLSchema\" last_line_contains=\"type DefaultMySQLSchema\" %}}\n\n### Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/publisher.go\" first_line_contains=\"type PublisherConfig struct\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/subscriber.go\" first_line_contains=\"type SubscriberConfig struct\" last_line_contains=\"}\" %}}\n\n## Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/publisher.go\" first_line_contains=\"func NewPublisher\" last_line_contains=\"func NewPublisher\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sql/main.go\" first_line_contains=\"publisher, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/publisher.go\" first_line_contains=\"// Publish \" last_line_contains=\"func (p *Publisher) Publish\" %}}\n\n### Transactions\n\nIf you need to publish messages within a database transaction, you have to pass a `*sql.Tx` in the `NewPublisher`\nconstructor. You have to create one publisher for each transaction.\n\nExample:\n{{% 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\" %}}\n\n## Subscribing\n\nTo create a subscriber, you need to pass not only proper schema adapter, but also an offsets adapter.\n\n* For MySQL schema use `DefaultMySQLOffsetsAdapter`\n* For PostgreSQL schema use `DefaultPostgreSQLOffsetsAdapter`\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/subscriber.go\" first_line_contains=\"func NewSubscriber\" last_line_contains=\"func NewSubscriber\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sql/main.go\" first_line_contains=\"subscriber, err :=\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% 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\" %}}\n\n## Offsets Adapter\n\nThe logic for storing offsets of messages is provided by the `OffsetsAdapter`. If your schema uses auto-incremented integer as the row ID,\nit should work out of the box with default offset adapters.\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/offsets_adapter.go\" first_line_contains=\"type OffsetsAdapter\" %}}\n\n## Queue\n\nInstead of the default Pub/Sub schema, you can use the *queue* schema and offsets adapters.\n\nIt's a simpler schema that doesn't support consumer groups.\nHowever, it has other advantages.\n\nIt lets you specify a custom `WHERE` clause for getting the messages.\nYou can use it to filter messages by some condition in the payload or in the metadata.\n\nAdditionally, you can choose to delete messages from the table after they are acknowledged.\nThanks to this, the table doesn't grow in size with time.\n\nThis schema is supported by both PostgreSQL and MySQL.\nThe example below is based on PostgreSQL, but the same approach can be used with MySQL. \n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/queue_schema_adapter_postgresql.go\" first_line_contains=\"// PostgreSQLQueueSchema\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sql/pkg/sql/queue_offsets_adapter_postgresql.go\" first_line_contains=\"// PostgreSQLQueueOffsetsAdapter\" last_line_contains=\"}\" %}}\n\n## Caveats\n\n### Using last processed transaction ID in PostgreSQL to ensure no messages are lost\n\nIn some cases, PostgreSQL `SERIAL` is not incremental.\nThe `SERIAL` value is generated while the transaction is in progress, not when it is committed.\nIf transactions are committed in a different order than they were started, message offsets based on `SERIAL` values will not be incremental.\n\nTo keep storing acknowledgment information efficient, Watermill keeps only the last message's acknowledgment information.\nTo ensure no messages are missed when a message order is not kept, Watermill also uses the transaction ID to ensure no message is lost.\nFor more details, see [Watermill#311](https://github.com/ThreeDotsLabs/watermill/issues/311).\n\nIt is important to note that very long-running transactions may result in delayed message delivery.\nFor instance, if a transaction is running for an hour, no messages will be delivered until the transaction is committed.\nWhile we do not recommend the use of such long transactions, **if they are necessary, we advise the use of the [Queue schema adapter](#queue), which does not depend on the transaction ID.**\nYou have nothing to worry about if you don't have such long transactions.\n\nIf you are migrating your data to a new database, you may need to set `last_processed_transaction_id` in your offsets table.\n"
  },
  {
    "path": "docs/content/pubsubs/sqlite.md",
    "content": "+++\ntitle = \"SQLite\"\ndescription = \"A lightweight, file-based SQL database engine\"\ndate = 2025-05-08T11:30:00+02:00\nbref = \"A lightweight, file-based SQL database engine\"\nweight = 121\n+++\n\n**Beta Version Warning: this Pub/Sub is stable, but it has not been widely tested in production environments. It may be sensitive to certain edge cases and combinations of configuration parameters.**\n\nSQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured SQL database engine. Our SQLite Pub/Sub implementation provides two **CGO-free** driver variants optimized for different use cases. Both drivers use pure Go implementations of SQLite, enabling cross-compilation and avoiding CGO dependencies while maintaining full SQLite functionality.\n\nSQLite Pub/Subs provide the easiest way to publish and process events durably, since you do not have to set up or manage a separate database. The database is just a file on disk. Some cloud compute providers offer distributed SQLite clusters, which can provide both durability and unmatched read performance. Tuned SQLite is [~35% faster](https://sqlite.org/fasterthanfs.html) than the Linux file system.\n\nYou can find a fully functional example with SQLite in the [Watermill examples](https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/pubsubs/sqlite).\n\n## ModernC vs ZombieZen Driver\n\nThe vanilla **ModernC driver** is compatible with the Golang standard library SQL package and works without CGO. It has fewer dependencies than the ZombieZen variant and uses the `modernc.org/sqlite` pure Go SQLite implementation. Most users should pick this driver for full compatibility with the standard library.\n\nThe advanced **ZombieZen driver** abandons the standard Golang library SQL conventions in favor of [the more orthogonal API and higher performance potential](https://crawshaw.io/blog/go-and-sqlite). Under the hood, it also uses the ModernC SQLite3 implementation and does not need CGO. Advanced SQLite users might prefer this driver for its performance benefits. It is about **6 times faster** than the ModernC variant. It is currently more stable due to lower level control. It is faster than even the CGO SQLite variants on standard library interfaces, and with some tuning should become the absolute speed champion of persistent message brokers over time.\n\n### Characteristics\n\n| Feature             | Implements | Note                                              |\n|---------------------|------------|---------------------------------------------------|\n| ConsumerGroups      | yes        | See `ConsumerGroupMatcher` in `SubscriberOptions` |\n| ExactlyOnceDelivery | no         |                                                   |\n| GuaranteedOrder     | yes        |                                                   |\n| Persistent          | yes        |                                                   |\n\nLike the [BoltDB](../bolt/) implementation, the SQLite drivers are imbeddable into your application. They do not require any additional infrastructure other than a mounted persistent disk. Their advantage over the BoltDB Pub/Sub is supporting consumer groups and guaranteed order.\n\n## Vanilla ModernC Driver\n\n### Installation\n\n```bash\ngo get github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc@latest\n```\n\n### Usage\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite/main.go\" first_line_contains=\"import (\" last_line_contains=\"}\" padding_after=\"1\" %}}\n\n### Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitemodernc/publisher.go\" first_line_contains=\"type PublisherOptions struct\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitemodernc/subscriber.go\" first_line_contains=\"type SubscriberOptions struct\" last_line_contains=\"}\" %}}\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitemodernc/publisher.go\" first_line_contains=\"// NewPublisher\" last_line_contains=\"func NewPublisher\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitemodernc/publisher.go\" first_line_contains=\"// Publish \" last_line_contains=\"func (p *publisher) Publish\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite/main.go\" first_line_contains=\"publisher, err := wmsqlitemodernc.NewPublisher(\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite/main.go\" first_line_contains=\"func publishMessages(\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n#### Publishing in transaction\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite/transaction.go\" first_line_contains=\"import (\" padding_after=\"1\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitemodernc/subscriber.go\" first_line_contains=\"// NewSubscriber\" last_line_contains=\"func NewSubscriber\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite/main.go\" first_line_contains=\"subscriber, err := wmsqlitemodernc.NewSubscriber(\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitemodernc/subscriber.go\" first_line_contains=\"// Subscribe \" last_line_contains=\"func (s *subscriber) Subscribe\" %}}\n\n## Advanced ZombieZen Driver\n\n### Installation\n\n```bash\ngo get -u github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitezombiezen@latest\n```\n\n### Usage\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite-zombiezen/main.go\" first_line_contains=\"import (\" last_line_contains=\"}\" padding_after=\"1\" %}}\n\n### Configuration\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitezombiezen/publisher.go\" first_line_contains=\"type PublisherOptions struct\" last_line_contains=\"}\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitezombiezen/subscriber.go\" first_line_contains=\"type SubscriberOptions struct\" last_line_contains=\"}\" %}}\n\n### Publishing\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitezombiezen/publisher.go\" first_line_contains=\"// NewPublisher\" last_line_contains=\"func NewPublisher\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitezombiezen/publisher.go\" first_line_contains=\"// Publish \" last_line_contains=\"func (p *publisher) Publish\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite-zombiezen/main.go\" first_line_contains=\"publisher, err := wmsqlitezombiezen.NewPublisher(\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite-zombiezen/main.go\" first_line_contains=\"func publishMessages(\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n#### Publishing in transaction\n\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite-zombiezen/transaction.go\" first_line_contains=\"import (\" padding_after=\"1\" %}}\n\n### Subscribing\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitezombiezen/subscriber.go\" first_line_contains=\"// NewSubscriber\" last_line_contains=\"func NewSubscriber\" %}}\n\nExample:\n{{% load-snippet-partial file=\"src-link/_examples/pubsubs/sqlite-zombiezen/main.go\" first_line_contains=\"subscriber, err := wmsqlitezombiezen.NewSubscriber(\" last_line_contains=\"panic(err)\" padding_after=\"1\" %}}\n\n{{% load-snippet-partial file=\"src-link/watermill-sqlite/wmsqlitezombiezen/subscriber.go\" first_line_contains=\"// Subscribe \" last_line_contains=\"func (s *subscriber) Subscribe\" %}}\n\n## Marshaler\n\nWatermill's messages are stored in SQLite using JSON serialization. Both drivers use the same marshaling approach - messages are automatically marshaled to and from JSON format when publishing and subscribing.\n\nThe default marshaler handles:\n\n- Message payload (stored as JSON blob)\n- Message metadata (stored as JSON object)\n- Message UUID (stored as TEXT)\n- Timestamps for ordering and consumer group management\n\nBoth drivers automatically handle message marshaling and unmarshaling, so no custom marshaler configuration is typically required.\n\n## Caveats\n\nSQLite3 does not support querying `FOR UPDATE`, which is used for row locking when subscribers in the same consumer group read an event batch in official Watermill SQL PubSub implementations. Current architectural decision is to lock a consumer group offset using `unixepoch()+lockTimeout` time stamp. While one consumed message is processing per group, the offset lock time is extended by `lockTimeout` periodically by `time.Ticker`. If the subscriber is unable to finish the consumer group batch, other subscribers will take over the lock as soon as the grace period runs out. A time lock fulfills the role of a traditional database network timeout that terminates transactions when its client disconnects.\n\nAll the normal SQLite limitations apply to Watermill. The connections are file handles. Create new connections for concurrent processing. If you must share a connection, protect it with a mutual exclusion lock. If you are writing within a transaction, create a connection for that transaction only.\n"
  },
  {
    "path": "docs/content/support.md",
    "content": "+++\ntitle = \"Support\"\ndescription = \"\"\n+++\n\n### Community Support\n\nJoin us on the `#watermill` channel on the [Three Dots Labs discord](https://discord.gg/QV6VFg4YQE).\n\n### Professional Support\n\nFor enterprise support, please contact us by e-mail: contact@threedotslabs.com\n\nYou can also use the [contact form on our website](https://threedots.tech/contact/?utm_source=watermill-docs).\n"
  },
  {
    "path": "docs/extract_middleware_godocs.py",
    "content": "#!/usr/bin/python3\nimport os\nimport re\n\n\nclass MiddlewareSourceFile:\n    def __init__(self, filepath: str):\n        self._definitions = []\n        self._filepath = filepath\n        with open(filepath, 'r') as f:\n            self._src = f.readlines()\n            self.name = os.path.basename(self._filepath)\n\n        for (line_no, line) in enumerate(self._src):\n            # function or struct definitions\n            if line.startswith('func') or re.match(r'^type \\S+? struct', line):\n                # with godocs\n                if line_no > 0 and (self._src[line_no - 1]).startswith('//'):\n                    self.add_func_with_godoc(line_no - 1)\n\n    def add_func_with_godoc(self, line_no: int):\n        func_def = ''\n\n        # find the first line of godoc\n        while True:\n            if line_no - 1 > 0 and self._src[line_no - 1].startswith('//'):\n                line_no -= 1\n            else:\n                break\n\n        while True:\n            line_content = self._src[line_no]\n            func_def += line_content\n            line_no += 1\n\n            # go fmt, I believe in you\n            if line_content == '}\\n':\n                break\n\n        self._definitions.append(func_def)\n\n    def format(self) -> str:\n        if not self._definitions:\n            return None\n\n        middleware_name = self.name.strip('.go').replace('_', ' ')\n        middleware_name = capitalize(middleware_name)\n        s = '### {}\\n\\n'.format(middleware_name)\n\n        for func_def in self._definitions:\n            s += '```go\\n{}```\\n'.format(func_def)\n\n        return s\n\n\ndef capitalize(s: str) -> str:\n    words = s.split()\n    for i, word in enumerate(words):\n        words[i] = word[0].upper() + word[1:]\n\n    return ' '.join(words)\n\n\nif __name__ == '__main__':\n    go_sources = []\n    for root, dirs, files in os.walk('../message/router/middleware'):\n        go_sources = [MiddlewareSourceFile(os.path.join(root, f)) for f in files if f.endswith('.go') and not f.endswith('_test.go')]\n\n    for src_file in go_sources:\n        formatted = src_file.format()\n        if formatted:\n            print(formatted)\n            print('\\n')\n"
  },
  {
    "path": "docs/layouts/_default/_markup/render-link.html",
    "content": "<a href=\"{{ .Destination | safeURL }}\"{{ with .Title}} title=\"{{ . }}\"{{ end }}{{ if or (strings.HasPrefix .Destination \"http\") (strings.HasPrefix .Destination \"https\") }} target=\"_blank\"{{ end }} >{{ .Text | safeHTML }}</a>\n"
  },
  {
    "path": "docs/layouts/_default/learn.html",
    "content": "{{ define \"main\" }}\r\n<div class=\"learn-container\">\r\n  <div class=\"learn-header\">\r\n    <h1 class=\"learn-main-title\">Learn Watermill</h1>\r\n  </div>\r\n\r\n  <div class=\"learn-section\">\r\n    <h2 class=\"learn-section-title\">How do you like to learn?</h2>\r\n    <div class=\"learn-row\">\r\n      {{ range $index, $option := .Params.learning_options }}\r\n      <a href=\"{{ .link }}\" class=\"learn-card\">\r\n        <div class=\"learn-card-icon\">\r\n          {{ .icon | safeHTML }}\r\n        </div>\r\n        <h3>{{ .title }}</h3>\r\n        <span class=\"learn-card-secondary\">{{ .subtitle }}</span>\r\n        <p>{{ .description }}</p>\r\n      </a>\r\n      {{ end }}\r\n    </div>\r\n  </div>\r\n\r\n  <div class=\"learn-section\">\r\n    <h2 class=\"learn-section-title\">Dive Deeper</h2>\r\n    <div class=\"learn-row\">\r\n      {{ range .Params.deeper_options }}\r\n      <a href=\"{{ .link }}\" class=\"learn-card\" {{ if .external }}target=\"_blank\" rel=\"noopener\"{{ end }}>\r\n        <div class=\"learn-card-icon\">\r\n          {{ .icon | safeHTML }}\r\n        </div>\r\n        <h3>{{ .title }}</h3>\r\n        <p>{{ .description }}</p>\r\n      </a>\r\n      {{ end }}\r\n    </div>\r\n  </div>\r\n</div>\r\n{{ end }}\r\n"
  },
  {
    "path": "docs/layouts/_default/quickstart.html",
    "content": "{{ define \"main\" }}\r\n<div class=\"quickstart-container\">\r\n  <div class=\"quickstart-header\">\r\n    <h1 class=\"quickstart-main-title\">{{ .Title }}</h1>\r\n    <p class=\"quickstart-subtitle\">{{ .Params.description }}</p>\r\n  </div>\r\n\r\n  <div class=\"quickstart-content\">\r\n    {{ .Content }}\r\n  </div>\r\n</div>\r\n{{ end }}"
  },
  {
    "path": "docs/layouts/index.html",
    "content": "{{ define \"main\" }}\r\n<section class=\"section container-fluid mt-n3 pb-3\">\r\n  <div class=\"row justify-content-center\">\r\n    <div class=\"text-center mb-4\">\r\n      <img src=\"/img/gopher.svg\" alt=\"Logo\" style=\"width: 220px\" />\r\n    </div>\r\n    <div class=\"col-lg-12 text-center\">\r\n      <h1>{{ .Title }}</h1>\r\n    </div>\r\n    <div class=\"col-lg-9 col-xl-8 text-center\">\r\n      <p class=\"lead\">{{ .Params.lead | safeHTML }}</p>\r\n      <a class=\"btn btn-primary btn-cta rounded-pill btn-lg m-3\" href=\"/learn/\" role=\"button\">\r\n        Get Started\r\n      </a>\r\n      <a class=\"btn btn-outline-primary btn-cta rounded-pill btn-lg my-3\" href=\"https://github.com/ThreeDotsLabs/watermill\" role=\"button\">\r\n        See on GitHub\r\n      </a>\r\n      {{ .Content }}\r\n    </div>\r\n  </div>\r\n</section>\r\n\r\n<section class=\"section py-5 homepage-features\">\r\n  <div class=\"container\">\r\n    <!-- EventBus Section -->\r\n    <div class=\"row justify-content-center mb-5 pb-5 homepage-section-gradient\">\r\n      <div class=\"col-lg-11 col-xl-10 text-center\">\r\n        <h2 class=\"display-4 fw-bold mb-4 gradient-text-1\">Publish Events</h2>\r\n        <p class=\"lead text-muted mb-5\">Work with Go structs.<br/>\r\n            Decouple your services with asynchronous processing.\r\n        </p>\r\n\r\n          <div class=\"expressive-code homepage-code-block\">\r\n              <figure class=\"frame not-content\">\r\n{{ highlight `event := UserRegistered{\r\n    UserID:   id,\r\n    Email:    email,\r\n    JoinedAt: time.Now(),\r\n}\r\n\r\nerr := eventBus.Publish(ctx, event)` \"go\" \"\" }}\r\n              </figure>\r\n          </div>\r\n      </div>\r\n    </div>\r\n\r\n    <!-- EventProcessor Section -->\r\n    <div class=\"row justify-content-center mb-5 pb-5 homepage-section-gradient\">\r\n      <div class=\"col-lg-11 col-xl-10 text-center\">\r\n        <h2 class=\"display-4 fw-bold mb-4 gradient-text-1\">Handle Events</h2>\r\n        <p class=\"lead text-muted mb-5\">Focus on the business logic.<br/>\r\n            Watermill handles routing, serialization and low-level details.\r\n        </p>\r\n\r\n          <div class=\"expressive-code homepage-code-block\">\r\n              <figure class=\"frame not-content\">\r\n{{ highlight `eventProcessor.AddHandlers(\r\n    cqrs.NewEventHandler(\"SendWelcomeEmail\", sendWelcomeEmail),\r\n)\r\n\r\nfunc sendWelcomeEmail(ctx context.Context, event *UserRegistered) error {\r\n    return emailService.Send(event.Email, \"Welcome!\")\r\n}` \"go\" \"\" }}\r\n              </figure>\r\n          </div>\r\n      </div>\r\n    </div>\r\n\r\n    <!-- Router Middleware Section -->\r\n    <div class=\"row justify-content-center mb-5 pb-5 homepage-section-gradient\">\r\n      <div class=\"col-lg-11 col-xl-10 text-center\">\r\n        <h2 class=\"display-4 fw-bold mb-4 gradient-text-1\">Simple API you already know</h2>\r\n        <p class=\"lead text-muted mb-5\">Use the familiar concepts, similar to an HTTP Router with support for middleware.</p>\r\n          <div class=\"expressive-code homepage-code-block\">\r\n              <figure class=\"frame not-content\">\r\n{{ highlight `router.AddMiddleware(\r\n    middleware.Recoverer,\r\n    middleware.CorrelationID,\r\n    middleware.Timeout(30 * time.Second),\r\n)` \"go\" \"\" }}\r\n              </figure>\r\n          </div>\r\n      </div>\r\n    </div>\r\n\r\n    <!-- Pub/Sub Support Section -->\r\n    <div class=\"row justify-content-center pb-5 homepage-section-gradient\">\r\n      <div class=\"col-lg-11 col-xl-10 text-center\">\r\n        <h2 class=\"display-4 fw-bold mb-4 gradient-text-1\">Use your favorite Pub/Sub</h2>\r\n        <p class=\"lead text-muted mb-5\">Work with Kafka, RabbitMQ, PostgreSQL, Redis, and more Pub/Subs with the same API. Switch providers without changing your application code.</p>\r\n\r\n        <!-- Pub/Sub Logos Collage -->\r\n        <div class=\"pubsub-logos-container mb-5\">\r\n          <div class=\"pubsub-logos-grid\">\r\n              <a href=\"/pubsubs/aws/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/aws.png\" alt=\"AWS SQS/SNS\" />\r\n                  <span>AWS SNS/SQS</span>\r\n              </a>\r\n              <a href=\"/pubsubs/bolt/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/bolt.png\" alt=\"BoltDB\" />\r\n                  <span>BoltDB</span>\r\n              </a>\r\n              <a href=\"/pubsubs/firestore/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/firestore.png\" alt=\"Google Firestore\" />\r\n                  <span>Firestore</span>\r\n              </a>\r\n              <a href=\"/pubsubs/gochannel/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/gochannel.png\" alt=\"Go Channel\" />\r\n                  <span>Go Channel</span>\r\n              </a>\r\n              <a href=\"/pubsubs/googlecloud/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/gcp.png\" alt=\"Google Cloud Pub/Sub\" />\r\n                  <span>Google Cloud Pub/Sub</span>\r\n              </a>\r\n              <a href=\"/pubsubs/http/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/http.png\" alt=\"HTTP\" />\r\n                  <span>HTTP</span>\r\n              </a>\r\n              <a href=\"/pubsubs/io/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/io.png\" alt=\"I/O\" />\r\n                  <span>I/O</span>\r\n              </a>\r\n              <a href=\"/pubsubs/kafka/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/kafka.png\" alt=\"Apache Kafka\" />\r\n                  <span>Kafka</span>\r\n              </a>\r\n              <a href=\"/pubsubs/sql/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/mysql.png\" alt=\"MySQL\" />\r\n                  <span>MySQL</span>\r\n              </a>\r\n              <a href=\"/pubsubs/nats/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/nats.png\" alt=\"NATS\" />\r\n                  <span>NATS</span>\r\n              </a>\r\n              <a href=\"/pubsubs/sql/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/postgresql.png\" alt=\"PostgreSQL\" />\r\n                  <span>PostgreSQL</span>\r\n              </a>\r\n              <a href=\"/pubsubs/amqp/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/rabbitmq.png\" alt=\"RabbitMQ\" />\r\n                  <span>RabbitMQ</span>\r\n              </a>\r\n              <a href=\"/pubsubs/redisstream/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/redis.png\" alt=\"Redis\" />\r\n                  <span>Redis</span>\r\n              </a>\r\n              <a href=\"/pubsubs/sqlite/\" class=\"pubsub-logo-item\">\r\n                  <img src=\"/img/pubsub-logos/sqlite.png\" alt=\"SQLite\" />\r\n                  <span>SQLite</span>\r\n              </a>\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n\r\n      <!-- Library vs Framework Section -->\r\n      <div class=\"row justify-content-center mb-5 pb-5 homepage-section-gradient\">\r\n          <div class=\"col-md-12 col-lg-11 col-xl-10 text-center\">\r\n              <h2 class=\"display-4 fw-bold mb-4 gradient-text-1\">Library ≠ Framework</h2>\r\n              <p class=\"lead text-muted mb-5\">Use any architecture you want. No vendor lock-in.</p>\r\n              <div class=\"expressive-code homepage-code-block\">\r\n                  <figure class=\"frame not-content\">\r\n{{ highlight `func (h *Handler) RegisterUser(w http.ResponseWriter, r *http.Request) {\r\n    user := createUser(r)\r\n\r\n    // Your existing business logic\r\n    err := h.userRepo.Save(user)\r\n    if err != nil {\r\n        // ...\r\n    }\r\n\r\n    // Publish an event using Watermill\r\n    err := h.eventBus.Publish(r.Context(), &UserRegistered{...})\r\n    // ...\r\n}` \"go\" \"\" }}\r\n                  </figure>\r\n              </div>\r\n          </div>\r\n      </div>\r\n\r\n      <!-- Get Started Section -->\r\n      <div class=\"row justify-content-center text-center pt-5 mt-5 homepage-section-gradient\">\r\n          <div class=\"col-lg-11 col-xl-10\">\r\n              <img src=\"/img/gopher.svg\" alt=\"Watermill Logo\" style=\"width: 160px;\" class=\"mb-2\" />\r\n              <h2 class=\"display-5 fw-bold mb-4\">Start building</h2>\r\n              <div class=\"mt-3\">\r\n                  <a class=\"btn btn-primary btn-cta rounded-pill btn-lg\" href=\"/learn/\" role=\"button\">\r\n                      Get Started\r\n                  </a>\r\n              </div>\r\n          </div>\r\n      </div>\r\n  </div>\r\n</section>\r\n{{ end }}\r\n\r\n{{ define \"sidebar-prefooter\" }}\r\n  {{ if site.Params.doks.backgroundDots -}}\r\n  <div class=\"d-flex justify-content-start\">\r\n    <div class=\"bg-dots\"></div>\r\n  </div>\r\n  {{ end -}}\r\n{{ end }}\r\n\r\n{{ define \"sidebar-footer\" }}\r\n{{ if site.Params.doks.sectionFooter -}}\r\n<section class=\"section section-md container-fluid bg-light\">\r\n  <div class=\"row justify-content-center text-center\">\r\n    <div class=\"col-lg-7\">\r\n      <h2 class=\"mt-2\">Start building with Doks today</h2>\r\n      <a class=\"btn btn-primary rounded-pill px-4 my-2\" href=\"/docs/{{ if site.Params.doks.docsVersioning }}{{ site.Params.doks.docsVersion }}/{{ end }}prologue/introduction/\" role=\"button\">{{ i18n \"get-started\" }}</a>\r\n    </div>\r\n  </div>\r\n</section>\r\n{{ end -}}\r\n{{ end }}\r\n"
  },
  {
    "path": "docs/layouts/partials/footer/footer.html",
    "content": "{{- if not (or .IsHome .Params.HideBanner) -}}\r\n<div class=\"text-center mb-5 event-driven-banner\">\r\n  <a href=\"https://threedots.tech/event-driven/?utm_source=watermill-docs\">\r\n    <img width=\"400\" src=\"https://threedots.tech/event-driven-banner.png\">\r\n    <br>\r\n    Check our online hands-on training\r\n  </a>\r\n</div>\r\n{{- end -}}\r\n\r\n<footer class=\"footer text-muted\">\r\n  <div class=\"container-{{ site.Params.doks.containerBreakpoint | default \"lg\" }}\">\r\n    <div class=\"row\">\r\n      <div class=\"col-lg-8 text-center text-lg-start\">\r\n        <a href=\"https://threedots.tech\">\r\n          <img class=\"only-dark\" fetchpriority=\"high\" decoding=\"async\"  width=\"200\" height=\"31\" src=\"https://threedots.tech/images/logo-darkmode.svg\" alt=\"Three Dots Labs\">\r\n          <img class=\"only-light\" fetchpriority=\"high\" decoding=\"async\"  width=\"200\" height=\"31\" src=\"https://threedots.tech/images/logo.svg\" alt=\"Three Dots Labs\">\r\n        </a>\r\n      </div>\r\n      <div class=\"col-lg-8 text-center text-lg-end\">\r\n        <p>© Three Dots Labs 2014 — {{ dateFormat \"2006\" now }}</p>\r\n      </div>\r\n    </div>\r\n  </div>\r\n\r\n  <div class=\"text-center mt-5\">\r\n    <p>\r\n    </p>\r\n\r\n    <p><small>\r\n      Watermill is open-source software and is not backed by venture capital.\r\n      <br>\r\n      We are an independent, bootstrapped company.\r\n    </small>\r\n    </p>\r\n  </div>\r\n</footer>\r\n"
  },
  {
    "path": "docs/layouts/partials/footer/script-footer-custom.html",
    "content": "{{/* Put your custom <script></script> tags here */}}\r\n\r\n{{/* EXAMPLE - only load script for production\r\n{{ if eq (hugo.Environment) \"production\" -}}\r\n  {{ partial \"footer/esbuild\" (dict \"src\" \"js/instantpage.js\" \"load\" \"async\" \"transpile\" false) -}}\r\n{{ end -}}\r\n*/}}\r\n\r\n{{/* EXAMPLE - only load script for a page type e.g. contact or gallery\r\n{{ if eq .Type \"gallery\" -}}\r\n  {{ partial \"footer/esbuild\" (dict \"src\" \"js/gallery.js\" \"load\" \"async\" \"transpile\" false) -}}\r\n{{ end -}}\r\n*/}}\r\n\r\n<script defer data-domain=\"watermill.io\" src=\"https://threedots.tech/js/po.js\"></script>\r\n<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>\r\n"
  },
  {
    "path": "docs/layouts/partials/head/custom-head.html",
    "content": "<!-- Custom head -->\r\n\r\n<!-- google fonts -->\r\n{{ $pf:= \"Heebo:wght@400;600\" }}\r\n{{ $sf:= \"Quicksand:wght@700\" }}\r\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\r\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\r\n<script>\r\n  (function () {\r\n    const googleFont = document.createElement(\"link\");\r\n    googleFont.href = \"https://fonts.googleapis.com/css2?family={{$pf | safeURL}}{{with $sf}}&family={{. | safeURL}}{{end}}&display=swap\";\r\n    googleFont.type = \"text/css\";\r\n    googleFont.rel = \"stylesheet\";\r\n    document.head.appendChild(googleFont);\r\n  })();\r\n</script>\r\n"
  },
  {
    "path": "docs/layouts/partials/head/resource-hints.html",
    "content": "<link rel=\"preload\" href=\"{{ \"fonts/quicksand/quicksand-v31-latin-regular.woff2\" | absURL }}\" as=\"font\" type=\"font/woff2\" crossorigin>\r\n<link rel=\"preload\" href=\"{{ \"fonts/quicksand/quicksand-v31-latin-500.woff2\" | absURL }}\" as=\"font\" type=\"font/woff2\" crossorigin>\r\n<link rel=\"preload\" href=\"{{ \"fonts/quicksand/quicksand-v31-latin-700.woff2\" | absURL }}\" as=\"font\" type=\"font/woff2\" crossorigin>\r\n"
  },
  {
    "path": "docs/layouts/partials/head/script-header.html",
    "content": "<!-- Insert scripts NOT needed by stylesheets here -->\r\n"
  },
  {
    "path": "docs/layouts/partials/header/header.html",
    "content": "{{ if site.Params.doks.alert -}}\r\n  {{ partial \"header/alert.html\" . }}\r\n{{ end -}}\r\n\r\n{{ if site.Params.doks.navbarSticky -}}\r\n<div class=\"sticky-top\">\r\n{{ end -}}\r\n\r\n{{ if site.Params.doks.headerBar -}}\r\n<div class=\"header-bar\"></div>\r\n{{ end -}}\r\n\r\n<header class=\"navbar navbar-expand-lg\">\r\n  {{ with site.Params.doks.containerBreakpoint -}}\r\n    <div class=\"container-{{ . }}\">\r\n  {{ else -}}\r\n    <div class=\"container\">\r\n  {{ end -}}\r\n\r\n    <!-- Site title -->\r\n    <a class=\"navbar-brand me-auto me-lg-3\" href=\"{{ relLangURL \"\" }}\">{{ .Site.Title }}</a>\r\n\r\n    <!-- FlexSearch mobile -->\r\n    {{ partial \"main/showFlexSearch\" . }}\r\n    {{ $showFlexSearch := .Scratch.Get \"showFlexSearch\" -}}\r\n    {{ if $showFlexSearch -}}\r\n    <button type=\"button\" id=\"searchToggleMobile\" class=\"btn btn-link nav-link mx-2 d-lg-none\" aria-label=\"Search website\">\r\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-search\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n        <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n        <circle cx=\"10\" cy=\"10\" r=\"7\"></circle>\r\n        <line x1=\"21\" y1=\"21\" x2=\"15\" y2=\"15\"></line>\r\n      </svg>\r\n    </button>\r\n    {{ end -}}\r\n\r\n    <!-- Section navigation -->\r\n    {{ if (in site.Params.doks.sectionNav .Section) -}}\r\n    <button class=\"btn btn-link d-lg-none\" type=\"button\" data-bs-toggle=\"offcanvas\" data-bs-target=\"#offcanvasNavSection\" aria-controls=\"offcanvasNavSection\" aria-label=\"Open section navigation menu\">\r\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-dots-vertical\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n        <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n        <path d=\"M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0\"></path>\r\n        <path d=\"M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0\"></path>\r\n        <path d=\"M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0\"></path>\r\n      </svg>\r\n    </button>\r\n    <div class=\"offcanvas offcanvas-start d-lg-none\" tabindex=\"-1\" id=\"offcanvasNavSection\" aria-labelledby=\"offcanvasNavSectionLabel\">\r\n      {{ if site.Params.doks.headerBar -}}\r\n        <div class=\"header-bar\"></div>\r\n      {{ end -}}\r\n      <div class=\"offcanvas-header\">\r\n        <h5 class=\"offcanvas-title\" id=\"offcanvasNavSectionLabel\">{{ .Section | humanize }}</h5>\r\n        <button type=\"button\" class=\"btn btn-link nav-link p-0 ms-auto\" data-bs-dismiss=\"offcanvas\" aria-label=\"Close\">\r\n          <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-x\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n            <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n            <path d=\"M18 6l-12 12\"></path>\r\n            <path d=\"M6 6l12 12\"></path>\r\n          </svg>\r\n        </button>\r\n      </div>\r\n      <div class=\"offcanvas-body\">\r\n        <aside class=\"doks-sidebar mt-n3\">\r\n          <nav id=\"doks-docs-nav\" aria-label=\"Tertiary navigation\">\r\n            {{ partial \"sidebar/section-menu.html\" . }}\r\n          </nav>\r\n        </aside>\r\n      </div>\r\n    </div>\r\n    {{ end -}}\r\n\r\n    <!-- Main navigation button -->\r\n    <button class=\"btn btn-link nav-link mx-2 order-3 d-lg-none\" type=\"button\" data-bs-toggle=\"offcanvas\" data-bs-target=\"#offcanvasNavMain\" aria-controls=\"offcanvasNavMain\" aria-label=\"Open main navigation menu\">\r\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-menu\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n        <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n        <line x1=\"4\" y1=\"8\" x2=\"20\" y2=\"8\"></line>\r\n        <line x1=\"4\" y1=\"16\" x2=\"20\" y2=\"16\"></line>\r\n      </svg>\r\n    </button>\r\n\r\n    <!-- Main navigation -->\r\n    <div class=\"offcanvas offcanvas-end h-auto\" tabindex=\"-1\" id=\"offcanvasNavMain\" aria-labelledby=\"offcanvasNavMainLabel\">\r\n      {{ if site.Params.doks.headerBar -}}\r\n        <div class=\"header-bar d-lg-none\"></div>\r\n      {{ end -}}\r\n      <div class=\"offcanvas-header\">\r\n        <h5 class=\"offcanvas-title\" id=\"offcanvasNavMainLabel\">{{ site.Title }}</h5>\r\n        <button type=\"button\" class=\"btn btn-link nav-link p-0 ms-auto\" data-bs-dismiss=\"offcanvas\" aria-label=\"Close\">\r\n          <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-x\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n            <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n            <path d=\"M18 6l-12 12\"></path>\r\n            <path d=\"M6 6l12 12\"></path>\r\n         </svg>\r\n        </button>\r\n      </div>\r\n      <!--\r\n      <div class=\"offcanvas-header\">\r\n        <h5 class=\"offcanvas-title fw-bold\" id=\"offcanvasNavMainLabel\">{{ .Site.Params.Title }}</h5>\r\n        <button class=\"btn btn-link nav-link ms-auto\" type=\"button\" data-bs-dismiss=\"offcanvas\" aria-label=\"Close menu\">\r\n          <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-x\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n            <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n            <path d=\"M18 6l-12 12\"></path>\r\n            <path d=\"M6 6l12 12\"></path>\r\n          </svg>\r\n        </button>\r\n      </div>\r\n      -->\r\n      <div class=\"offcanvas-body d-flex flex-column flex-lg-row justify-content-between\">\r\n        <ul class=\"navbar-nav flex-grow-1\">\r\n          {{- $current := . -}}\r\n          {{- $section := $current.Section -}}\r\n          {{ range .Site.Menus.main -}}\r\n            {{- $active := or ($current.IsMenuCurrent \"main\" .) ($current.HasMenuCurrent \"main\" .) -}}\r\n            {{- $active = or $active (eq .Name $current.Title) -}}\r\n            {{- $active = or $active (and (eq .Name ($section | humanize)) (eq $current.Section $section)) -}}\r\n            {{- $active = or $active (and (eq .Name \"Blog\") (eq $current.Section \"blog\" \"authors\")) -}}\r\n            {{ if .HasChildren -}}\r\n              <li class=\"nav-item dropdown\">\r\n                <a class=\"nav-link dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\r\n                  {{ .Name -}}\r\n                  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-chevron-down\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n                    <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n                    <path d=\"M6 9l6 6l6 -6\"></path>\r\n                  </svg>\r\n                </a>\r\n                <ul class=\"dropdown-menu shadow rounded border-0\">\r\n                  {{ range .Children -}}\r\n                  {{- $active = eq .Name $current.Title -}}\r\n                    <li><a class=\"dropdown-item{{ if $active }} active{{ end }}\" href=\"{{ .URL | absURL }}\"{{ if $active }} aria-current=\"true\"{{ end }}>{{ .Name }}</a></li>\r\n                  {{ end -}}\r\n                </ul>\r\n              </li>\r\n            {{ else -}}\r\n              <li class=\"nav-item\">\r\n                <a class=\"nav-link{{ if $active }} active{{ end }}\" href=\"{{ .URL | absURL }}\"{{ if $active }} aria-current=\"true\"{{ end }}>{{ .Name }}{{ .Post | safeHTML }}</a>\r\n              </li>\r\n            {{ end -}}\r\n          {{ end -}}\r\n        </ul>\r\n\r\n        <!-- FlexSearch desktop -->\r\n        {{ partial \"main/showFlexSearch\" . }}\r\n        {{ $showFlexSearch := .Scratch.Get \"showFlexSearch\" -}}\r\n        {{ if $showFlexSearch -}}\r\n        <button type=\"button\" id=\"searchToggleDesktop\" class=\"btn btn-link nav-link p-2 d-none d-lg-block\" aria-label=\"Search website\">\r\n          <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-search\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n            <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n            <circle cx=\"10\" cy=\"10\" r=\"7\"></circle>\r\n            <line x1=\"21\" y1=\"21\" x2=\"15\" y2=\"15\"></line>\r\n          </svg>\r\n        </button>\r\n        {{ end -}}\r\n\r\n        <!-- Language dropdown -->\r\n        {{ if eq site.Params.doks.multilingualMode true -}}\r\n\r\n        <div class=\"dropdown mt-1 order-lg-2\">\r\n          <button class=\"btn btn-dropdown dropdown-toggle\" id=\"doks-languages\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\" data-bs-display=\"static\">\r\n            <span class=\"dropdown-caret\">\r\n              <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-language\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n                <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n                <path d=\"M4 5h7\"></path>\r\n                <path d=\"M9 3v2c0 4.418 -2.239 8 -5 8\"></path>\r\n                <path d=\"M5 9c0 2.144 2.952 3.908 6.7 4\"></path>\r\n                <path d=\"M12 20l4 -9l4 9\"></path>\r\n                <path d=\"M19.1 18h-6.2\"></path>\r\n              </svg>\r\n              <span id=\"doks-language-current\">{{ .Site.Language.LanguageName }}</span>\r\n              <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-chevron-down\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n                <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n                <path d=\"M6 9l6 6l6 -6\"></path>\r\n              </svg>\r\n            </span>\r\n          </button>\r\n          <ul class=\"dropdown-menu dropdown-menu-lg-end me-lg-2 shadow rounded border-0\" aria-labelledby=\"doks-languages\">\r\n\r\n            <li><span class=\"dropdown-item current\" aria-current=\"true\">{{ .Site.Language.LanguageName }}</span></li>\r\n\r\n            <li><hr class=\"dropdown-divider\"></li>\r\n\r\n            {{ if site.Params.doks.showMissingLanguages -}}\r\n              {{ $translatedLangs := slice -}}\r\n              {{ range .Translations -}}\r\n                {{ $translatedLangs = $translatedLangs | append .Lang }}\r\n              {{- end }}\r\n              {{ range site.Languages -}}\r\n                {{ if and (ne $.Lang .Lang) (not (in $.Params.skipTranslations .Lang)) -}}\r\n                  {{ $isTranslated := in $translatedLangs .Lang -}}\r\n                  <li><a class=\"dropdown-item {{ if not $isTranslated }}untranslated{{ end }}\" rel=\"alternate\" href=\"{{ if $isTranslated }}{{ (index (where $.Translations \"Lang\" .Lang) 0).RelPermalink }}{{ else }}{{ .Lang | relURL }}{{ end }}\" hreflang=\"{{ .Lang }}\" lang=\"{{ .Lang }}\">{{ .LanguageName }}</a></li>\r\n                {{- end }}\r\n              {{- end }}\r\n            {{ else -}}\r\n              {{ range .Translations -}}\r\n                <li><a class=\"dropdown-item\" rel=\"alternate\" href=\"{{ .RelPermalink }}\" hreflang=\"{{ .Lang }}\" lang=\"{{ .Lang }}\">{{ .Language.LanguageName }}</a></li>\r\n              {{- end }}\r\n            {{- end }}\r\n              <!--\r\n              <li><hr class=\"dropdown-divider\"></li>\r\n              <li><a class=\"dropdown-item\" href=\"/docs/contributing/how-to-contribute/\">Help Translate</a></li>\r\n              -->\r\n          </ul>\r\n        </div>\r\n        {{ end -}}\r\n\r\n        <!-- Versioning dropdown  -->\r\n        {{ if eq site.Params.doks.docsVersioning true -}}\r\n\r\n        <div class=\"dropdown mt-1 order-lg-3\">\r\n          <button class=\"btn btn-dropdown dropdown-toggle\" id=\"doks-versions\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\" data-bs-display=\"static\" aria-label=\"Toggle version menu\">\r\n            <span class=\"d-none\">Doks</span> v{{ site.Params.doks.docsVersion }}\r\n            <span class=\"dropdown-caret\">\r\n              <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-chevron-down\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n                <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n                <path d=\"M6 9l6 6l6 -6\"></path>\r\n              </svg>\r\n            </span>\r\n          </button>\r\n          <ul class=\"dropdown-menu dropdown-menu-lg-end me-lg-2 shadow rounded border-0\" aria-labelledby=\"doks-versions\">\r\n            <li><a class=\"dropdown-item current\" aria-current=\"true\" href=\"/docs/{{ site.Params.doks.docsVersion }}/prologue/introduction/\">Latest ({{ site.Params.doks.docsVersion }}.x)</a></li>\r\n            <li><hr class=\"dropdown-divider\"></li>\r\n            <li><a class=\"dropdown-item\" href=\"/docs/0.2/prologue/introduction/\">v0.2.x</a></li>\r\n            <li><a class=\"dropdown-item\" href=\"/docs/0.1/prologue/introduction/\">v0.1.x</a></li>\r\n            <li><hr class=\"dropdown-divider\"></li>\r\n            <li><a class=\"dropdown-item\" href=\"/docs/versions/\">All versions</a></li>\r\n          </ul>\r\n        </div>\r\n        {{ end -}}\r\n\r\n        <!-- Color mode toggler -->\r\n        {{ if and (eq site.Params.doks.colorMode \"auto\") site.Params.doks.colorModeToggler -}}\r\n        <button id=\"buttonColorMode\" class=\"btn btn-link mx-auto nav-link p-0 ms-lg-2 me-lg-1\" type=\"button\" aria-label=\"Toggle theme\">\r\n          <svg data-bs-theme-value=\"dark\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-moon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n            <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n            <path d=\"M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z\"></path>\r\n          </svg>\r\n          <svg data-bs-theme-value=\"light\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-tabler icon-tabler-sun\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n            <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\r\n            <path d=\"M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0m-5 0h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7\"></path>\r\n          </svg>\r\n        </button>\r\n        {{ end -}}\r\n\r\n        <!-- Social menu -->\r\n        {{ if .Site.Menus.social -}}\r\n        <ul id=\"socialMenu\" class=\"nav mx-auto flex-row order-lg-4\">\r\n          {{ range .Site.Menus.social -}}\r\n            <li class=\"nav-item\">\r\n              <a class=\"nav-link social-link\" href=\"{{ .URL | relURL }}\">{{ .Pre | safeHTML }}<small class=\"ms-2 visually-hidden\">{{ .Name | safeHTML }}</small></a>\r\n            </li>\r\n          {{ end -}}\r\n        </ul>\r\n        {{ end -}}\r\n\r\n\r\n        <!-- Navbar button mobile -->\r\n        {{ if site.Params.doks.navBarButton -}}\r\n          <a class=\"btn btn-primary rounded-pill mt-2 btn-block d-lg-none\" href=\"{{ site.Params.doks.navBarButtonUrl | absURL }}\" role=\"button\">{{ site.Params.doks.navBarButtonText }}</a>\r\n        {{ end -}}\r\n      </div>\r\n    </div>\r\n\r\n    <!-- Navbar button desktop -->\r\n    {{ if site.Params.doks.navBarButton -}}\r\n      <a class=\"btn btn-primary rounded-pill ms-3 me-2 px-4 order-lg-3 d-none d-lg-block\" href=\"{{ site.Params.doks.navBarButtonUrl | absURL }}\" role=\"button\">{{ site.Params.doks.navBarButtonText }}</a>\r\n    {{ end -}}\r\n\r\n  </div>\r\n</header>\r\n{{ if site.Params.doks.navbarSticky -}}\r\n</div>\r\n{{ end -}}\r\n\r\n<!-- Search modal -->\r\n{{ if site.Params.doks.flexSearch -}}\r\n{{ partial \"header/search-modal\" . }}\r\n{{ end -}}\r\n\r\n\r\n"
  },
  {
    "path": "docs/layouts/partials/main/edit-page.html",
    "content": "{{ $parts := slice site.Params.doks.docsRepo }}\r\n\r\n{{ if (eq site.Params.doks.repoHost \"GitHub\") }}\r\n  {{ $parts = $parts | append \"edit\" site.Params.doks.docsRepoBranch }}\r\n{{ else if (eq site.Params.doks.repoHost \"Gitea\") }}\r\n  {{ $parts = $parts | append \"_edit\" site.Params.doks.docsRepoBranch }}\r\n{{ else if (eq site.Params.doks.repoHost \"GitLab\") }}\r\n  {{ $parts = $parts | append \"-/blob\" site.Params.doks.docsRepoBranch }}\r\n{{ else if (eq site.Params.doks.repoHost \"Bitbucket\") }}\r\n  {{ $parts = $parts | append \"src\" site.Params.doks.docsRepoBranch }}\r\n{{ else if (eq site.Params.doks.repoHost \"BitbucketServer\") }}\r\n  {{ $parts = $parts | append \"browse\" site.Params.doks.docsRepoBranch }}\r\n{{ end }}\r\n\r\n{{ if isset .Site.Params \"docsRepoSubPath\" }}\r\n  {{ if not (eq site.Params.doks.docsRepoSubPath \"\") }}\r\n    {{ $parts = $parts | append site.Params.doks.docsRepoSubPath }}\r\n  {{ end }}\r\n{{ end }}\r\n\r\n{{ $filePath := replace .File.Path \"\\\\\" \"/\" }}\r\n\r\n{{ $lang := \"\" }}\r\n{{ if site.Params.doks.multilingualMode }}\r\n  {{ $lang = .Lang }}\r\n{{ end }}\r\n\r\n{{ $parts = $parts | append \"docs/content\" $filePath }}\r\n\r\n{{ $url := delimit $parts \"/\" }}\r\n\r\n<div class=\"edit-page\">\r\n  <a href=\"{{ $url }}\">\r\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-edit-2\">\r\n      <path d=\"M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z\"></path>\r\n    </svg>\r\n    Help us improve this page\r\n  </a>\r\n</div>\r\n"
  },
  {
    "path": "docs/layouts/partials/private/has-headings.html",
    "content": "{{ $hasHeadings := false }}\r\n{{ if (isset .Fragments \"Headings\") }}\r\n  {{ $hasHeadings = gt (len .Fragments.Headings) 0 }}\r\n{{ end }}\r\n{{ $hasHeadings }}\r\n"
  },
  {
    "path": "docs/layouts/partials/seo/opengraph.html",
    "content": "{{/* Based on: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/opengraph.html */}}\n<meta property=\"og:title\" content=\"{{ .Title }}\">\n<meta property=\"og:description\" content=\"{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}\">\n<meta property=\"og:type\" content=\"{{ if .IsPage }}article{{ else }}website{{ end }}\">\n<meta property=\"og:url\" content=\"{{ .Permalink }}\">\n\n{{ $imagePermalink := (printf \"https://academy-api.threedots.tech/ssr/image.png?%s\" (collections.Querify \"url\" .Permalink) ) }}\n<meta property=\"og:image\" content=\"{{ $imagePermalink }}\">\n\n{{- if .IsPage }}\n{{- $iso8601 := \"2006-01-02T15:04:05-07:00\" -}}\n<meta property=\"article:section\" content=\"{{ .Section }}\">\n{{ with .PublishDate }}<meta property=\"article:published_time\" {{ .Format $iso8601 | printf \"content=%q\" | safeHTMLAttr }}>{{ end }}\n{{ with .Lastmod }}<meta property=\"article:modified_time\" {{ .Format $iso8601 | printf \"content=%q\" | safeHTMLAttr }}>{{ end }}\n{{- end -}}\n\n{{- with .Params.audio }}<meta property=\"og:audio\" content=\"{{ . }}\">{{ end }}\n{{- with .Params.locale }}<meta property=\"og:locale\" content=\"{{ . }}\">{{ end }}\n{{- with .Site.Params.title }}<meta property=\"og:site_name\" content=\"{{ . }}\">{{ end }}\n{{- with .Params.videos }}{{- range . }}\n<meta property=\"og:video\" content=\"{{ . | absURL }}\">\n{{ end }}{{ end }}\n\n{{- /* If it is part of a series, link to related articles */}}\n{{- $permalink := .Permalink }}\n{{- $siteSeries := .Site.Taxonomies.series }}\n{{- if $siteSeries }}\n{{ with .Params.series }}{{- range $name := . }}\n  {{- $series := index $siteSeries ($name | urlize) }}\n  {{- range $page := first 6 $series.Pages }}\n    {{- if ne $page.Permalink $permalink }}<meta property=\"og:see_also\" content=\"{{ $page.Permalink }}\">{{ end }}\n  {{- end }}\n{{ end }}{{ end }}\n{{- end }}\n\n{{- /* Deprecate site.Social.facebook_admin in favor of site.Params.social.facebook_admin */}}\n{{- $facebookAdmin := \"\" }}\n{{- with site.Params.social }}\n  {{- if reflect.IsMap . }}\n    {{- $facebookAdmin = .facebook_admin }}\n  {{- end }}\n{{- else }}\n  {{- with site.Social.facebook_admin }}\n    {{- $facebookAdmin = . }}\n    {{- warnf \"The social key in site configuration is deprecated. Use params.social.facebook_admin instead.\" }}\n  {{- end }}\n{{- end }}\n\n{{- /* Facebook Page Admin ID for Domain Insights */}}\n{{ with $facebookAdmin }}<meta property=\"fb:admins\" content=\"{{ . }}\">{{ end }}\n"
  },
  {
    "path": "docs/layouts/partials/seo/twitter.html",
    "content": "{{/* Based on: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/twitter_cards.html */}}\n\n{{ $imagePermalink := (printf \"https://academy-api.threedots.tech/ssr/image.png?%s\" (collections.Querify \"url\" .Permalink) ) }}\n<meta name=\"twitter:image\" content=\"{{ $imagePermalink }}\">\n<meta name=\"twitter:card\" content=\"summary_large_image\">\n\n\n<meta name=\"twitter:title\" content=\"{{ .Title }}\">\n<meta name=\"twitter:description\" content=\"{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end -}}\">\n\n{{- /* Deprecate site.Social.twitter in favor of site.Params.social.twitter */}}\n{{- $twitterSite := \"\" }}\n{{- with site.Params.social }}\n  {{- if reflect.IsMap . }}\n    {{- $twitterSite = .twitter }}\n  {{- end }}\n{{- else }}\n  {{- with site.Social.twitter }}\n    {{- $twitterSite = . }}\n    {{- warnf \"The social key in site configuration is deprecated. Use params.social.twitter instead.\" }}\n  {{- end }}\n{{- end }}\n\n{{- with $twitterSite }}\n  {{- $content := . }}\n  {{- if not (strings.HasPrefix . \"@\") }}\n    {{- $content = printf \"@%v\" $twitterSite }}\n  {{- end }}\n<meta name=\"twitter:site\" content=\"{{ $content }}\">\n{{- end }}\n"
  },
  {
    "path": "docs/layouts/partials/sidebar/section-menu.html",
    "content": "{{- with site.Menus.sidebar }}\r\n    {{ partial \"sidebar/render-section-menu.html\" (dict \"currentPage\" $ \"nodes\" .) }}\r\n{{- else }}\r\n  {{- with (.Site.GetPage \"section\" .Section).Sections }}\r\n    {{ partial \"sidebar/render-section-menu.html\" (dict \"currentPage\" $ \"nodes\" .) }}\r\n  {{- end }}\r\n{{- end }}\r\n"
  },
  {
    "path": "docs/layouts/shortcodes/load-snippet-partial.html",
    "content": "{{ $file := (.Get \"file\") }}\n{{ $content := readFile $file }}\n\n{{ $first_line_contains := (.Get \"first_line_contains\") }}\n{{ $last_line_contains := (.Get \"last_line_contains\") }}\n{{ $last_line_equals := (.Get \"last_line_equals\") }}\n\n{{ $show_line := false }}\n\n{{/*if true, first or last line was not found*/}}\n{{ $first_line_found := false}}\n{{ $last_line_found := false}}\n\n{{ $padding_after := (.Get \"padding_after\" | default \"0\" | int) }}\n\n{{ $first_line_num := 0 }}\n{{ $last_line_num := 0 }}\n\n{{ $linkFile := $file }}\n{{ $repo := \"watermill\" }}\n\n{{ if in $file \"src-link/watermill-\" }}\n    {{ $repo = index (findRE \"watermill-[a-z]+\" $linkFile) 0 }}\n    {{ $linkFile = replace $linkFile $repo \"\" }}\n    {{ $linkFile = replace $linkFile \"src-link//\" \"\" }}\n{{ else if in $linkFile \"src-link/\" }}\n    {{ $linkFile = replace $linkFile \"src-link/\" \"\" }}\n{{ else }}\n    {{ $linkFile = print \"docs/content/\" $linkFile }}\n{{ end }}\n\n{{ $lines := slice }}\n\n{{ range $elem_key, $elem_val := split $content \"\\n\" }}\n    {{ $line_num := (add $elem_key 1) }}\n\n    {{ if and (not $first_line_found) (in $elem_val $first_line_contains) }}\n        {{ if ne $elem_key 0 }}\n            {{ $lines = $lines | append \"// ...\" }}\n        {{ end }}\n\n        {{ $show_line = true }}\n        {{ $first_line_found = true}}\n        {{ $first_line_num = $line_num }}\n    {{ end }}\n\n    {{ if $show_line }}\n        {{ $lines = $lines | append $elem_val }}\n    {{ end }}\n\n    {{ if and ($first_line_found) (in $elem_val $last_line_contains) (ne $last_line_contains \"\") }}\n        {{ $last_line_found = true }}\n    {{ end }}\n\n    {{ if and ($first_line_found) (eq $elem_val $last_line_equals) (ne $last_line_equals \"\") }}\n        {{ $last_line_found = true }}\n    {{ end }}\n\n    {{ if and $last_line_found $show_line }}\n        {{ if gt $padding_after 0 }}\n            {{ $padding_after = sub $padding_after 1}}\n        {{ else }}\n            {{ $lines = $lines | append \"// ...\" }}\n            {{ $show_line = false }}\n            {{ $last_line_num = $line_num }}\n        {{ end }}\n    {{ end }}\n{{ end }}\n\n{{/* Calculate minimum indentation (common whitespace) */}}\n{{ $min_indent := 999999 }}\n{{ range $lines }}\n    {{ if and (ne . \"// ...\") (ne (trim . \" \\t\") \"\") }}\n        {{ $leading := 0 }}\n        {{ range $i, $char := split . \"\" }}\n            {{ if or (eq $char \" \") (eq $char \"\\t\") }}\n                {{ $leading = add $leading 1 }}\n            {{ else }}\n                {{ break }}\n            {{ end }}\n        {{ end }}\n        {{ if lt $leading $min_indent }}\n            {{ $min_indent = $leading }}\n        {{ end }}\n    {{ end }}\n{{ end }}\n\n{{/* Remove common leading whitespace from all lines */}}\n{{ $normalized_lines := slice }}\n{{ range $lines }}\n    {{ if or (eq . \"// ...\") (eq (trim . \" \\t\") \"\") }}\n        {{ $normalized_lines = $normalized_lines | append . }}\n    {{ else if gt $min_indent 0 }}\n        {{ $normalized_lines = $normalized_lines | append (substr . $min_indent) }}\n    {{ else }}\n        {{ $normalized_lines = $normalized_lines | append . }}\n    {{ end }}\n{{ end }}\n\n<div class=\"expressive-code\">\n    <figure class=\"frame not-content\">\n        {{ transform.Highlight (delimit $normalized_lines \"\\n\" | safeHTML) (.Get \"type\" | default \"go\") }}\n    </figure>\n</div>\n\n<small class=\"smaller\">Full source: [github.com/ThreeDotsLabs/{{ $repo }}/{{ $linkFile }}](https://github.com/ThreeDotsLabs/{{ $repo }}/tree/master/{{ $linkFile }}{{ if ne $first_line_num 0 }}#L{{ $first_line_num }}{{ end }})</small>\n\n{{if not $first_line_found }}\n    {{ errorf \"`first_line_contains` %s not found in %s snippet\" $first_line_contains $file }}\n{{end}}\n\n{{if and (not $last_line_found) (ne $last_line_contains \"\") }}\n    {{ errorf \"`last_line_contains` %s not found in %s snippet\" $last_line_contains $file }}\n{{end}}\n"
  },
  {
    "path": "docs/layouts/shortcodes/load-snippet.html",
    "content": "{{ $file := (.Get \"file\") }}\n{{ $content := readFile $file }}\n\n{{ $start_line := (.Get \"start_line\") | default \"0\" }}\n{{ $end_line := (.Get \"end_line\") | default \"0\" }}\n\n{{ $has_start_line := (ne $start_line \"0\") }}\n{{ $has_end_line := (ne $end_line \"0\") }}\n\n{{ $lines := slice }}\n\n{{ $linkFile := $file }}\n{{ $repo := \"watermill\" }}\n\n{{ if in $file \"src-link/watermill-\" }}\n    {{ $repo = index (findRE \"watermill-[a-z]+\" $linkFile) 0 }}\n    {{ $linkFile = replace $linkFile $repo \"\" }}\n    {{ $linkFile = replace $linkFile \"src-link//\" \"\" }}\n{{ else if in $linkFile \"src-link/\" }}\n    {{ $linkFile = replace $linkFile \"src-link/\" \"\" }}\n{{ else }}\n    {{ $linkFile = print \"docs/content/\" $linkFile }}\n{{ end }}\n\n{{ range $elem_key, $elem_val := split $content \"\\n\" }}\n    {{if and (or (not $has_start_line) (ge (add $elem_key 1) ($start_line | int))) (or (not $has_end_line) (le (add $elem_key 1) ($end_line | int)))}}\n        {{ $lines = $lines | append $elem_val }}\n    {{ end }}\n{{ end }}\n\n<div class=\"expressive-code\">\n    <figure class=\"frame not-content\">\n        {{ transform.Highlight (delimit $lines \"\\n\" | safeHTML) (.Get \"type\" | default \"go\") }}\n    </figure>\n</div>\n\n<small class=\"smaller\">Full source: [{{ $linkFile }}](https://github.com/ThreeDotsLabs/watermill/tree/master/{{ $linkFile }})</small>\n"
  },
  {
    "path": "docs/layouts/shortcodes/readfile.html",
    "content": "{{$file := .Get \"file\"}}\n{{- if eq (.Get \"markdown\") \"true\" -}}\n{{- $file  | readFile | markdownify -}}\n{{- else -}}\n{{ $file  | readFile | safeHTML }}\n{{- end -}}\n"
  },
  {
    "path": "docs/layouts/shortcodes/tab.html",
    "content": "<!--\r\n  Source: https://github.com/alex-shpak/hugo-book/blob/master/layouts/shortcodes/tab.html\r\n-->\r\n\r\n{{ if .Parent -}}\r\n  {{ $name := .Get 0 }}\r\n  {{ $slug := .Get 1 }}\r\n  {{ $group := printf \"tabs-%s\" (.Parent.Get 0) }}\r\n\r\n  {{ if not (.Parent.Scratch.Get $group) }}\r\n    {{ .Parent.Scratch.Set $group slice }}\r\n  {{ end }}\r\n\r\n  {{ .Parent.Scratch.Add $group (dict \"Name\" $name \"Slug\" $slug \"Content\" .Inner) }}\r\n{{ else -}}\r\n  {{ errorf \"%q: 'tab' shortcode must be inside 'tabs' shortcode\" .Page.Path }}\r\n{{ end -}}\r\n"
  },
  {
    "path": "docs/layouts/shortcodes/tabs.html",
    "content": "<!--\r\n  Based on: https://github.com/alex-shpak/hugo-book/blob/master/layouts/shortcodes/tabs.html\r\n-->\r\n\r\n{{ if .Inner }}{{ end }}\r\n{{ $id := .Get 0 }}\r\n{{ $group := printf \"tabs-%s\" $id }}\r\n\r\n<nav>\r\n  <div class=\"nav nav-tabs\" id=\"nav-tab\" role=\"tablist\">\r\n  {{ range $index, $tab := .Scratch.Get $group -}}\r\n    <button data-toggle-tab=\"{{ $tab.Slug }}\" class=\"nav-link{{ if not $index }} active{{ end }}\" id=\"{{ printf \"%s-%d\" $group $index }}-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#{{ printf \"%s-%d\" $group $index }}\" type=\"button\" role=\"tab\" aria-controls=\"{{ printf \"%s-%d\" $group $index }}\" aria-selected=\"true\">\r\n      {{ $tab.Name -}}\r\n    </button>\r\n  {{ end -}}\r\n  </div>\r\n</nav>\r\n\r\n<div class=\"tab-content\" id=\"nav-tabContent\">\r\n  {{ range $index, $tab := .Scratch.Get $group -}}\r\n    <div data-pane=\"{{ $tab.Slug }}\" class=\"tab-pane fade{{ if not $index }} show active{{ end }}\" id=\"{{ printf \"%s-%d\" $group $index }}\" role=\"tabpanel\" aria-labelledby=\"{{ printf \"%s-%d\" $group $index }}-tab\" tabindex=\"0\">\r\n      {{ .Content | $.Page.RenderString -}}\r\n    </div>\r\n  {{ end -}}\r\n</div>\r\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"watermill-docs\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Doks theme\",\n  \"author\": \"Thulite\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"create\": \"hugo new\",\n    \"dev\": \"hugo server --disableFastRender --noHTTPCache\",\n    \"format\": \"prettier **/** -w -c\",\n    \"build\": \"hugo --minify --gc -b ${URL}\",\n    \"build:branch\": \"hugo --minify --gc -b ${DEPLOY_URL}\",\n    \"preview\": \"vite preview --outDir public\"\n  },\n  \"dependencies\": {\n    \"@tabler/icons\": \"^3.12.0\",\n    \"@thulite/doks-core\": \"^1.7.0\",\n    \"@thulite/images\": \"^3.3.0\",\n    \"@thulite/inline-svg\": \"^1.1.0\",\n    \"@thulite/seo\": \"^2.4.0\",\n    \"github-buttons\": \"^2.29.1\",\n    \"thulite\": \"^2.5.0\"\n  },\n  \"devDependencies\": {\n    \"prettier\": \"^3.3.3\",\n    \"vite\": \"^5.4.12\"\n  },\n  \"engines\": {\n    \"node\": \">=20.11.0\"\n  }\n}\n"
  },
  {
    "path": "docs/resources/_gen/assets/scss/app.scss_901a6e181e810c5c7347a10d84f037ab.content",
    "content": "@charset \"UTF-8\";\n/* Bluish cyan */\n/* Gray */\n/* Yellow */\n/* Khaki */\n/* Purple */\n/* Vermilion */\n:root[data-bs-theme=\"light\"],\n[data-bs-theme=\"light\"] ::backdrop {\n  --sl-color-white: hsl(224, 10%, 10%);\n  --sl-color-gray-1: hsl(224, 14%, 16%);\n  --sl-color-gray-2: hsl(224, 10%, 23%);\n  --sl-color-gray-3: hsl(224, 7%, 36%);\n  --sl-color-gray-4: hsl(224, 6%, 56%);\n  --sl-color-gray-5: hsl(224, 6%, 77%);\n  --sl-color-gray-6: hsl(224, 20%, 94%);\n  --sl-color-gray-7: hsl(224, 19%, 97%);\n  --sl-color-black: hsl(0, 0%, 100%); }\n\n:root,\n::backdrop {\n  --sl-color-white: hsl(0, 0%, 100%);\n  --sl-color-gray-1: hsl(224, 20%, 94%);\n  --sl-color-gray-2: hsl(224, 6%, 77%);\n  --sl-color-gray-3: hsl(224, 6%, 56%);\n  --sl-color-gray-4: hsl(224, 7%, 36%);\n  --sl-color-gray-5: hsl(224, 10%, 23%);\n  --sl-color-gray-6: hsl(224, 14%, 16%);\n  --sl-color-black: hsl(224, 10%, 10%);\n  --sl-hue-orange: 41;\n  --sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);\n  --sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);\n  --sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);\n  --sl-hue-green: 101;\n  --sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);\n  --sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);\n  --sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);\n  --sl-hue-blue: 234;\n  --sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);\n  --sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);\n  --sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);\n  --sl-hue-purple: 281;\n  --sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);\n  --sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);\n  --sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);\n  --sl-hue-red: 339;\n  --sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);\n  --sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);\n  --sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);\n  --sl-color-accent-low: hsl(224, 54%, 20%);\n  --sl-color-accent: hsl(224, 100%, 60%);\n  --sl-color-accent-high: hsl(224, 100%, 85%);\n  --sl-color-text: var(--sl-color-gray-2);\n  --sl-color-text-accent: var(--sl-color-accent-high);\n  --sl-color-text-invert: var(--sl-color-accent-low);\n  --sl-color-bg: var(--sl-color-black);\n  --sl-color-bg-nav: var(--sl-color-gray-6);\n  --sl-color-bg-sidebar: var(--sl-color-gray-6);\n  --sl-color-bg-inline-code: var(--sl-color-gray-5);\n  --sl-color-hairline-light: var(--sl-color-gray-5);\n  --sl-color-hairline: var(--sl-color-gray-6);\n  --sl-color-hairline-shade: var(--sl-color-black);\n  --sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);\n  --sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12), 0px 2px 1px hsla(0, 0%, 0%, 0.24);\n  --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);\n  --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);\n  --sl-text-xs: 0.8125rem;\n  --sl-text-sm: 0.875rem;\n  --sl-text-base: 1rem;\n  --sl-text-lg: 1.125rem;\n  --sl-text-xl: 1.25rem;\n  --sl-text-2xl: 1.5rem;\n  --sl-text-3xl: 1.8125rem;\n  --sl-text-4xl: 2.1875rem;\n  --sl-text-5xl: 2.625rem;\n  --sl-text-6xl: 4rem;\n  --sl-text-body: var(--sl-text-base);\n  --sl-text-body-sm: var(--sl-text-xs);\n  --sl-text-code: var(--sl-text-sm);\n  --sl-text-code-sm: var(--sl-text-xs);\n  --sl-text-h1: var(--sl-text-4xl);\n  --sl-text-h2: var(--sl-text-3xl);\n  --sl-text-h3: var(--sl-text-2xl);\n  --sl-text-h4: var(--sl-text-xl);\n  --sl-text-h5: var(--sl-text-lg);\n  --sl-line-height: 1.8;\n  --sl-line-height-headings: 1.2;\n  --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\";\n  --sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  --__sl-font: var(--sl-font, \"\"), var(--sl-font-system);\n  --__sl-font-mono: var(--sl-font-mono, \"\"), var(--sl-font-system-mono);\n  --sl-nav-height: 3.5rem;\n  --sl-nav-pad-x: 1rem;\n  --sl-nav-pad-y: 0.75rem;\n  --sl-mobile-toc-height: 3rem;\n  --sl-sidebar-width: 18.75rem;\n  --sl-sidebar-pad-x: 1rem;\n  --sl-content-width: 45rem;\n  --sl-content-pad-x: 1rem;\n  --sl-menu-button-size: 2rem;\n  --sl-nav-gap: var(--sl-content-pad-x);\n  --sl-outline-offset-inside: -0.1875rem;\n  --sl-z-index-toc: 4;\n  --sl-z-index-menu: 5;\n  --sl-z-index-navbar: 10;\n  --sl-z-index-skiplink: 20; }\n\n:root {\n  --purple-hsl: 255, 60%, 60%;\n  --overlay-blurple: hsla(var(--purple-hsl), 0.2); }\n\n:root {\n  --ec-brdRad: 0px;\n  --ec-brdWd: 1px;\n  --ec-brdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-codeFontFml: var(--__sl-font-mono);\n  --ec-codeFontSize: var(--sl-text-code);\n  --ec-codeFontWg: 400;\n  --ec-codeLineHt: var(--sl-line-height);\n  --ec-codePadBlk: 0.75rem;\n  --ec-codePadInl: 1rem;\n  --ec-codeBg: #011627;\n  --ec-codeFg: #d6deeb;\n  --ec-codeSelBg: #1d3b53;\n  --ec-uiFontFml: var(--__sl-font);\n  --ec-uiFontSize: 0.9rem;\n  --ec-uiFontWg: 400;\n  --ec-uiLineHt: 1.65;\n  --ec-uiPadBlk: 0.25rem;\n  --ec-uiPadInl: 1rem;\n  --ec-uiSelBg: #234d708c;\n  --ec-uiSelFg: #ffffff;\n  --ec-focusBrd: #122d42;\n  --ec-sbThumbCol: #ffffff17;\n  --ec-sbThumbHoverCol: #ffffff49;\n  --ec-tm-lineMarkerAccentMarg: 0rem;\n  --ec-tm-lineMarkerAccentWd: 0.15rem;\n  --ec-tm-lineDiffIndMargLeft: 0.25rem;\n  --ec-tm-inlMarkerBrdWd: 1.5px;\n  --ec-tm-inlMarkerBrdRad: 0.2rem;\n  --ec-tm-inlMarkerPad: 0.15rem;\n  --ec-tm-insDiffIndContent: \"+\";\n  --ec-tm-delDiffIndContent: \"-\";\n  --ec-tm-markBg: #ffffff17;\n  --ec-tm-markBrdCol: #ffffff40;\n  --ec-tm-insBg: #1e571599;\n  --ec-tm-insBrdCol: #487f3bd0;\n  --ec-tm-insDiffIndCol: #79b169d0;\n  --ec-tm-delBg: #862d2799;\n  --ec-tm-delBrdCol: #b4554bd0;\n  --ec-tm-delDiffIndCol: #ed8779d0;\n  --ec-frm-shdCol: #011627;\n  --ec-frm-frameBoxShdCssVal: none;\n  --ec-frm-edActTabBg: var(--sl-color-gray-6);\n  --ec-frm-edActTabFg: var(--sl-color-text);\n  --ec-frm-edActTabBrdCol: transparent;\n  --ec-frm-edActTabIndHt: 1px;\n  --ec-frm-edActTabIndTopCol: var(--sl-color-accent-high);\n  --ec-frm-edActTabIndBtmCol: transparent;\n  --ec-frm-edTabsMargInlStart: 0;\n  --ec-frm-edTabsMargBlkStart: 0;\n  --ec-frm-edTabBrdRad: 0px;\n  --ec-frm-edTabBarBg: var(--sl-color-black);\n  --ec-frm-edTabBarBrdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-edBg: var(--sl-color-gray-6);\n  --ec-frm-trmTtbDotsFg: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-trmTtbDotsOpa: 0.75;\n  --ec-frm-trmTtbBg: var(--sl-color-black);\n  --ec-frm-trmTtbFg: var(--sl-color-text);\n  --ec-frm-trmTtbBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-trmBg: var(--sl-color-gray-6);\n  --ec-frm-inlBtnFg: var(--sl-color-text);\n  --ec-frm-inlBtnBg: var(--sl-color-text);\n  --ec-frm-inlBtnBgIdleOpa: 0;\n  --ec-frm-inlBtnBgHoverOrFocusOpa: 0.2;\n  --ec-frm-inlBtnBgActOpa: 0.3;\n  --ec-frm-inlBtnBrd: var(--sl-color-text);\n  --ec-frm-inlBtnBrdOpa: 0.4;\n  --ec-frm-tooltipSuccessBg: #158744;\n  --ec-frm-tooltipSuccessFg: white; }\n\n:root,\n[data-bs-theme=\"light\"] {\n  --bs-blue: #3347ff;\n  --bs-indigo: #6610f2;\n  --bs-purple: #bd53ee;\n  --bs-pink: #d63384;\n  --bs-red: #ee5389;\n  --bs-orange: #fd7e14;\n  --bs-yellow: #eebd53;\n  --bs-green: #84ee53;\n  --bs-teal: #20c997;\n  --bs-cyan: #0dcaf0;\n  --bs-black: #000;\n  --bs-white: #fff;\n  --bs-gray: #6c757d;\n  --bs-gray-dark: #343a40;\n  --bs-gray-100: #f8f9fa;\n  --bs-gray-200: #e9ecef;\n  --bs-gray-300: #dee2e6;\n  --bs-gray-400: #ced4da;\n  --bs-gray-500: #adb5bd;\n  --bs-gray-600: #6c757d;\n  --bs-gray-700: #495057;\n  --bs-gray-800: #343a40;\n  --bs-gray-900: #212529;\n  --bs-primary: #4f46e5;\n  --bs-secondary: #6c757d;\n  --bs-success: #84ee53;\n  --bs-info: #3347ff;\n  --bs-warning: #eebd53;\n  --bs-danger: #ee5389;\n  --bs-light: #f8f9fa;\n  --bs-dark: #212529;\n  --bs-primary-rgb: 79, 70, 229;\n  --bs-secondary-rgb: 108, 117, 125;\n  --bs-success-rgb: 132.2821, 238.017, 83.283;\n  --bs-info-rgb: 51, 71.4, 255;\n  --bs-warning-rgb: 238.017, 189.0179, 83.283;\n  --bs-danger-rgb: 238.017, 83.283, 137.4399;\n  --bs-light-rgb: 248, 249, 250;\n  --bs-dark-rgb: 33, 37, 41;\n  --bs-primary-text-emphasis: #201c5c;\n  --bs-secondary-text-emphasis: #2b2f32;\n  --bs-success-text-emphasis: #355f21;\n  --bs-info-text-emphasis: #141d66;\n  --bs-warning-text-emphasis: #5f4c21;\n  --bs-danger-text-emphasis: #5f2137;\n  --bs-light-text-emphasis: #495057;\n  --bs-dark-text-emphasis: #495057;\n  --bs-primary-bg-subtle: #dcdafa;\n  --bs-secondary-bg-subtle: #e2e3e5;\n  --bs-success-bg-subtle: #e6fcdd;\n  --bs-info-bg-subtle: #d6daff;\n  --bs-warning-bg-subtle: #fcf2dd;\n  --bs-danger-bg-subtle: #fcdde7;\n  --bs-light-bg-subtle: #fcfcfd;\n  --bs-dark-bg-subtle: #ced4da;\n  --bs-primary-border-subtle: #b9b5f5;\n  --bs-secondary-border-subtle: #c4c8cb;\n  --bs-success-border-subtle: #cef8ba;\n  --bs-info-border-subtle: #adb6ff;\n  --bs-warning-border-subtle: #f8e5ba;\n  --bs-danger-border-subtle: #f8bad0;\n  --bs-light-border-subtle: #e9ecef;\n  --bs-dark-border-subtle: #adb5bd;\n  --bs-white-rgb: 255, 255, 255;\n  --bs-black-rgb: 0, 0, 0;\n  --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\";\n  --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n  --bs-body-font-family: var(--bs-font-sans-serif);\n  --bs-body-font-size: 1rem;\n  --bs-body-font-weight: 400;\n  --bs-body-line-height: 1.5;\n  --bs-body-color: #1d2d35;\n  --bs-body-color-rgb: 29, 45, 53;\n  --bs-body-bg: #fff;\n  --bs-body-bg-rgb: 255, 255, 255;\n  --bs-emphasis-color: #000;\n  --bs-emphasis-color-rgb: 0, 0, 0;\n  --bs-secondary-color: rgba(29, 45, 53, 0.75);\n  --bs-secondary-color-rgb: 29, 45, 53;\n  --bs-secondary-bg: #e9ecef;\n  --bs-secondary-bg-rgb: 233, 236, 239;\n  --bs-tertiary-color: rgba(29, 45, 53, 0.5);\n  --bs-tertiary-color-rgb: 29, 45, 53;\n  --bs-tertiary-bg: #f8f9fa;\n  --bs-tertiary-bg-rgb: 248, 249, 250;\n  --bs-heading-color: inherit;\n  --bs-link-color: #4f46e5;\n  --bs-link-color-rgb: 79, 70, 229;\n  --bs-link-decoration: none;\n  --bs-link-hover-color: #3f38b7;\n  --bs-link-hover-color-rgb: 63, 56, 183;\n  --bs-link-hover-decoration: underline;\n  --bs-code-color: #d63384;\n  --bs-highlight-color: #1d2d35;\n  --bs-highlight-bg: #fcf2dd;\n  --bs-border-width: 1px;\n  --bs-border-style: solid;\n  --bs-border-color: #dee2e6;\n  --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n  --bs-border-radius: 0.375rem;\n  --bs-border-radius-sm: 0.25rem;\n  --bs-border-radius-lg: 0.5rem;\n  --bs-border-radius-xl: 1rem;\n  --bs-border-radius-xxl: 2rem;\n  --bs-border-radius-2xl: var(--bs-border-radius-xxl);\n  --bs-border-radius-pill: 50rem;\n  --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n  --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n  --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);\n  --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);\n  --bs-focus-ring-width: 0.25rem;\n  --bs-focus-ring-opacity: 0.25;\n  --bs-focus-ring-color: rgba(79, 70, 229, 0.25);\n  --bs-form-valid-color: #84ee53;\n  --bs-form-valid-border-color: #84ee53;\n  --bs-form-invalid-color: #ee5389;\n  --bs-form-invalid-border-color: #ee5389; }\n\n[data-bs-theme=\"dark\"] {\n  color-scheme: dark;\n  --bs-body-color: #c1c3c8;\n  --bs-body-color-rgb: 192.831, 194.7078, 199.869;\n  --bs-body-bg: #17181c;\n  --bs-body-bg-rgb: 22.95, 24.31, 28.05;\n  --bs-emphasis-color: #fff;\n  --bs-emphasis-color-rgb: 255, 255, 255;\n  --bs-secondary-color: rgba(193, 195, 200, 0.75);\n  --bs-secondary-color-rgb: 192.831, 194.7078, 199.869;\n  --bs-secondary-bg: #343a40;\n  --bs-secondary-bg-rgb: 52, 58, 64;\n  --bs-tertiary-color: rgba(193, 195, 200, 0.5);\n  --bs-tertiary-color-rgb: 192.831, 194.7078, 199.869;\n  --bs-tertiary-bg: #2b3035;\n  --bs-tertiary-bg-rgb: 43, 48, 53;\n  --bs-primary-text-emphasis: #9590ef;\n  --bs-secondary-text-emphasis: #a7acb1;\n  --bs-success-text-emphasis: #b5f598;\n  --bs-info-text-emphasis: #8591ff;\n  --bs-warning-text-emphasis: #f5d798;\n  --bs-danger-text-emphasis: #f598b8;\n  --bs-light-text-emphasis: #f8f9fa;\n  --bs-dark-text-emphasis: #dee2e6;\n  --bs-primary-bg-subtle: #100e2e;\n  --bs-secondary-bg-subtle: #161719;\n  --bs-success-bg-subtle: #1a3011;\n  --bs-info-bg-subtle: #0a0e33;\n  --bs-warning-bg-subtle: #302611;\n  --bs-danger-bg-subtle: #30111b;\n  --bs-light-bg-subtle: #23262f;\n  --bs-dark-bg-subtle: #1a1d20;\n  --bs-primary-border-subtle: #2f2a89;\n  --bs-secondary-border-subtle: #41464b;\n  --bs-success-border-subtle: #4f8f32;\n  --bs-info-border-subtle: #1f2b99;\n  --bs-warning-border-subtle: #8f7132;\n  --bs-danger-border-subtle: #8f3252;\n  --bs-light-border-subtle: #353841;\n  --bs-dark-border-subtle: #343a40;\n  --bs-heading-color: white;\n  --bs-link-color: #b3c7ff;\n  --bs-link-hover-color: #c2d2ff;\n  --bs-link-color-rgb: 178.5, 198.9, 255;\n  --bs-link-hover-color-rgb: 194, 210, 255;\n  --bs-code-color: #e685b5;\n  --bs-highlight-color: #c1c3c8;\n  --bs-highlight-bg: #5f4c21;\n  --bs-border-color: #495057;\n  --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n  --bs-form-valid-color: #b5f598;\n  --bs-form-valid-border-color: #b5f598;\n  --bs-form-invalid-color: #f598b8;\n  --bs-form-invalid-border-color: #f598b8; }\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box; }\n\n@media (prefers-reduced-motion: no-preference) {\n  :root {\n    scroll-behavior: smooth; } }\n\nbody {\n  margin: 0;\n  font-family: var(--bs-body-font-family);\n  font-size: var(--bs-body-font-size);\n  font-weight: var(--bs-body-font-weight);\n  line-height: var(--bs-body-line-height);\n  color: var(--bs-body-color);\n  text-align: var(--bs-body-text-align);\n  background-color: var(--bs-body-bg);\n  -webkit-text-size-adjust: 100%;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }\n\nhr {\n  margin: 1rem 0;\n  color: inherit;\n  border: 0;\n  border-top: var(--bs-border-width) solid;\n  opacity: 0.25; }\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n  margin-top: 0;\n  margin-bottom: 0.5rem;\n  font-weight: 700;\n  line-height: 1.2;\n  color: var(--bs-heading-color); }\n\nh1, .h1 {\n  font-size: calc(1.375rem + 1.5vw); }\n  @media (min-width: 1200px) {\n    h1, .h1 {\n      font-size: 2.5rem; } }\nh2, .h2 {\n  font-size: calc(1.325rem + 0.9vw); }\n  @media (min-width: 1200px) {\n    h2, .h2 {\n      font-size: 2rem; } }\nh3, .h3 {\n  font-size: calc(1.3rem + 0.6vw); }\n  @media (min-width: 1200px) {\n    h3, .h3 {\n      font-size: 1.75rem; } }\nh4, .h4 {\n  font-size: calc(1.275rem + 0.3vw); }\n  @media (min-width: 1200px) {\n    h4, .h4 {\n      font-size: 1.5rem; } }\nh5, .h5 {\n  font-size: 1.25rem; }\n\nh6, .h6 {\n  font-size: 1rem; }\n\np {\n  margin-top: 0;\n  margin-bottom: 1rem; }\n\nabbr[title] {\n  text-decoration: underline dotted;\n  cursor: help;\n  text-decoration-skip-ink: none; }\n\naddress {\n  margin-bottom: 1rem;\n  font-style: normal;\n  line-height: inherit; }\n\nol,\nul {\n  padding-left: 2rem; }\n\nol,\nul,\ndl {\n  margin-top: 0;\n  margin-bottom: 1rem; }\n\nol ol,\nul ul,\nol ul,\nul ol {\n  margin-bottom: 0; }\n\ndt {\n  font-weight: 700; }\n\ndd {\n  margin-bottom: .5rem;\n  margin-left: 0; }\n\nblockquote {\n  margin: 0 0 1rem; }\n\nb,\nstrong {\n  font-weight: bolder; }\n\nsmall, .small {\n  font-size: 0.875em; }\n\nmark, .mark {\n  padding: 0.1875em;\n  color: var(--bs-highlight-color);\n  background-color: var(--bs-highlight-bg); }\n\nsub,\nsup {\n  position: relative;\n  font-size: 0.75em;\n  line-height: 0;\n  vertical-align: baseline; }\n\nsub {\n  bottom: -.25em; }\n\nsup {\n  top: -.5em; }\n\na {\n  color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n  text-decoration: none; }\n  a:hover {\n    --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n    text-decoration: underline; }\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n  color: inherit;\n  text-decoration: none; }\n\npre,\ncode,\nkbd,\nsamp {\n  font-family: var(--bs-font-monospace);\n  font-size: 1em; }\n\npre {\n  display: block;\n  margin-top: 0;\n  margin-bottom: 1rem;\n  overflow: auto;\n  font-size: 0.875em; }\n  pre code {\n    font-size: inherit;\n    color: inherit;\n    word-break: normal; }\n\ncode {\n  font-size: 0.875em;\n  color: var(--bs-code-color);\n  word-wrap: break-word; }\n  a > code {\n    color: inherit; }\n\nkbd {\n  padding: 0.1875rem 0.375rem;\n  font-size: 0.875em;\n  color: var(--bs-body-bg);\n  background-color: var(--bs-body-color);\n  border-radius: 0.25rem; }\n  kbd kbd {\n    padding: 0;\n    font-size: 1em; }\n\nfigure {\n  margin: 0 0 1rem; }\n\nimg,\nsvg {\n  vertical-align: middle; }\n\ntable {\n  caption-side: bottom;\n  border-collapse: collapse; }\n\ncaption {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  color: var(--bs-secondary-color);\n  text-align: left; }\n\nth {\n  text-align: inherit;\n  text-align: -webkit-match-parent; }\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n  border-color: inherit;\n  border-style: solid;\n  border-width: 0; }\n\nlabel {\n  display: inline-block; }\n\nbutton {\n  border-radius: 0; }\n\nbutton:focus:not(:focus-visible) {\n  outline: 0; }\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n  margin: 0;\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit; }\n\nbutton,\nselect {\n  text-transform: none; }\n\n[role=\"button\"] {\n  cursor: pointer; }\n\nselect {\n  word-wrap: normal; }\n  select:disabled {\n    opacity: 1; }\n\n[list]:not([type=\"date\"]):not([type=\"datetime-local\"]):not([type=\"month\"]):not([type=\"week\"]):not([type=\"time\"])::-webkit-calendar-picker-indicator {\n  display: none !important; }\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button; }\n  button:not(:disabled),\n  [type=\"button\"]:not(:disabled),\n  [type=\"reset\"]:not(:disabled),\n  [type=\"submit\"]:not(:disabled) {\n    cursor: pointer; }\n\n::-moz-focus-inner {\n  padding: 0;\n  border-style: none; }\n\ntextarea {\n  resize: vertical; }\n\nfieldset {\n  min-width: 0;\n  padding: 0;\n  margin: 0;\n  border: 0; }\n\nlegend {\n  float: left;\n  width: 100%;\n  padding: 0;\n  margin-bottom: 0.5rem;\n  font-size: calc(1.275rem + 0.3vw);\n  line-height: inherit; }\n  @media (min-width: 1200px) {\n    legend {\n      font-size: 1.5rem; } }\n  legend + * {\n    clear: left; }\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n  padding: 0; }\n\n::-webkit-inner-spin-button {\n  height: auto; }\n\n[type=\"search\"] {\n  -webkit-appearance: textfield;\n  outline-offset: -2px; }\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n  direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n  -webkit-appearance: none; }\n\n::-webkit-color-swatch-wrapper {\n  padding: 0; }\n\n::file-selector-button {\n  font: inherit;\n  -webkit-appearance: button; }\n\noutput {\n  display: inline-block; }\n\niframe {\n  border: 0; }\n\nsummary {\n  display: list-item;\n  cursor: pointer; }\n\nprogress {\n  vertical-align: baseline; }\n\n[hidden] {\n  display: none !important; }\n\n.lead {\n  font-size: 1.25rem;\n  font-weight: 400; }\n\n.display-1 {\n  font-size: calc(1.625rem + 4.5vw);\n  font-weight: 300;\n  line-height: 1.2; }\n  @media (min-width: 1200px) {\n    .display-1 {\n      font-size: 5rem; } }\n.display-2 {\n  font-size: calc(1.575rem + 3.9vw);\n  font-weight: 300;\n  line-height: 1.2; }\n  @media (min-width: 1200px) {\n    .display-2 {\n      font-size: 4.5rem; } }\n.display-3 {\n  font-size: calc(1.525rem + 3.3vw);\n  font-weight: 300;\n  line-height: 1.2; }\n  @media (min-width: 1200px) {\n    .display-3 {\n      font-size: 4rem; } }\n.display-4 {\n  font-size: calc(1.475rem + 2.7vw);\n  font-weight: 300;\n  line-height: 1.2; }\n  @media (min-width: 1200px) {\n    .display-4 {\n      font-size: 3.5rem; } }\n.display-5 {\n  font-size: calc(1.425rem + 2.1vw);\n  font-weight: 300;\n  line-height: 1.2; }\n  @media (min-width: 1200px) {\n    .display-5 {\n      font-size: 3rem; } }\n.display-6 {\n  font-size: calc(1.375rem + 1.5vw);\n  font-weight: 300;\n  line-height: 1.2; }\n  @media (min-width: 1200px) {\n    .display-6 {\n      font-size: 2.5rem; } }\n.list-unstyled, ul.list-star li, ul.list-package li, ul.list-speech-balloon li, ul.list-books li, ul.list-toolbox li, .comment-list {\n  padding-left: 0;\n  list-style: none; }\n\n.list-inline {\n  padding-left: 0;\n  list-style: none; }\n\n.list-inline-item {\n  display: inline-block; }\n  .list-inline-item:not(:last-child) {\n    margin-right: 0.5rem; }\n\n.initialism {\n  font-size: 0.875em;\n  text-transform: uppercase; }\n\n.blockquote {\n  margin-bottom: 1rem;\n  font-size: 1.25rem; }\n  .blockquote > :last-child {\n    margin-bottom: 0; }\n\n.blockquote-footer {\n  margin-top: -1rem;\n  margin-bottom: 1rem;\n  font-size: 0.875em;\n  color: #6c757d; }\n  .blockquote-footer::before {\n    content: \"\\2014\\00A0\"; }\n\n.img-fluid {\n  max-width: 100%;\n  height: auto; }\n\n.img-thumbnail {\n  padding: 0.25rem;\n  background-color: var(--bs-body-bg);\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n  max-width: 100%;\n  height: auto; }\n\n.figure {\n  display: inline-block; }\n\n.figure-img {\n  margin-bottom: 0.5rem;\n  line-height: 1; }\n\n.figure-caption {\n  font-size: 0.875em;\n  color: var(--bs-secondary-color); }\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n  --bs-gutter-x: 3rem;\n  --bs-gutter-y: 0;\n  width: 100%;\n  padding-right: calc(var(--bs-gutter-x) * .5);\n  padding-left: calc(var(--bs-gutter-x) * .5);\n  margin-right: auto;\n  margin-left: auto; }\n\n@media (min-width: 576px) {\n  .container-sm, .container {\n    max-width: 540px; } }\n\n@media (min-width: 768px) {\n  .container-md, .container-sm, .container {\n    max-width: 720px; } }\n\n@media (min-width: 992px) {\n  .container-lg, .container-md, .container-sm, .container {\n    max-width: 960px; } }\n\n@media (min-width: 1200px) {\n  .container-xl, .container-lg, .container-md, .container-sm, .container {\n    max-width: 1240px; } }\n\n@media (min-width: 1400px) {\n  .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n    max-width: 1820px; } }\n\n:root {\n  --bs-breakpoint-xs: 0;\n  --bs-breakpoint-sm: 576px;\n  --bs-breakpoint-md: 768px;\n  --bs-breakpoint-lg: 992px;\n  --bs-breakpoint-xl: 1200px;\n  --bs-breakpoint-xxl: 1400px; }\n\n.row {\n  --bs-gutter-x: 3rem;\n  --bs-gutter-y: 0;\n  display: flex;\n  flex-wrap: wrap;\n  margin-top: calc(-1 * var(--bs-gutter-y));\n  margin-right: calc(-.5 * var(--bs-gutter-x));\n  margin-left: calc(-.5 * var(--bs-gutter-x)); }\n  .row > * {\n    flex-shrink: 0;\n    width: 100%;\n    max-width: 100%;\n    padding-right: calc(var(--bs-gutter-x) * .5);\n    padding-left: calc(var(--bs-gutter-x) * .5);\n    margin-top: var(--bs-gutter-y); }\n\n.col {\n  flex: 1 0 0%; }\n\n.row-cols-auto > * {\n  flex: 0 0 auto;\n  width: auto; }\n\n.row-cols-1 > * {\n  flex: 0 0 auto;\n  width: 100%; }\n\n.row-cols-2 > * {\n  flex: 0 0 auto;\n  width: 50%; }\n\n.row-cols-3 > * {\n  flex: 0 0 auto;\n  width: 33.33333333%; }\n\n.row-cols-4 > * {\n  flex: 0 0 auto;\n  width: 25%; }\n\n.row-cols-5 > * {\n  flex: 0 0 auto;\n  width: 20%; }\n\n.row-cols-6 > * {\n  flex: 0 0 auto;\n  width: 16.66666667%; }\n\n.col-auto {\n  flex: 0 0 auto;\n  width: auto; }\n\n.col-1 {\n  flex: 0 0 auto;\n  width: 6.25%; }\n\n.col-2 {\n  flex: 0 0 auto;\n  width: 12.5%; }\n\n.col-3 {\n  flex: 0 0 auto;\n  width: 18.75%; }\n\n.col-4 {\n  flex: 0 0 auto;\n  width: 25%; }\n\n.col-5 {\n  flex: 0 0 auto;\n  width: 31.25%; }\n\n.col-6 {\n  flex: 0 0 auto;\n  width: 37.5%; }\n\n.col-7 {\n  flex: 0 0 auto;\n  width: 43.75%; }\n\n.col-8 {\n  flex: 0 0 auto;\n  width: 50%; }\n\n.col-9 {\n  flex: 0 0 auto;\n  width: 56.25%; }\n\n.col-10 {\n  flex: 0 0 auto;\n  width: 62.5%; }\n\n.col-11 {\n  flex: 0 0 auto;\n  width: 68.75%; }\n\n.col-12 {\n  flex: 0 0 auto;\n  width: 75%; }\n\n.col-13 {\n  flex: 0 0 auto;\n  width: 81.25%; }\n\n.col-14 {\n  flex: 0 0 auto;\n  width: 87.5%; }\n\n.col-15 {\n  flex: 0 0 auto;\n  width: 93.75%; }\n\n.col-16 {\n  flex: 0 0 auto;\n  width: 100%; }\n\n.offset-1 {\n  margin-left: 6.25%; }\n\n.offset-2 {\n  margin-left: 12.5%; }\n\n.offset-3 {\n  margin-left: 18.75%; }\n\n.offset-4 {\n  margin-left: 25%; }\n\n.offset-5 {\n  margin-left: 31.25%; }\n\n.offset-6 {\n  margin-left: 37.5%; }\n\n.offset-7 {\n  margin-left: 43.75%; }\n\n.offset-8 {\n  margin-left: 50%; }\n\n.offset-9 {\n  margin-left: 56.25%; }\n\n.offset-10 {\n  margin-left: 62.5%; }\n\n.offset-11 {\n  margin-left: 68.75%; }\n\n.offset-12 {\n  margin-left: 75%; }\n\n.offset-13 {\n  margin-left: 81.25%; }\n\n.offset-14 {\n  margin-left: 87.5%; }\n\n.offset-15 {\n  margin-left: 93.75%; }\n\n.g-0,\n.gx-0 {\n  --bs-gutter-x: 0; }\n\n.g-0,\n.gy-0 {\n  --bs-gutter-y: 0; }\n\n.g-1,\n.gx-1 {\n  --bs-gutter-x: 0.25rem; }\n\n.g-1,\n.gy-1 {\n  --bs-gutter-y: 0.25rem; }\n\n.g-2,\n.gx-2 {\n  --bs-gutter-x: 0.5rem; }\n\n.g-2,\n.gy-2 {\n  --bs-gutter-y: 0.5rem; }\n\n.g-3,\n.gx-3 {\n  --bs-gutter-x: 1rem; }\n\n.g-3,\n.gy-3 {\n  --bs-gutter-y: 1rem; }\n\n.g-4,\n.gx-4 {\n  --bs-gutter-x: 1.5rem; }\n\n.g-4,\n.gy-4 {\n  --bs-gutter-y: 1.5rem; }\n\n.g-5,\n.gx-5 {\n  --bs-gutter-x: 3rem; }\n\n.g-5,\n.gy-5 {\n  --bs-gutter-y: 3rem; }\n\n@media (min-width: 576px) {\n  .col-sm {\n    flex: 1 0 0%; }\n  .row-cols-sm-auto > * {\n    flex: 0 0 auto;\n    width: auto; }\n  .row-cols-sm-1 > * {\n    flex: 0 0 auto;\n    width: 100%; }\n  .row-cols-sm-2 > * {\n    flex: 0 0 auto;\n    width: 50%; }\n  .row-cols-sm-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%; }\n  .row-cols-sm-4 > * {\n    flex: 0 0 auto;\n    width: 25%; }\n  .row-cols-sm-5 > * {\n    flex: 0 0 auto;\n    width: 20%; }\n  .row-cols-sm-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%; }\n  .col-sm-auto {\n    flex: 0 0 auto;\n    width: auto; }\n  .col-sm-1 {\n    flex: 0 0 auto;\n    width: 6.25%; }\n  .col-sm-2 {\n    flex: 0 0 auto;\n    width: 12.5%; }\n  .col-sm-3 {\n    flex: 0 0 auto;\n    width: 18.75%; }\n  .col-sm-4 {\n    flex: 0 0 auto;\n    width: 25%; }\n  .col-sm-5 {\n    flex: 0 0 auto;\n    width: 31.25%; }\n  .col-sm-6 {\n    flex: 0 0 auto;\n    width: 37.5%; }\n  .col-sm-7 {\n    flex: 0 0 auto;\n    width: 43.75%; }\n  .col-sm-8 {\n    flex: 0 0 auto;\n    width: 50%; }\n  .col-sm-9 {\n    flex: 0 0 auto;\n    width: 56.25%; }\n  .col-sm-10 {\n    flex: 0 0 auto;\n    width: 62.5%; }\n  .col-sm-11 {\n    flex: 0 0 auto;\n    width: 68.75%; }\n  .col-sm-12 {\n    flex: 0 0 auto;\n    width: 75%; }\n  .col-sm-13 {\n    flex: 0 0 auto;\n    width: 81.25%; }\n  .col-sm-14 {\n    flex: 0 0 auto;\n    width: 87.5%; }\n  .col-sm-15 {\n    flex: 0 0 auto;\n    width: 93.75%; }\n  .col-sm-16 {\n    flex: 0 0 auto;\n    width: 100%; }\n  .offset-sm-0 {\n    margin-left: 0; }\n  .offset-sm-1 {\n    margin-left: 6.25%; }\n  .offset-sm-2 {\n    margin-left: 12.5%; }\n  .offset-sm-3 {\n    margin-left: 18.75%; }\n  .offset-sm-4 {\n    margin-left: 25%; }\n  .offset-sm-5 {\n    margin-left: 31.25%; }\n  .offset-sm-6 {\n    margin-left: 37.5%; }\n  .offset-sm-7 {\n    margin-left: 43.75%; }\n  .offset-sm-8 {\n    margin-left: 50%; }\n  .offset-sm-9 {\n    margin-left: 56.25%; }\n  .offset-sm-10 {\n    margin-left: 62.5%; }\n  .offset-sm-11 {\n    margin-left: 68.75%; }\n  .offset-sm-12 {\n    margin-left: 75%; }\n  .offset-sm-13 {\n    margin-left: 81.25%; }\n  .offset-sm-14 {\n    margin-left: 87.5%; }\n  .offset-sm-15 {\n    margin-left: 93.75%; }\n  .g-sm-0,\n  .gx-sm-0 {\n    --bs-gutter-x: 0; }\n  .g-sm-0,\n  .gy-sm-0 {\n    --bs-gutter-y: 0; }\n  .g-sm-1,\n  .gx-sm-1 {\n    --bs-gutter-x: 0.25rem; }\n  .g-sm-1,\n  .gy-sm-1 {\n    --bs-gutter-y: 0.25rem; }\n  .g-sm-2,\n  .gx-sm-2 {\n    --bs-gutter-x: 0.5rem; }\n  .g-sm-2,\n  .gy-sm-2 {\n    --bs-gutter-y: 0.5rem; }\n  .g-sm-3,\n  .gx-sm-3 {\n    --bs-gutter-x: 1rem; }\n  .g-sm-3,\n  .gy-sm-3 {\n    --bs-gutter-y: 1rem; }\n  .g-sm-4,\n  .gx-sm-4 {\n    --bs-gutter-x: 1.5rem; }\n  .g-sm-4,\n  .gy-sm-4 {\n    --bs-gutter-y: 1.5rem; }\n  .g-sm-5,\n  .gx-sm-5 {\n    --bs-gutter-x: 3rem; }\n  .g-sm-5,\n  .gy-sm-5 {\n    --bs-gutter-y: 3rem; } }\n\n@media (min-width: 768px) {\n  .col-md {\n    flex: 1 0 0%; }\n  .row-cols-md-auto > * {\n    flex: 0 0 auto;\n    width: auto; }\n  .row-cols-md-1 > * {\n    flex: 0 0 auto;\n    width: 100%; }\n  .row-cols-md-2 > * {\n    flex: 0 0 auto;\n    width: 50%; }\n  .row-cols-md-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%; }\n  .row-cols-md-4 > * {\n    flex: 0 0 auto;\n    width: 25%; }\n  .row-cols-md-5 > * {\n    flex: 0 0 auto;\n    width: 20%; }\n  .row-cols-md-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%; }\n  .col-md-auto {\n    flex: 0 0 auto;\n    width: auto; }\n  .col-md-1 {\n    flex: 0 0 auto;\n    width: 6.25%; }\n  .col-md-2 {\n    flex: 0 0 auto;\n    width: 12.5%; }\n  .col-md-3 {\n    flex: 0 0 auto;\n    width: 18.75%; }\n  .col-md-4 {\n    flex: 0 0 auto;\n    width: 25%; }\n  .col-md-5 {\n    flex: 0 0 auto;\n    width: 31.25%; }\n  .col-md-6 {\n    flex: 0 0 auto;\n    width: 37.5%; }\n  .col-md-7 {\n    flex: 0 0 auto;\n    width: 43.75%; }\n  .col-md-8 {\n    flex: 0 0 auto;\n    width: 50%; }\n  .col-md-9 {\n    flex: 0 0 auto;\n    width: 56.25%; }\n  .col-md-10 {\n    flex: 0 0 auto;\n    width: 62.5%; }\n  .col-md-11 {\n    flex: 0 0 auto;\n    width: 68.75%; }\n  .col-md-12 {\n    flex: 0 0 auto;\n    width: 75%; }\n  .col-md-13 {\n    flex: 0 0 auto;\n    width: 81.25%; }\n  .col-md-14 {\n    flex: 0 0 auto;\n    width: 87.5%; }\n  .col-md-15 {\n    flex: 0 0 auto;\n    width: 93.75%; }\n  .col-md-16 {\n    flex: 0 0 auto;\n    width: 100%; }\n  .offset-md-0 {\n    margin-left: 0; }\n  .offset-md-1 {\n    margin-left: 6.25%; }\n  .offset-md-2 {\n    margin-left: 12.5%; }\n  .offset-md-3 {\n    margin-left: 18.75%; }\n  .offset-md-4 {\n    margin-left: 25%; }\n  .offset-md-5 {\n    margin-left: 31.25%; }\n  .offset-md-6 {\n    margin-left: 37.5%; }\n  .offset-md-7 {\n    margin-left: 43.75%; }\n  .offset-md-8 {\n    margin-left: 50%; }\n  .offset-md-9 {\n    margin-left: 56.25%; }\n  .offset-md-10 {\n    margin-left: 62.5%; }\n  .offset-md-11 {\n    margin-left: 68.75%; }\n  .offset-md-12 {\n    margin-left: 75%; }\n  .offset-md-13 {\n    margin-left: 81.25%; }\n  .offset-md-14 {\n    margin-left: 87.5%; }\n  .offset-md-15 {\n    margin-left: 93.75%; }\n  .g-md-0,\n  .gx-md-0 {\n    --bs-gutter-x: 0; }\n  .g-md-0,\n  .gy-md-0 {\n    --bs-gutter-y: 0; }\n  .g-md-1,\n  .gx-md-1 {\n    --bs-gutter-x: 0.25rem; }\n  .g-md-1,\n  .gy-md-1 {\n    --bs-gutter-y: 0.25rem; }\n  .g-md-2,\n  .gx-md-2 {\n    --bs-gutter-x: 0.5rem; }\n  .g-md-2,\n  .gy-md-2 {\n    --bs-gutter-y: 0.5rem; }\n  .g-md-3,\n  .gx-md-3 {\n    --bs-gutter-x: 1rem; }\n  .g-md-3,\n  .gy-md-3 {\n    --bs-gutter-y: 1rem; }\n  .g-md-4,\n  .gx-md-4 {\n    --bs-gutter-x: 1.5rem; }\n  .g-md-4,\n  .gy-md-4 {\n    --bs-gutter-y: 1.5rem; }\n  .g-md-5,\n  .gx-md-5 {\n    --bs-gutter-x: 3rem; }\n  .g-md-5,\n  .gy-md-5 {\n    --bs-gutter-y: 3rem; } }\n\n@media (min-width: 992px) {\n  .col-lg {\n    flex: 1 0 0%; }\n  .row-cols-lg-auto > * {\n    flex: 0 0 auto;\n    width: auto; }\n  .row-cols-lg-1 > * {\n    flex: 0 0 auto;\n    width: 100%; }\n  .row-cols-lg-2 > * {\n    flex: 0 0 auto;\n    width: 50%; }\n  .row-cols-lg-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%; }\n  .row-cols-lg-4 > * {\n    flex: 0 0 auto;\n    width: 25%; }\n  .row-cols-lg-5 > * {\n    flex: 0 0 auto;\n    width: 20%; }\n  .row-cols-lg-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%; }\n  .col-lg-auto {\n    flex: 0 0 auto;\n    width: auto; }\n  .col-lg-1 {\n    flex: 0 0 auto;\n    width: 6.25%; }\n  .col-lg-2 {\n    flex: 0 0 auto;\n    width: 12.5%; }\n  .col-lg-3 {\n    flex: 0 0 auto;\n    width: 18.75%; }\n  .col-lg-4 {\n    flex: 0 0 auto;\n    width: 25%; }\n  .col-lg-5 {\n    flex: 0 0 auto;\n    width: 31.25%; }\n  .col-lg-6 {\n    flex: 0 0 auto;\n    width: 37.5%; }\n  .col-lg-7 {\n    flex: 0 0 auto;\n    width: 43.75%; }\n  .col-lg-8 {\n    flex: 0 0 auto;\n    width: 50%; }\n  .col-lg-9 {\n    flex: 0 0 auto;\n    width: 56.25%; }\n  .col-lg-10 {\n    flex: 0 0 auto;\n    width: 62.5%; }\n  .col-lg-11 {\n    flex: 0 0 auto;\n    width: 68.75%; }\n  .col-lg-12 {\n    flex: 0 0 auto;\n    width: 75%; }\n  .col-lg-13 {\n    flex: 0 0 auto;\n    width: 81.25%; }\n  .col-lg-14 {\n    flex: 0 0 auto;\n    width: 87.5%; }\n  .col-lg-15 {\n    flex: 0 0 auto;\n    width: 93.75%; }\n  .col-lg-16 {\n    flex: 0 0 auto;\n    width: 100%; }\n  .offset-lg-0 {\n    margin-left: 0; }\n  .offset-lg-1 {\n    margin-left: 6.25%; }\n  .offset-lg-2 {\n    margin-left: 12.5%; }\n  .offset-lg-3 {\n    margin-left: 18.75%; }\n  .offset-lg-4 {\n    margin-left: 25%; }\n  .offset-lg-5 {\n    margin-left: 31.25%; }\n  .offset-lg-6 {\n    margin-left: 37.5%; }\n  .offset-lg-7 {\n    margin-left: 43.75%; }\n  .offset-lg-8 {\n    margin-left: 50%; }\n  .offset-lg-9 {\n    margin-left: 56.25%; }\n  .offset-lg-10 {\n    margin-left: 62.5%; }\n  .offset-lg-11 {\n    margin-left: 68.75%; }\n  .offset-lg-12 {\n    margin-left: 75%; }\n  .offset-lg-13 {\n    margin-left: 81.25%; }\n  .offset-lg-14 {\n    margin-left: 87.5%; }\n  .offset-lg-15 {\n    margin-left: 93.75%; }\n  .g-lg-0,\n  .gx-lg-0 {\n    --bs-gutter-x: 0; }\n  .g-lg-0,\n  .gy-lg-0 {\n    --bs-gutter-y: 0; }\n  .g-lg-1,\n  .gx-lg-1 {\n    --bs-gutter-x: 0.25rem; }\n  .g-lg-1,\n  .gy-lg-1 {\n    --bs-gutter-y: 0.25rem; }\n  .g-lg-2,\n  .gx-lg-2 {\n    --bs-gutter-x: 0.5rem; }\n  .g-lg-2,\n  .gy-lg-2 {\n    --bs-gutter-y: 0.5rem; }\n  .g-lg-3,\n  .gx-lg-3 {\n    --bs-gutter-x: 1rem; }\n  .g-lg-3,\n  .gy-lg-3 {\n    --bs-gutter-y: 1rem; }\n  .g-lg-4,\n  .gx-lg-4 {\n    --bs-gutter-x: 1.5rem; }\n  .g-lg-4,\n  .gy-lg-4 {\n    --bs-gutter-y: 1.5rem; }\n  .g-lg-5,\n  .gx-lg-5 {\n    --bs-gutter-x: 3rem; }\n  .g-lg-5,\n  .gy-lg-5 {\n    --bs-gutter-y: 3rem; } }\n\n@media (min-width: 1200px) {\n  .col-xl {\n    flex: 1 0 0%; }\n  .row-cols-xl-auto > * {\n    flex: 0 0 auto;\n    width: auto; }\n  .row-cols-xl-1 > * {\n    flex: 0 0 auto;\n    width: 100%; }\n  .row-cols-xl-2 > * {\n    flex: 0 0 auto;\n    width: 50%; }\n  .row-cols-xl-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%; }\n  .row-cols-xl-4 > * {\n    flex: 0 0 auto;\n    width: 25%; }\n  .row-cols-xl-5 > * {\n    flex: 0 0 auto;\n    width: 20%; }\n  .row-cols-xl-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%; }\n  .col-xl-auto {\n    flex: 0 0 auto;\n    width: auto; }\n  .col-xl-1 {\n    flex: 0 0 auto;\n    width: 6.25%; }\n  .col-xl-2 {\n    flex: 0 0 auto;\n    width: 12.5%; }\n  .col-xl-3 {\n    flex: 0 0 auto;\n    width: 18.75%; }\n  .col-xl-4 {\n    flex: 0 0 auto;\n    width: 25%; }\n  .col-xl-5 {\n    flex: 0 0 auto;\n    width: 31.25%; }\n  .col-xl-6 {\n    flex: 0 0 auto;\n    width: 37.5%; }\n  .col-xl-7 {\n    flex: 0 0 auto;\n    width: 43.75%; }\n  .col-xl-8 {\n    flex: 0 0 auto;\n    width: 50%; }\n  .col-xl-9 {\n    flex: 0 0 auto;\n    width: 56.25%; }\n  .col-xl-10 {\n    flex: 0 0 auto;\n    width: 62.5%; }\n  .col-xl-11 {\n    flex: 0 0 auto;\n    width: 68.75%; }\n  .col-xl-12 {\n    flex: 0 0 auto;\n    width: 75%; }\n  .col-xl-13 {\n    flex: 0 0 auto;\n    width: 81.25%; }\n  .col-xl-14 {\n    flex: 0 0 auto;\n    width: 87.5%; }\n  .col-xl-15 {\n    flex: 0 0 auto;\n    width: 93.75%; }\n  .col-xl-16 {\n    flex: 0 0 auto;\n    width: 100%; }\n  .offset-xl-0 {\n    margin-left: 0; }\n  .offset-xl-1 {\n    margin-left: 6.25%; }\n  .offset-xl-2 {\n    margin-left: 12.5%; }\n  .offset-xl-3 {\n    margin-left: 18.75%; }\n  .offset-xl-4 {\n    margin-left: 25%; }\n  .offset-xl-5 {\n    margin-left: 31.25%; }\n  .offset-xl-6 {\n    margin-left: 37.5%; }\n  .offset-xl-7 {\n    margin-left: 43.75%; }\n  .offset-xl-8 {\n    margin-left: 50%; }\n  .offset-xl-9 {\n    margin-left: 56.25%; }\n  .offset-xl-10 {\n    margin-left: 62.5%; }\n  .offset-xl-11 {\n    margin-left: 68.75%; }\n  .offset-xl-12 {\n    margin-left: 75%; }\n  .offset-xl-13 {\n    margin-left: 81.25%; }\n  .offset-xl-14 {\n    margin-left: 87.5%; }\n  .offset-xl-15 {\n    margin-left: 93.75%; }\n  .g-xl-0,\n  .gx-xl-0 {\n    --bs-gutter-x: 0; }\n  .g-xl-0,\n  .gy-xl-0 {\n    --bs-gutter-y: 0; }\n  .g-xl-1,\n  .gx-xl-1 {\n    --bs-gutter-x: 0.25rem; }\n  .g-xl-1,\n  .gy-xl-1 {\n    --bs-gutter-y: 0.25rem; }\n  .g-xl-2,\n  .gx-xl-2 {\n    --bs-gutter-x: 0.5rem; }\n  .g-xl-2,\n  .gy-xl-2 {\n    --bs-gutter-y: 0.5rem; }\n  .g-xl-3,\n  .gx-xl-3 {\n    --bs-gutter-x: 1rem; }\n  .g-xl-3,\n  .gy-xl-3 {\n    --bs-gutter-y: 1rem; }\n  .g-xl-4,\n  .gx-xl-4 {\n    --bs-gutter-x: 1.5rem; }\n  .g-xl-4,\n  .gy-xl-4 {\n    --bs-gutter-y: 1.5rem; }\n  .g-xl-5,\n  .gx-xl-5 {\n    --bs-gutter-x: 3rem; }\n  .g-xl-5,\n  .gy-xl-5 {\n    --bs-gutter-y: 3rem; } }\n\n@media (min-width: 1400px) {\n  .col-xxl {\n    flex: 1 0 0%; }\n  .row-cols-xxl-auto > * {\n    flex: 0 0 auto;\n    width: auto; }\n  .row-cols-xxl-1 > * {\n    flex: 0 0 auto;\n    width: 100%; }\n  .row-cols-xxl-2 > * {\n    flex: 0 0 auto;\n    width: 50%; }\n  .row-cols-xxl-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%; }\n  .row-cols-xxl-4 > * {\n    flex: 0 0 auto;\n    width: 25%; }\n  .row-cols-xxl-5 > * {\n    flex: 0 0 auto;\n    width: 20%; }\n  .row-cols-xxl-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%; }\n  .col-xxl-auto {\n    flex: 0 0 auto;\n    width: auto; }\n  .col-xxl-1 {\n    flex: 0 0 auto;\n    width: 6.25%; }\n  .col-xxl-2 {\n    flex: 0 0 auto;\n    width: 12.5%; }\n  .col-xxl-3 {\n    flex: 0 0 auto;\n    width: 18.75%; }\n  .col-xxl-4 {\n    flex: 0 0 auto;\n    width: 25%; }\n  .col-xxl-5 {\n    flex: 0 0 auto;\n    width: 31.25%; }\n  .col-xxl-6 {\n    flex: 0 0 auto;\n    width: 37.5%; }\n  .col-xxl-7 {\n    flex: 0 0 auto;\n    width: 43.75%; }\n  .col-xxl-8 {\n    flex: 0 0 auto;\n    width: 50%; }\n  .col-xxl-9 {\n    flex: 0 0 auto;\n    width: 56.25%; }\n  .col-xxl-10 {\n    flex: 0 0 auto;\n    width: 62.5%; }\n  .col-xxl-11 {\n    flex: 0 0 auto;\n    width: 68.75%; }\n  .col-xxl-12 {\n    flex: 0 0 auto;\n    width: 75%; }\n  .col-xxl-13 {\n    flex: 0 0 auto;\n    width: 81.25%; }\n  .col-xxl-14 {\n    flex: 0 0 auto;\n    width: 87.5%; }\n  .col-xxl-15 {\n    flex: 0 0 auto;\n    width: 93.75%; }\n  .col-xxl-16 {\n    flex: 0 0 auto;\n    width: 100%; }\n  .offset-xxl-0 {\n    margin-left: 0; }\n  .offset-xxl-1 {\n    margin-left: 6.25%; }\n  .offset-xxl-2 {\n    margin-left: 12.5%; }\n  .offset-xxl-3 {\n    margin-left: 18.75%; }\n  .offset-xxl-4 {\n    margin-left: 25%; }\n  .offset-xxl-5 {\n    margin-left: 31.25%; }\n  .offset-xxl-6 {\n    margin-left: 37.5%; }\n  .offset-xxl-7 {\n    margin-left: 43.75%; }\n  .offset-xxl-8 {\n    margin-left: 50%; }\n  .offset-xxl-9 {\n    margin-left: 56.25%; }\n  .offset-xxl-10 {\n    margin-left: 62.5%; }\n  .offset-xxl-11 {\n    margin-left: 68.75%; }\n  .offset-xxl-12 {\n    margin-left: 75%; }\n  .offset-xxl-13 {\n    margin-left: 81.25%; }\n  .offset-xxl-14 {\n    margin-left: 87.5%; }\n  .offset-xxl-15 {\n    margin-left: 93.75%; }\n  .g-xxl-0,\n  .gx-xxl-0 {\n    --bs-gutter-x: 0; }\n  .g-xxl-0,\n  .gy-xxl-0 {\n    --bs-gutter-y: 0; }\n  .g-xxl-1,\n  .gx-xxl-1 {\n    --bs-gutter-x: 0.25rem; }\n  .g-xxl-1,\n  .gy-xxl-1 {\n    --bs-gutter-y: 0.25rem; }\n  .g-xxl-2,\n  .gx-xxl-2 {\n    --bs-gutter-x: 0.5rem; }\n  .g-xxl-2,\n  .gy-xxl-2 {\n    --bs-gutter-y: 0.5rem; }\n  .g-xxl-3,\n  .gx-xxl-3 {\n    --bs-gutter-x: 1rem; }\n  .g-xxl-3,\n  .gy-xxl-3 {\n    --bs-gutter-y: 1rem; }\n  .g-xxl-4,\n  .gx-xxl-4 {\n    --bs-gutter-x: 1.5rem; }\n  .g-xxl-4,\n  .gy-xxl-4 {\n    --bs-gutter-y: 1.5rem; }\n  .g-xxl-5,\n  .gx-xxl-5 {\n    --bs-gutter-x: 3rem; }\n  .g-xxl-5,\n  .gy-xxl-5 {\n    --bs-gutter-y: 3rem; } }\n\n.clearfix::after {\n  display: block;\n  clear: both;\n  content: \"\"; }\n\n.text-bg-primary {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-secondary {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-success {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-info {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-warning {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-danger {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-light {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.text-bg-dark {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important; }\n\n.link-primary {\n  color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-primary:hover, .link-primary:focus {\n    color: RGBA(63, 56, 183, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(63, 56, 183, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-secondary {\n  color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-secondary:hover, .link-secondary:focus {\n    color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-success {\n  color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-success:hover, .link-success:focus {\n    color: RGBA(157, 241, 118, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(157, 241, 118, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-info {\n  color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-info:hover, .link-info:focus {\n    color: RGBA(41, 57, 204, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(41, 57, 204, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-warning {\n  color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-warning:hover, .link-warning:focus {\n    color: RGBA(241, 202, 118, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(241, 202, 118, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-danger {\n  color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-danger:hover, .link-danger:focus {\n    color: RGBA(241, 118, 161, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(241, 118, 161, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-light {\n  color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-light:hover, .link-light:focus {\n    color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-dark {\n  color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-dark:hover, .link-dark:focus {\n    color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;\n    text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-body-emphasis {\n  color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n  .link-body-emphasis:hover, .link-body-emphasis:focus {\n    color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;\n    text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; }\n\n.focus-ring:focus {\n  outline: 0;\n  box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); }\n\n.icon-link {\n  display: inline-flex;\n  gap: 0.375rem;\n  align-items: center;\n  text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));\n  text-underline-offset: 0.25em;\n  backface-visibility: hidden; }\n  .icon-link > .bi {\n    flex-shrink: 0;\n    width: 1em;\n    height: 1em;\n    fill: currentcolor;\n    transition: 0.2s ease-in-out transform; }\n    @media (prefers-reduced-motion: reduce) {\n      .icon-link > .bi {\n        transition: none; } }\n.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi {\n  transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0)); }\n\n.ratio {\n  position: relative;\n  width: 100%; }\n  .ratio::before {\n    display: block;\n    padding-top: var(--bs-aspect-ratio);\n    content: \"\"; }\n  .ratio > * {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%; }\n\n.ratio-1x1 {\n  --bs-aspect-ratio: 100%; }\n\n.ratio-4x3 {\n  --bs-aspect-ratio: calc(3 / 4 * 100%); }\n\n.ratio-16x9 {\n  --bs-aspect-ratio: calc(9 / 16 * 100%); }\n\n.ratio-21x9 {\n  --bs-aspect-ratio: calc(9 / 21 * 100%); }\n\n.fixed-top {\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  z-index: 1030; }\n\n.fixed-bottom {\n  position: fixed;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1030; }\n\n.sticky-top {\n  position: sticky;\n  top: 0;\n  z-index: 1020; }\n\n.sticky-bottom {\n  position: sticky;\n  bottom: 0;\n  z-index: 1020; }\n\n@media (min-width: 576px) {\n  .sticky-sm-top {\n    position: sticky;\n    top: 0;\n    z-index: 1020; }\n  .sticky-sm-bottom {\n    position: sticky;\n    bottom: 0;\n    z-index: 1020; } }\n\n@media (min-width: 768px) {\n  .sticky-md-top {\n    position: sticky;\n    top: 0;\n    z-index: 1020; }\n  .sticky-md-bottom {\n    position: sticky;\n    bottom: 0;\n    z-index: 1020; } }\n\n@media (min-width: 992px) {\n  .sticky-lg-top {\n    position: sticky;\n    top: 0;\n    z-index: 1020; }\n  .sticky-lg-bottom {\n    position: sticky;\n    bottom: 0;\n    z-index: 1020; } }\n\n@media (min-width: 1200px) {\n  .sticky-xl-top {\n    position: sticky;\n    top: 0;\n    z-index: 1020; }\n  .sticky-xl-bottom {\n    position: sticky;\n    bottom: 0;\n    z-index: 1020; } }\n\n@media (min-width: 1400px) {\n  .sticky-xxl-top {\n    position: sticky;\n    top: 0;\n    z-index: 1020; }\n  .sticky-xxl-bottom {\n    position: sticky;\n    bottom: 0;\n    z-index: 1020; } }\n\n.hstack {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  align-self: stretch; }\n\n.vstack {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n  align-self: stretch; }\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n  width: 1px !important;\n  height: 1px !important;\n  padding: 0 !important;\n  margin: -1px !important;\n  overflow: hidden !important;\n  clip: rect(0, 0, 0, 0) !important;\n  white-space: nowrap !important;\n  border: 0 !important; }\n  .visually-hidden:not(caption),\n  .visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {\n    position: absolute !important; }\n\n.stretched-link::after {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1;\n  content: \"\"; }\n\n.text-truncate {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap; }\n\n.vr {\n  display: inline-block;\n  align-self: stretch;\n  width: var(--bs-border-width);\n  min-height: 1em;\n  background-color: currentcolor;\n  opacity: 0.25; }\n\n.table, table {\n  --bs-table-color-type: initial;\n  --bs-table-bg-type: initial;\n  --bs-table-color-state: initial;\n  --bs-table-bg-state: initial;\n  --bs-table-color: var(--bs-emphasis-color);\n  --bs-table-bg: var(--bs-body-bg);\n  --bs-table-border-color: var(--bs-border-color);\n  --bs-table-accent-bg: transparent;\n  --bs-table-striped-color: var(--bs-emphasis-color);\n  --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);\n  --bs-table-active-color: var(--bs-emphasis-color);\n  --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);\n  --bs-table-hover-color: var(--bs-emphasis-color);\n  --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);\n  width: 100%;\n  margin-bottom: 1rem;\n  vertical-align: top;\n  border-color: var(--bs-table-border-color); }\n  .table > :not(caption) > * > *, table > :not(caption) > * > * {\n    padding: 0.5rem 0.5rem;\n    color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));\n    background-color: var(--bs-table-bg);\n    border-bottom-width: var(--bs-border-width);\n    box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); }\n  .table > tbody, table > tbody {\n    vertical-align: inherit; }\n  .table > thead, table > thead {\n    vertical-align: bottom; }\n\n.table-group-divider {\n  border-top: calc(var(--bs-border-width) * 2) solid currentcolor; }\n\n.caption-top {\n  caption-side: top; }\n\n.table-sm > :not(caption) > * > * {\n  padding: 0.25rem 0.25rem; }\n\n.table-bordered > :not(caption) > * {\n  border-width: var(--bs-border-width) 0; }\n  .table-bordered > :not(caption) > * > * {\n    border-width: 0 var(--bs-border-width); }\n\n.table-borderless > :not(caption) > * > * {\n  border-bottom-width: 0; }\n\n.table-borderless > :not(:first-child) {\n  border-top-width: 0; }\n\n.table-striped > tbody > tr:nth-of-type(odd) > * {\n  --bs-table-color-type: var(--bs-table-striped-color);\n  --bs-table-bg-type: var(--bs-table-striped-bg); }\n\n.table-striped-columns > :not(caption) > tr > :nth-child(even) {\n  --bs-table-color-type: var(--bs-table-striped-color);\n  --bs-table-bg-type: var(--bs-table-striped-bg); }\n\n.table-active {\n  --bs-table-color-state: var(--bs-table-active-color);\n  --bs-table-bg-state: var(--bs-table-active-bg); }\n\n.table-hover > tbody > tr:hover > * {\n  --bs-table-color-state: var(--bs-table-hover-color);\n  --bs-table-bg-state: var(--bs-table-hover-bg); }\n\n.table-primary {\n  --bs-table-color: #000;\n  --bs-table-bg: #dcdafa;\n  --bs-table-border-color: #b0aec8;\n  --bs-table-striped-bg: #d1cfee;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #c6c4e1;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #cccae7;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-secondary {\n  --bs-table-color: #000;\n  --bs-table-bg: #e2e3e5;\n  --bs-table-border-color: #b5b6b7;\n  --bs-table-striped-bg: #d7d8da;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #cbccce;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #d1d2d4;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-success {\n  --bs-table-color: #000;\n  --bs-table-bg: #e6fcdd;\n  --bs-table-border-color: #b8cab1;\n  --bs-table-striped-bg: #dbefd2;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #cfe3c7;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #d5e9cc;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-info {\n  --bs-table-color: #000;\n  --bs-table-bg: #d6daff;\n  --bs-table-border-color: #abaecc;\n  --bs-table-striped-bg: #cbcff2;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #c1c4e6;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #c6caec;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-warning {\n  --bs-table-color: #000;\n  --bs-table-bg: #fcf2dd;\n  --bs-table-border-color: #cac2b1;\n  --bs-table-striped-bg: #efe6d2;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #e3dac7;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #e9e0cc;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-danger {\n  --bs-table-color: #000;\n  --bs-table-bg: #fcdde7;\n  --bs-table-border-color: #cab1b9;\n  --bs-table-striped-bg: #efd2db;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #e3c7d0;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #e9ccd6;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-light {\n  --bs-table-color: #000;\n  --bs-table-bg: #f8f9fa;\n  --bs-table-border-color: #c6c7c8;\n  --bs-table-striped-bg: #ecedee;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #dfe0e1;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #e5e6e7;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-dark, [data-bs-theme=\"dark\"] table {\n  --bs-table-color: #fff;\n  --bs-table-bg: #212529;\n  --bs-table-border-color: #4d5154;\n  --bs-table-striped-bg: #2c3034;\n  --bs-table-striped-color: #fff;\n  --bs-table-active-bg: #373b3e;\n  --bs-table-active-color: #fff;\n  --bs-table-hover-bg: #323539;\n  --bs-table-hover-color: #fff;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color); }\n\n.table-responsive {\n  overflow-x: auto;\n  -webkit-overflow-scrolling: touch; }\n\n@media (max-width: 575.98px) {\n  .table-responsive-sm {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch; } }\n\n@media (max-width: 767.98px) {\n  .table-responsive-md {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch; } }\n\n@media (max-width: 991.98px) {\n  .table-responsive-lg {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch; } }\n\n@media (max-width: 1199.98px) {\n  .table-responsive-xl {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch; } }\n\n@media (max-width: 1399.98px) {\n  .table-responsive-xxl {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch; } }\n\n.form-label {\n  margin-bottom: 0.5rem; }\n\n.col-form-label {\n  padding-top: calc(0.375rem + var(--bs-border-width));\n  padding-bottom: calc(0.375rem + var(--bs-border-width));\n  margin-bottom: 0;\n  font-size: inherit;\n  line-height: 1.5; }\n\n.col-form-label-lg {\n  padding-top: calc(0.5rem + var(--bs-border-width));\n  padding-bottom: calc(0.5rem + var(--bs-border-width));\n  font-size: 1.25rem; }\n\n.col-form-label-sm {\n  padding-top: calc(0.25rem + var(--bs-border-width));\n  padding-bottom: calc(0.25rem + var(--bs-border-width));\n  font-size: 0.875rem; }\n\n.form-text {\n  margin-top: 0.25rem;\n  font-size: 0.875em;\n  color: var(--bs-secondary-color); }\n\n.form-control, .search-form .search-field, .comment-form input[type=\"text\"],\n.comment-form input[type=\"email\"],\n.comment-form input[type=\"url\"],\n.comment-form textarea {\n  display: block;\n  width: 100%;\n  padding: 0.375rem 0.75rem;\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  appearance: none;\n  background-color: var(--bs-body-bg);\n  background-clip: padding-box;\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; }\n  @media (prefers-reduced-motion: reduce) {\n    .form-control, .search-form .search-field, .comment-form input[type=\"text\"],\n    .comment-form input[type=\"email\"],\n    .comment-form input[type=\"url\"],\n    .comment-form textarea {\n      transition: none; } }\n  .form-control[type=\"file\"], .search-form [type=\"file\"].search-field, .comment-form input[type=\"file\"][type=\"text\"],\n  .comment-form input[type=\"file\"][type=\"email\"],\n  .comment-form input[type=\"file\"][type=\"url\"],\n  .comment-form textarea[type=\"file\"] {\n    overflow: hidden; }\n    .form-control[type=\"file\"]:not(:disabled):not([readonly]), .search-form [type=\"file\"].search-field:not(:disabled):not([readonly]), .comment-form input[type=\"file\"][type=\"text\"]:not(:disabled):not([readonly]),\n    .comment-form input[type=\"file\"][type=\"email\"]:not(:disabled):not([readonly]),\n    .comment-form input[type=\"file\"][type=\"url\"]:not(:disabled):not([readonly]),\n    .comment-form textarea[type=\"file\"]:not(:disabled):not([readonly]) {\n      cursor: pointer; }\n  .form-control:focus, .search-form .search-field:focus, .comment-form input[type=\"text\"]:focus,\n  .comment-form input[type=\"email\"]:focus,\n  .comment-form input[type=\"url\"]:focus,\n  .comment-form textarea:focus {\n    color: var(--bs-body-color);\n    background-color: var(--bs-body-bg);\n    border-color: #a7a3f2;\n    outline: 0;\n    box-shadow: none; }\n  .form-control::-webkit-date-and-time-value, .search-form .search-field::-webkit-date-and-time-value, .comment-form input[type=\"text\"]::-webkit-date-and-time-value,\n  .comment-form input[type=\"email\"]::-webkit-date-and-time-value,\n  .comment-form input[type=\"url\"]::-webkit-date-and-time-value,\n  .comment-form textarea::-webkit-date-and-time-value {\n    min-width: 85px;\n    height: 1.5em;\n    margin: 0; }\n  .form-control::-webkit-datetime-edit, .search-form .search-field::-webkit-datetime-edit, .comment-form input[type=\"text\"]::-webkit-datetime-edit,\n  .comment-form input[type=\"email\"]::-webkit-datetime-edit,\n  .comment-form input[type=\"url\"]::-webkit-datetime-edit,\n  .comment-form textarea::-webkit-datetime-edit {\n    display: block;\n    padding: 0; }\n  .form-control::placeholder, .search-form .search-field::placeholder, .comment-form input[type=\"text\"]::placeholder,\n  .comment-form input[type=\"email\"]::placeholder,\n  .comment-form input[type=\"url\"]::placeholder,\n  .comment-form textarea::placeholder {\n    color: var(--bs-secondary-color);\n    opacity: 1; }\n  .form-control:disabled, .search-form .search-field:disabled, .comment-form input[type=\"text\"]:disabled,\n  .comment-form input[type=\"email\"]:disabled,\n  .comment-form input[type=\"url\"]:disabled,\n  .comment-form textarea:disabled {\n    background-color: var(--bs-secondary-bg);\n    opacity: 1; }\n  .form-control::file-selector-button, .search-form .search-field::file-selector-button, .comment-form input[type=\"text\"]::file-selector-button,\n  .comment-form input[type=\"email\"]::file-selector-button,\n  .comment-form input[type=\"url\"]::file-selector-button,\n  .comment-form textarea::file-selector-button {\n    padding: 0.375rem 0.75rem;\n    margin: -0.375rem -0.75rem;\n    margin-inline-end: 0.75rem;\n    color: var(--bs-body-color);\n    background-color: var(--bs-tertiary-bg);\n    pointer-events: none;\n    border-color: inherit;\n    border-style: solid;\n    border-width: 0;\n    border-inline-end-width: var(--bs-border-width);\n    border-radius: 0;\n    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; }\n    @media (prefers-reduced-motion: reduce) {\n      .form-control::file-selector-button, .search-form .search-field::file-selector-button, .comment-form input[type=\"text\"]::file-selector-button,\n      .comment-form input[type=\"email\"]::file-selector-button,\n      .comment-form input[type=\"url\"]::file-selector-button,\n      .comment-form textarea::file-selector-button {\n        transition: none; } }\n  .form-control:hover:not(:disabled):not([readonly])::file-selector-button, .search-form .search-field:hover:not(:disabled):not([readonly])::file-selector-button, .comment-form input[type=\"text\"]:hover:not(:disabled):not([readonly])::file-selector-button,\n  .comment-form input[type=\"email\"]:hover:not(:disabled):not([readonly])::file-selector-button,\n  .comment-form input[type=\"url\"]:hover:not(:disabled):not([readonly])::file-selector-button,\n  .comment-form textarea:hover:not(:disabled):not([readonly])::file-selector-button {\n    background-color: var(--bs-secondary-bg); }\n\n.form-control-plaintext {\n  display: block;\n  width: 100%;\n  padding: 0.375rem 0;\n  margin-bottom: 0;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  background-color: transparent;\n  border: solid transparent;\n  border-width: var(--bs-border-width) 0; }\n  .form-control-plaintext:focus {\n    outline: 0; }\n  .form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n    padding-right: 0;\n    padding-left: 0; }\n\n.form-control-sm {\n  min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n  padding: 0.25rem 0.5rem;\n  font-size: 0.875rem;\n  border-radius: var(--bs-border-radius-sm); }\n  .form-control-sm::file-selector-button {\n    padding: 0.25rem 0.5rem;\n    margin: -0.25rem -0.5rem;\n    margin-inline-end: 0.5rem; }\n\n.form-control-lg {\n  min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n  padding: 0.5rem 1rem;\n  font-size: 1.25rem;\n  border-radius: var(--bs-border-radius-lg); }\n  .form-control-lg::file-selector-button {\n    padding: 0.5rem 1rem;\n    margin: -0.5rem -1rem;\n    margin-inline-end: 1rem; }\n\ntextarea.form-control, .search-form textarea.search-field {\n  min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); }\n\ntextarea.form-control-sm {\n  min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); }\n\ntextarea.form-control-lg {\n  min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); }\n\n.form-control-color {\n  width: 3rem;\n  height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n  padding: 0.375rem; }\n  .form-control-color:not(:disabled):not([readonly]) {\n    cursor: pointer; }\n  .form-control-color::-moz-color-swatch {\n    border: 0 !important;\n    border-radius: var(--bs-border-radius); }\n  .form-control-color::-webkit-color-swatch {\n    border: 0 !important;\n    border-radius: var(--bs-border-radius); }\n  .form-control-color.form-control-sm {\n    height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); }\n  .form-control-color.form-control-lg {\n    height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); }\n\n.form-select {\n  --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n  display: block;\n  width: 100%;\n  padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  appearance: none;\n  background-color: var(--bs-body-bg);\n  background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none);\n  background-repeat: no-repeat;\n  background-position: right 0.75rem center;\n  background-size: 16px 12px;\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; }\n  @media (prefers-reduced-motion: reduce) {\n    .form-select {\n      transition: none; } }\n  .form-select:focus {\n    border-color: #a7a3f2;\n    outline: 0;\n    box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.25); }\n  .form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n    padding-right: 0.75rem;\n    background-image: none; }\n  .form-select:disabled {\n    background-color: var(--bs-secondary-bg); }\n  .form-select:-moz-focusring {\n    color: transparent;\n    text-shadow: 0 0 0 var(--bs-body-color); }\n\n.form-select-sm {\n  padding-top: 0.25rem;\n  padding-bottom: 0.25rem;\n  padding-left: 0.5rem;\n  font-size: 0.875rem;\n  border-radius: var(--bs-border-radius-sm); }\n\n.form-select-lg {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  padding-left: 1rem;\n  font-size: 1.25rem;\n  border-radius: var(--bs-border-radius-lg); }\n\n[data-bs-theme=\"dark\"] .form-select {\n  --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23c1c3c8' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\"); }\n\n.form-check {\n  display: block;\n  min-height: 1.5rem;\n  padding-left: 1.5em;\n  margin-bottom: 0.125rem; }\n  .form-check .form-check-input, .form-check li input[type=\"checkbox\"], li .form-check input[type=\"checkbox\"] {\n    float: left;\n    margin-left: -1.5em; }\n\n.form-check-reverse {\n  padding-right: 1.5em;\n  padding-left: 0;\n  text-align: right; }\n  .form-check-reverse .form-check-input, .form-check-reverse li input[type=\"checkbox\"], li .form-check-reverse input[type=\"checkbox\"] {\n    float: right;\n    margin-right: -1.5em;\n    margin-left: 0; }\n\n.form-check-input, li input[type=\"checkbox\"] {\n  --bs-form-check-bg: var(--bs-body-bg);\n  flex-shrink: 0;\n  width: 1em;\n  height: 1em;\n  margin-top: 0.25em;\n  vertical-align: top;\n  appearance: none;\n  background-color: var(--bs-form-check-bg);\n  background-image: var(--bs-form-check-bg-image);\n  background-repeat: no-repeat;\n  background-position: center;\n  background-size: contain;\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  print-color-adjust: exact; }\n  .form-check-input[type=\"checkbox\"], li input[type=\"checkbox\"] {\n    border-radius: 0.25em; }\n  .form-check-input[type=\"radio\"], li input[type=\"radio\"][type=\"checkbox\"] {\n    border-radius: 50%; }\n  .form-check-input:active, li input[type=\"checkbox\"]:active {\n    filter: brightness(90%); }\n  .form-check-input:focus, li input[type=\"checkbox\"]:focus {\n    border-color: #a7a3f2;\n    outline: 0;\n    box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.25); }\n  .form-check-input:checked, li input[type=\"checkbox\"]:checked {\n    background-color: #4f46e5;\n    border-color: #4f46e5; }\n    .form-check-input:checked[type=\"checkbox\"], li input:checked[type=\"checkbox\"] {\n      --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\"); }\n    .form-check-input:checked[type=\"radio\"], li input[type=\"checkbox\"]:checked[type=\"radio\"] {\n      --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\"); }\n  .form-check-input[type=\"checkbox\"]:indeterminate, li input[type=\"checkbox\"]:indeterminate {\n    background-color: #4f46e5;\n    border-color: #4f46e5;\n    --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\"); }\n  .form-check-input:disabled, li input[type=\"checkbox\"]:disabled {\n    pointer-events: none;\n    filter: none;\n    opacity: 0.5; }\n  .form-check-input[disabled] ~ .form-check-label, li input[disabled][type=\"checkbox\"] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label, li input[type=\"checkbox\"]:disabled ~ .form-check-label {\n    cursor: default;\n    opacity: 0.5; }\n\n.form-switch {\n  padding-left: 2.5em; }\n  .form-switch .form-check-input, .form-switch li input[type=\"checkbox\"], li .form-switch input[type=\"checkbox\"] {\n    --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n    width: 2em;\n    margin-left: -2.5em;\n    background-image: var(--bs-form-switch-bg);\n    background-position: left center;\n    border-radius: 2em;\n    transition: background-position 0.15s ease-in-out; }\n    @media (prefers-reduced-motion: reduce) {\n      .form-switch .form-check-input, .form-switch li input[type=\"checkbox\"], li .form-switch input[type=\"checkbox\"] {\n        transition: none; } }\n    .form-switch .form-check-input:focus, .form-switch li input[type=\"checkbox\"]:focus, li .form-switch input[type=\"checkbox\"]:focus {\n      --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23a7a3f2'/%3e%3c/svg%3e\"); }\n    .form-switch .form-check-input:checked, .form-switch li input[type=\"checkbox\"]:checked, li .form-switch input[type=\"checkbox\"]:checked {\n      background-position: right center;\n      --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\"); }\n  .form-switch.form-check-reverse {\n    padding-right: 2.5em;\n    padding-left: 0; }\n    .form-switch.form-check-reverse .form-check-input, .form-switch.form-check-reverse li input[type=\"checkbox\"], li .form-switch.form-check-reverse input[type=\"checkbox\"] {\n      margin-right: -2.5em;\n      margin-left: 0; }\n\n.form-check-inline {\n  display: inline-block;\n  margin-right: 1rem; }\n\n.btn-check {\n  position: absolute;\n  clip: rect(0, 0, 0, 0);\n  pointer-events: none; }\n  .btn-check[disabled] + .btn, .search-form .btn-check[disabled] + .search-submit, .comment-form .btn-check[disabled] + input[type=\"submit\"], .btn-check:disabled + .btn, .search-form .btn-check:disabled + .search-submit, .comment-form .btn-check:disabled + input[type=\"submit\"] {\n    pointer-events: none;\n    filter: none;\n    opacity: 0.65; }\n\n[data-bs-theme=\"dark\"] .form-switch .form-check-input:not(:checked):not(:focus), [data-bs-theme=\"dark\"] .form-switch li input[type=\"checkbox\"]:not(:checked):not(:focus), li [data-bs-theme=\"dark\"] .form-switch input[type=\"checkbox\"]:not(:checked):not(:focus) {\n  --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e\"); }\n\n.form-range {\n  width: 100%;\n  height: 1rem;\n  padding: 0;\n  appearance: none;\n  background-color: transparent; }\n  .form-range:focus {\n    outline: 0; }\n    .form-range:focus::-webkit-slider-thumb {\n      box-shadow: 0 0 0 1px #fff, none; }\n    .form-range:focus::-moz-range-thumb {\n      box-shadow: 0 0 0 1px #fff, none; }\n  .form-range::-moz-focus-outer {\n    border: 0; }\n  .form-range::-webkit-slider-thumb {\n    width: 1rem;\n    height: 1rem;\n    margin-top: -0.25rem;\n    appearance: none;\n    background-color: #4f46e5;\n    border: 0;\n    border-radius: 1rem;\n    transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; }\n    @media (prefers-reduced-motion: reduce) {\n      .form-range::-webkit-slider-thumb {\n        transition: none; } }\n    .form-range::-webkit-slider-thumb:active {\n      background-color: #cac8f7; }\n  .form-range::-webkit-slider-runnable-track {\n    width: 100%;\n    height: 0.5rem;\n    color: transparent;\n    cursor: pointer;\n    background-color: var(--bs-secondary-bg);\n    border-color: transparent;\n    border-radius: 1rem; }\n  .form-range::-moz-range-thumb {\n    width: 1rem;\n    height: 1rem;\n    appearance: none;\n    background-color: #4f46e5;\n    border: 0;\n    border-radius: 1rem;\n    transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; }\n    @media (prefers-reduced-motion: reduce) {\n      .form-range::-moz-range-thumb {\n        transition: none; } }\n    .form-range::-moz-range-thumb:active {\n      background-color: #cac8f7; }\n  .form-range::-moz-range-track {\n    width: 100%;\n    height: 0.5rem;\n    color: transparent;\n    cursor: pointer;\n    background-color: var(--bs-secondary-bg);\n    border-color: transparent;\n    border-radius: 1rem; }\n  .form-range:disabled {\n    pointer-events: none; }\n    .form-range:disabled::-webkit-slider-thumb {\n      background-color: var(--bs-secondary-color); }\n    .form-range:disabled::-moz-range-thumb {\n      background-color: var(--bs-secondary-color); }\n\n.form-floating {\n  position: relative; }\n  .form-floating > .form-control, .search-form .form-floating > .search-field, .comment-form .form-floating > input[type=\"text\"],\n  .comment-form .form-floating > input[type=\"email\"],\n  .comment-form .form-floating > input[type=\"url\"],\n  .comment-form .form-floating > textarea,\n  .form-floating > .form-control-plaintext,\n  .form-floating > .form-select {\n    height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n    min-height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n    line-height: 1.25; }\n  .form-floating > label {\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: 2;\n    height: 100%;\n    padding: 1rem 0.75rem;\n    overflow: hidden;\n    text-align: start;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    pointer-events: none;\n    border: var(--bs-border-width) solid transparent;\n    transform-origin: 0 0;\n    transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; }\n    @media (prefers-reduced-motion: reduce) {\n      .form-floating > label {\n        transition: none; } }\n  .form-floating > .form-control, .search-form .form-floating > .search-field, .comment-form .form-floating > input[type=\"text\"],\n  .comment-form .form-floating > input[type=\"email\"],\n  .comment-form .form-floating > input[type=\"url\"],\n  .comment-form .form-floating > textarea,\n  .form-floating > .form-control-plaintext {\n    padding: 1rem 0.75rem; }\n    .form-floating > .form-control::placeholder, .search-form .form-floating > .search-field::placeholder, .comment-form .form-floating > input[type=\"text\"]::placeholder,\n    .comment-form .form-floating > input[type=\"email\"]::placeholder,\n    .comment-form .form-floating > input[type=\"url\"]::placeholder,\n    .comment-form .form-floating > textarea::placeholder,\n    .form-floating > .form-control-plaintext::placeholder {\n      color: transparent; }\n    .form-floating > .form-control:focus, .search-form .form-floating > .search-field:focus, .comment-form .form-floating > input[type=\"text\"]:focus,\n    .comment-form .form-floating > input[type=\"email\"]:focus,\n    .comment-form .form-floating > input[type=\"url\"]:focus,\n    .comment-form .form-floating > textarea:focus, .form-floating > .form-control:not(:placeholder-shown), .search-form .form-floating > .search-field:not(:placeholder-shown), .comment-form .form-floating > input[type=\"text\"]:not(:placeholder-shown),\n    .comment-form .form-floating > input[type=\"email\"]:not(:placeholder-shown),\n    .comment-form .form-floating > input[type=\"url\"]:not(:placeholder-shown),\n    .comment-form .form-floating > textarea:not(:placeholder-shown),\n    .form-floating > .form-control-plaintext:focus,\n    .form-floating > .form-control-plaintext:not(:placeholder-shown) {\n      padding-top: 1.625rem;\n      padding-bottom: 0.625rem; }\n    .form-floating > .form-control:-webkit-autofill, .search-form .form-floating > .search-field:-webkit-autofill, .comment-form .form-floating > input[type=\"text\"]:-webkit-autofill,\n    .comment-form .form-floating > input[type=\"email\"]:-webkit-autofill,\n    .comment-form .form-floating > input[type=\"url\"]:-webkit-autofill,\n    .comment-form .form-floating > textarea:-webkit-autofill,\n    .form-floating > .form-control-plaintext:-webkit-autofill {\n      padding-top: 1.625rem;\n      padding-bottom: 0.625rem; }\n  .form-floating > .form-select {\n    padding-top: 1.625rem;\n    padding-bottom: 0.625rem; }\n  .form-floating > .form-control:focus ~ label, .search-form .form-floating > .search-field:focus ~ label, .comment-form .form-floating > input[type=\"text\"]:focus ~ label,\n  .comment-form .form-floating > input[type=\"email\"]:focus ~ label,\n  .comment-form .form-floating > input[type=\"url\"]:focus ~ label,\n  .comment-form .form-floating > textarea:focus ~ label,\n  .form-floating > .form-control:not(:placeholder-shown) ~ label,\n  .search-form .form-floating > .search-field:not(:placeholder-shown) ~ label,\n  .comment-form .form-floating > input[type=\"text\"]:not(:placeholder-shown) ~ label,\n  .comment-form .form-floating > input[type=\"email\"]:not(:placeholder-shown) ~ label,\n  .comment-form .form-floating > input[type=\"url\"]:not(:placeholder-shown) ~ label,\n  .comment-form .form-floating > textarea:not(:placeholder-shown) ~ label,\n  .form-floating > .form-control-plaintext ~ label,\n  .form-floating > .form-select ~ label {\n    color: rgba(var(--bs-body-color-rgb), 0.65);\n    transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); }\n    .form-floating > .form-control:focus ~ label::after, .search-form .form-floating > .search-field:focus ~ label::after, .comment-form .form-floating > input[type=\"text\"]:focus ~ label::after,\n    .comment-form .form-floating > input[type=\"email\"]:focus ~ label::after,\n    .comment-form .form-floating > input[type=\"url\"]:focus ~ label::after,\n    .comment-form .form-floating > textarea:focus ~ label::after,\n    .form-floating > .form-control:not(:placeholder-shown) ~ label::after,\n    .search-form .form-floating > .search-field:not(:placeholder-shown) ~ label::after,\n    .comment-form .form-floating > input[type=\"text\"]:not(:placeholder-shown) ~ label::after,\n    .comment-form .form-floating > input[type=\"email\"]:not(:placeholder-shown) ~ label::after,\n    .comment-form .form-floating > input[type=\"url\"]:not(:placeholder-shown) ~ label::after,\n    .comment-form .form-floating > textarea:not(:placeholder-shown) ~ label::after,\n    .form-floating > .form-control-plaintext ~ label::after,\n    .form-floating > .form-select ~ label::after {\n      position: absolute;\n      inset: 1rem 0.375rem;\n      z-index: -1;\n      height: 1.5em;\n      content: \"\";\n      background-color: var(--bs-body-bg);\n      border-radius: var(--bs-border-radius); }\n  .form-floating > .form-control:-webkit-autofill ~ label, .search-form .form-floating > .search-field:-webkit-autofill ~ label, .comment-form .form-floating > input[type=\"text\"]:-webkit-autofill ~ label,\n  .comment-form .form-floating > input[type=\"email\"]:-webkit-autofill ~ label,\n  .comment-form .form-floating > input[type=\"url\"]:-webkit-autofill ~ label,\n  .comment-form .form-floating > textarea:-webkit-autofill ~ label {\n    color: rgba(var(--bs-body-color-rgb), 0.65);\n    transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); }\n  .form-floating > .form-control-plaintext ~ label {\n    border-width: var(--bs-border-width) 0; }\n  .form-floating > :disabled ~ label,\n  .form-floating > .form-control:disabled ~ label {\n    color: #6c757d; }\n    .form-floating > :disabled ~ label::after,\n    .form-floating > .form-control:disabled ~ label::after {\n      background-color: var(--bs-secondary-bg); }\n\n.input-group {\n  position: relative;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: stretch;\n  width: 100%; }\n  .input-group > .form-control, .search-form .input-group > .search-field, .comment-form .input-group > input[type=\"text\"],\n  .comment-form .input-group > input[type=\"email\"],\n  .comment-form .input-group > input[type=\"url\"],\n  .comment-form .input-group > textarea,\n  .input-group > .form-select,\n  .input-group > .form-floating {\n    position: relative;\n    flex: 1 1 auto;\n    width: 1%;\n    min-width: 0; }\n  .input-group > .form-control:focus, .search-form .input-group > .search-field:focus, .comment-form .input-group > input[type=\"text\"]:focus,\n  .comment-form .input-group > input[type=\"email\"]:focus,\n  .comment-form .input-group > input[type=\"url\"]:focus,\n  .comment-form .input-group > textarea:focus,\n  .input-group > .form-select:focus,\n  .input-group > .form-floating:focus-within {\n    z-index: 5; }\n  .input-group .btn, .input-group .search-form .search-submit, .search-form .input-group .search-submit, .input-group .comment-form input[type=\"submit\"], .comment-form .input-group input[type=\"submit\"] {\n    position: relative;\n    z-index: 2; }\n    .input-group .btn:focus, .input-group .search-form .search-submit:focus, .search-form .input-group .search-submit:focus, .input-group .comment-form input[type=\"submit\"]:focus, .comment-form .input-group input[type=\"submit\"]:focus {\n      z-index: 5; }\n\n.input-group-text {\n  display: flex;\n  align-items: center;\n  padding: 0.375rem 0.75rem;\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  text-align: center;\n  white-space: nowrap;\n  background-color: var(--bs-tertiary-bg);\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius); }\n\n.input-group-lg > .form-control, .search-form .input-group-lg > .search-field, .comment-form .input-group-lg > input[type=\"text\"],\n.comment-form .input-group-lg > input[type=\"email\"],\n.comment-form .input-group-lg > input[type=\"url\"],\n.comment-form .input-group-lg > textarea,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn,\n.search-form .input-group-lg > .search-submit,\n.comment-form .input-group-lg > input[type=\"submit\"] {\n  padding: 0.5rem 1rem;\n  font-size: 1.25rem;\n  border-radius: var(--bs-border-radius-lg); }\n\n.input-group-sm > .form-control, .search-form .input-group-sm > .search-field, .comment-form .input-group-sm > input[type=\"text\"],\n.comment-form .input-group-sm > input[type=\"email\"],\n.comment-form .input-group-sm > input[type=\"url\"],\n.comment-form .input-group-sm > textarea,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn,\n.search-form .input-group-sm > .search-submit,\n.comment-form .input-group-sm > input[type=\"submit\"] {\n  padding: 0.25rem 0.5rem;\n  font-size: 0.875rem;\n  border-radius: var(--bs-border-radius-sm); }\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n  padding-right: 3rem; }\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n + 3),\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,\n.search-form .input-group:not(.has-validation) > .form-floating:not(:last-child) > .search-field,\n.comment-form .input-group:not(.has-validation) > .form-floating:not(:last-child) > input[type=\"text\"],\n.comment-form .input-group:not(.has-validation) > .form-floating:not(:last-child) > input[type=\"email\"],\n.comment-form .input-group:not(.has-validation) > .form-floating:not(:last-child) > input[type=\"url\"],\n.comment-form .input-group:not(.has-validation) > .form-floating:not(:last-child) > textarea,\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0; }\n\n.input-group.has-validation > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n + 4),\n.input-group.has-validation > .form-floating:nth-last-child(n + 3) > .form-control,\n.search-form .input-group.has-validation > .form-floating:nth-last-child(n + 3) > .search-field,\n.comment-form .input-group.has-validation > .form-floating:nth-last-child(n + 3) > input[type=\"text\"],\n.comment-form .input-group.has-validation > .form-floating:nth-last-child(n + 3) > input[type=\"email\"],\n.comment-form .input-group.has-validation > .form-floating:nth-last-child(n + 3) > input[type=\"url\"],\n.comment-form .input-group.has-validation > .form-floating:nth-last-child(n + 3) > textarea,\n.input-group.has-validation > .form-floating:nth-last-child(n + 3) > .form-select {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0; }\n\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n  margin-left: calc(var(--bs-border-width) * -1);\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0; }\n\n.input-group > .form-floating:not(:first-child) > .form-control, .search-form .input-group > .form-floating:not(:first-child) > .search-field, .comment-form .input-group > .form-floating:not(:first-child) > input[type=\"text\"],\n.comment-form .input-group > .form-floating:not(:first-child) > input[type=\"email\"],\n.comment-form .input-group > .form-floating:not(:first-child) > input[type=\"url\"],\n.comment-form .input-group > .form-floating:not(:first-child) > textarea,\n.input-group > .form-floating:not(:first-child) > .form-select {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0; }\n\n.valid-feedback {\n  display: none;\n  width: 100%;\n  margin-top: 0.25rem;\n  font-size: 0.875em;\n  color: var(--bs-form-valid-color); }\n\n.valid-tooltip {\n  position: absolute;\n  top: 100%;\n  z-index: 5;\n  display: none;\n  max-width: 100%;\n  padding: 0.25rem 0.5rem;\n  margin-top: .1rem;\n  font-size: 0.875rem;\n  color: #fff;\n  background-color: var(--bs-success);\n  border-radius: var(--bs-border-radius); }\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n  display: block; }\n\n.was-validated .form-control:valid, .was-validated .search-form .search-field:valid, .search-form .was-validated .search-field:valid, .was-validated .comment-form input[type=\"text\"]:valid, .comment-form .was-validated input[type=\"text\"]:valid,\n.was-validated .comment-form input[type=\"email\"]:valid,\n.comment-form .was-validated input[type=\"email\"]:valid,\n.was-validated .comment-form input[type=\"url\"]:valid,\n.comment-form .was-validated input[type=\"url\"]:valid,\n.was-validated .comment-form textarea:valid,\n.comment-form .was-validated textarea:valid, .form-control.is-valid, .search-form .is-valid.search-field, .comment-form input.is-valid[type=\"text\"],\n.comment-form input.is-valid[type=\"email\"],\n.comment-form input.is-valid[type=\"url\"],\n.comment-form textarea.is-valid {\n  border-color: var(--bs-form-valid-border-color);\n  padding-right: calc(1.5em + 0.75rem);\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2384ee53' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat;\n  background-position: right calc(0.375em + 0.1875rem) center;\n  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); }\n  .was-validated .form-control:valid:focus, .was-validated .search-form .search-field:valid:focus, .search-form .was-validated .search-field:valid:focus, .was-validated .comment-form input[type=\"text\"]:valid:focus, .comment-form .was-validated input[type=\"text\"]:valid:focus,\n  .was-validated .comment-form input[type=\"email\"]:valid:focus,\n  .comment-form .was-validated input[type=\"email\"]:valid:focus,\n  .was-validated .comment-form input[type=\"url\"]:valid:focus,\n  .comment-form .was-validated input[type=\"url\"]:valid:focus,\n  .was-validated .comment-form textarea:valid:focus,\n  .comment-form .was-validated textarea:valid:focus, .form-control.is-valid:focus, .search-form .is-valid.search-field:focus, .comment-form input.is-valid[type=\"text\"]:focus,\n  .comment-form input.is-valid[type=\"email\"]:focus,\n  .comment-form input.is-valid[type=\"url\"]:focus,\n  .comment-form textarea.is-valid:focus {\n    border-color: var(--bs-form-valid-border-color);\n    box-shadow: 0 0 0 0 rgba(var(--bs-success-rgb), 0.25); }\n\n.was-validated textarea.form-control:valid, .was-validated .search-form textarea.search-field:valid, .search-form .was-validated textarea.search-field:valid, textarea.form-control.is-valid, .search-form textarea.is-valid.search-field {\n  padding-right: calc(1.5em + 0.75rem);\n  background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); }\n\n.was-validated .form-select:valid, .form-select.is-valid {\n  border-color: var(--bs-form-valid-border-color); }\n  .was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n    --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2384ee53' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n    padding-right: 4.125rem;\n    background-position: right 0.75rem center, center right 2.25rem;\n    background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); }\n  .was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n    border-color: var(--bs-form-valid-border-color);\n    box-shadow: 0 0 0 0 rgba(var(--bs-success-rgb), 0.25); }\n\n.was-validated .form-control-color:valid, .form-control-color.is-valid {\n  width: calc(3rem + calc(1.5em + 0.75rem)); }\n\n.was-validated .form-check-input:valid, .was-validated li input[type=\"checkbox\"]:valid, li .was-validated input[type=\"checkbox\"]:valid, .form-check-input.is-valid, li input.is-valid[type=\"checkbox\"] {\n  border-color: var(--bs-form-valid-border-color); }\n  .was-validated .form-check-input:valid:checked, .was-validated li input[type=\"checkbox\"]:valid:checked, li .was-validated input[type=\"checkbox\"]:valid:checked, .form-check-input.is-valid:checked, li input.is-valid[type=\"checkbox\"]:checked {\n    background-color: var(--bs-form-valid-color); }\n  .was-validated .form-check-input:valid:focus, .was-validated li input[type=\"checkbox\"]:valid:focus, li .was-validated input[type=\"checkbox\"]:valid:focus, .form-check-input.is-valid:focus, li input.is-valid[type=\"checkbox\"]:focus {\n    box-shadow: 0 0 0 0 rgba(var(--bs-success-rgb), 0.25); }\n  .was-validated .form-check-input:valid ~ .form-check-label, .was-validated li input[type=\"checkbox\"]:valid ~ .form-check-label, li .was-validated input[type=\"checkbox\"]:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label, li input.is-valid[type=\"checkbox\"] ~ .form-check-label {\n    color: var(--bs-form-valid-color); }\n\n.form-check-inline .form-check-input ~ .valid-feedback, .form-check-inline li input[type=\"checkbox\"] ~ .valid-feedback, li .form-check-inline input[type=\"checkbox\"] ~ .valid-feedback {\n  margin-left: .5em; }\n\n.was-validated .input-group > .form-control:not(:focus):valid, .was-validated .search-form .input-group > .search-field:not(:focus):valid, .search-form .was-validated .input-group > .search-field:not(:focus):valid, .was-validated .comment-form .input-group > input[type=\"text\"]:not(:focus):valid, .comment-form .was-validated .input-group > input[type=\"text\"]:not(:focus):valid,\n.was-validated .comment-form .input-group > input[type=\"email\"]:not(:focus):valid,\n.comment-form .was-validated .input-group > input[type=\"email\"]:not(:focus):valid,\n.was-validated .comment-form .input-group > input[type=\"url\"]:not(:focus):valid,\n.comment-form .was-validated .input-group > input[type=\"url\"]:not(:focus):valid,\n.was-validated .comment-form .input-group > textarea:not(:focus):valid,\n.comment-form .was-validated .input-group > textarea:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, .search-form .input-group > .search-field:not(:focus).is-valid, .comment-form .input-group > input[type=\"text\"]:not(:focus).is-valid,\n.comment-form .input-group > input[type=\"email\"]:not(:focus).is-valid,\n.comment-form .input-group > input[type=\"url\"]:not(:focus).is-valid,\n.comment-form .input-group > textarea:not(:focus).is-valid, .was-validated .input-group > .form-select:not(:focus):valid,\n.input-group > .form-select:not(:focus).is-valid, .was-validated .input-group > .form-floating:not(:focus-within):valid,\n.input-group > .form-floating:not(:focus-within).is-valid {\n  z-index: 3; }\n\n.invalid-feedback {\n  display: none;\n  width: 100%;\n  margin-top: 0.25rem;\n  font-size: 0.875em;\n  color: var(--bs-form-invalid-color); }\n\n.invalid-tooltip {\n  position: absolute;\n  top: 100%;\n  z-index: 5;\n  display: none;\n  max-width: 100%;\n  padding: 0.25rem 0.5rem;\n  margin-top: .1rem;\n  font-size: 0.875rem;\n  color: #fff;\n  background-color: var(--bs-danger);\n  border-radius: var(--bs-border-radius); }\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n  display: block; }\n\n.was-validated .form-control:invalid, .was-validated .search-form .search-field:invalid, .search-form .was-validated .search-field:invalid, .was-validated .comment-form input[type=\"text\"]:invalid, .comment-form .was-validated input[type=\"text\"]:invalid,\n.was-validated .comment-form input[type=\"email\"]:invalid,\n.comment-form .was-validated input[type=\"email\"]:invalid,\n.was-validated .comment-form input[type=\"url\"]:invalid,\n.comment-form .was-validated input[type=\"url\"]:invalid,\n.was-validated .comment-form textarea:invalid,\n.comment-form .was-validated textarea:invalid, .form-control.is-invalid, .search-form .is-invalid.search-field, .comment-form input.is-invalid[type=\"text\"],\n.comment-form input.is-invalid[type=\"email\"],\n.comment-form input.is-invalid[type=\"url\"],\n.comment-form textarea.is-invalid {\n  border-color: var(--bs-form-invalid-border-color);\n  padding-right: calc(1.5em + 0.75rem);\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ee5389'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ee5389' stroke='none'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat;\n  background-position: right calc(0.375em + 0.1875rem) center;\n  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); }\n  .was-validated .form-control:invalid:focus, .was-validated .search-form .search-field:invalid:focus, .search-form .was-validated .search-field:invalid:focus, .was-validated .comment-form input[type=\"text\"]:invalid:focus, .comment-form .was-validated input[type=\"text\"]:invalid:focus,\n  .was-validated .comment-form input[type=\"email\"]:invalid:focus,\n  .comment-form .was-validated input[type=\"email\"]:invalid:focus,\n  .was-validated .comment-form input[type=\"url\"]:invalid:focus,\n  .comment-form .was-validated input[type=\"url\"]:invalid:focus,\n  .was-validated .comment-form textarea:invalid:focus,\n  .comment-form .was-validated textarea:invalid:focus, .form-control.is-invalid:focus, .search-form .is-invalid.search-field:focus, .comment-form input.is-invalid[type=\"text\"]:focus,\n  .comment-form input.is-invalid[type=\"email\"]:focus,\n  .comment-form input.is-invalid[type=\"url\"]:focus,\n  .comment-form textarea.is-invalid:focus {\n    border-color: var(--bs-form-invalid-border-color);\n    box-shadow: 0 0 0 0 rgba(var(--bs-danger-rgb), 0.25); }\n\n.was-validated textarea.form-control:invalid, .was-validated .search-form textarea.search-field:invalid, .search-form .was-validated textarea.search-field:invalid, textarea.form-control.is-invalid, .search-form textarea.is-invalid.search-field {\n  padding-right: calc(1.5em + 0.75rem);\n  background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); }\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n  border-color: var(--bs-form-invalid-border-color); }\n  .was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n    --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ee5389'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ee5389' stroke='none'/%3e%3c/svg%3e\");\n    padding-right: 4.125rem;\n    background-position: right 0.75rem center, center right 2.25rem;\n    background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); }\n  .was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n    border-color: var(--bs-form-invalid-border-color);\n    box-shadow: 0 0 0 0 rgba(var(--bs-danger-rgb), 0.25); }\n\n.was-validated .form-control-color:invalid, .form-control-color.is-invalid {\n  width: calc(3rem + calc(1.5em + 0.75rem)); }\n\n.was-validated .form-check-input:invalid, .was-validated li input[type=\"checkbox\"]:invalid, li .was-validated input[type=\"checkbox\"]:invalid, .form-check-input.is-invalid, li input.is-invalid[type=\"checkbox\"] {\n  border-color: var(--bs-form-invalid-border-color); }\n  .was-validated .form-check-input:invalid:checked, .was-validated li input[type=\"checkbox\"]:invalid:checked, li .was-validated input[type=\"checkbox\"]:invalid:checked, .form-check-input.is-invalid:checked, li input.is-invalid[type=\"checkbox\"]:checked {\n    background-color: var(--bs-form-invalid-color); }\n  .was-validated .form-check-input:invalid:focus, .was-validated li input[type=\"checkbox\"]:invalid:focus, li .was-validated input[type=\"checkbox\"]:invalid:focus, .form-check-input.is-invalid:focus, li input.is-invalid[type=\"checkbox\"]:focus {\n    box-shadow: 0 0 0 0 rgba(var(--bs-danger-rgb), 0.25); }\n  .was-validated .form-check-input:invalid ~ .form-check-label, .was-validated li input[type=\"checkbox\"]:invalid ~ .form-check-label, li .was-validated input[type=\"checkbox\"]:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label, li input.is-invalid[type=\"checkbox\"] ~ .form-check-label {\n    color: var(--bs-form-invalid-color); }\n\n.form-check-inline .form-check-input ~ .invalid-feedback, .form-check-inline li input[type=\"checkbox\"] ~ .invalid-feedback, li .form-check-inline input[type=\"checkbox\"] ~ .invalid-feedback {\n  margin-left: .5em; }\n\n.was-validated .input-group > .form-control:not(:focus):invalid, .was-validated .search-form .input-group > .search-field:not(:focus):invalid, .search-form .was-validated .input-group > .search-field:not(:focus):invalid, .was-validated .comment-form .input-group > input[type=\"text\"]:not(:focus):invalid, .comment-form .was-validated .input-group > input[type=\"text\"]:not(:focus):invalid,\n.was-validated .comment-form .input-group > input[type=\"email\"]:not(:focus):invalid,\n.comment-form .was-validated .input-group > input[type=\"email\"]:not(:focus):invalid,\n.was-validated .comment-form .input-group > input[type=\"url\"]:not(:focus):invalid,\n.comment-form .was-validated .input-group > input[type=\"url\"]:not(:focus):invalid,\n.was-validated .comment-form .input-group > textarea:not(:focus):invalid,\n.comment-form .was-validated .input-group > textarea:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, .search-form .input-group > .search-field:not(:focus).is-invalid, .comment-form .input-group > input[type=\"text\"]:not(:focus).is-invalid,\n.comment-form .input-group > input[type=\"email\"]:not(:focus).is-invalid,\n.comment-form .input-group > input[type=\"url\"]:not(:focus).is-invalid,\n.comment-form .input-group > textarea:not(:focus).is-invalid, .was-validated .input-group > .form-select:not(:focus):invalid,\n.input-group > .form-select:not(:focus).is-invalid, .was-validated .input-group > .form-floating:not(:focus-within):invalid,\n.input-group > .form-floating:not(:focus-within).is-invalid {\n  z-index: 4; }\n\n.btn, .search-form .search-submit, .comment-form input[type=\"submit\"] {\n  --bs-btn-padding-x: 0.75rem;\n  --bs-btn-padding-y: 0.375rem;\n  --bs-btn-font-family: ;\n  --bs-btn-font-size: 1rem;\n  --bs-btn-font-weight: 400;\n  --bs-btn-line-height: 1.5;\n  --bs-btn-color: var(--bs-body-color);\n  --bs-btn-bg: transparent;\n  --bs-btn-border-width: var(--bs-border-width);\n  --bs-btn-border-color: transparent;\n  --bs-btn-border-radius: var(--bs-border-radius);\n  --bs-btn-hover-border-color: transparent;\n  --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n  --bs-btn-disabled-opacity: 0.65;\n  --bs-btn-focus-box-shadow: 0 0 0 0 rgba(var(--bs-btn-focus-shadow-rgb), .5);\n  display: inline-block;\n  padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);\n  font-family: var(--bs-btn-font-family);\n  font-size: var(--bs-btn-font-size);\n  font-weight: var(--bs-btn-font-weight);\n  line-height: var(--bs-btn-line-height);\n  color: var(--bs-btn-color);\n  text-align: center;\n  vertical-align: middle;\n  cursor: pointer;\n  user-select: none;\n  border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);\n  border-radius: var(--bs-btn-border-radius);\n  background-color: var(--bs-btn-bg);\n  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; }\n  @media (prefers-reduced-motion: reduce) {\n    .btn, .search-form .search-submit, .comment-form input[type=\"submit\"] {\n      transition: none; } }\n  .btn:hover, .search-form .search-submit:hover, .comment-form input[type=\"submit\"]:hover {\n    color: var(--bs-btn-hover-color);\n    text-decoration: none;\n    background-color: var(--bs-btn-hover-bg);\n    border-color: var(--bs-btn-hover-border-color); }\n  .btn-check + .btn:hover, .search-form .btn-check + .search-submit:hover, .comment-form .btn-check + input[type=\"submit\"]:hover {\n    color: var(--bs-btn-color);\n    background-color: var(--bs-btn-bg);\n    border-color: var(--bs-btn-border-color); }\n  .btn:focus-visible, .search-form .search-submit:focus-visible, .comment-form input[type=\"submit\"]:focus-visible {\n    color: var(--bs-btn-hover-color);\n    background-color: var(--bs-btn-hover-bg);\n    border-color: var(--bs-btn-hover-border-color);\n    outline: 0;\n    box-shadow: var(--bs-btn-focus-box-shadow); }\n  .btn-check:focus-visible + .btn, .search-form .btn-check:focus-visible + .search-submit, .comment-form .btn-check:focus-visible + input[type=\"submit\"] {\n    border-color: var(--bs-btn-hover-border-color);\n    outline: 0;\n    box-shadow: var(--bs-btn-focus-box-shadow); }\n  .btn-check:checked + .btn, .search-form .btn-check:checked + .search-submit, .comment-form .btn-check:checked + input[type=\"submit\"], :not(.btn-check) + .btn:active, .search-form :not(.btn-check) + .search-submit:active, .comment-form :not(.btn-check) + input[type=\"submit\"]:active, .btn:first-child:active, .search-form .search-submit:first-child:active, .comment-form input[type=\"submit\"]:first-child:active, .btn.active, .search-form .active.search-submit, .comment-form input.active[type=\"submit\"], .btn.show, .search-form .show.search-submit, .comment-form input.show[type=\"submit\"] {\n    color: var(--bs-btn-active-color);\n    background-color: var(--bs-btn-active-bg);\n    border-color: var(--bs-btn-active-border-color); }\n    .btn-check:checked + .btn:focus-visible, .search-form .btn-check:checked + .search-submit:focus-visible, .comment-form .btn-check:checked + input[type=\"submit\"]:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .search-form :not(.btn-check) + .search-submit:active:focus-visible, .comment-form :not(.btn-check) + input[type=\"submit\"]:active:focus-visible, .btn:first-child:active:focus-visible, .search-form .search-submit:first-child:active:focus-visible, .comment-form input[type=\"submit\"]:first-child:active:focus-visible, .btn.active:focus-visible, .search-form .active.search-submit:focus-visible, .comment-form input.active[type=\"submit\"]:focus-visible, .btn.show:focus-visible, .search-form .show.search-submit:focus-visible, .comment-form input.show[type=\"submit\"]:focus-visible {\n      box-shadow: var(--bs-btn-focus-box-shadow); }\n  .btn-check:checked:focus-visible + .btn, .search-form .btn-check:checked:focus-visible + .search-submit, .comment-form .btn-check:checked:focus-visible + input[type=\"submit\"] {\n    box-shadow: var(--bs-btn-focus-box-shadow); }\n  .btn:disabled, .search-form .search-submit:disabled, .comment-form input[type=\"submit\"]:disabled, .btn.disabled, .search-form .disabled.search-submit, .comment-form input.disabled[type=\"submit\"], fieldset:disabled .btn, fieldset:disabled .search-form .search-submit, .search-form fieldset:disabled .search-submit, fieldset:disabled .comment-form input[type=\"submit\"], .comment-form fieldset:disabled input[type=\"submit\"] {\n    color: var(--bs-btn-disabled-color);\n    pointer-events: none;\n    background-color: var(--bs-btn-disabled-bg);\n    border-color: var(--bs-btn-disabled-border-color);\n    opacity: var(--bs-btn-disabled-opacity); }\n\n.btn-primary {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #4f46e5;\n  --bs-btn-border-color: #4f46e5;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #433cc3;\n  --bs-btn-hover-border-color: #3f38b7;\n  --bs-btn-focus-shadow-rgb: 105, 98, 233;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #3f38b7;\n  --bs-btn-active-border-color: #3b35ac;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #4f46e5;\n  --bs-btn-disabled-border-color: #4f46e5; }\n\n.btn-secondary, .search-form .search-submit, .comment-form input[type=\"submit\"] {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #6c757d;\n  --bs-btn-border-color: #6c757d;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #5c636a;\n  --bs-btn-hover-border-color: #565e64;\n  --bs-btn-focus-shadow-rgb: 130, 138, 145;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #565e64;\n  --bs-btn-active-border-color: #51585e;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #6c757d;\n  --bs-btn-disabled-border-color: #6c757d; }\n\n.btn-success {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #84ee53;\n  --bs-btn-border-color: #84ee53;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #97f16d;\n  --bs-btn-hover-border-color: #91f064;\n  --bs-btn-focus-shadow-rgb: 112, 202, 71;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #9df176;\n  --bs-btn-active-border-color: #91f064;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #84ee53;\n  --bs-btn-disabled-border-color: #84ee53; }\n\n.btn-info {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #3347ff;\n  --bs-btn-border-color: #3347ff;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #2b3dd9;\n  --bs-btn-hover-border-color: #2939cc;\n  --bs-btn-focus-shadow-rgb: 82, 99, 255;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #2939cc;\n  --bs-btn-active-border-color: #2636bf;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #3347ff;\n  --bs-btn-disabled-border-color: #3347ff; }\n\n.btn-warning {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #eebd53;\n  --bs-btn-border-color: #eebd53;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #f1c76d;\n  --bs-btn-hover-border-color: #f0c464;\n  --bs-btn-focus-shadow-rgb: 202, 161, 71;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #f1ca76;\n  --bs-btn-active-border-color: #f0c464;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #eebd53;\n  --bs-btn-disabled-border-color: #eebd53; }\n\n.btn-danger {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #ee5389;\n  --bs-btn-border-color: #ee5389;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #f16d9b;\n  --bs-btn-hover-border-color: #f06495;\n  --bs-btn-focus-shadow-rgb: 202, 71, 117;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #f176a1;\n  --bs-btn-active-border-color: #f06495;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #ee5389;\n  --bs-btn-disabled-border-color: #ee5389; }\n\n.btn-light {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #f8f9fa;\n  --bs-btn-border-color: #f8f9fa;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #d3d4d5;\n  --bs-btn-hover-border-color: #c6c7c8;\n  --bs-btn-focus-shadow-rgb: 211, 212, 213;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #c6c7c8;\n  --bs-btn-active-border-color: #babbbc;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #f8f9fa;\n  --bs-btn-disabled-border-color: #f8f9fa; }\n\n.btn-dark {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #212529;\n  --bs-btn-border-color: #212529;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #424649;\n  --bs-btn-hover-border-color: #373b3e;\n  --bs-btn-focus-shadow-rgb: 66, 70, 73;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #4d5154;\n  --bs-btn-active-border-color: #373b3e;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #212529;\n  --bs-btn-disabled-border-color: #212529; }\n\n.btn-outline-primary {\n  --bs-btn-color: #4f46e5;\n  --bs-btn-border-color: #4f46e5;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #4f46e5;\n  --bs-btn-hover-border-color: #4f46e5;\n  --bs-btn-focus-shadow-rgb: 79, 70, 229;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #4f46e5;\n  --bs-btn-active-border-color: #4f46e5;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #4f46e5;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #4f46e5;\n  --bs-gradient: none; }\n\n.btn-outline-secondary {\n  --bs-btn-color: #6c757d;\n  --bs-btn-border-color: #6c757d;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #6c757d;\n  --bs-btn-hover-border-color: #6c757d;\n  --bs-btn-focus-shadow-rgb: 108, 117, 125;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #6c757d;\n  --bs-btn-active-border-color: #6c757d;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #6c757d;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #6c757d;\n  --bs-gradient: none; }\n\n.btn-outline-success {\n  --bs-btn-color: #84ee53;\n  --bs-btn-border-color: #84ee53;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #84ee53;\n  --bs-btn-hover-border-color: #84ee53;\n  --bs-btn-focus-shadow-rgb: 132.2821, 238.017, 83.283;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #84ee53;\n  --bs-btn-active-border-color: #84ee53;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #84ee53;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #84ee53;\n  --bs-gradient: none; }\n\n.btn-outline-info {\n  --bs-btn-color: #3347ff;\n  --bs-btn-border-color: #3347ff;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #3347ff;\n  --bs-btn-hover-border-color: #3347ff;\n  --bs-btn-focus-shadow-rgb: 51, 71.4, 255;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #3347ff;\n  --bs-btn-active-border-color: #3347ff;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #3347ff;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #3347ff;\n  --bs-gradient: none; }\n\n.btn-outline-warning {\n  --bs-btn-color: #eebd53;\n  --bs-btn-border-color: #eebd53;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #eebd53;\n  --bs-btn-hover-border-color: #eebd53;\n  --bs-btn-focus-shadow-rgb: 238.017, 189.0179, 83.283;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #eebd53;\n  --bs-btn-active-border-color: #eebd53;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #eebd53;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #eebd53;\n  --bs-gradient: none; }\n\n.btn-outline-danger {\n  --bs-btn-color: #ee5389;\n  --bs-btn-border-color: #ee5389;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #ee5389;\n  --bs-btn-hover-border-color: #ee5389;\n  --bs-btn-focus-shadow-rgb: 238.017, 83.283, 137.4399;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #ee5389;\n  --bs-btn-active-border-color: #ee5389;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #ee5389;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #ee5389;\n  --bs-gradient: none; }\n\n.btn-outline-light {\n  --bs-btn-color: #f8f9fa;\n  --bs-btn-border-color: #f8f9fa;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #f8f9fa;\n  --bs-btn-hover-border-color: #f8f9fa;\n  --bs-btn-focus-shadow-rgb: 248, 249, 250;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #f8f9fa;\n  --bs-btn-active-border-color: #f8f9fa;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #f8f9fa;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #f8f9fa;\n  --bs-gradient: none; }\n\n.btn-outline-dark {\n  --bs-btn-color: #212529;\n  --bs-btn-border-color: #212529;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #212529;\n  --bs-btn-hover-border-color: #212529;\n  --bs-btn-focus-shadow-rgb: 33, 37, 41;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #212529;\n  --bs-btn-active-border-color: #212529;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #212529;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #212529;\n  --bs-gradient: none; }\n\n.btn-link {\n  --bs-btn-font-weight: 400;\n  --bs-btn-color: var(--bs-link-color);\n  --bs-btn-bg: transparent;\n  --bs-btn-border-color: transparent;\n  --bs-btn-hover-color: var(--bs-link-hover-color);\n  --bs-btn-hover-border-color: transparent;\n  --bs-btn-active-color: var(--bs-link-hover-color);\n  --bs-btn-active-border-color: transparent;\n  --bs-btn-disabled-color: #6c757d;\n  --bs-btn-disabled-border-color: transparent;\n  --bs-btn-box-shadow: 0 0 0 #000;\n  --bs-btn-focus-shadow-rgb: 105, 98, 233;\n  text-decoration: none; }\n  .btn-link:hover, .btn-link:focus-visible {\n    text-decoration: underline; }\n  .btn-link:focus-visible {\n    color: var(--bs-btn-color); }\n  .btn-link:hover {\n    color: var(--bs-btn-hover-color); }\n\n.btn-lg, .btn-group-lg > .btn, .search-form .btn-group-lg > .search-submit, .comment-form .btn-group-lg > input[type=\"submit\"] {\n  --bs-btn-padding-y: 0.5rem;\n  --bs-btn-padding-x: 1rem;\n  --bs-btn-font-size: 1.25rem;\n  --bs-btn-border-radius: var(--bs-border-radius-lg); }\n\n.btn-sm, .btn-group-sm > .btn, .search-form .btn-group-sm > .search-submit, .comment-form .btn-group-sm > input[type=\"submit\"] {\n  --bs-btn-padding-y: 0.25rem;\n  --bs-btn-padding-x: 0.5rem;\n  --bs-btn-font-size: 0.875rem;\n  --bs-btn-border-radius: var(--bs-border-radius-sm); }\n\n.fade {\n  transition: opacity 0.15s linear; }\n  @media (prefers-reduced-motion: reduce) {\n    .fade {\n      transition: none; } }\n  .fade:not(.show) {\n    opacity: 0; }\n\n.collapse:not(.show) {\n  display: none; }\n\n.collapsing {\n  height: 0;\n  overflow: hidden;\n  transition: height 0.35s ease; }\n  @media (prefers-reduced-motion: reduce) {\n    .collapsing {\n      transition: none; } }\n  .collapsing.collapse-horizontal {\n    width: 0;\n    height: auto;\n    transition: width 0.35s ease; }\n    @media (prefers-reduced-motion: reduce) {\n      .collapsing.collapse-horizontal {\n        transition: none; } }\n.dropup,\n.dropend,\n.dropdown,\n.dropstart,\n.dropup-center,\n.dropdown-center {\n  position: relative; }\n\n.dropdown-toggle {\n  white-space: nowrap; }\n  .dropdown-toggle::after {\n    display: inline-block;\n    margin-left: 0.255em;\n    vertical-align: 0.255em;\n    content: \"\";\n    border-top: 0.3em solid;\n    border-right: 0.3em solid transparent;\n    border-bottom: 0;\n    border-left: 0.3em solid transparent; }\n  .dropdown-toggle:empty::after {\n    margin-left: 0; }\n\n.dropdown-menu {\n  --bs-dropdown-zindex: 1000;\n  --bs-dropdown-min-width: 10rem;\n  --bs-dropdown-padding-x: 0;\n  --bs-dropdown-padding-y: 0.5rem;\n  --bs-dropdown-spacer: 0.125rem;\n  --bs-dropdown-font-size: 1rem;\n  --bs-dropdown-color: var(--bs-body-color);\n  --bs-dropdown-bg: var(--bs-body-bg);\n  --bs-dropdown-border-color: var(--bs-border-color-translucent);\n  --bs-dropdown-border-radius: var(--bs-border-radius);\n  --bs-dropdown-border-width: var(--bs-border-width);\n  --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));\n  --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n  --bs-dropdown-divider-margin-y: 0.5rem;\n  --bs-dropdown-box-shadow: var(--bs-box-shadow);\n  --bs-dropdown-link-color: var(--bs-body-color);\n  --bs-dropdown-link-hover-color: var(--bs-body-color);\n  --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);\n  --bs-dropdown-link-active-color: #fff;\n  --bs-dropdown-link-active-bg: #4f46e5;\n  --bs-dropdown-link-disabled-color: var(--bs-tertiary-color);\n  --bs-dropdown-item-padding-x: 1rem;\n  --bs-dropdown-item-padding-y: 0.25rem;\n  --bs-dropdown-header-color: #6c757d;\n  --bs-dropdown-header-padding-x: 1rem;\n  --bs-dropdown-header-padding-y: 0.5rem;\n  position: absolute;\n  z-index: var(--bs-dropdown-zindex);\n  display: none;\n  min-width: var(--bs-dropdown-min-width);\n  padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);\n  margin: 0;\n  font-size: var(--bs-dropdown-font-size);\n  color: var(--bs-dropdown-color);\n  text-align: left;\n  list-style: none;\n  background-color: var(--bs-dropdown-bg);\n  background-clip: padding-box;\n  border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);\n  border-radius: var(--bs-dropdown-border-radius); }\n  .dropdown-menu[data-bs-popper] {\n    top: 100%;\n    left: 0;\n    margin-top: var(--bs-dropdown-spacer); }\n\n.dropdown-menu-start {\n  --bs-position: start; }\n  .dropdown-menu-start[data-bs-popper] {\n    right: auto;\n    left: 0; }\n\n.dropdown-menu-end {\n  --bs-position: end; }\n  .dropdown-menu-end[data-bs-popper] {\n    right: 0;\n    left: auto; }\n\n@media (min-width: 576px) {\n  .dropdown-menu-sm-start {\n    --bs-position: start; }\n    .dropdown-menu-sm-start[data-bs-popper] {\n      right: auto;\n      left: 0; }\n  .dropdown-menu-sm-end {\n    --bs-position: end; }\n    .dropdown-menu-sm-end[data-bs-popper] {\n      right: 0;\n      left: auto; } }\n\n@media (min-width: 768px) {\n  .dropdown-menu-md-start {\n    --bs-position: start; }\n    .dropdown-menu-md-start[data-bs-popper] {\n      right: auto;\n      left: 0; }\n  .dropdown-menu-md-end {\n    --bs-position: end; }\n    .dropdown-menu-md-end[data-bs-popper] {\n      right: 0;\n      left: auto; } }\n\n@media (min-width: 992px) {\n  .dropdown-menu-lg-start {\n    --bs-position: start; }\n    .dropdown-menu-lg-start[data-bs-popper] {\n      right: auto;\n      left: 0; }\n  .dropdown-menu-lg-end {\n    --bs-position: end; }\n    .dropdown-menu-lg-end[data-bs-popper] {\n      right: 0;\n      left: auto; } }\n\n@media (min-width: 1200px) {\n  .dropdown-menu-xl-start {\n    --bs-position: start; }\n    .dropdown-menu-xl-start[data-bs-popper] {\n      right: auto;\n      left: 0; }\n  .dropdown-menu-xl-end {\n    --bs-position: end; }\n    .dropdown-menu-xl-end[data-bs-popper] {\n      right: 0;\n      left: auto; } }\n\n@media (min-width: 1400px) {\n  .dropdown-menu-xxl-start {\n    --bs-position: start; }\n    .dropdown-menu-xxl-start[data-bs-popper] {\n      right: auto;\n      left: 0; }\n  .dropdown-menu-xxl-end {\n    --bs-position: end; }\n    .dropdown-menu-xxl-end[data-bs-popper] {\n      right: 0;\n      left: auto; } }\n\n.dropup .dropdown-menu[data-bs-popper] {\n  top: auto;\n  bottom: 100%;\n  margin-top: 0;\n  margin-bottom: var(--bs-dropdown-spacer); }\n\n.dropup .dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0;\n  border-right: 0.3em solid transparent;\n  border-bottom: 0.3em solid;\n  border-left: 0.3em solid transparent; }\n\n.dropup .dropdown-toggle:empty::after {\n  margin-left: 0; }\n\n.dropend .dropdown-menu[data-bs-popper] {\n  top: 0;\n  right: auto;\n  left: 100%;\n  margin-top: 0;\n  margin-left: var(--bs-dropdown-spacer); }\n\n.dropend .dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0.3em solid transparent;\n  border-right: 0;\n  border-bottom: 0.3em solid transparent;\n  border-left: 0.3em solid; }\n\n.dropend .dropdown-toggle:empty::after {\n  margin-left: 0; }\n\n.dropend .dropdown-toggle::after {\n  vertical-align: 0; }\n\n.dropstart .dropdown-menu[data-bs-popper] {\n  top: 0;\n  right: 100%;\n  left: auto;\n  margin-top: 0;\n  margin-right: var(--bs-dropdown-spacer); }\n\n.dropstart .dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\"; }\n\n.dropstart .dropdown-toggle::after {\n  display: none; }\n\n.dropstart .dropdown-toggle::before {\n  display: inline-block;\n  margin-right: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0.3em solid transparent;\n  border-right: 0.3em solid;\n  border-bottom: 0.3em solid transparent; }\n\n.dropstart .dropdown-toggle:empty::after {\n  margin-left: 0; }\n\n.dropstart .dropdown-toggle::before {\n  vertical-align: 0; }\n\n.dropdown-divider {\n  height: 0;\n  margin: var(--bs-dropdown-divider-margin-y) 0;\n  overflow: hidden;\n  border-top: 1px solid var(--bs-dropdown-divider-bg);\n  opacity: 1; }\n\n.dropdown-item {\n  display: block;\n  width: 100%;\n  padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n  clear: both;\n  font-weight: 400;\n  color: var(--bs-dropdown-link-color);\n  text-align: inherit;\n  white-space: nowrap;\n  background-color: transparent;\n  border: 0;\n  border-radius: var(--bs-dropdown-item-border-radius, 0); }\n  .dropdown-item:hover, .dropdown-item:focus {\n    color: var(--bs-dropdown-link-hover-color);\n    text-decoration: none;\n    background-color: var(--bs-dropdown-link-hover-bg); }\n  .dropdown-item.active, .dropdown-item:active {\n    color: var(--bs-dropdown-link-active-color);\n    text-decoration: none;\n    background-color: var(--bs-dropdown-link-active-bg); }\n  .dropdown-item.disabled, .dropdown-item:disabled {\n    color: var(--bs-dropdown-link-disabled-color);\n    pointer-events: none;\n    background-color: transparent; }\n\n.dropdown-menu.show {\n  display: block; }\n\n.dropdown-header {\n  display: block;\n  padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);\n  margin-bottom: 0;\n  font-size: 0.875rem;\n  color: var(--bs-dropdown-header-color);\n  white-space: nowrap; }\n\n.dropdown-item-text {\n  display: block;\n  padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n  color: var(--bs-dropdown-link-color); }\n\n.dropdown-menu-dark {\n  --bs-dropdown-color: #dee2e6;\n  --bs-dropdown-bg: #343a40;\n  --bs-dropdown-border-color: var(--bs-border-color-translucent);\n  --bs-dropdown-box-shadow: ;\n  --bs-dropdown-link-color: #dee2e6;\n  --bs-dropdown-link-hover-color: #fff;\n  --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n  --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);\n  --bs-dropdown-link-active-color: #fff;\n  --bs-dropdown-link-active-bg: #4f46e5;\n  --bs-dropdown-link-disabled-color: #adb5bd;\n  --bs-dropdown-header-color: #adb5bd; }\n\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-flex;\n  vertical-align: middle; }\n  .btn-group > .btn, .search-form .btn-group > .search-submit, .comment-form .btn-group > input[type=\"submit\"],\n  .btn-group-vertical > .btn,\n  .search-form .btn-group-vertical > .search-submit,\n  .comment-form .btn-group-vertical > input[type=\"submit\"] {\n    position: relative;\n    flex: 1 1 auto; }\n  .btn-group > .btn-check:checked + .btn, .search-form .btn-group > .btn-check:checked + .search-submit, .comment-form .btn-group > .btn-check:checked + input[type=\"submit\"],\n  .btn-group > .btn-check:focus + .btn,\n  .search-form .btn-group > .btn-check:focus + .search-submit,\n  .comment-form .btn-group > .btn-check:focus + input[type=\"submit\"],\n  .btn-group > .btn:hover,\n  .search-form .btn-group > .search-submit:hover,\n  .comment-form .btn-group > input[type=\"submit\"]:hover,\n  .btn-group > .btn:focus,\n  .search-form .btn-group > .search-submit:focus,\n  .comment-form .btn-group > input[type=\"submit\"]:focus,\n  .btn-group > .btn:active,\n  .search-form .btn-group > .search-submit:active,\n  .comment-form .btn-group > input[type=\"submit\"]:active,\n  .btn-group > .btn.active,\n  .search-form .btn-group > .active.search-submit,\n  .comment-form .btn-group > input.active[type=\"submit\"],\n  .btn-group-vertical > .btn-check:checked + .btn,\n  .search-form .btn-group-vertical > .btn-check:checked + .search-submit,\n  .comment-form .btn-group-vertical > .btn-check:checked + input[type=\"submit\"],\n  .btn-group-vertical > .btn-check:focus + .btn,\n  .search-form .btn-group-vertical > .btn-check:focus + .search-submit,\n  .comment-form .btn-group-vertical > .btn-check:focus + input[type=\"submit\"],\n  .btn-group-vertical > .btn:hover,\n  .search-form .btn-group-vertical > .search-submit:hover,\n  .comment-form .btn-group-vertical > input[type=\"submit\"]:hover,\n  .btn-group-vertical > .btn:focus,\n  .search-form .btn-group-vertical > .search-submit:focus,\n  .comment-form .btn-group-vertical > input[type=\"submit\"]:focus,\n  .btn-group-vertical > .btn:active,\n  .search-form .btn-group-vertical > .search-submit:active,\n  .comment-form .btn-group-vertical > input[type=\"submit\"]:active,\n  .btn-group-vertical > .btn.active,\n  .search-form .btn-group-vertical > .active.search-submit,\n  .comment-form .btn-group-vertical > input.active[type=\"submit\"] {\n    z-index: 1; }\n\n.btn-toolbar {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: flex-start; }\n  .btn-toolbar .input-group {\n    width: auto; }\n\n.btn-group {\n  border-radius: var(--bs-border-radius); }\n  .btn-group > :not(.btn-check:first-child) + .btn, .search-form .btn-group > :not(.btn-check:first-child) + .search-submit, .comment-form .btn-group > :not(.btn-check:first-child) + input[type=\"submit\"],\n  .btn-group > .btn-group:not(:first-child) {\n    margin-left: calc(var(--bs-border-width) * -1); }\n  .btn-group > .btn:not(:last-child):not(.dropdown-toggle), .search-form .btn-group > .search-submit:not(:last-child):not(.dropdown-toggle), .comment-form .btn-group > input[type=\"submit\"]:not(:last-child):not(.dropdown-toggle),\n  .btn-group > .btn.dropdown-toggle-split:first-child,\n  .search-form .btn-group > .dropdown-toggle-split.search-submit:first-child,\n  .comment-form .btn-group > input.dropdown-toggle-split[type=\"submit\"]:first-child,\n  .btn-group > .btn-group:not(:last-child) > .btn,\n  .search-form .btn-group > .btn-group:not(:last-child) > .search-submit,\n  .comment-form .btn-group > .btn-group:not(:last-child) > input[type=\"submit\"] {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0; }\n  .btn-group > .btn:nth-child(n + 3), .search-form .btn-group > .search-submit:nth-child(n + 3), .comment-form .btn-group > input[type=\"submit\"]:nth-child(n + 3),\n  .btn-group > :not(.btn-check) + .btn,\n  .search-form .btn-group > :not(.btn-check) + .search-submit,\n  .comment-form .btn-group > :not(.btn-check) + input[type=\"submit\"],\n  .btn-group > .btn-group:not(:first-child) > .btn,\n  .search-form .btn-group > .btn-group:not(:first-child) > .search-submit,\n  .comment-form .btn-group > .btn-group:not(:first-child) > input[type=\"submit\"] {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0; }\n\n.dropdown-toggle-split {\n  padding-right: 0.5625rem;\n  padding-left: 0.5625rem; }\n  .dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n    margin-left: 0; }\n  .dropstart .dropdown-toggle-split::before {\n    margin-right: 0; }\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split, .search-form .btn-group-sm > .search-submit + .dropdown-toggle-split, .comment-form .btn-group-sm > input[type=\"submit\"] + .dropdown-toggle-split {\n  padding-right: 0.375rem;\n  padding-left: 0.375rem; }\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split, .search-form .btn-group-lg > .search-submit + .dropdown-toggle-split, .comment-form .btn-group-lg > input[type=\"submit\"] + .dropdown-toggle-split {\n  padding-right: 0.75rem;\n  padding-left: 0.75rem; }\n\n.btn-group-vertical {\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center; }\n  .btn-group-vertical > .btn, .search-form .btn-group-vertical > .search-submit, .comment-form .btn-group-vertical > input[type=\"submit\"],\n  .btn-group-vertical > .btn-group {\n    width: 100%; }\n  .btn-group-vertical > .btn:not(:first-child), .search-form .btn-group-vertical > .search-submit:not(:first-child), .comment-form .btn-group-vertical > input[type=\"submit\"]:not(:first-child),\n  .btn-group-vertical > .btn-group:not(:first-child) {\n    margin-top: calc(var(--bs-border-width) * -1); }\n  .btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), .search-form .btn-group-vertical > .search-submit:not(:last-child):not(.dropdown-toggle), .comment-form .btn-group-vertical > input[type=\"submit\"]:not(:last-child):not(.dropdown-toggle),\n  .btn-group-vertical > .btn-group:not(:last-child) > .btn,\n  .search-form .btn-group-vertical > .btn-group:not(:last-child) > .search-submit,\n  .comment-form .btn-group-vertical > .btn-group:not(:last-child) > input[type=\"submit\"] {\n    border-bottom-right-radius: 0;\n    border-bottom-left-radius: 0; }\n  .btn-group-vertical > .btn ~ .btn, .search-form .btn-group-vertical > .search-submit ~ .btn, .search-form .btn-group-vertical > .btn ~ .search-submit, .search-form .btn-group-vertical > .search-submit ~ .search-submit, .comment-form .btn-group-vertical > input[type=\"submit\"] ~ .btn, .comment-form .search-form .btn-group-vertical > input[type=\"submit\"] ~ .search-submit, .search-form .comment-form .btn-group-vertical > input[type=\"submit\"] ~ .search-submit, .comment-form .btn-group-vertical > .btn ~ input[type=\"submit\"], .comment-form .search-form .btn-group-vertical > .search-submit ~ input[type=\"submit\"], .search-form .comment-form .btn-group-vertical > .search-submit ~ input[type=\"submit\"], .comment-form .btn-group-vertical > input[type=\"submit\"] ~ input[type=\"submit\"],\n  .btn-group-vertical > .btn-group:not(:first-child) > .btn,\n  .search-form .btn-group-vertical > .btn-group:not(:first-child) > .search-submit,\n  .comment-form .btn-group-vertical > .btn-group:not(:first-child) > input[type=\"submit\"] {\n    border-top-left-radius: 0;\n    border-top-right-radius: 0; }\n\n.nav {\n  --bs-nav-link-padding-x: 1rem;\n  --bs-nav-link-padding-y: 0.5rem;\n  --bs-nav-link-font-weight: ;\n  --bs-nav-link-color: var(--bs-link-color);\n  --bs-nav-link-hover-color: var(--bs-link-hover-color);\n  --bs-nav-link-disabled-color: var(--bs-secondary-color);\n  display: flex;\n  flex-wrap: wrap;\n  padding-left: 0;\n  margin-bottom: 0;\n  list-style: none; }\n\n.nav-link, .banner .nav a {\n  display: block;\n  padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);\n  font-size: var(--bs-nav-link-font-size);\n  font-weight: var(--bs-nav-link-font-weight);\n  color: var(--bs-nav-link-color);\n  background: none;\n  border: 0;\n  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; }\n  @media (prefers-reduced-motion: reduce) {\n    .nav-link, .banner .nav a {\n      transition: none; } }\n  .nav-link:hover, .banner .nav a:hover, .nav-link:focus, .banner .nav a:focus {\n    color: var(--bs-nav-link-hover-color);\n    text-decoration: none; }\n  .nav-link:focus-visible, .banner .nav a:focus-visible {\n    outline: 0;\n    box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.25); }\n  .nav-link.disabled, .banner .nav a.disabled, .nav-link:disabled, .banner .nav a:disabled {\n    color: var(--bs-nav-link-disabled-color);\n    pointer-events: none;\n    cursor: default; }\n\n.nav-tabs {\n  --bs-nav-tabs-border-width: var(--bs-border-width);\n  --bs-nav-tabs-border-color: var(--bs-border-color);\n  --bs-nav-tabs-border-radius: var(--bs-border-radius);\n  --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);\n  --bs-nav-tabs-link-active-color: var(--bs-emphasis-color);\n  --bs-nav-tabs-link-active-bg: var(--bs-body-bg);\n  --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);\n  border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); }\n  .nav-tabs .nav-link, .nav-tabs .banner .nav a, .banner .nav .nav-tabs a {\n    margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));\n    border: var(--bs-nav-tabs-border-width) solid transparent;\n    border-top-left-radius: var(--bs-nav-tabs-border-radius);\n    border-top-right-radius: var(--bs-nav-tabs-border-radius); }\n    .nav-tabs .nav-link:hover, .nav-tabs .banner .nav a:hover, .banner .nav .nav-tabs a:hover, .nav-tabs .nav-link:focus, .nav-tabs .banner .nav a:focus, .banner .nav .nav-tabs a:focus {\n      isolation: isolate;\n      border-color: var(--bs-nav-tabs-link-hover-border-color); }\n  .nav-tabs .nav-link.active, .nav-tabs .banner .nav a.active, .banner .nav .nav-tabs a.active,\n  .nav-tabs .nav-item.show .nav-link,\n  .nav-tabs .nav-item.show .banner .nav a,\n  .banner .nav .nav-tabs .nav-item.show a,\n  .nav-tabs .banner .nav li.show .nav-link,\n  .nav-tabs .banner .nav li.show a,\n  .banner .nav .nav-tabs li.show .nav-link,\n  .banner .nav .nav-tabs li.show a {\n    color: var(--bs-nav-tabs-link-active-color);\n    background-color: var(--bs-nav-tabs-link-active-bg);\n    border-color: var(--bs-nav-tabs-link-active-border-color); }\n  .nav-tabs .dropdown-menu {\n    margin-top: calc(-1 * var(--bs-nav-tabs-border-width));\n    border-top-left-radius: 0;\n    border-top-right-radius: 0; }\n\n.nav-pills {\n  --bs-nav-pills-border-radius: var(--bs-border-radius);\n  --bs-nav-pills-link-active-color: #fff;\n  --bs-nav-pills-link-active-bg: #4f46e5; }\n  .nav-pills .nav-link, .nav-pills .banner .nav a, .banner .nav .nav-pills a {\n    border-radius: var(--bs-nav-pills-border-radius); }\n  .nav-pills .nav-link.active, .nav-pills .banner .nav a.active, .banner .nav .nav-pills a.active,\n  .nav-pills .show > .nav-link,\n  .nav-pills .banner .nav .show > a,\n  .banner .nav .nav-pills .show > a {\n    color: var(--bs-nav-pills-link-active-color);\n    background-color: var(--bs-nav-pills-link-active-bg); }\n\n.nav-underline {\n  --bs-nav-underline-gap: 1rem;\n  --bs-nav-underline-border-width: 0.125rem;\n  --bs-nav-underline-link-active-color: var(--bs-emphasis-color);\n  gap: var(--bs-nav-underline-gap); }\n  .nav-underline .nav-link, .nav-underline .banner .nav a, .banner .nav .nav-underline a {\n    padding-right: 0;\n    padding-left: 0;\n    border-bottom: var(--bs-nav-underline-border-width) solid transparent; }\n    .nav-underline .nav-link:hover, .nav-underline .banner .nav a:hover, .banner .nav .nav-underline a:hover, .nav-underline .nav-link:focus, .nav-underline .banner .nav a:focus, .banner .nav .nav-underline a:focus {\n      border-bottom-color: currentcolor; }\n  .nav-underline .nav-link.active, .nav-underline .banner .nav a.active, .banner .nav .nav-underline a.active,\n  .nav-underline .show > .nav-link,\n  .nav-underline .banner .nav .show > a,\n  .banner .nav .nav-underline .show > a {\n    font-weight: 700;\n    color: var(--bs-nav-underline-link-active-color);\n    border-bottom-color: currentcolor; }\n\n.nav-fill > .nav-link, .banner .nav .nav-fill > a,\n.nav-fill .nav-item,\n.nav-fill .banner .nav li,\n.banner .nav .nav-fill li {\n  flex: 1 1 auto;\n  text-align: center; }\n\n.nav-justified > .nav-link, .banner .nav .nav-justified > a,\n.nav-justified .nav-item,\n.nav-justified .banner .nav li,\n.banner .nav .nav-justified li {\n  flex-basis: 0;\n  flex-grow: 1;\n  text-align: center; }\n\n.nav-fill .nav-item .nav-link, .nav-fill .nav-item .banner .nav a, .banner .nav .nav-fill .nav-item a, .nav-fill .banner .nav li .nav-link, .nav-fill .banner .nav li a, .banner .nav .nav-fill li .nav-link, .banner .nav .nav-fill li a,\n.nav-justified .nav-item .nav-link,\n.nav-justified .nav-item .banner .nav a,\n.banner .nav .nav-justified .nav-item a,\n.nav-justified .banner .nav li .nav-link,\n.nav-justified .banner .nav li a,\n.banner .nav .nav-justified li .nav-link,\n.banner .nav .nav-justified li a {\n  width: 100%; }\n\n.tab-content > .tab-pane {\n  display: none; }\n\n.tab-content > .active {\n  display: block; }\n\n.navbar {\n  --bs-navbar-padding-x: 0;\n  --bs-navbar-padding-y: 0.5rem;\n  --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);\n  --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);\n  --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);\n  --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);\n  --bs-navbar-brand-padding-y: 0.3125rem;\n  --bs-navbar-brand-margin-end: 1rem;\n  --bs-navbar-brand-font-size: 1.25rem;\n  --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);\n  --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);\n  --bs-navbar-nav-link-padding-x: 0.5rem;\n  --bs-navbar-toggler-padding-y: 0.25rem;\n  --bs-navbar-toggler-padding-x: 0.75rem;\n  --bs-navbar-toggler-font-size: 1.25rem;\n  --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\");\n  --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);\n  --bs-navbar-toggler-border-radius: var(--bs-border-radius);\n  --bs-navbar-toggler-focus-width: 0;\n  --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;\n  position: relative;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: space-between;\n  padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x); }\n  .navbar > .container,\n  .navbar > .container-fluid,\n  .navbar > .container-sm,\n  .navbar > .container-md,\n  .navbar > .container-lg,\n  .navbar > .container-xl,\n  .navbar > .container-xxl {\n    display: flex;\n    flex-wrap: inherit;\n    align-items: center;\n    justify-content: space-between; }\n\n.navbar-brand {\n  padding-top: var(--bs-navbar-brand-padding-y);\n  padding-bottom: var(--bs-navbar-brand-padding-y);\n  margin-right: var(--bs-navbar-brand-margin-end);\n  font-size: var(--bs-navbar-brand-font-size);\n  color: var(--bs-navbar-brand-color);\n  white-space: nowrap; }\n  .navbar-brand:hover, .navbar-brand:focus {\n    color: var(--bs-navbar-brand-hover-color);\n    text-decoration: none; }\n\n.navbar-nav {\n  --bs-nav-link-padding-x: 0;\n  --bs-nav-link-padding-y: 0.5rem;\n  --bs-nav-link-font-weight: ;\n  --bs-nav-link-color: var(--bs-navbar-color);\n  --bs-nav-link-hover-color: var(--bs-navbar-hover-color);\n  --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);\n  display: flex;\n  flex-direction: column;\n  padding-left: 0;\n  margin-bottom: 0;\n  list-style: none; }\n  .navbar-nav .nav-link.active, .navbar-nav .banner .nav a.active, .banner .nav .navbar-nav a.active, .navbar-nav .nav-link.show, .navbar-nav .banner .nav a.show, .banner .nav .navbar-nav a.show {\n    color: var(--bs-navbar-active-color); }\n  .navbar-nav .dropdown-menu {\n    position: static; }\n\n.navbar-text {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  color: var(--bs-navbar-color); }\n  .navbar-text a,\n  .navbar-text a:hover,\n  .navbar-text a:focus {\n    color: var(--bs-navbar-active-color); }\n\n.navbar-collapse {\n  flex-basis: 100%;\n  flex-grow: 1;\n  align-items: center; }\n\n.navbar-toggler {\n  padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);\n  font-size: var(--bs-navbar-toggler-font-size);\n  line-height: 1;\n  color: var(--bs-navbar-color);\n  background-color: transparent;\n  border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);\n  border-radius: var(--bs-navbar-toggler-border-radius);\n  transition: var(--bs-navbar-toggler-transition); }\n  @media (prefers-reduced-motion: reduce) {\n    .navbar-toggler {\n      transition: none; } }\n  .navbar-toggler:hover {\n    text-decoration: none; }\n  .navbar-toggler:focus {\n    text-decoration: none;\n    outline: 0;\n    box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width); }\n\n.navbar-toggler-icon {\n  display: inline-block;\n  width: 1.5em;\n  height: 1.5em;\n  vertical-align: middle;\n  background-image: var(--bs-navbar-toggler-icon-bg);\n  background-repeat: no-repeat;\n  background-position: center;\n  background-size: 100%; }\n\n.navbar-nav-scroll {\n  max-height: var(--bs-scroll-height, 75vh);\n  overflow-y: auto; }\n\n@media (min-width: 576px) {\n  .navbar-expand-sm {\n    flex-wrap: nowrap;\n    justify-content: flex-start; }\n    .navbar-expand-sm .navbar-nav {\n      flex-direction: row; }\n      .navbar-expand-sm .navbar-nav .dropdown-menu {\n        position: absolute; }\n      .navbar-expand-sm .navbar-nav .nav-link, .navbar-expand-sm .navbar-nav .banner .nav a, .banner .nav .navbar-expand-sm .navbar-nav a {\n        padding-right: var(--bs-navbar-nav-link-padding-x);\n        padding-left: var(--bs-navbar-nav-link-padding-x); }\n    .navbar-expand-sm .navbar-nav-scroll {\n      overflow: visible; }\n    .navbar-expand-sm .navbar-collapse {\n      display: flex !important;\n      flex-basis: auto; }\n    .navbar-expand-sm .navbar-toggler {\n      display: none; }\n    .navbar-expand-sm .offcanvas {\n      position: static;\n      z-index: auto;\n      flex-grow: 1;\n      width: auto !important;\n      height: auto !important;\n      visibility: visible !important;\n      background-color: transparent !important;\n      border: 0 !important;\n      transform: none !important;\n      transition: none; }\n      .navbar-expand-sm .offcanvas .offcanvas-header {\n        display: none; }\n      .navbar-expand-sm .offcanvas .offcanvas-body {\n        display: flex;\n        flex-grow: 0;\n        padding: 0;\n        overflow-y: visible; } }\n\n@media (min-width: 768px) {\n  .navbar-expand-md {\n    flex-wrap: nowrap;\n    justify-content: flex-start; }\n    .navbar-expand-md .navbar-nav {\n      flex-direction: row; }\n      .navbar-expand-md .navbar-nav .dropdown-menu {\n        position: absolute; }\n      .navbar-expand-md .navbar-nav .nav-link, .navbar-expand-md .navbar-nav .banner .nav a, .banner .nav .navbar-expand-md .navbar-nav a {\n        padding-right: var(--bs-navbar-nav-link-padding-x);\n        padding-left: var(--bs-navbar-nav-link-padding-x); }\n    .navbar-expand-md .navbar-nav-scroll {\n      overflow: visible; }\n    .navbar-expand-md .navbar-collapse {\n      display: flex !important;\n      flex-basis: auto; }\n    .navbar-expand-md .navbar-toggler {\n      display: none; }\n    .navbar-expand-md .offcanvas {\n      position: static;\n      z-index: auto;\n      flex-grow: 1;\n      width: auto !important;\n      height: auto !important;\n      visibility: visible !important;\n      background-color: transparent !important;\n      border: 0 !important;\n      transform: none !important;\n      transition: none; }\n      .navbar-expand-md .offcanvas .offcanvas-header {\n        display: none; }\n      .navbar-expand-md .offcanvas .offcanvas-body {\n        display: flex;\n        flex-grow: 0;\n        padding: 0;\n        overflow-y: visible; } }\n\n@media (min-width: 992px) {\n  .navbar-expand-lg {\n    flex-wrap: nowrap;\n    justify-content: flex-start; }\n    .navbar-expand-lg .navbar-nav {\n      flex-direction: row; }\n      .navbar-expand-lg .navbar-nav .dropdown-menu {\n        position: absolute; }\n      .navbar-expand-lg .navbar-nav .nav-link, .navbar-expand-lg .navbar-nav .banner .nav a, .banner .nav .navbar-expand-lg .navbar-nav a {\n        padding-right: var(--bs-navbar-nav-link-padding-x);\n        padding-left: var(--bs-navbar-nav-link-padding-x); }\n    .navbar-expand-lg .navbar-nav-scroll {\n      overflow: visible; }\n    .navbar-expand-lg .navbar-collapse {\n      display: flex !important;\n      flex-basis: auto; }\n    .navbar-expand-lg .navbar-toggler {\n      display: none; }\n    .navbar-expand-lg .offcanvas {\n      position: static;\n      z-index: auto;\n      flex-grow: 1;\n      width: auto !important;\n      height: auto !important;\n      visibility: visible !important;\n      background-color: transparent !important;\n      border: 0 !important;\n      transform: none !important;\n      transition: none; }\n      .navbar-expand-lg .offcanvas .offcanvas-header {\n        display: none; }\n      .navbar-expand-lg .offcanvas .offcanvas-body {\n        display: flex;\n        flex-grow: 0;\n        padding: 0;\n        overflow-y: visible; } }\n\n@media (min-width: 1200px) {\n  .navbar-expand-xl {\n    flex-wrap: nowrap;\n    justify-content: flex-start; }\n    .navbar-expand-xl .navbar-nav {\n      flex-direction: row; }\n      .navbar-expand-xl .navbar-nav .dropdown-menu {\n        position: absolute; }\n      .navbar-expand-xl .navbar-nav .nav-link, .navbar-expand-xl .navbar-nav .banner .nav a, .banner .nav .navbar-expand-xl .navbar-nav a {\n        padding-right: var(--bs-navbar-nav-link-padding-x);\n        padding-left: var(--bs-navbar-nav-link-padding-x); }\n    .navbar-expand-xl .navbar-nav-scroll {\n      overflow: visible; }\n    .navbar-expand-xl .navbar-collapse {\n      display: flex !important;\n      flex-basis: auto; }\n    .navbar-expand-xl .navbar-toggler {\n      display: none; }\n    .navbar-expand-xl .offcanvas {\n      position: static;\n      z-index: auto;\n      flex-grow: 1;\n      width: auto !important;\n      height: auto !important;\n      visibility: visible !important;\n      background-color: transparent !important;\n      border: 0 !important;\n      transform: none !important;\n      transition: none; }\n      .navbar-expand-xl .offcanvas .offcanvas-header {\n        display: none; }\n      .navbar-expand-xl .offcanvas .offcanvas-body {\n        display: flex;\n        flex-grow: 0;\n        padding: 0;\n        overflow-y: visible; } }\n\n@media (min-width: 1400px) {\n  .navbar-expand-xxl {\n    flex-wrap: nowrap;\n    justify-content: flex-start; }\n    .navbar-expand-xxl .navbar-nav {\n      flex-direction: row; }\n      .navbar-expand-xxl .navbar-nav .dropdown-menu {\n        position: absolute; }\n      .navbar-expand-xxl .navbar-nav .nav-link, .navbar-expand-xxl .navbar-nav .banner .nav a, .banner .nav .navbar-expand-xxl .navbar-nav a {\n        padding-right: var(--bs-navbar-nav-link-padding-x);\n        padding-left: var(--bs-navbar-nav-link-padding-x); }\n    .navbar-expand-xxl .navbar-nav-scroll {\n      overflow: visible; }\n    .navbar-expand-xxl .navbar-collapse {\n      display: flex !important;\n      flex-basis: auto; }\n    .navbar-expand-xxl .navbar-toggler {\n      display: none; }\n    .navbar-expand-xxl .offcanvas {\n      position: static;\n      z-index: auto;\n      flex-grow: 1;\n      width: auto !important;\n      height: auto !important;\n      visibility: visible !important;\n      background-color: transparent !important;\n      border: 0 !important;\n      transform: none !important;\n      transition: none; }\n      .navbar-expand-xxl .offcanvas .offcanvas-header {\n        display: none; }\n      .navbar-expand-xxl .offcanvas .offcanvas-body {\n        display: flex;\n        flex-grow: 0;\n        padding: 0;\n        overflow-y: visible; } }\n\n.navbar-expand {\n  flex-wrap: nowrap;\n  justify-content: flex-start; }\n  .navbar-expand .navbar-nav {\n    flex-direction: row; }\n    .navbar-expand .navbar-nav .dropdown-menu {\n      position: absolute; }\n    .navbar-expand .navbar-nav .nav-link, .navbar-expand .navbar-nav .banner .nav a, .banner .nav .navbar-expand .navbar-nav a {\n      padding-right: var(--bs-navbar-nav-link-padding-x);\n      padding-left: var(--bs-navbar-nav-link-padding-x); }\n  .navbar-expand .navbar-nav-scroll {\n    overflow: visible; }\n  .navbar-expand .navbar-collapse {\n    display: flex !important;\n    flex-basis: auto; }\n  .navbar-expand .navbar-toggler {\n    display: none; }\n  .navbar-expand .offcanvas {\n    position: static;\n    z-index: auto;\n    flex-grow: 1;\n    width: auto !important;\n    height: auto !important;\n    visibility: visible !important;\n    background-color: transparent !important;\n    border: 0 !important;\n    transform: none !important;\n    transition: none; }\n    .navbar-expand .offcanvas .offcanvas-header {\n      display: none; }\n    .navbar-expand .offcanvas .offcanvas-body {\n      display: flex;\n      flex-grow: 0;\n      padding: 0;\n      overflow-y: visible; }\n\n.navbar-dark,\n.navbar[data-bs-theme=\"dark\"] {\n  --bs-navbar-color: #c1c3c8;\n  --bs-navbar-hover-color: #b3c7ff;\n  --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);\n  --bs-navbar-active-color: #b3c7ff;\n  --bs-navbar-brand-color: #b3c7ff;\n  --bs-navbar-brand-hover-color: #b3c7ff;\n  --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);\n  --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\"); }\n\n[data-bs-theme=\"dark\"] .navbar-toggler-icon {\n  --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\"); }\n\n.card {\n  --bs-card-spacer-y: 1rem;\n  --bs-card-spacer-x: 1rem;\n  --bs-card-title-spacer-y: 0.5rem;\n  --bs-card-title-color: ;\n  --bs-card-subtitle-color: ;\n  --bs-card-border-width: var(--bs-border-width);\n  --bs-card-border-color: #e9ecef;\n  --bs-card-border-radius: var(--bs-border-radius);\n  --bs-card-box-shadow: ;\n  --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n  --bs-card-cap-padding-y: 0.5rem;\n  --bs-card-cap-padding-x: 1rem;\n  --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);\n  --bs-card-cap-color: ;\n  --bs-card-height: ;\n  --bs-card-color: ;\n  --bs-card-bg: var(--bs-body-bg);\n  --bs-card-img-overlay-padding: 1rem;\n  --bs-card-group-margin: 1.5rem;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  min-width: 0;\n  height: var(--bs-card-height);\n  color: var(--bs-body-color);\n  word-wrap: break-word;\n  background-color: var(--bs-card-bg);\n  background-clip: border-box;\n  border: var(--bs-card-border-width) solid var(--bs-card-border-color);\n  border-radius: var(--bs-card-border-radius); }\n  .card > hr {\n    margin-right: 0;\n    margin-left: 0; }\n  .card > .list-group {\n    border-top: inherit;\n    border-bottom: inherit; }\n    .card > .list-group:first-child {\n      border-top-width: 0;\n      border-top-left-radius: var(--bs-card-inner-border-radius);\n      border-top-right-radius: var(--bs-card-inner-border-radius); }\n    .card > .list-group:last-child {\n      border-bottom-width: 0;\n      border-bottom-right-radius: var(--bs-card-inner-border-radius);\n      border-bottom-left-radius: var(--bs-card-inner-border-radius); }\n  .card > .card-header + .list-group,\n  .card > .list-group + .card-footer {\n    border-top: 0; }\n\n.card-body {\n  flex: 1 1 auto;\n  padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);\n  color: var(--bs-card-color); }\n\n.card-title {\n  margin-bottom: var(--bs-card-title-spacer-y);\n  color: var(--bs-card-title-color); }\n\n.card-subtitle {\n  margin-top: calc(-.5 * var(--bs-card-title-spacer-y));\n  margin-bottom: 0;\n  color: var(--bs-card-subtitle-color); }\n\n.card-text:last-child {\n  margin-bottom: 0; }\n\n.card-link:hover {\n  text-decoration: none; }\n\n.card-link + .card-link {\n  margin-left: var(--bs-card-spacer-x); }\n\n.card-header {\n  padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n  margin-bottom: 0;\n  color: var(--bs-card-cap-color);\n  background-color: var(--bs-card-cap-bg);\n  border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color); }\n  .card-header:first-child {\n    border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0; }\n\n.card-footer {\n  padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n  color: var(--bs-card-cap-color);\n  background-color: var(--bs-card-cap-bg);\n  border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); }\n  .card-footer:last-child {\n    border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius); }\n\n.card-header-tabs {\n  margin-right: calc(-.5 * var(--bs-card-cap-padding-x));\n  margin-bottom: calc(-1 * var(--bs-card-cap-padding-y));\n  margin-left: calc(-.5 * var(--bs-card-cap-padding-x));\n  border-bottom: 0; }\n  .card-header-tabs .nav-link.active, .card-header-tabs .banner .nav a.active, .banner .nav .card-header-tabs a.active {\n    background-color: var(--bs-card-bg);\n    border-bottom-color: var(--bs-card-bg); }\n\n.card-header-pills {\n  margin-right: calc(-.5 * var(--bs-card-cap-padding-x));\n  margin-left: calc(-.5 * var(--bs-card-cap-padding-x)); }\n\n.card-img-overlay {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  padding: var(--bs-card-img-overlay-padding);\n  border-radius: var(--bs-card-inner-border-radius); }\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n  width: 100%; }\n\n.card-img,\n.card-img-top {\n  border-top-left-radius: var(--bs-card-inner-border-radius);\n  border-top-right-radius: var(--bs-card-inner-border-radius); }\n\n.card-img,\n.card-img-bottom {\n  border-bottom-right-radius: var(--bs-card-inner-border-radius);\n  border-bottom-left-radius: var(--bs-card-inner-border-radius); }\n\n.card-group > .card {\n  margin-bottom: var(--bs-card-group-margin); }\n\n@media (min-width: 576px) {\n  .card-group {\n    display: flex;\n    flex-flow: row wrap; }\n    .card-group > .card {\n      flex: 1 0 0%;\n      margin-bottom: 0; }\n      .card-group > .card + .card {\n        margin-left: 0;\n        border-left: 0; }\n      .card-group > .card:not(:last-child) {\n        border-top-right-radius: 0;\n        border-bottom-right-radius: 0; }\n        .card-group > .card:not(:last-child) .card-img-top,\n        .card-group > .card:not(:last-child) .card-header {\n          border-top-right-radius: 0; }\n        .card-group > .card:not(:last-child) .card-img-bottom,\n        .card-group > .card:not(:last-child) .card-footer {\n          border-bottom-right-radius: 0; }\n      .card-group > .card:not(:first-child) {\n        border-top-left-radius: 0;\n        border-bottom-left-radius: 0; }\n        .card-group > .card:not(:first-child) .card-img-top,\n        .card-group > .card:not(:first-child) .card-header {\n          border-top-left-radius: 0; }\n        .card-group > .card:not(:first-child) .card-img-bottom,\n        .card-group > .card:not(:first-child) .card-footer {\n          border-bottom-left-radius: 0; } }\n\n.accordion {\n  --bs-accordion-color: var(--bs-body-color);\n  --bs-accordion-bg: var(--bs-body-bg);\n  --bs-accordion-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, border-radius 0.15s ease;\n  --bs-accordion-border-color: var(--bs-border-color);\n  --bs-accordion-border-width: var(--bs-border-width);\n  --bs-accordion-border-radius: var(--bs-border-radius);\n  --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n  --bs-accordion-btn-padding-x: 1.25rem;\n  --bs-accordion-btn-padding-y: 1rem;\n  --bs-accordion-btn-color: var(--bs-body-color);\n  --bs-accordion-btn-bg: var(--bs-accordion-bg);\n  --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%231d2d35' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e\");\n  --bs-accordion-btn-icon-width: 1.25rem;\n  --bs-accordion-btn-icon-transform: rotate(-180deg);\n  --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;\n  --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23201c5c' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e\");\n  --bs-accordion-btn-focus-box-shadow: none;\n  --bs-accordion-body-padding-x: 1.25rem;\n  --bs-accordion-body-padding-y: 1rem;\n  --bs-accordion-active-color: var(--bs-primary-text-emphasis);\n  --bs-accordion-active-bg: var(--bs-primary-bg-subtle); }\n\n.accordion-button {\n  position: relative;\n  display: flex;\n  align-items: center;\n  width: 100%;\n  padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);\n  font-size: 1rem;\n  color: var(--bs-accordion-btn-color);\n  text-align: left;\n  background-color: var(--bs-accordion-btn-bg);\n  border: 0;\n  border-radius: 0;\n  overflow-anchor: none;\n  transition: var(--bs-accordion-transition); }\n  @media (prefers-reduced-motion: reduce) {\n    .accordion-button {\n      transition: none; } }\n  .accordion-button:not(.collapsed) {\n    color: var(--bs-accordion-active-color);\n    background-color: var(--bs-accordion-active-bg);\n    box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); }\n    .accordion-button:not(.collapsed)::after {\n      background-image: var(--bs-accordion-btn-active-icon);\n      transform: var(--bs-accordion-btn-icon-transform); }\n  .accordion-button::after {\n    flex-shrink: 0;\n    width: var(--bs-accordion-btn-icon-width);\n    height: var(--bs-accordion-btn-icon-width);\n    margin-left: auto;\n    content: \"\";\n    background-image: var(--bs-accordion-btn-icon);\n    background-repeat: no-repeat;\n    background-size: var(--bs-accordion-btn-icon-width);\n    transition: var(--bs-accordion-btn-icon-transition); }\n    @media (prefers-reduced-motion: reduce) {\n      .accordion-button::after {\n        transition: none; } }\n  .accordion-button:hover {\n    z-index: 2; }\n  .accordion-button:focus {\n    z-index: 3;\n    outline: 0;\n    box-shadow: var(--bs-accordion-btn-focus-box-shadow); }\n\n.accordion-header {\n  margin-bottom: 0; }\n\n.accordion-item {\n  color: var(--bs-accordion-color);\n  background-color: var(--bs-accordion-bg);\n  border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); }\n  .accordion-item:first-of-type {\n    border-top-left-radius: var(--bs-accordion-border-radius);\n    border-top-right-radius: var(--bs-accordion-border-radius); }\n    .accordion-item:first-of-type > .accordion-header .accordion-button {\n      border-top-left-radius: var(--bs-accordion-inner-border-radius);\n      border-top-right-radius: var(--bs-accordion-inner-border-radius); }\n  .accordion-item:not(:first-of-type) {\n    border-top: 0; }\n  .accordion-item:last-of-type {\n    border-bottom-right-radius: var(--bs-accordion-border-radius);\n    border-bottom-left-radius: var(--bs-accordion-border-radius); }\n    .accordion-item:last-of-type > .accordion-header .accordion-button.collapsed {\n      border-bottom-right-radius: var(--bs-accordion-inner-border-radius);\n      border-bottom-left-radius: var(--bs-accordion-inner-border-radius); }\n    .accordion-item:last-of-type > .accordion-collapse {\n      border-bottom-right-radius: var(--bs-accordion-border-radius);\n      border-bottom-left-radius: var(--bs-accordion-border-radius); }\n\n.accordion-body {\n  padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); }\n\n.accordion-flush > .accordion-item {\n  border-right: 0;\n  border-left: 0;\n  border-radius: 0; }\n  .accordion-flush > .accordion-item:first-child {\n    border-top: 0; }\n  .accordion-flush > .accordion-item:last-child {\n    border-bottom: 0; }\n  .accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {\n    border-radius: 0; }\n  .accordion-flush > .accordion-item > .accordion-collapse {\n    border-radius: 0; }\n\n[data-bs-theme=\"dark\"] .accordion-button::after {\n  --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%239590ef'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n  --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%239590ef'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\"); }\n\n.breadcrumb {\n  --bs-breadcrumb-padding-x: 0;\n  --bs-breadcrumb-padding-y: 0;\n  --bs-breadcrumb-margin-bottom: 1rem;\n  --bs-breadcrumb-bg: ;\n  --bs-breadcrumb-border-radius: ;\n  --bs-breadcrumb-divider-color: var(--bs-secondary-color);\n  --bs-breadcrumb-item-padding-x: 0.5rem;\n  --bs-breadcrumb-item-active-color: var(--bs-secondary-color);\n  display: flex;\n  flex-wrap: wrap;\n  padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);\n  margin-bottom: var(--bs-breadcrumb-margin-bottom);\n  font-size: var(--bs-breadcrumb-font-size);\n  list-style: none;\n  background-color: var(--bs-breadcrumb-bg);\n  border-radius: var(--bs-breadcrumb-border-radius); }\n\n.breadcrumb-item + .breadcrumb-item {\n  padding-left: var(--bs-breadcrumb-item-padding-x); }\n  .breadcrumb-item + .breadcrumb-item::before {\n    float: left;\n    padding-right: var(--bs-breadcrumb-item-padding-x);\n    color: var(--bs-breadcrumb-divider-color);\n    content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */; }\n\n.breadcrumb-item.active {\n  color: var(--bs-breadcrumb-item-active-color); }\n\n.pagination {\n  --bs-pagination-padding-x: 0.75rem;\n  --bs-pagination-padding-y: 0.375rem;\n  --bs-pagination-font-size: 1rem;\n  --bs-pagination-color: var(--bs-link-color);\n  --bs-pagination-bg: var(--bs-body-bg);\n  --bs-pagination-border-width: var(--bs-border-width);\n  --bs-pagination-border-color: var(--bs-border-color);\n  --bs-pagination-border-radius: var(--bs-border-radius);\n  --bs-pagination-hover-color: var(--bs-link-hover-color);\n  --bs-pagination-hover-bg: var(--bs-tertiary-bg);\n  --bs-pagination-hover-border-color: var(--bs-border-color);\n  --bs-pagination-focus-color: var(--bs-link-hover-color);\n  --bs-pagination-focus-bg: var(--bs-secondary-bg);\n  --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.25);\n  --bs-pagination-active-color: #fff;\n  --bs-pagination-active-bg: #4f46e5;\n  --bs-pagination-active-border-color: #4f46e5;\n  --bs-pagination-disabled-color: var(--bs-secondary-color);\n  --bs-pagination-disabled-bg: var(--bs-secondary-bg);\n  --bs-pagination-disabled-border-color: var(--bs-border-color);\n  display: flex;\n  padding-left: 0;\n  list-style: none; }\n\n.page-link {\n  position: relative;\n  display: block;\n  padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);\n  font-size: var(--bs-pagination-font-size);\n  color: var(--bs-pagination-color);\n  background-color: var(--bs-pagination-bg);\n  border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);\n  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; }\n  @media (prefers-reduced-motion: reduce) {\n    .page-link {\n      transition: none; } }\n  .page-link:hover {\n    z-index: 2;\n    color: var(--bs-pagination-hover-color);\n    text-decoration: none;\n    background-color: var(--bs-pagination-hover-bg);\n    border-color: var(--bs-pagination-hover-border-color); }\n  .page-link:focus {\n    z-index: 3;\n    color: var(--bs-pagination-focus-color);\n    background-color: var(--bs-pagination-focus-bg);\n    outline: 0;\n    box-shadow: var(--bs-pagination-focus-box-shadow); }\n  .page-link.active, .active > .page-link {\n    z-index: 3;\n    color: var(--bs-pagination-active-color);\n    background-color: var(--bs-pagination-active-bg);\n    border-color: var(--bs-pagination-active-border-color); }\n  .page-link.disabled, .disabled > .page-link {\n    color: var(--bs-pagination-disabled-color);\n    pointer-events: none;\n    background-color: var(--bs-pagination-disabled-bg);\n    border-color: var(--bs-pagination-disabled-border-color); }\n\n.page-item:not(:first-child) .page-link {\n  margin-left: calc(var(--bs-border-width) * -1); }\n\n.page-item:first-child .page-link {\n  border-top-left-radius: var(--bs-pagination-border-radius);\n  border-bottom-left-radius: var(--bs-pagination-border-radius); }\n\n.page-item:last-child .page-link {\n  border-top-right-radius: var(--bs-pagination-border-radius);\n  border-bottom-right-radius: var(--bs-pagination-border-radius); }\n\n.pagination-lg {\n  --bs-pagination-padding-x: 1.5rem;\n  --bs-pagination-padding-y: 0.75rem;\n  --bs-pagination-font-size: 1.25rem;\n  --bs-pagination-border-radius: var(--bs-border-radius-lg); }\n\n.pagination-sm {\n  --bs-pagination-padding-x: 0.5rem;\n  --bs-pagination-padding-y: 0.25rem;\n  --bs-pagination-font-size: 0.875rem;\n  --bs-pagination-border-radius: var(--bs-border-radius-sm); }\n\n.badge {\n  --bs-badge-padding-x: 0.65em;\n  --bs-badge-padding-y: 0.35em;\n  --bs-badge-font-size: 0.75em;\n  --bs-badge-font-weight: 700;\n  --bs-badge-color: #fff;\n  --bs-badge-border-radius: var(--bs-border-radius);\n  display: inline-block;\n  padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);\n  font-size: var(--bs-badge-font-size);\n  font-weight: var(--bs-badge-font-weight);\n  line-height: 1;\n  color: var(--bs-badge-color);\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: var(--bs-badge-border-radius); }\n  .badge:empty {\n    display: none; }\n\n.btn .badge, .search-form .search-submit .badge, .comment-form input[type=\"submit\"] .badge {\n  position: relative;\n  top: -1px; }\n\n.alert {\n  --bs-alert-bg: transparent;\n  --bs-alert-padding-x: 1.5rem;\n  --bs-alert-padding-y: 1rem;\n  --bs-alert-margin-bottom: 0;\n  --bs-alert-color: inherit;\n  --bs-alert-border-color: transparent;\n  --bs-alert-border: 0 solid var(--bs-alert-border-color);\n  --bs-alert-border-radius: 0;\n  --bs-alert-link-color: inherit;\n  position: relative;\n  padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);\n  margin-bottom: var(--bs-alert-margin-bottom);\n  color: var(--bs-alert-color);\n  background-color: var(--bs-alert-bg);\n  border: var(--bs-alert-border);\n  border-radius: var(--bs-alert-border-radius); }\n\n.alert-heading {\n  color: inherit; }\n\n.alert-link {\n  font-weight: 700;\n  color: var(--bs-alert-link-color); }\n\n.alert-dismissible {\n  padding-right: 4.5rem; }\n  .alert-dismissible .btn-close {\n    position: absolute;\n    top: 0;\n    right: 0;\n    z-index: 2;\n    padding: 1.25rem 1.5rem; }\n\n.alert-primary {\n  --bs-alert-color: var(--bs-primary-text-emphasis);\n  --bs-alert-bg: var(--bs-primary-bg-subtle);\n  --bs-alert-border-color: var(--bs-primary-border-subtle);\n  --bs-alert-link-color: var(--bs-primary-text-emphasis); }\n\n.alert-secondary {\n  --bs-alert-color: var(--bs-secondary-text-emphasis);\n  --bs-alert-bg: var(--bs-secondary-bg-subtle);\n  --bs-alert-border-color: var(--bs-secondary-border-subtle);\n  --bs-alert-link-color: var(--bs-secondary-text-emphasis); }\n\n.alert-success {\n  --bs-alert-color: var(--bs-success-text-emphasis);\n  --bs-alert-bg: var(--bs-success-bg-subtle);\n  --bs-alert-border-color: var(--bs-success-border-subtle);\n  --bs-alert-link-color: var(--bs-success-text-emphasis); }\n\n.alert-info {\n  --bs-alert-color: var(--bs-info-text-emphasis);\n  --bs-alert-bg: var(--bs-info-bg-subtle);\n  --bs-alert-border-color: var(--bs-info-border-subtle);\n  --bs-alert-link-color: var(--bs-info-text-emphasis); }\n\n.alert-warning {\n  --bs-alert-color: var(--bs-warning-text-emphasis);\n  --bs-alert-bg: var(--bs-warning-bg-subtle);\n  --bs-alert-border-color: var(--bs-warning-border-subtle);\n  --bs-alert-link-color: var(--bs-warning-text-emphasis); }\n\n.alert-danger {\n  --bs-alert-color: var(--bs-danger-text-emphasis);\n  --bs-alert-bg: var(--bs-danger-bg-subtle);\n  --bs-alert-border-color: var(--bs-danger-border-subtle);\n  --bs-alert-link-color: var(--bs-danger-text-emphasis); }\n\n.alert-light {\n  --bs-alert-color: var(--bs-light-text-emphasis);\n  --bs-alert-bg: var(--bs-light-bg-subtle);\n  --bs-alert-border-color: var(--bs-light-border-subtle);\n  --bs-alert-link-color: var(--bs-light-text-emphasis); }\n\n.alert-dark {\n  --bs-alert-color: var(--bs-dark-text-emphasis);\n  --bs-alert-bg: var(--bs-dark-bg-subtle);\n  --bs-alert-border-color: var(--bs-dark-border-subtle);\n  --bs-alert-link-color: var(--bs-dark-text-emphasis); }\n\n@keyframes progress-bar-stripes {\n  0% {\n    background-position-x: 1rem; } }\n\n.progress,\n.progress-stacked {\n  --bs-progress-height: 1rem;\n  --bs-progress-font-size: 0.75rem;\n  --bs-progress-bg: var(--bs-secondary-bg);\n  --bs-progress-border-radius: var(--bs-border-radius);\n  --bs-progress-box-shadow: var(--bs-box-shadow-inset);\n  --bs-progress-bar-color: #fff;\n  --bs-progress-bar-bg: #4f46e5;\n  --bs-progress-bar-transition: width 0.6s ease;\n  display: flex;\n  height: var(--bs-progress-height);\n  overflow: hidden;\n  font-size: var(--bs-progress-font-size);\n  background-color: var(--bs-progress-bg);\n  border-radius: var(--bs-progress-border-radius); }\n\n.progress-bar {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  overflow: hidden;\n  color: var(--bs-progress-bar-color);\n  text-align: center;\n  white-space: nowrap;\n  background-color: var(--bs-progress-bar-bg);\n  transition: var(--bs-progress-bar-transition); }\n  @media (prefers-reduced-motion: reduce) {\n    .progress-bar {\n      transition: none; } }\n.progress-bar-striped {\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-size: var(--bs-progress-height) var(--bs-progress-height); }\n\n.progress-stacked > .progress {\n  overflow: visible; }\n\n.progress-stacked > .progress > .progress-bar {\n  width: 100%; }\n\n.progress-bar-animated {\n  animation: 1s linear infinite progress-bar-stripes; }\n  @media (prefers-reduced-motion: reduce) {\n    .progress-bar-animated {\n      animation: none; } }\n.list-group {\n  --bs-list-group-color: var(--bs-body-color);\n  --bs-list-group-bg: var(--bs-body-bg);\n  --bs-list-group-border-color: var(--bs-border-color);\n  --bs-list-group-border-width: var(--bs-border-width);\n  --bs-list-group-border-radius: var(--bs-border-radius);\n  --bs-list-group-item-padding-x: 1rem;\n  --bs-list-group-item-padding-y: 0.5rem;\n  --bs-list-group-action-color: var(--bs-secondary-color);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);\n  --bs-list-group-action-active-color: var(--bs-body-color);\n  --bs-list-group-action-active-bg: var(--bs-secondary-bg);\n  --bs-list-group-disabled-color: var(--bs-secondary-color);\n  --bs-list-group-disabled-bg: var(--bs-body-bg);\n  --bs-list-group-active-color: #fff;\n  --bs-list-group-active-bg: #4f46e5;\n  --bs-list-group-active-border-color: #4f46e5;\n  display: flex;\n  flex-direction: column;\n  padding-left: 0;\n  margin-bottom: 0;\n  border-radius: var(--bs-list-group-border-radius); }\n\n.list-group-numbered {\n  list-style-type: none;\n  counter-reset: section; }\n  .list-group-numbered > .list-group-item::before {\n    content: counters(section, \".\") \". \";\n    counter-increment: section; }\n\n.list-group-item-action {\n  width: 100%;\n  color: var(--bs-list-group-action-color);\n  text-align: inherit; }\n  .list-group-item-action:hover, .list-group-item-action:focus {\n    z-index: 1;\n    color: var(--bs-list-group-action-hover-color);\n    text-decoration: none;\n    background-color: var(--bs-list-group-action-hover-bg); }\n  .list-group-item-action:active {\n    color: var(--bs-list-group-action-active-color);\n    background-color: var(--bs-list-group-action-active-bg); }\n\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);\n  color: var(--bs-list-group-color);\n  background-color: var(--bs-list-group-bg);\n  border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); }\n  .list-group-item:first-child {\n    border-top-left-radius: inherit;\n    border-top-right-radius: inherit; }\n  .list-group-item:last-child {\n    border-bottom-right-radius: inherit;\n    border-bottom-left-radius: inherit; }\n  .list-group-item.disabled, .list-group-item:disabled {\n    color: var(--bs-list-group-disabled-color);\n    pointer-events: none;\n    background-color: var(--bs-list-group-disabled-bg); }\n  .list-group-item.active {\n    z-index: 2;\n    color: var(--bs-list-group-active-color);\n    background-color: var(--bs-list-group-active-bg);\n    border-color: var(--bs-list-group-active-border-color); }\n  .list-group-item + .list-group-item {\n    border-top-width: 0; }\n    .list-group-item + .list-group-item.active {\n      margin-top: calc(-1 * var(--bs-list-group-border-width));\n      border-top-width: var(--bs-list-group-border-width); }\n\n.list-group-horizontal {\n  flex-direction: row; }\n  .list-group-horizontal > .list-group-item:first-child:not(:last-child) {\n    border-bottom-left-radius: var(--bs-list-group-border-radius);\n    border-top-right-radius: 0; }\n  .list-group-horizontal > .list-group-item:last-child:not(:first-child) {\n    border-top-right-radius: var(--bs-list-group-border-radius);\n    border-bottom-left-radius: 0; }\n  .list-group-horizontal > .list-group-item.active {\n    margin-top: 0; }\n  .list-group-horizontal > .list-group-item + .list-group-item {\n    border-top-width: var(--bs-list-group-border-width);\n    border-left-width: 0; }\n    .list-group-horizontal > .list-group-item + .list-group-item.active {\n      margin-left: calc(-1 * var(--bs-list-group-border-width));\n      border-left-width: var(--bs-list-group-border-width); }\n\n@media (min-width: 576px) {\n  .list-group-horizontal-sm {\n    flex-direction: row; }\n    .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) {\n      border-bottom-left-radius: var(--bs-list-group-border-radius);\n      border-top-right-radius: 0; }\n    .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) {\n      border-top-right-radius: var(--bs-list-group-border-radius);\n      border-bottom-left-radius: 0; }\n    .list-group-horizontal-sm > .list-group-item.active {\n      margin-top: 0; }\n    .list-group-horizontal-sm > .list-group-item + .list-group-item {\n      border-top-width: var(--bs-list-group-border-width);\n      border-left-width: 0; }\n      .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n        margin-left: calc(-1 * var(--bs-list-group-border-width));\n        border-left-width: var(--bs-list-group-border-width); } }\n\n@media (min-width: 768px) {\n  .list-group-horizontal-md {\n    flex-direction: row; }\n    .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) {\n      border-bottom-left-radius: var(--bs-list-group-border-radius);\n      border-top-right-radius: 0; }\n    .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) {\n      border-top-right-radius: var(--bs-list-group-border-radius);\n      border-bottom-left-radius: 0; }\n    .list-group-horizontal-md > .list-group-item.active {\n      margin-top: 0; }\n    .list-group-horizontal-md > .list-group-item + .list-group-item {\n      border-top-width: var(--bs-list-group-border-width);\n      border-left-width: 0; }\n      .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n        margin-left: calc(-1 * var(--bs-list-group-border-width));\n        border-left-width: var(--bs-list-group-border-width); } }\n\n@media (min-width: 992px) {\n  .list-group-horizontal-lg {\n    flex-direction: row; }\n    .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) {\n      border-bottom-left-radius: var(--bs-list-group-border-radius);\n      border-top-right-radius: 0; }\n    .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) {\n      border-top-right-radius: var(--bs-list-group-border-radius);\n      border-bottom-left-radius: 0; }\n    .list-group-horizontal-lg > .list-group-item.active {\n      margin-top: 0; }\n    .list-group-horizontal-lg > .list-group-item + .list-group-item {\n      border-top-width: var(--bs-list-group-border-width);\n      border-left-width: 0; }\n      .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n        margin-left: calc(-1 * var(--bs-list-group-border-width));\n        border-left-width: var(--bs-list-group-border-width); } }\n\n@media (min-width: 1200px) {\n  .list-group-horizontal-xl {\n    flex-direction: row; }\n    .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) {\n      border-bottom-left-radius: var(--bs-list-group-border-radius);\n      border-top-right-radius: 0; }\n    .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) {\n      border-top-right-radius: var(--bs-list-group-border-radius);\n      border-bottom-left-radius: 0; }\n    .list-group-horizontal-xl > .list-group-item.active {\n      margin-top: 0; }\n    .list-group-horizontal-xl > .list-group-item + .list-group-item {\n      border-top-width: var(--bs-list-group-border-width);\n      border-left-width: 0; }\n      .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n        margin-left: calc(-1 * var(--bs-list-group-border-width));\n        border-left-width: var(--bs-list-group-border-width); } }\n\n@media (min-width: 1400px) {\n  .list-group-horizontal-xxl {\n    flex-direction: row; }\n    .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) {\n      border-bottom-left-radius: var(--bs-list-group-border-radius);\n      border-top-right-radius: 0; }\n    .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) {\n      border-top-right-radius: var(--bs-list-group-border-radius);\n      border-bottom-left-radius: 0; }\n    .list-group-horizontal-xxl > .list-group-item.active {\n      margin-top: 0; }\n    .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n      border-top-width: var(--bs-list-group-border-width);\n      border-left-width: 0; }\n      .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n        margin-left: calc(-1 * var(--bs-list-group-border-width));\n        border-left-width: var(--bs-list-group-border-width); } }\n\n.list-group-flush {\n  border-radius: 0; }\n  .list-group-flush > .list-group-item {\n    border-width: 0 0 var(--bs-list-group-border-width); }\n    .list-group-flush > .list-group-item:last-child {\n      border-bottom-width: 0; }\n\n.list-group-item-primary {\n  --bs-list-group-color: var(--bs-primary-text-emphasis);\n  --bs-list-group-bg: var(--bs-primary-bg-subtle);\n  --bs-list-group-border-color: var(--bs-primary-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-primary-border-subtle);\n  --bs-list-group-active-color: var(--bs-primary-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-primary-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-primary-text-emphasis); }\n\n.list-group-item-secondary {\n  --bs-list-group-color: var(--bs-secondary-text-emphasis);\n  --bs-list-group-bg: var(--bs-secondary-bg-subtle);\n  --bs-list-group-border-color: var(--bs-secondary-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);\n  --bs-list-group-active-color: var(--bs-secondary-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-secondary-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis); }\n\n.list-group-item-success {\n  --bs-list-group-color: var(--bs-success-text-emphasis);\n  --bs-list-group-bg: var(--bs-success-bg-subtle);\n  --bs-list-group-border-color: var(--bs-success-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-success-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-success-border-subtle);\n  --bs-list-group-active-color: var(--bs-success-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-success-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-success-text-emphasis); }\n\n.list-group-item-info {\n  --bs-list-group-color: var(--bs-info-text-emphasis);\n  --bs-list-group-bg: var(--bs-info-bg-subtle);\n  --bs-list-group-border-color: var(--bs-info-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-info-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-info-border-subtle);\n  --bs-list-group-active-color: var(--bs-info-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-info-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-info-text-emphasis); }\n\n.list-group-item-warning {\n  --bs-list-group-color: var(--bs-warning-text-emphasis);\n  --bs-list-group-bg: var(--bs-warning-bg-subtle);\n  --bs-list-group-border-color: var(--bs-warning-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-warning-border-subtle);\n  --bs-list-group-active-color: var(--bs-warning-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-warning-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-warning-text-emphasis); }\n\n.list-group-item-danger {\n  --bs-list-group-color: var(--bs-danger-text-emphasis);\n  --bs-list-group-bg: var(--bs-danger-bg-subtle);\n  --bs-list-group-border-color: var(--bs-danger-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-danger-border-subtle);\n  --bs-list-group-active-color: var(--bs-danger-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-danger-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-danger-text-emphasis); }\n\n.list-group-item-light {\n  --bs-list-group-color: var(--bs-light-text-emphasis);\n  --bs-list-group-bg: var(--bs-light-bg-subtle);\n  --bs-list-group-border-color: var(--bs-light-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-light-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-light-border-subtle);\n  --bs-list-group-active-color: var(--bs-light-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-light-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-light-text-emphasis); }\n\n.list-group-item-dark {\n  --bs-list-group-color: var(--bs-dark-text-emphasis);\n  --bs-list-group-bg: var(--bs-dark-bg-subtle);\n  --bs-list-group-border-color: var(--bs-dark-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-dark-border-subtle);\n  --bs-list-group-active-color: var(--bs-dark-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-dark-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-dark-text-emphasis); }\n\n.btn-close {\n  --bs-btn-close-color: #000;\n  --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\");\n  --bs-btn-close-opacity: 0.5;\n  --bs-btn-close-hover-opacity: 0.75;\n  --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.25);\n  --bs-btn-close-focus-opacity: 1;\n  --bs-btn-close-disabled-opacity: 0.25;\n  --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);\n  box-sizing: content-box;\n  width: 1em;\n  height: 1em;\n  padding: 0.25em 0.25em;\n  color: var(--bs-btn-close-color);\n  background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;\n  border: 0;\n  border-radius: 0.375rem;\n  opacity: var(--bs-btn-close-opacity); }\n  .btn-close:hover {\n    color: var(--bs-btn-close-color);\n    text-decoration: none;\n    opacity: var(--bs-btn-close-hover-opacity); }\n  .btn-close:focus {\n    outline: 0;\n    box-shadow: var(--bs-btn-close-focus-shadow);\n    opacity: var(--bs-btn-close-focus-opacity); }\n  .btn-close:disabled, .btn-close.disabled {\n    pointer-events: none;\n    user-select: none;\n    opacity: var(--bs-btn-close-disabled-opacity); }\n\n.btn-close-white {\n  filter: var(--bs-btn-close-white-filter); }\n\n[data-bs-theme=\"dark\"] .btn-close {\n  filter: var(--bs-btn-close-white-filter); }\n\n.toast {\n  --bs-toast-zindex: 1090;\n  --bs-toast-padding-x: 0.75rem;\n  --bs-toast-padding-y: 0.5rem;\n  --bs-toast-spacing: 3rem;\n  --bs-toast-max-width: 350px;\n  --bs-toast-font-size: 0.875rem;\n  --bs-toast-color: ;\n  --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n  --bs-toast-border-width: var(--bs-border-width);\n  --bs-toast-border-color: var(--bs-border-color-translucent);\n  --bs-toast-border-radius: var(--bs-border-radius);\n  --bs-toast-box-shadow: var(--bs-box-shadow);\n  --bs-toast-header-color: var(--bs-secondary-color);\n  --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n  --bs-toast-header-border-color: var(--bs-border-color-translucent);\n  width: var(--bs-toast-max-width);\n  max-width: 100%;\n  font-size: var(--bs-toast-font-size);\n  color: var(--bs-toast-color);\n  pointer-events: auto;\n  background-color: var(--bs-toast-bg);\n  background-clip: padding-box;\n  border: var(--bs-toast-border-width) solid var(--bs-toast-border-color);\n  box-shadow: var(--bs-toast-box-shadow);\n  border-radius: var(--bs-toast-border-radius); }\n  .toast.showing {\n    opacity: 0; }\n  .toast:not(.show) {\n    display: none; }\n\n.toast-container {\n  --bs-toast-zindex: 1090;\n  position: absolute;\n  z-index: var(--bs-toast-zindex);\n  width: max-content;\n  max-width: 100%;\n  pointer-events: none; }\n  .toast-container > :not(:last-child) {\n    margin-bottom: var(--bs-toast-spacing); }\n\n.toast-header {\n  display: flex;\n  align-items: center;\n  padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);\n  color: var(--bs-toast-header-color);\n  background-color: var(--bs-toast-header-bg);\n  background-clip: padding-box;\n  border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);\n  border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n  border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); }\n  .toast-header .btn-close {\n    margin-right: calc(-.5 * var(--bs-toast-padding-x));\n    margin-left: var(--bs-toast-padding-x); }\n\n.toast-body {\n  padding: var(--bs-toast-padding-x);\n  word-wrap: break-word; }\n\n.modal {\n  --bs-modal-zindex: 1055;\n  --bs-modal-width: 500px;\n  --bs-modal-padding: 1rem;\n  --bs-modal-margin: 0.5rem;\n  --bs-modal-color: ;\n  --bs-modal-bg: var(--bs-body-bg);\n  --bs-modal-border-color: var(--bs-border-color-translucent);\n  --bs-modal-border-width: var(--bs-border-width);\n  --bs-modal-border-radius: var(--bs-border-radius-lg);\n  --bs-modal-box-shadow: var(--bs-box-shadow-sm);\n  --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));\n  --bs-modal-header-padding-x: 1rem;\n  --bs-modal-header-padding-y: 1rem;\n  --bs-modal-header-padding: 1rem 1rem;\n  --bs-modal-header-border-color: var(--bs-border-color);\n  --bs-modal-header-border-width: var(--bs-border-width);\n  --bs-modal-title-line-height: 1.5;\n  --bs-modal-footer-gap: 0.5rem;\n  --bs-modal-footer-bg: ;\n  --bs-modal-footer-border-color: var(--bs-border-color);\n  --bs-modal-footer-border-width: var(--bs-border-width);\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: var(--bs-modal-zindex);\n  display: none;\n  width: 100%;\n  height: 100%;\n  overflow-x: hidden;\n  overflow-y: auto;\n  outline: 0; }\n\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: var(--bs-modal-margin);\n  pointer-events: none; }\n  .modal.fade .modal-dialog {\n    transition: transform 0.3s ease-out;\n    transform: translate(0, -50px); }\n    @media (prefers-reduced-motion: reduce) {\n      .modal.fade .modal-dialog {\n        transition: none; } }\n  .modal.show .modal-dialog {\n    transform: none; }\n  .modal.modal-static .modal-dialog {\n    transform: scale(1.02); }\n\n.modal-dialog-scrollable {\n  height: calc(100% - var(--bs-modal-margin) * 2); }\n  .modal-dialog-scrollable .modal-content {\n    max-height: 100%;\n    overflow: hidden; }\n  .modal-dialog-scrollable .modal-body {\n    overflow-y: auto; }\n\n.modal-dialog-centered {\n  display: flex;\n  align-items: center;\n  min-height: calc(100% - var(--bs-modal-margin) * 2); }\n\n.modal-content {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  color: var(--bs-modal-color);\n  pointer-events: auto;\n  background-color: var(--bs-modal-bg);\n  background-clip: padding-box;\n  border: var(--bs-modal-border-width) solid var(--bs-modal-border-color);\n  border-radius: var(--bs-modal-border-radius);\n  outline: 0; }\n\n.modal-backdrop {\n  --bs-backdrop-zindex: 1050;\n  --bs-backdrop-bg: #000;\n  --bs-backdrop-opacity: 0.5;\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: var(--bs-backdrop-zindex);\n  width: 100vw;\n  height: 100vh;\n  background-color: var(--bs-backdrop-bg); }\n  .modal-backdrop.fade {\n    opacity: 0; }\n  .modal-backdrop.show {\n    opacity: var(--bs-backdrop-opacity); }\n\n.modal-header {\n  display: flex;\n  flex-shrink: 0;\n  align-items: center;\n  padding: var(--bs-modal-header-padding);\n  border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);\n  border-top-left-radius: var(--bs-modal-inner-border-radius);\n  border-top-right-radius: var(--bs-modal-inner-border-radius); }\n  .modal-header .btn-close {\n    padding: calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);\n    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; }\n\n.modal-title {\n  margin-bottom: 0;\n  line-height: var(--bs-modal-title-line-height); }\n\n.modal-body {\n  position: relative;\n  flex: 1 1 auto;\n  padding: var(--bs-modal-padding); }\n\n.modal-footer {\n  display: flex;\n  flex-shrink: 0;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: flex-end;\n  padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);\n  background-color: var(--bs-modal-footer-bg);\n  border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);\n  border-bottom-right-radius: var(--bs-modal-inner-border-radius);\n  border-bottom-left-radius: var(--bs-modal-inner-border-radius); }\n  .modal-footer > * {\n    margin: calc(var(--bs-modal-footer-gap) * .5); }\n\n@media (min-width: 576px) {\n  .modal {\n    --bs-modal-margin: 1.75rem;\n    --bs-modal-box-shadow: var(--bs-box-shadow); }\n  .modal-dialog {\n    max-width: var(--bs-modal-width);\n    margin-right: auto;\n    margin-left: auto; }\n  .modal-sm {\n    --bs-modal-width: 300px; } }\n\n@media (min-width: 992px) {\n  .modal-lg,\n  .modal-xl {\n    --bs-modal-width: 800px; } }\n\n@media (min-width: 1200px) {\n  .modal-xl {\n    --bs-modal-width: 1140px; } }\n\n.modal-fullscreen {\n  width: 100vw;\n  max-width: none;\n  height: 100%;\n  margin: 0; }\n  .modal-fullscreen .modal-content {\n    height: 100%;\n    border: 0;\n    border-radius: 0; }\n  .modal-fullscreen .modal-header,\n  .modal-fullscreen .modal-footer {\n    border-radius: 0; }\n  .modal-fullscreen .modal-body {\n    overflow-y: auto; }\n\n@media (max-width: 575.98px) {\n  .modal-fullscreen-sm-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0; }\n    .modal-fullscreen-sm-down .modal-content {\n      height: 100%;\n      border: 0;\n      border-radius: 0; }\n    .modal-fullscreen-sm-down .modal-header,\n    .modal-fullscreen-sm-down .modal-footer {\n      border-radius: 0; }\n    .modal-fullscreen-sm-down .modal-body {\n      overflow-y: auto; } }\n\n@media (max-width: 767.98px) {\n  .modal-fullscreen-md-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0; }\n    .modal-fullscreen-md-down .modal-content {\n      height: 100%;\n      border: 0;\n      border-radius: 0; }\n    .modal-fullscreen-md-down .modal-header,\n    .modal-fullscreen-md-down .modal-footer {\n      border-radius: 0; }\n    .modal-fullscreen-md-down .modal-body {\n      overflow-y: auto; } }\n\n@media (max-width: 991.98px) {\n  .modal-fullscreen-lg-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0; }\n    .modal-fullscreen-lg-down .modal-content {\n      height: 100%;\n      border: 0;\n      border-radius: 0; }\n    .modal-fullscreen-lg-down .modal-header,\n    .modal-fullscreen-lg-down .modal-footer {\n      border-radius: 0; }\n    .modal-fullscreen-lg-down .modal-body {\n      overflow-y: auto; } }\n\n@media (max-width: 1199.98px) {\n  .modal-fullscreen-xl-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0; }\n    .modal-fullscreen-xl-down .modal-content {\n      height: 100%;\n      border: 0;\n      border-radius: 0; }\n    .modal-fullscreen-xl-down .modal-header,\n    .modal-fullscreen-xl-down .modal-footer {\n      border-radius: 0; }\n    .modal-fullscreen-xl-down .modal-body {\n      overflow-y: auto; } }\n\n@media (max-width: 1399.98px) {\n  .modal-fullscreen-xxl-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0; }\n    .modal-fullscreen-xxl-down .modal-content {\n      height: 100%;\n      border: 0;\n      border-radius: 0; }\n    .modal-fullscreen-xxl-down .modal-header,\n    .modal-fullscreen-xxl-down .modal-footer {\n      border-radius: 0; }\n    .modal-fullscreen-xxl-down .modal-body {\n      overflow-y: auto; } }\n\n.tooltip {\n  --bs-tooltip-zindex: 1080;\n  --bs-tooltip-max-width: 200px;\n  --bs-tooltip-padding-x: 0.5rem;\n  --bs-tooltip-padding-y: 0.25rem;\n  --bs-tooltip-margin: ;\n  --bs-tooltip-font-size: 0.875rem;\n  --bs-tooltip-color: var(--bs-body-bg);\n  --bs-tooltip-bg: var(--bs-emphasis-color);\n  --bs-tooltip-border-radius: var(--bs-border-radius);\n  --bs-tooltip-opacity: 0.9;\n  --bs-tooltip-arrow-width: 0.8rem;\n  --bs-tooltip-arrow-height: 0.4rem;\n  z-index: var(--bs-tooltip-zindex);\n  display: block;\n  margin: var(--bs-tooltip-margin);\n  font-family: var(--bs-font-sans-serif);\n  font-style: normal;\n  font-weight: 400;\n  line-height: 1.5;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  letter-spacing: normal;\n  word-break: normal;\n  white-space: normal;\n  word-spacing: normal;\n  line-break: auto;\n  font-size: var(--bs-tooltip-font-size);\n  word-wrap: break-word;\n  opacity: 0; }\n  .tooltip.show {\n    opacity: var(--bs-tooltip-opacity); }\n  .tooltip .tooltip-arrow {\n    display: block;\n    width: var(--bs-tooltip-arrow-width);\n    height: var(--bs-tooltip-arrow-height); }\n    .tooltip .tooltip-arrow::before {\n      position: absolute;\n      content: \"\";\n      border-color: transparent;\n      border-style: solid; }\n\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=\"top\"] .tooltip-arrow {\n  bottom: calc(-1 * var(--bs-tooltip-arrow-height)); }\n  .bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=\"top\"] .tooltip-arrow::before {\n    top: -1px;\n    border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;\n    border-top-color: var(--bs-tooltip-bg); }\n\n/* rtl:begin:ignore */\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=\"right\"] .tooltip-arrow {\n  left: calc(-1 * var(--bs-tooltip-arrow-height));\n  width: var(--bs-tooltip-arrow-height);\n  height: var(--bs-tooltip-arrow-width); }\n  .bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=\"right\"] .tooltip-arrow::before {\n    right: -1px;\n    border-width: calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;\n    border-right-color: var(--bs-tooltip-bg); }\n\n/* rtl:end:ignore */\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=\"bottom\"] .tooltip-arrow {\n  top: calc(-1 * var(--bs-tooltip-arrow-height)); }\n  .bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=\"bottom\"] .tooltip-arrow::before {\n    bottom: -1px;\n    border-width: 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);\n    border-bottom-color: var(--bs-tooltip-bg); }\n\n/* rtl:begin:ignore */\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=\"left\"] .tooltip-arrow {\n  right: calc(-1 * var(--bs-tooltip-arrow-height));\n  width: var(--bs-tooltip-arrow-height);\n  height: var(--bs-tooltip-arrow-width); }\n  .bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=\"left\"] .tooltip-arrow::before {\n    left: -1px;\n    border-width: calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);\n    border-left-color: var(--bs-tooltip-bg); }\n\n/* rtl:end:ignore */\n.tooltip-inner {\n  max-width: var(--bs-tooltip-max-width);\n  padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);\n  color: var(--bs-tooltip-color);\n  text-align: center;\n  background-color: var(--bs-tooltip-bg);\n  border-radius: var(--bs-tooltip-border-radius); }\n\n.popover {\n  --bs-popover-zindex: 1070;\n  --bs-popover-max-width: 276px;\n  --bs-popover-font-size: 0.875rem;\n  --bs-popover-bg: var(--bs-body-bg);\n  --bs-popover-border-width: var(--bs-border-width);\n  --bs-popover-border-color: var(--bs-border-color-translucent);\n  --bs-popover-border-radius: var(--bs-border-radius-lg);\n  --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));\n  --bs-popover-box-shadow: var(--bs-box-shadow);\n  --bs-popover-header-padding-x: 1rem;\n  --bs-popover-header-padding-y: 0.5rem;\n  --bs-popover-header-font-size: 1rem;\n  --bs-popover-header-color: inherit;\n  --bs-popover-header-bg: var(--bs-secondary-bg);\n  --bs-popover-body-padding-x: 1rem;\n  --bs-popover-body-padding-y: 1rem;\n  --bs-popover-body-color: var(--bs-body-color);\n  --bs-popover-arrow-width: 1rem;\n  --bs-popover-arrow-height: 0.5rem;\n  --bs-popover-arrow-border: var(--bs-popover-border-color);\n  z-index: var(--bs-popover-zindex);\n  display: block;\n  max-width: var(--bs-popover-max-width);\n  font-family: var(--bs-font-sans-serif);\n  font-style: normal;\n  font-weight: 400;\n  line-height: 1.5;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  letter-spacing: normal;\n  word-break: normal;\n  white-space: normal;\n  word-spacing: normal;\n  line-break: auto;\n  font-size: var(--bs-popover-font-size);\n  word-wrap: break-word;\n  background-color: var(--bs-popover-bg);\n  background-clip: padding-box;\n  border: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n  border-radius: var(--bs-popover-border-radius); }\n  .popover .popover-arrow {\n    display: block;\n    width: var(--bs-popover-arrow-width);\n    height: var(--bs-popover-arrow-height); }\n    .popover .popover-arrow::before, .popover .popover-arrow::after {\n      position: absolute;\n      display: block;\n      content: \"\";\n      border-color: transparent;\n      border-style: solid;\n      border-width: 0; }\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=\"top\"] > .popover-arrow {\n  bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); }\n  .bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"top\"] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"top\"] > .popover-arrow::after {\n    border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0; }\n  .bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"top\"] > .popover-arrow::before {\n    bottom: 0;\n    border-top-color: var(--bs-popover-arrow-border); }\n  .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"top\"] > .popover-arrow::after {\n    bottom: var(--bs-popover-border-width);\n    border-top-color: var(--bs-popover-bg); }\n\n/* rtl:begin:ignore */\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=\"right\"] > .popover-arrow {\n  left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n  width: var(--bs-popover-arrow-height);\n  height: var(--bs-popover-arrow-width); }\n  .bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"right\"] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"right\"] > .popover-arrow::after {\n    border-width: calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0; }\n  .bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"right\"] > .popover-arrow::before {\n    left: 0;\n    border-right-color: var(--bs-popover-arrow-border); }\n  .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"right\"] > .popover-arrow::after {\n    left: var(--bs-popover-border-width);\n    border-right-color: var(--bs-popover-bg); }\n\n/* rtl:end:ignore */\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=\"bottom\"] > .popover-arrow {\n  top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); }\n  .bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"bottom\"] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"bottom\"] > .popover-arrow::after {\n    border-width: 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height); }\n  .bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"bottom\"] > .popover-arrow::before {\n    top: 0;\n    border-bottom-color: var(--bs-popover-arrow-border); }\n  .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"bottom\"] > .popover-arrow::after {\n    top: var(--bs-popover-border-width);\n    border-bottom-color: var(--bs-popover-bg); }\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=\"bottom\"] .popover-header::before {\n  position: absolute;\n  top: 0;\n  left: 50%;\n  display: block;\n  width: var(--bs-popover-arrow-width);\n  margin-left: calc(-.5 * var(--bs-popover-arrow-width));\n  content: \"\";\n  border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); }\n\n/* rtl:begin:ignore */\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=\"left\"] > .popover-arrow {\n  right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n  width: var(--bs-popover-arrow-height);\n  height: var(--bs-popover-arrow-width); }\n  .bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"left\"] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"left\"] > .popover-arrow::after {\n    border-width: calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height); }\n  .bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=\"left\"] > .popover-arrow::before {\n    right: 0;\n    border-left-color: var(--bs-popover-arrow-border); }\n  .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=\"left\"] > .popover-arrow::after {\n    right: var(--bs-popover-border-width);\n    border-left-color: var(--bs-popover-bg); }\n\n/* rtl:end:ignore */\n.popover-header {\n  padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);\n  margin-bottom: 0;\n  font-size: var(--bs-popover-header-font-size);\n  color: var(--bs-popover-header-color);\n  background-color: var(--bs-popover-header-bg);\n  border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n  border-top-left-radius: var(--bs-popover-inner-border-radius);\n  border-top-right-radius: var(--bs-popover-inner-border-radius); }\n  .popover-header:empty {\n    display: none; }\n\n.popover-body {\n  padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);\n  color: var(--bs-popover-body-color); }\n\n.carousel {\n  position: relative; }\n\n.carousel.pointer-event {\n  touch-action: pan-y; }\n\n.carousel-inner {\n  position: relative;\n  width: 100%;\n  overflow: hidden; }\n  .carousel-inner::after {\n    display: block;\n    clear: both;\n    content: \"\"; }\n\n.carousel-item {\n  position: relative;\n  display: none;\n  float: left;\n  width: 100%;\n  margin-right: -100%;\n  backface-visibility: hidden;\n  transition: transform 0.6s ease-in-out; }\n  @media (prefers-reduced-motion: reduce) {\n    .carousel-item {\n      transition: none; } }\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n  display: block; }\n\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n  transform: translateX(100%); }\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n  transform: translateX(-100%); }\n\n.carousel-fade .carousel-item {\n  opacity: 0;\n  transition-property: opacity;\n  transform: none; }\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n  z-index: 1;\n  opacity: 1; }\n\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n  z-index: 0;\n  opacity: 0;\n  transition: opacity 0s 0.6s; }\n  @media (prefers-reduced-motion: reduce) {\n    .carousel-fade .active.carousel-item-start,\n    .carousel-fade .active.carousel-item-end {\n      transition: none; } }\n.carousel-control-prev,\n.carousel-control-next {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  z-index: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 15%;\n  padding: 0;\n  color: #fff;\n  text-align: center;\n  background: none;\n  border: 0;\n  opacity: 0.5;\n  transition: opacity 0.15s ease; }\n  @media (prefers-reduced-motion: reduce) {\n    .carousel-control-prev,\n    .carousel-control-next {\n      transition: none; } }\n  .carousel-control-prev:hover, .carousel-control-prev:focus,\n  .carousel-control-next:hover,\n  .carousel-control-next:focus {\n    color: #fff;\n    text-decoration: none;\n    outline: 0;\n    opacity: 0.9; }\n\n.carousel-control-prev {\n  left: 0; }\n\n.carousel-control-next {\n  right: 0; }\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n  display: inline-block;\n  width: 2rem;\n  height: 2rem;\n  background-repeat: no-repeat;\n  background-position: 50%;\n  background-size: 100% 100%; }\n\n.carousel-control-prev-icon {\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\")*/; }\n\n.carousel-control-next-icon {\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\")*/; }\n\n.carousel-indicators {\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 2;\n  display: flex;\n  justify-content: center;\n  padding: 0;\n  margin-right: 15%;\n  margin-bottom: 1rem;\n  margin-left: 15%; }\n  .carousel-indicators [data-bs-target] {\n    box-sizing: content-box;\n    flex: 0 1 auto;\n    width: 30px;\n    height: 3px;\n    padding: 0;\n    margin-right: 3px;\n    margin-left: 3px;\n    text-indent: -999px;\n    cursor: pointer;\n    background-color: #fff;\n    background-clip: padding-box;\n    border: 0;\n    border-top: 10px solid transparent;\n    border-bottom: 10px solid transparent;\n    opacity: 0.5;\n    transition: opacity 0.6s ease; }\n    @media (prefers-reduced-motion: reduce) {\n      .carousel-indicators [data-bs-target] {\n        transition: none; } }\n  .carousel-indicators .active {\n    opacity: 1; }\n\n.carousel-caption {\n  position: absolute;\n  right: 15%;\n  bottom: 1.25rem;\n  left: 15%;\n  padding-top: 1.25rem;\n  padding-bottom: 1.25rem;\n  color: #fff;\n  text-align: center; }\n\n.carousel-dark .carousel-control-prev-icon,\n.carousel-dark .carousel-control-next-icon {\n  filter: invert(1) grayscale(100); }\n\n.carousel-dark .carousel-indicators [data-bs-target] {\n  background-color: #000; }\n\n.carousel-dark .carousel-caption {\n  color: #000; }\n\n[data-bs-theme=\"dark\"] .carousel .carousel-control-prev-icon,\n[data-bs-theme=\"dark\"] .carousel .carousel-control-next-icon, [data-bs-theme=\"dark\"].carousel .carousel-control-prev-icon,\n[data-bs-theme=\"dark\"].carousel .carousel-control-next-icon {\n  filter: invert(1) grayscale(100); }\n\n[data-bs-theme=\"dark\"] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=\"dark\"].carousel .carousel-indicators [data-bs-target] {\n  background-color: #000; }\n\n[data-bs-theme=\"dark\"] .carousel .carousel-caption, [data-bs-theme=\"dark\"].carousel .carousel-caption {\n  color: #000; }\n\n.spinner-grow,\n.spinner-border {\n  display: inline-block;\n  width: var(--bs-spinner-width);\n  height: var(--bs-spinner-height);\n  vertical-align: var(--bs-spinner-vertical-align);\n  border-radius: 50%;\n  animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); }\n\n@keyframes spinner-border {\n  to {\n    transform: rotate(360deg) /* rtl:ignore */; } }\n\n.spinner-border {\n  --bs-spinner-width: 2rem;\n  --bs-spinner-height: 2rem;\n  --bs-spinner-vertical-align: -0.125em;\n  --bs-spinner-border-width: 0.25em;\n  --bs-spinner-animation-speed: 0.75s;\n  --bs-spinner-animation-name: spinner-border;\n  border: var(--bs-spinner-border-width) solid currentcolor;\n  border-right-color: transparent; }\n\n.spinner-border-sm {\n  --bs-spinner-width: 1rem;\n  --bs-spinner-height: 1rem;\n  --bs-spinner-border-width: 0.2em; }\n\n@keyframes spinner-grow {\n  0% {\n    transform: scale(0); }\n  50% {\n    opacity: 1;\n    transform: none; } }\n\n.spinner-grow {\n  --bs-spinner-width: 2rem;\n  --bs-spinner-height: 2rem;\n  --bs-spinner-vertical-align: -0.125em;\n  --bs-spinner-animation-speed: 0.75s;\n  --bs-spinner-animation-name: spinner-grow;\n  background-color: currentcolor;\n  opacity: 0; }\n\n.spinner-grow-sm {\n  --bs-spinner-width: 1rem;\n  --bs-spinner-height: 1rem; }\n\n@media (prefers-reduced-motion: reduce) {\n  .spinner-border,\n  .spinner-grow {\n    --bs-spinner-animation-speed: 1.5s; } }\n\n.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm {\n  --bs-offcanvas-zindex: 1045;\n  --bs-offcanvas-width: 332px;\n  --bs-offcanvas-height: 30vh;\n  --bs-offcanvas-padding-x: 1rem;\n  --bs-offcanvas-padding-y: 1rem;\n  --bs-offcanvas-color: var(--bs-body-color);\n  --bs-offcanvas-bg: var(--bs-body-bg);\n  --bs-offcanvas-border-width: var(--bs-border-width);\n  --bs-offcanvas-border-color: var(--bs-border-color-translucent);\n  --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);\n  --bs-offcanvas-transition: transform 0.3s ease-in-out;\n  --bs-offcanvas-title-line-height: 1.5; }\n\n@media (max-width: 575.98px) {\n  .offcanvas-sm {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition); } }\n  @media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {\n    .offcanvas-sm {\n      transition: none; } }\n@media (max-width: 575.98px) {\n    .offcanvas-sm.offcanvas-start {\n      top: 0;\n      left: 0;\n      width: var(--bs-offcanvas-width);\n      border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(-100%); }\n    .offcanvas-sm.offcanvas-end {\n      top: 0;\n      right: 0;\n      width: var(--bs-offcanvas-width);\n      border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(100%); }\n    .offcanvas-sm.offcanvas-top {\n      top: 0;\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(-100%); }\n    .offcanvas-sm.offcanvas-bottom {\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(100%); }\n    .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) {\n      transform: none; }\n    .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show {\n      visibility: visible; } }\n\n@media (min-width: 576px) {\n  .offcanvas-sm {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important; }\n    .offcanvas-sm .offcanvas-header {\n      display: none; }\n    .offcanvas-sm .offcanvas-body {\n      display: flex;\n      flex-grow: 0;\n      padding: 0;\n      overflow-y: visible;\n      background-color: transparent !important; } }\n\n@media (max-width: 767.98px) {\n  .offcanvas-md {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition); } }\n  @media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {\n    .offcanvas-md {\n      transition: none; } }\n@media (max-width: 767.98px) {\n    .offcanvas-md.offcanvas-start {\n      top: 0;\n      left: 0;\n      width: var(--bs-offcanvas-width);\n      border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(-100%); }\n    .offcanvas-md.offcanvas-end {\n      top: 0;\n      right: 0;\n      width: var(--bs-offcanvas-width);\n      border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(100%); }\n    .offcanvas-md.offcanvas-top {\n      top: 0;\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(-100%); }\n    .offcanvas-md.offcanvas-bottom {\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(100%); }\n    .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) {\n      transform: none; }\n    .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show {\n      visibility: visible; } }\n\n@media (min-width: 768px) {\n  .offcanvas-md {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important; }\n    .offcanvas-md .offcanvas-header {\n      display: none; }\n    .offcanvas-md .offcanvas-body {\n      display: flex;\n      flex-grow: 0;\n      padding: 0;\n      overflow-y: visible;\n      background-color: transparent !important; } }\n\n@media (max-width: 991.98px) {\n  .offcanvas-lg {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition); } }\n  @media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {\n    .offcanvas-lg {\n      transition: none; } }\n@media (max-width: 991.98px) {\n    .offcanvas-lg.offcanvas-start {\n      top: 0;\n      left: 0;\n      width: var(--bs-offcanvas-width);\n      border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(-100%); }\n    .offcanvas-lg.offcanvas-end {\n      top: 0;\n      right: 0;\n      width: var(--bs-offcanvas-width);\n      border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(100%); }\n    .offcanvas-lg.offcanvas-top {\n      top: 0;\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(-100%); }\n    .offcanvas-lg.offcanvas-bottom {\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(100%); }\n    .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) {\n      transform: none; }\n    .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show {\n      visibility: visible; } }\n\n@media (min-width: 992px) {\n  .offcanvas-lg {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important; }\n    .offcanvas-lg .offcanvas-header {\n      display: none; }\n    .offcanvas-lg .offcanvas-body {\n      display: flex;\n      flex-grow: 0;\n      padding: 0;\n      overflow-y: visible;\n      background-color: transparent !important; } }\n\n@media (max-width: 1199.98px) {\n  .offcanvas-xl {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition); } }\n  @media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) {\n    .offcanvas-xl {\n      transition: none; } }\n@media (max-width: 1199.98px) {\n    .offcanvas-xl.offcanvas-start {\n      top: 0;\n      left: 0;\n      width: var(--bs-offcanvas-width);\n      border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(-100%); }\n    .offcanvas-xl.offcanvas-end {\n      top: 0;\n      right: 0;\n      width: var(--bs-offcanvas-width);\n      border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(100%); }\n    .offcanvas-xl.offcanvas-top {\n      top: 0;\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(-100%); }\n    .offcanvas-xl.offcanvas-bottom {\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(100%); }\n    .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) {\n      transform: none; }\n    .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show {\n      visibility: visible; } }\n\n@media (min-width: 1200px) {\n  .offcanvas-xl {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important; }\n    .offcanvas-xl .offcanvas-header {\n      display: none; }\n    .offcanvas-xl .offcanvas-body {\n      display: flex;\n      flex-grow: 0;\n      padding: 0;\n      overflow-y: visible;\n      background-color: transparent !important; } }\n\n@media (max-width: 1399.98px) {\n  .offcanvas-xxl {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition); } }\n  @media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) {\n    .offcanvas-xxl {\n      transition: none; } }\n@media (max-width: 1399.98px) {\n    .offcanvas-xxl.offcanvas-start {\n      top: 0;\n      left: 0;\n      width: var(--bs-offcanvas-width);\n      border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(-100%); }\n    .offcanvas-xxl.offcanvas-end {\n      top: 0;\n      right: 0;\n      width: var(--bs-offcanvas-width);\n      border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateX(100%); }\n    .offcanvas-xxl.offcanvas-top {\n      top: 0;\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(-100%); }\n    .offcanvas-xxl.offcanvas-bottom {\n      right: 0;\n      left: 0;\n      height: var(--bs-offcanvas-height);\n      max-height: 100%;\n      border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n      transform: translateY(100%); }\n    .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) {\n      transform: none; }\n    .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show {\n      visibility: visible; } }\n\n@media (min-width: 1400px) {\n  .offcanvas-xxl {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important; }\n    .offcanvas-xxl .offcanvas-header {\n      display: none; }\n    .offcanvas-xxl .offcanvas-body {\n      display: flex;\n      flex-grow: 0;\n      padding: 0;\n      overflow-y: visible;\n      background-color: transparent !important; } }\n\n.offcanvas {\n  position: fixed;\n  bottom: 0;\n  z-index: var(--bs-offcanvas-zindex);\n  display: flex;\n  flex-direction: column;\n  max-width: 100%;\n  color: var(--bs-offcanvas-color);\n  visibility: hidden;\n  background-color: var(--bs-offcanvas-bg);\n  background-clip: padding-box;\n  outline: 0;\n  transition: var(--bs-offcanvas-transition); }\n  @media (prefers-reduced-motion: reduce) {\n    .offcanvas {\n      transition: none; } }\n  .offcanvas.offcanvas-start {\n    top: 0;\n    left: 0;\n    width: var(--bs-offcanvas-width);\n    border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(-100%); }\n  .offcanvas.offcanvas-end {\n    top: 0;\n    right: 0;\n    width: var(--bs-offcanvas-width);\n    border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(100%); }\n  .offcanvas.offcanvas-top {\n    top: 0;\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(-100%); }\n  .offcanvas.offcanvas-bottom {\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(100%); }\n  .offcanvas.showing, .offcanvas.show:not(.hiding) {\n    transform: none; }\n  .offcanvas.showing, .offcanvas.hiding, .offcanvas.show {\n    visibility: visible; }\n\n.offcanvas-backdrop {\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: 1040;\n  width: 100vw;\n  height: 100vh;\n  background-color: #000; }\n  .offcanvas-backdrop.fade {\n    opacity: 0; }\n  .offcanvas-backdrop.show {\n    opacity: 0.5; }\n\n.offcanvas-header {\n  display: flex;\n  align-items: center;\n  padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); }\n  .offcanvas-header .btn-close {\n    padding: calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);\n    margin: calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto; }\n\n.offcanvas-title {\n  margin-bottom: 0;\n  line-height: var(--bs-offcanvas-title-line-height); }\n\n.offcanvas-body {\n  flex-grow: 1;\n  padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n  overflow-y: auto; }\n\n.placeholder {\n  display: inline-block;\n  min-height: 1em;\n  vertical-align: middle;\n  cursor: wait;\n  background-color: currentcolor;\n  opacity: 0.5; }\n  .placeholder.btn::before, .search-form .placeholder.search-submit::before, .comment-form input.placeholder[type=\"submit\"]::before {\n    display: inline-block;\n    content: \"\"; }\n\n.placeholder-xs {\n  min-height: .6em; }\n\n.placeholder-sm {\n  min-height: .8em; }\n\n.placeholder-lg {\n  min-height: 1.2em; }\n\n.placeholder-glow .placeholder {\n  animation: placeholder-glow 2s ease-in-out infinite; }\n\n@keyframes placeholder-glow {\n  50% {\n    opacity: 0.2; } }\n\n.placeholder-wave {\n  mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n  mask-size: 200% 100%;\n  animation: placeholder-wave 2s linear infinite; }\n\n@keyframes placeholder-wave {\n  100% {\n    mask-position: -200% 0%; } }\n\n.align-baseline {\n  vertical-align: baseline !important; }\n\n.align-top {\n  vertical-align: top !important; }\n\n.align-middle {\n  vertical-align: middle !important; }\n\n.align-bottom {\n  vertical-align: bottom !important; }\n\n.align-text-bottom {\n  vertical-align: text-bottom !important; }\n\n.align-text-top {\n  vertical-align: text-top !important; }\n\n.float-start {\n  float: left !important; }\n\n.float-end {\n  float: right !important; }\n\n.float-none {\n  float: none !important; }\n\n.object-fit-contain {\n  object-fit: contain !important; }\n\n.object-fit-cover {\n  object-fit: cover !important; }\n\n.object-fit-fill {\n  object-fit: fill !important; }\n\n.object-fit-scale {\n  object-fit: scale-down !important; }\n\n.object-fit-none {\n  object-fit: none !important; }\n\n.opacity-0 {\n  opacity: 0 !important; }\n\n.opacity-25 {\n  opacity: 0.25 !important; }\n\n.opacity-50 {\n  opacity: 0.5 !important; }\n\n.opacity-75 {\n  opacity: 0.75 !important; }\n\n.opacity-100 {\n  opacity: 1 !important; }\n\n.overflow-auto {\n  overflow: auto !important; }\n\n.overflow-hidden {\n  overflow: hidden !important; }\n\n.overflow-visible {\n  overflow: visible !important; }\n\n.overflow-scroll {\n  overflow: scroll !important; }\n\n.overflow-x-auto {\n  overflow-x: auto !important; }\n\n.overflow-x-hidden {\n  overflow-x: hidden !important; }\n\n.overflow-x-visible {\n  overflow-x: visible !important; }\n\n.overflow-x-scroll {\n  overflow-x: scroll !important; }\n\n.overflow-y-auto {\n  overflow-y: auto !important; }\n\n.overflow-y-hidden {\n  overflow-y: hidden !important; }\n\n.overflow-y-visible {\n  overflow-y: visible !important; }\n\n.overflow-y-scroll {\n  overflow-y: scroll !important; }\n\n.d-inline {\n  display: inline !important; }\n\n.d-inline-block {\n  display: inline-block !important; }\n\n.d-block {\n  display: block !important; }\n\n.d-grid {\n  display: grid !important; }\n\n.d-inline-grid {\n  display: inline-grid !important; }\n\n.d-table {\n  display: table !important; }\n\n.d-table-row {\n  display: table-row !important; }\n\n.d-table-cell {\n  display: table-cell !important; }\n\n.d-flex {\n  display: flex !important; }\n\n.d-inline-flex {\n  display: inline-flex !important; }\n\n.d-none {\n  display: none !important; }\n\n.shadow {\n  box-shadow: var(--bs-box-shadow) !important; }\n\n.shadow-sm {\n  box-shadow: var(--bs-box-shadow-sm) !important; }\n\n.shadow-lg {\n  box-shadow: var(--bs-box-shadow-lg) !important; }\n\n.shadow-none {\n  box-shadow: none !important; }\n\n.focus-ring-primary {\n  --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-secondary {\n  --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-success {\n  --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-info {\n  --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-warning {\n  --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-danger {\n  --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-light {\n  --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity)); }\n\n.focus-ring-dark {\n  --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity)); }\n\n.position-static {\n  position: static !important; }\n\n.position-relative {\n  position: relative !important; }\n\n.position-absolute {\n  position: absolute !important; }\n\n.position-fixed {\n  position: fixed !important; }\n\n.position-sticky {\n  position: sticky !important; }\n\n.top-0 {\n  top: 0 !important; }\n\n.top-50 {\n  top: 50% !important; }\n\n.top-100 {\n  top: 100% !important; }\n\n.bottom-0 {\n  bottom: 0 !important; }\n\n.bottom-50 {\n  bottom: 50% !important; }\n\n.bottom-100 {\n  bottom: 100% !important; }\n\n.start-0 {\n  left: 0 !important; }\n\n.start-50 {\n  left: 50% !important; }\n\n.start-100 {\n  left: 100% !important; }\n\n.end-0 {\n  right: 0 !important; }\n\n.end-50 {\n  right: 50% !important; }\n\n.end-100 {\n  right: 100% !important; }\n\n.translate-middle {\n  transform: translate(-50%, -50%) !important; }\n\n.translate-middle-x {\n  transform: translateX(-50%) !important; }\n\n.translate-middle-y {\n  transform: translateY(-50%) !important; }\n\n.border {\n  border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; }\n\n.border-0 {\n  border: 0 !important; }\n\n.border-top {\n  border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; }\n\n.border-top-0 {\n  border-top: 0 !important; }\n\n.border-end {\n  border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; }\n\n.border-end-0 {\n  border-right: 0 !important; }\n\n.border-bottom {\n  border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; }\n\n.border-bottom-0 {\n  border-bottom: 0 !important; }\n\n.border-start {\n  border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; }\n\n.border-start-0 {\n  border-left: 0 !important; }\n\n.border-primary {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important; }\n\n.border-secondary {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important; }\n\n.border-success {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important; }\n\n.border-info {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important; }\n\n.border-warning {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important; }\n\n.border-danger {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; }\n\n.border-light {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; }\n\n.border-dark {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important; }\n\n.border-black {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important; }\n\n.border-white {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; }\n\n.border-primary-subtle {\n  border-color: var(--bs-primary-border-subtle) !important; }\n\n.border-secondary-subtle {\n  border-color: var(--bs-secondary-border-subtle) !important; }\n\n.border-success-subtle {\n  border-color: var(--bs-success-border-subtle) !important; }\n\n.border-info-subtle {\n  border-color: var(--bs-info-border-subtle) !important; }\n\n.border-warning-subtle {\n  border-color: var(--bs-warning-border-subtle) !important; }\n\n.border-danger-subtle {\n  border-color: var(--bs-danger-border-subtle) !important; }\n\n.border-light-subtle {\n  border-color: var(--bs-light-border-subtle) !important; }\n\n.border-dark-subtle {\n  border-color: var(--bs-dark-border-subtle) !important; }\n\n.border-1 {\n  border-width: 1px !important; }\n\n.border-2 {\n  border-width: 2px !important; }\n\n.border-3 {\n  border-width: 3px !important; }\n\n.border-4 {\n  border-width: 4px !important; }\n\n.border-5 {\n  border-width: 5px !important; }\n\n.border-opacity-10 {\n  --bs-border-opacity: 0.1; }\n\n.border-opacity-25 {\n  --bs-border-opacity: 0.25; }\n\n.border-opacity-50 {\n  --bs-border-opacity: 0.5; }\n\n.border-opacity-75 {\n  --bs-border-opacity: 0.75; }\n\n.border-opacity-100 {\n  --bs-border-opacity: 1; }\n\n.w-25 {\n  width: 25% !important; }\n\n.w-50 {\n  width: 50% !important; }\n\n.w-75 {\n  width: 75% !important; }\n\n.w-100 {\n  width: 100% !important; }\n\n.w-auto {\n  width: auto !important; }\n\n.mw-100 {\n  max-width: 100% !important; }\n\n.vw-100 {\n  width: 100vw !important; }\n\n.min-vw-100 {\n  min-width: 100vw !important; }\n\n.h-25 {\n  height: 25% !important; }\n\n.h-50 {\n  height: 50% !important; }\n\n.h-75 {\n  height: 75% !important; }\n\n.h-100 {\n  height: 100% !important; }\n\n.h-auto {\n  height: auto !important; }\n\n.mh-100 {\n  max-height: 100% !important; }\n\n.vh-100 {\n  height: 100vh !important; }\n\n.min-vh-100 {\n  min-height: 100vh !important; }\n\n.flex-fill {\n  flex: 1 1 auto !important; }\n\n.flex-row {\n  flex-direction: row !important; }\n\n.flex-column {\n  flex-direction: column !important; }\n\n.flex-row-reverse {\n  flex-direction: row-reverse !important; }\n\n.flex-column-reverse {\n  flex-direction: column-reverse !important; }\n\n.flex-grow-0 {\n  flex-grow: 0 !important; }\n\n.flex-grow-1 {\n  flex-grow: 1 !important; }\n\n.flex-shrink-0 {\n  flex-shrink: 0 !important; }\n\n.flex-shrink-1 {\n  flex-shrink: 1 !important; }\n\n.flex-wrap {\n  flex-wrap: wrap !important; }\n\n.flex-nowrap {\n  flex-wrap: nowrap !important; }\n\n.flex-wrap-reverse {\n  flex-wrap: wrap-reverse !important; }\n\n.justify-content-start {\n  justify-content: flex-start !important; }\n\n.justify-content-end {\n  justify-content: flex-end !important; }\n\n.justify-content-center {\n  justify-content: center !important; }\n\n.justify-content-between {\n  justify-content: space-between !important; }\n\n.justify-content-around {\n  justify-content: space-around !important; }\n\n.justify-content-evenly {\n  justify-content: space-evenly !important; }\n\n.align-items-start {\n  align-items: flex-start !important; }\n\n.align-items-end {\n  align-items: flex-end !important; }\n\n.align-items-center {\n  align-items: center !important; }\n\n.align-items-baseline {\n  align-items: baseline !important; }\n\n.align-items-stretch {\n  align-items: stretch !important; }\n\n.align-content-start {\n  align-content: flex-start !important; }\n\n.align-content-end {\n  align-content: flex-end !important; }\n\n.align-content-center {\n  align-content: center !important; }\n\n.align-content-between {\n  align-content: space-between !important; }\n\n.align-content-around {\n  align-content: space-around !important; }\n\n.align-content-stretch {\n  align-content: stretch !important; }\n\n.align-self-auto {\n  align-self: auto !important; }\n\n.align-self-start {\n  align-self: flex-start !important; }\n\n.align-self-end {\n  align-self: flex-end !important; }\n\n.align-self-center {\n  align-self: center !important; }\n\n.align-self-baseline {\n  align-self: baseline !important; }\n\n.align-self-stretch {\n  align-self: stretch !important; }\n\n.order-first {\n  order: -1 !important; }\n\n.order-0 {\n  order: 0 !important; }\n\n.order-1 {\n  order: 1 !important; }\n\n.order-2 {\n  order: 2 !important; }\n\n.order-3 {\n  order: 3 !important; }\n\n.order-4 {\n  order: 4 !important; }\n\n.order-5 {\n  order: 5 !important; }\n\n.order-last {\n  order: 6 !important; }\n\n.m-0 {\n  margin: 0 !important; }\n\n.m-1 {\n  margin: 0.25rem !important; }\n\n.m-2 {\n  margin: 0.5rem !important; }\n\n.m-3 {\n  margin: 1rem !important; }\n\n.m-4 {\n  margin: 1.5rem !important; }\n\n.m-5 {\n  margin: 3rem !important; }\n\n.m-auto {\n  margin: auto !important; }\n\n.mx-0 {\n  margin-right: 0 !important;\n  margin-left: 0 !important; }\n\n.mx-1 {\n  margin-right: 0.25rem !important;\n  margin-left: 0.25rem !important; }\n\n.mx-2 {\n  margin-right: 0.5rem !important;\n  margin-left: 0.5rem !important; }\n\n.mx-3 {\n  margin-right: 1rem !important;\n  margin-left: 1rem !important; }\n\n.mx-4 {\n  margin-right: 1.5rem !important;\n  margin-left: 1.5rem !important; }\n\n.mx-5 {\n  margin-right: 3rem !important;\n  margin-left: 3rem !important; }\n\n.mx-auto {\n  margin-right: auto !important;\n  margin-left: auto !important; }\n\n.my-0 {\n  margin-top: 0 !important;\n  margin-bottom: 0 !important; }\n\n.my-1 {\n  margin-top: 0.25rem !important;\n  margin-bottom: 0.25rem !important; }\n\n.my-2 {\n  margin-top: 0.5rem !important;\n  margin-bottom: 0.5rem !important; }\n\n.my-3 {\n  margin-top: 1rem !important;\n  margin-bottom: 1rem !important; }\n\n.my-4 {\n  margin-top: 1.5rem !important;\n  margin-bottom: 1.5rem !important; }\n\n.my-5 {\n  margin-top: 3rem !important;\n  margin-bottom: 3rem !important; }\n\n.my-auto {\n  margin-top: auto !important;\n  margin-bottom: auto !important; }\n\n.mt-0 {\n  margin-top: 0 !important; }\n\n.mt-1 {\n  margin-top: 0.25rem !important; }\n\n.mt-2 {\n  margin-top: 0.5rem !important; }\n\n.mt-3 {\n  margin-top: 1rem !important; }\n\n.mt-4 {\n  margin-top: 1.5rem !important; }\n\n.mt-5 {\n  margin-top: 3rem !important; }\n\n.mt-auto {\n  margin-top: auto !important; }\n\n.me-0 {\n  margin-right: 0 !important; }\n\n.me-1 {\n  margin-right: 0.25rem !important; }\n\n.me-2 {\n  margin-right: 0.5rem !important; }\n\n.me-3 {\n  margin-right: 1rem !important; }\n\n.me-4 {\n  margin-right: 1.5rem !important; }\n\n.me-5 {\n  margin-right: 3rem !important; }\n\n.me-auto {\n  margin-right: auto !important; }\n\n.mb-0 {\n  margin-bottom: 0 !important; }\n\n.mb-1 {\n  margin-bottom: 0.25rem !important; }\n\n.mb-2 {\n  margin-bottom: 0.5rem !important; }\n\n.mb-3 {\n  margin-bottom: 1rem !important; }\n\n.mb-4 {\n  margin-bottom: 1.5rem !important; }\n\n.mb-5 {\n  margin-bottom: 3rem !important; }\n\n.mb-auto {\n  margin-bottom: auto !important; }\n\n.ms-0 {\n  margin-left: 0 !important; }\n\n.ms-1 {\n  margin-left: 0.25rem !important; }\n\n.ms-2 {\n  margin-left: 0.5rem !important; }\n\n.ms-3 {\n  margin-left: 1rem !important; }\n\n.ms-4 {\n  margin-left: 1.5rem !important; }\n\n.ms-5 {\n  margin-left: 3rem !important; }\n\n.ms-auto {\n  margin-left: auto !important; }\n\n.m-n1 {\n  margin: -0.25rem !important; }\n\n.m-n2 {\n  margin: -0.5rem !important; }\n\n.m-n3 {\n  margin: -1rem !important; }\n\n.m-n4 {\n  margin: -1.5rem !important; }\n\n.m-n5 {\n  margin: -3rem !important; }\n\n.mx-n1 {\n  margin-right: -0.25rem !important;\n  margin-left: -0.25rem !important; }\n\n.mx-n2 {\n  margin-right: -0.5rem !important;\n  margin-left: -0.5rem !important; }\n\n.mx-n3 {\n  margin-right: -1rem !important;\n  margin-left: -1rem !important; }\n\n.mx-n4 {\n  margin-right: -1.5rem !important;\n  margin-left: -1.5rem !important; }\n\n.mx-n5 {\n  margin-right: -3rem !important;\n  margin-left: -3rem !important; }\n\n.my-n1 {\n  margin-top: -0.25rem !important;\n  margin-bottom: -0.25rem !important; }\n\n.my-n2 {\n  margin-top: -0.5rem !important;\n  margin-bottom: -0.5rem !important; }\n\n.my-n3 {\n  margin-top: -1rem !important;\n  margin-bottom: -1rem !important; }\n\n.my-n4 {\n  margin-top: -1.5rem !important;\n  margin-bottom: -1.5rem !important; }\n\n.my-n5 {\n  margin-top: -3rem !important;\n  margin-bottom: -3rem !important; }\n\n.mt-n1 {\n  margin-top: -0.25rem !important; }\n\n.mt-n2 {\n  margin-top: -0.5rem !important; }\n\n.mt-n3 {\n  margin-top: -1rem !important; }\n\n.mt-n4 {\n  margin-top: -1.5rem !important; }\n\n.mt-n5 {\n  margin-top: -3rem !important; }\n\n.me-n1 {\n  margin-right: -0.25rem !important; }\n\n.me-n2 {\n  margin-right: -0.5rem !important; }\n\n.me-n3 {\n  margin-right: -1rem !important; }\n\n.me-n4 {\n  margin-right: -1.5rem !important; }\n\n.me-n5 {\n  margin-right: -3rem !important; }\n\n.mb-n1 {\n  margin-bottom: -0.25rem !important; }\n\n.mb-n2 {\n  margin-bottom: -0.5rem !important; }\n\n.mb-n3 {\n  margin-bottom: -1rem !important; }\n\n.mb-n4 {\n  margin-bottom: -1.5rem !important; }\n\n.mb-n5 {\n  margin-bottom: -3rem !important; }\n\n.ms-n1 {\n  margin-left: -0.25rem !important; }\n\n.ms-n2 {\n  margin-left: -0.5rem !important; }\n\n.ms-n3 {\n  margin-left: -1rem !important; }\n\n.ms-n4 {\n  margin-left: -1.5rem !important; }\n\n.ms-n5 {\n  margin-left: -3rem !important; }\n\n.p-0 {\n  padding: 0 !important; }\n\n.p-1 {\n  padding: 0.25rem !important; }\n\n.p-2 {\n  padding: 0.5rem !important; }\n\n.p-3 {\n  padding: 1rem !important; }\n\n.p-4 {\n  padding: 1.5rem !important; }\n\n.p-5 {\n  padding: 3rem !important; }\n\n.px-0 {\n  padding-right: 0 !important;\n  padding-left: 0 !important; }\n\n.px-1 {\n  padding-right: 0.25rem !important;\n  padding-left: 0.25rem !important; }\n\n.px-2 {\n  padding-right: 0.5rem !important;\n  padding-left: 0.5rem !important; }\n\n.px-3 {\n  padding-right: 1rem !important;\n  padding-left: 1rem !important; }\n\n.px-4 {\n  padding-right: 1.5rem !important;\n  padding-left: 1.5rem !important; }\n\n.px-5 {\n  padding-right: 3rem !important;\n  padding-left: 3rem !important; }\n\n.py-0 {\n  padding-top: 0 !important;\n  padding-bottom: 0 !important; }\n\n.py-1 {\n  padding-top: 0.25rem !important;\n  padding-bottom: 0.25rem !important; }\n\n.py-2 {\n  padding-top: 0.5rem !important;\n  padding-bottom: 0.5rem !important; }\n\n.py-3 {\n  padding-top: 1rem !important;\n  padding-bottom: 1rem !important; }\n\n.py-4 {\n  padding-top: 1.5rem !important;\n  padding-bottom: 1.5rem !important; }\n\n.py-5 {\n  padding-top: 3rem !important;\n  padding-bottom: 3rem !important; }\n\n.pt-0 {\n  padding-top: 0 !important; }\n\n.pt-1 {\n  padding-top: 0.25rem !important; }\n\n.pt-2 {\n  padding-top: 0.5rem !important; }\n\n.pt-3 {\n  padding-top: 1rem !important; }\n\n.pt-4 {\n  padding-top: 1.5rem !important; }\n\n.pt-5 {\n  padding-top: 3rem !important; }\n\n.pe-0 {\n  padding-right: 0 !important; }\n\n.pe-1 {\n  padding-right: 0.25rem !important; }\n\n.pe-2 {\n  padding-right: 0.5rem !important; }\n\n.pe-3 {\n  padding-right: 1rem !important; }\n\n.pe-4 {\n  padding-right: 1.5rem !important; }\n\n.pe-5 {\n  padding-right: 3rem !important; }\n\n.pb-0 {\n  padding-bottom: 0 !important; }\n\n.pb-1 {\n  padding-bottom: 0.25rem !important; }\n\n.pb-2 {\n  padding-bottom: 0.5rem !important; }\n\n.pb-3 {\n  padding-bottom: 1rem !important; }\n\n.pb-4 {\n  padding-bottom: 1.5rem !important; }\n\n.pb-5 {\n  padding-bottom: 3rem !important; }\n\n.ps-0 {\n  padding-left: 0 !important; }\n\n.ps-1 {\n  padding-left: 0.25rem !important; }\n\n.ps-2 {\n  padding-left: 0.5rem !important; }\n\n.ps-3 {\n  padding-left: 1rem !important; }\n\n.ps-4 {\n  padding-left: 1.5rem !important; }\n\n.ps-5 {\n  padding-left: 3rem !important; }\n\n.gap-0 {\n  gap: 0 !important; }\n\n.gap-1 {\n  gap: 0.25rem !important; }\n\n.gap-2 {\n  gap: 0.5rem !important; }\n\n.gap-3 {\n  gap: 1rem !important; }\n\n.gap-4 {\n  gap: 1.5rem !important; }\n\n.gap-5 {\n  gap: 3rem !important; }\n\n.row-gap-0 {\n  row-gap: 0 !important; }\n\n.row-gap-1 {\n  row-gap: 0.25rem !important; }\n\n.row-gap-2 {\n  row-gap: 0.5rem !important; }\n\n.row-gap-3 {\n  row-gap: 1rem !important; }\n\n.row-gap-4 {\n  row-gap: 1.5rem !important; }\n\n.row-gap-5 {\n  row-gap: 3rem !important; }\n\n.column-gap-0 {\n  column-gap: 0 !important; }\n\n.column-gap-1 {\n  column-gap: 0.25rem !important; }\n\n.column-gap-2 {\n  column-gap: 0.5rem !important; }\n\n.column-gap-3 {\n  column-gap: 1rem !important; }\n\n.column-gap-4 {\n  column-gap: 1.5rem !important; }\n\n.column-gap-5 {\n  column-gap: 3rem !important; }\n\n.font-monospace {\n  font-family: var(--bs-font-monospace) !important; }\n\n.fs-1 {\n  font-size: calc(1.375rem + 1.5vw) !important; }\n\n.fs-2 {\n  font-size: calc(1.325rem + 0.9vw) !important; }\n\n.fs-3 {\n  font-size: calc(1.3rem + 0.6vw) !important; }\n\n.fs-4 {\n  font-size: calc(1.275rem + 0.3vw) !important; }\n\n.fs-5 {\n  font-size: 1.25rem !important; }\n\n.fs-6 {\n  font-size: 1rem !important; }\n\n.fst-italic {\n  font-style: italic !important; }\n\n.fst-normal {\n  font-style: normal !important; }\n\n.fw-lighter {\n  font-weight: lighter !important; }\n\n.fw-light {\n  font-weight: 300 !important; }\n\n.fw-normal {\n  font-weight: 400 !important; }\n\n.fw-medium {\n  font-weight: 500 !important; }\n\n.fw-semibold {\n  font-weight: 600 !important; }\n\n.fw-bold {\n  font-weight: 700 !important; }\n\n.fw-bolder {\n  font-weight: bolder !important; }\n\n.lh-1 {\n  line-height: 1 !important; }\n\n.lh-sm {\n  line-height: 1.25 !important; }\n\n.lh-base {\n  line-height: 1.5 !important; }\n\n.lh-lg {\n  line-height: 2 !important; }\n\n.text-start {\n  text-align: left !important; }\n\n.text-end {\n  text-align: right !important; }\n\n.text-center {\n  text-align: center !important; }\n\n.text-decoration-none {\n  text-decoration: none !important; }\n\n.text-decoration-underline {\n  text-decoration: underline !important; }\n\n.text-decoration-line-through {\n  text-decoration: line-through !important; }\n\n.text-lowercase {\n  text-transform: lowercase !important; }\n\n.text-uppercase {\n  text-transform: uppercase !important; }\n\n.text-capitalize {\n  text-transform: capitalize !important; }\n\n.text-wrap {\n  white-space: normal !important; }\n\n.text-nowrap {\n  white-space: nowrap !important; }\n\n/* rtl:begin:remove */\n.text-break {\n  word-wrap: break-word !important;\n  word-break: break-word !important; }\n\n/* rtl:end:remove */\n.text-primary {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; }\n\n.text-secondary {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; }\n\n.text-success {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; }\n\n.text-info {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; }\n\n.text-warning {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; }\n\n.text-danger {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; }\n\n.text-light {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; }\n\n.text-dark {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; }\n\n.text-black {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; }\n\n.text-white {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; }\n\n.text-body {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; }\n\n.text-muted {\n  --bs-text-opacity: 1;\n  color: var(--bs-secondary-color) !important; }\n\n.text-black-50 {\n  --bs-text-opacity: 1;\n  color: rgba(0, 0, 0, 0.5) !important; }\n\n.text-white-50 {\n  --bs-text-opacity: 1;\n  color: rgba(255, 255, 255, 0.5) !important; }\n\n.text-body-secondary {\n  --bs-text-opacity: 1;\n  color: var(--bs-secondary-color) !important; }\n\n.text-body-tertiary {\n  --bs-text-opacity: 1;\n  color: var(--bs-tertiary-color) !important; }\n\n.text-body-emphasis {\n  --bs-text-opacity: 1;\n  color: var(--bs-emphasis-color) !important; }\n\n.text-reset {\n  --bs-text-opacity: 1;\n  color: inherit !important; }\n\n.text-opacity-25 {\n  --bs-text-opacity: 0.25; }\n\n.text-opacity-50 {\n  --bs-text-opacity: 0.5; }\n\n.text-opacity-75 {\n  --bs-text-opacity: 0.75; }\n\n.text-opacity-100 {\n  --bs-text-opacity: 1; }\n\n.text-primary-emphasis {\n  color: var(--bs-primary-text-emphasis) !important; }\n\n.text-secondary-emphasis {\n  color: var(--bs-secondary-text-emphasis) !important; }\n\n.text-success-emphasis {\n  color: var(--bs-success-text-emphasis) !important; }\n\n.text-info-emphasis {\n  color: var(--bs-info-text-emphasis) !important; }\n\n.text-warning-emphasis {\n  color: var(--bs-warning-text-emphasis) !important; }\n\n.text-danger-emphasis {\n  color: var(--bs-danger-text-emphasis) !important; }\n\n.text-light-emphasis {\n  color: var(--bs-light-text-emphasis) !important; }\n\n.text-dark-emphasis {\n  color: var(--bs-dark-text-emphasis) !important; }\n\n.link-opacity-10 {\n  --bs-link-opacity: 0.1; }\n\n.link-opacity-10-hover:hover {\n  --bs-link-opacity: 0.1; }\n\n.link-opacity-25 {\n  --bs-link-opacity: 0.25; }\n\n.link-opacity-25-hover:hover {\n  --bs-link-opacity: 0.25; }\n\n.link-opacity-50 {\n  --bs-link-opacity: 0.5; }\n\n.link-opacity-50-hover:hover {\n  --bs-link-opacity: 0.5; }\n\n.link-opacity-75 {\n  --bs-link-opacity: 0.75; }\n\n.link-opacity-75-hover:hover {\n  --bs-link-opacity: 0.75; }\n\n.link-opacity-100 {\n  --bs-link-opacity: 1; }\n\n.link-opacity-100-hover:hover {\n  --bs-link-opacity: 1; }\n\n.link-offset-1 {\n  text-underline-offset: 0.125em !important; }\n\n.link-offset-1-hover:hover {\n  text-underline-offset: 0.125em !important; }\n\n.link-offset-2 {\n  text-underline-offset: 0.25em !important; }\n\n.link-offset-2-hover:hover {\n  text-underline-offset: 0.25em !important; }\n\n.link-offset-3 {\n  text-underline-offset: 0.375em !important; }\n\n.link-offset-3-hover:hover {\n  text-underline-offset: 0.375em !important; }\n\n.link-underline-primary {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-secondary {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-success {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-info {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-warning {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-danger {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-light {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline-dark {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; }\n\n.link-underline {\n  --bs-link-underline-opacity: 1;\n  text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; }\n\n.link-underline-opacity-0 {\n  --bs-link-underline-opacity: 0; }\n\n.link-underline-opacity-0-hover:hover {\n  --bs-link-underline-opacity: 0; }\n\n.link-underline-opacity-10 {\n  --bs-link-underline-opacity: 0.1; }\n\n.link-underline-opacity-10-hover:hover {\n  --bs-link-underline-opacity: 0.1; }\n\n.link-underline-opacity-25 {\n  --bs-link-underline-opacity: 0.25; }\n\n.link-underline-opacity-25-hover:hover {\n  --bs-link-underline-opacity: 0.25; }\n\n.link-underline-opacity-50 {\n  --bs-link-underline-opacity: 0.5; }\n\n.link-underline-opacity-50-hover:hover {\n  --bs-link-underline-opacity: 0.5; }\n\n.link-underline-opacity-75 {\n  --bs-link-underline-opacity: 0.75; }\n\n.link-underline-opacity-75-hover:hover {\n  --bs-link-underline-opacity: 0.75; }\n\n.link-underline-opacity-100 {\n  --bs-link-underline-opacity: 1; }\n\n.link-underline-opacity-100-hover:hover {\n  --bs-link-underline-opacity: 1; }\n\n.bg-primary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-secondary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-success {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-info {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-warning {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-danger {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-light {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-dark {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-black {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-white {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-body {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-transparent {\n  --bs-bg-opacity: 1;\n  background-color: transparent !important; }\n\n.bg-body-secondary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-body-tertiary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important; }\n\n.bg-opacity-10 {\n  --bs-bg-opacity: 0.1; }\n\n.bg-opacity-25 {\n  --bs-bg-opacity: 0.25; }\n\n.bg-opacity-50 {\n  --bs-bg-opacity: 0.5; }\n\n.bg-opacity-75 {\n  --bs-bg-opacity: 0.75; }\n\n.bg-opacity-100 {\n  --bs-bg-opacity: 1; }\n\n.bg-primary-subtle {\n  background-color: var(--bs-primary-bg-subtle) !important; }\n\n.bg-secondary-subtle {\n  background-color: var(--bs-secondary-bg-subtle) !important; }\n\n.bg-success-subtle {\n  background-color: var(--bs-success-bg-subtle) !important; }\n\n.bg-info-subtle {\n  background-color: var(--bs-info-bg-subtle) !important; }\n\n.bg-warning-subtle {\n  background-color: var(--bs-warning-bg-subtle) !important; }\n\n.bg-danger-subtle {\n  background-color: var(--bs-danger-bg-subtle) !important; }\n\n.bg-light-subtle {\n  background-color: var(--bs-light-bg-subtle) !important; }\n\n.bg-dark-subtle {\n  background-color: var(--bs-dark-bg-subtle) !important; }\n\n.bg-gradient {\n  background-image: var(--bs-gradient) !important; }\n\n.user-select-all {\n  user-select: all !important; }\n\n.user-select-auto {\n  user-select: auto !important; }\n\n.user-select-none {\n  user-select: none !important; }\n\n.pe-none {\n  pointer-events: none !important; }\n\n.pe-auto {\n  pointer-events: auto !important; }\n\n.rounded {\n  border-radius: var(--bs-border-radius) !important; }\n\n.rounded-0 {\n  border-radius: 0 !important; }\n\n.rounded-1 {\n  border-radius: var(--bs-border-radius-sm) !important; }\n\n.rounded-2 {\n  border-radius: var(--bs-border-radius) !important; }\n\n.rounded-3 {\n  border-radius: var(--bs-border-radius-lg) !important; }\n\n.rounded-4 {\n  border-radius: var(--bs-border-radius-xl) !important; }\n\n.rounded-5 {\n  border-radius: var(--bs-border-radius-xxl) !important; }\n\n.rounded-circle {\n  border-radius: 50% !important; }\n\n.rounded-pill {\n  border-radius: var(--bs-border-radius-pill) !important; }\n\n.rounded-top {\n  border-top-left-radius: var(--bs-border-radius) !important;\n  border-top-right-radius: var(--bs-border-radius) !important; }\n\n.rounded-top-0 {\n  border-top-left-radius: 0 !important;\n  border-top-right-radius: 0 !important; }\n\n.rounded-top-1 {\n  border-top-left-radius: var(--bs-border-radius-sm) !important;\n  border-top-right-radius: var(--bs-border-radius-sm) !important; }\n\n.rounded-top-2 {\n  border-top-left-radius: var(--bs-border-radius) !important;\n  border-top-right-radius: var(--bs-border-radius) !important; }\n\n.rounded-top-3 {\n  border-top-left-radius: var(--bs-border-radius-lg) !important;\n  border-top-right-radius: var(--bs-border-radius-lg) !important; }\n\n.rounded-top-4 {\n  border-top-left-radius: var(--bs-border-radius-xl) !important;\n  border-top-right-radius: var(--bs-border-radius-xl) !important; }\n\n.rounded-top-5 {\n  border-top-left-radius: var(--bs-border-radius-xxl) !important;\n  border-top-right-radius: var(--bs-border-radius-xxl) !important; }\n\n.rounded-top-circle {\n  border-top-left-radius: 50% !important;\n  border-top-right-radius: 50% !important; }\n\n.rounded-top-pill {\n  border-top-left-radius: var(--bs-border-radius-pill) !important;\n  border-top-right-radius: var(--bs-border-radius-pill) !important; }\n\n.rounded-end {\n  border-top-right-radius: var(--bs-border-radius) !important;\n  border-bottom-right-radius: var(--bs-border-radius) !important; }\n\n.rounded-end-0 {\n  border-top-right-radius: 0 !important;\n  border-bottom-right-radius: 0 !important; }\n\n.rounded-end-1 {\n  border-top-right-radius: var(--bs-border-radius-sm) !important;\n  border-bottom-right-radius: var(--bs-border-radius-sm) !important; }\n\n.rounded-end-2 {\n  border-top-right-radius: var(--bs-border-radius) !important;\n  border-bottom-right-radius: var(--bs-border-radius) !important; }\n\n.rounded-end-3 {\n  border-top-right-radius: var(--bs-border-radius-lg) !important;\n  border-bottom-right-radius: var(--bs-border-radius-lg) !important; }\n\n.rounded-end-4 {\n  border-top-right-radius: var(--bs-border-radius-xl) !important;\n  border-bottom-right-radius: var(--bs-border-radius-xl) !important; }\n\n.rounded-end-5 {\n  border-top-right-radius: var(--bs-border-radius-xxl) !important;\n  border-bottom-right-radius: var(--bs-border-radius-xxl) !important; }\n\n.rounded-end-circle {\n  border-top-right-radius: 50% !important;\n  border-bottom-right-radius: 50% !important; }\n\n.rounded-end-pill {\n  border-top-right-radius: var(--bs-border-radius-pill) !important;\n  border-bottom-right-radius: var(--bs-border-radius-pill) !important; }\n\n.rounded-bottom {\n  border-bottom-right-radius: var(--bs-border-radius) !important;\n  border-bottom-left-radius: var(--bs-border-radius) !important; }\n\n.rounded-bottom-0 {\n  border-bottom-right-radius: 0 !important;\n  border-bottom-left-radius: 0 !important; }\n\n.rounded-bottom-1 {\n  border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n  border-bottom-left-radius: var(--bs-border-radius-sm) !important; }\n\n.rounded-bottom-2 {\n  border-bottom-right-radius: var(--bs-border-radius) !important;\n  border-bottom-left-radius: var(--bs-border-radius) !important; }\n\n.rounded-bottom-3 {\n  border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n  border-bottom-left-radius: var(--bs-border-radius-lg) !important; }\n\n.rounded-bottom-4 {\n  border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n  border-bottom-left-radius: var(--bs-border-radius-xl) !important; }\n\n.rounded-bottom-5 {\n  border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n  border-bottom-left-radius: var(--bs-border-radius-xxl) !important; }\n\n.rounded-bottom-circle {\n  border-bottom-right-radius: 50% !important;\n  border-bottom-left-radius: 50% !important; }\n\n.rounded-bottom-pill {\n  border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n  border-bottom-left-radius: var(--bs-border-radius-pill) !important; }\n\n.rounded-start {\n  border-bottom-left-radius: var(--bs-border-radius) !important;\n  border-top-left-radius: var(--bs-border-radius) !important; }\n\n.rounded-start-0 {\n  border-bottom-left-radius: 0 !important;\n  border-top-left-radius: 0 !important; }\n\n.rounded-start-1 {\n  border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n  border-top-left-radius: var(--bs-border-radius-sm) !important; }\n\n.rounded-start-2 {\n  border-bottom-left-radius: var(--bs-border-radius) !important;\n  border-top-left-radius: var(--bs-border-radius) !important; }\n\n.rounded-start-3 {\n  border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n  border-top-left-radius: var(--bs-border-radius-lg) !important; }\n\n.rounded-start-4 {\n  border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n  border-top-left-radius: var(--bs-border-radius-xl) !important; }\n\n.rounded-start-5 {\n  border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n  border-top-left-radius: var(--bs-border-radius-xxl) !important; }\n\n.rounded-start-circle {\n  border-bottom-left-radius: 50% !important;\n  border-top-left-radius: 50% !important; }\n\n.rounded-start-pill {\n  border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n  border-top-left-radius: var(--bs-border-radius-pill) !important; }\n\n.visible {\n  visibility: visible !important; }\n\n.invisible {\n  visibility: hidden !important; }\n\n.z-n1 {\n  z-index: -1 !important; }\n\n.z-0 {\n  z-index: 0 !important; }\n\n.z-1 {\n  z-index: 1 !important; }\n\n.z-2 {\n  z-index: 2 !important; }\n\n.z-3 {\n  z-index: 3 !important; }\n\n@media (min-width: 576px) {\n  .float-sm-start {\n    float: left !important; }\n  .float-sm-end {\n    float: right !important; }\n  .float-sm-none {\n    float: none !important; }\n  .object-fit-sm-contain {\n    object-fit: contain !important; }\n  .object-fit-sm-cover {\n    object-fit: cover !important; }\n  .object-fit-sm-fill {\n    object-fit: fill !important; }\n  .object-fit-sm-scale {\n    object-fit: scale-down !important; }\n  .object-fit-sm-none {\n    object-fit: none !important; }\n  .d-sm-inline {\n    display: inline !important; }\n  .d-sm-inline-block {\n    display: inline-block !important; }\n  .d-sm-block {\n    display: block !important; }\n  .d-sm-grid {\n    display: grid !important; }\n  .d-sm-inline-grid {\n    display: inline-grid !important; }\n  .d-sm-table {\n    display: table !important; }\n  .d-sm-table-row {\n    display: table-row !important; }\n  .d-sm-table-cell {\n    display: table-cell !important; }\n  .d-sm-flex {\n    display: flex !important; }\n  .d-sm-inline-flex {\n    display: inline-flex !important; }\n  .d-sm-none {\n    display: none !important; }\n  .flex-sm-fill {\n    flex: 1 1 auto !important; }\n  .flex-sm-row {\n    flex-direction: row !important; }\n  .flex-sm-column {\n    flex-direction: column !important; }\n  .flex-sm-row-reverse {\n    flex-direction: row-reverse !important; }\n  .flex-sm-column-reverse {\n    flex-direction: column-reverse !important; }\n  .flex-sm-grow-0 {\n    flex-grow: 0 !important; }\n  .flex-sm-grow-1 {\n    flex-grow: 1 !important; }\n  .flex-sm-shrink-0 {\n    flex-shrink: 0 !important; }\n  .flex-sm-shrink-1 {\n    flex-shrink: 1 !important; }\n  .flex-sm-wrap {\n    flex-wrap: wrap !important; }\n  .flex-sm-nowrap {\n    flex-wrap: nowrap !important; }\n  .flex-sm-wrap-reverse {\n    flex-wrap: wrap-reverse !important; }\n  .justify-content-sm-start {\n    justify-content: flex-start !important; }\n  .justify-content-sm-end {\n    justify-content: flex-end !important; }\n  .justify-content-sm-center {\n    justify-content: center !important; }\n  .justify-content-sm-between {\n    justify-content: space-between !important; }\n  .justify-content-sm-around {\n    justify-content: space-around !important; }\n  .justify-content-sm-evenly {\n    justify-content: space-evenly !important; }\n  .align-items-sm-start {\n    align-items: flex-start !important; }\n  .align-items-sm-end {\n    align-items: flex-end !important; }\n  .align-items-sm-center {\n    align-items: center !important; }\n  .align-items-sm-baseline {\n    align-items: baseline !important; }\n  .align-items-sm-stretch {\n    align-items: stretch !important; }\n  .align-content-sm-start {\n    align-content: flex-start !important; }\n  .align-content-sm-end {\n    align-content: flex-end !important; }\n  .align-content-sm-center {\n    align-content: center !important; }\n  .align-content-sm-between {\n    align-content: space-between !important; }\n  .align-content-sm-around {\n    align-content: space-around !important; }\n  .align-content-sm-stretch {\n    align-content: stretch !important; }\n  .align-self-sm-auto {\n    align-self: auto !important; }\n  .align-self-sm-start {\n    align-self: flex-start !important; }\n  .align-self-sm-end {\n    align-self: flex-end !important; }\n  .align-self-sm-center {\n    align-self: center !important; }\n  .align-self-sm-baseline {\n    align-self: baseline !important; }\n  .align-self-sm-stretch {\n    align-self: stretch !important; }\n  .order-sm-first {\n    order: -1 !important; }\n  .order-sm-0 {\n    order: 0 !important; }\n  .order-sm-1 {\n    order: 1 !important; }\n  .order-sm-2 {\n    order: 2 !important; }\n  .order-sm-3 {\n    order: 3 !important; }\n  .order-sm-4 {\n    order: 4 !important; }\n  .order-sm-5 {\n    order: 5 !important; }\n  .order-sm-last {\n    order: 6 !important; }\n  .m-sm-0 {\n    margin: 0 !important; }\n  .m-sm-1 {\n    margin: 0.25rem !important; }\n  .m-sm-2 {\n    margin: 0.5rem !important; }\n  .m-sm-3 {\n    margin: 1rem !important; }\n  .m-sm-4 {\n    margin: 1.5rem !important; }\n  .m-sm-5 {\n    margin: 3rem !important; }\n  .m-sm-auto {\n    margin: auto !important; }\n  .mx-sm-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important; }\n  .mx-sm-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important; }\n  .mx-sm-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important; }\n  .mx-sm-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important; }\n  .mx-sm-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important; }\n  .mx-sm-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important; }\n  .mx-sm-auto {\n    margin-right: auto !important;\n    margin-left: auto !important; }\n  .my-sm-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important; }\n  .my-sm-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important; }\n  .my-sm-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important; }\n  .my-sm-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important; }\n  .my-sm-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important; }\n  .my-sm-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important; }\n  .my-sm-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important; }\n  .mt-sm-0 {\n    margin-top: 0 !important; }\n  .mt-sm-1 {\n    margin-top: 0.25rem !important; }\n  .mt-sm-2 {\n    margin-top: 0.5rem !important; }\n  .mt-sm-3 {\n    margin-top: 1rem !important; }\n  .mt-sm-4 {\n    margin-top: 1.5rem !important; }\n  .mt-sm-5 {\n    margin-top: 3rem !important; }\n  .mt-sm-auto {\n    margin-top: auto !important; }\n  .me-sm-0 {\n    margin-right: 0 !important; }\n  .me-sm-1 {\n    margin-right: 0.25rem !important; }\n  .me-sm-2 {\n    margin-right: 0.5rem !important; }\n  .me-sm-3 {\n    margin-right: 1rem !important; }\n  .me-sm-4 {\n    margin-right: 1.5rem !important; }\n  .me-sm-5 {\n    margin-right: 3rem !important; }\n  .me-sm-auto {\n    margin-right: auto !important; }\n  .mb-sm-0 {\n    margin-bottom: 0 !important; }\n  .mb-sm-1 {\n    margin-bottom: 0.25rem !important; }\n  .mb-sm-2 {\n    margin-bottom: 0.5rem !important; }\n  .mb-sm-3 {\n    margin-bottom: 1rem !important; }\n  .mb-sm-4 {\n    margin-bottom: 1.5rem !important; }\n  .mb-sm-5 {\n    margin-bottom: 3rem !important; }\n  .mb-sm-auto {\n    margin-bottom: auto !important; }\n  .ms-sm-0 {\n    margin-left: 0 !important; }\n  .ms-sm-1 {\n    margin-left: 0.25rem !important; }\n  .ms-sm-2 {\n    margin-left: 0.5rem !important; }\n  .ms-sm-3 {\n    margin-left: 1rem !important; }\n  .ms-sm-4 {\n    margin-left: 1.5rem !important; }\n  .ms-sm-5 {\n    margin-left: 3rem !important; }\n  .ms-sm-auto {\n    margin-left: auto !important; }\n  .m-sm-n1 {\n    margin: -0.25rem !important; }\n  .m-sm-n2 {\n    margin: -0.5rem !important; }\n  .m-sm-n3 {\n    margin: -1rem !important; }\n  .m-sm-n4 {\n    margin: -1.5rem !important; }\n  .m-sm-n5 {\n    margin: -3rem !important; }\n  .mx-sm-n1 {\n    margin-right: -0.25rem !important;\n    margin-left: -0.25rem !important; }\n  .mx-sm-n2 {\n    margin-right: -0.5rem !important;\n    margin-left: -0.5rem !important; }\n  .mx-sm-n3 {\n    margin-right: -1rem !important;\n    margin-left: -1rem !important; }\n  .mx-sm-n4 {\n    margin-right: -1.5rem !important;\n    margin-left: -1.5rem !important; }\n  .mx-sm-n5 {\n    margin-right: -3rem !important;\n    margin-left: -3rem !important; }\n  .my-sm-n1 {\n    margin-top: -0.25rem !important;\n    margin-bottom: -0.25rem !important; }\n  .my-sm-n2 {\n    margin-top: -0.5rem !important;\n    margin-bottom: -0.5rem !important; }\n  .my-sm-n3 {\n    margin-top: -1rem !important;\n    margin-bottom: -1rem !important; }\n  .my-sm-n4 {\n    margin-top: -1.5rem !important;\n    margin-bottom: -1.5rem !important; }\n  .my-sm-n5 {\n    margin-top: -3rem !important;\n    margin-bottom: -3rem !important; }\n  .mt-sm-n1 {\n    margin-top: -0.25rem !important; }\n  .mt-sm-n2 {\n    margin-top: -0.5rem !important; }\n  .mt-sm-n3 {\n    margin-top: -1rem !important; }\n  .mt-sm-n4 {\n    margin-top: -1.5rem !important; }\n  .mt-sm-n5 {\n    margin-top: -3rem !important; }\n  .me-sm-n1 {\n    margin-right: -0.25rem !important; }\n  .me-sm-n2 {\n    margin-right: -0.5rem !important; }\n  .me-sm-n3 {\n    margin-right: -1rem !important; }\n  .me-sm-n4 {\n    margin-right: -1.5rem !important; }\n  .me-sm-n5 {\n    margin-right: -3rem !important; }\n  .mb-sm-n1 {\n    margin-bottom: -0.25rem !important; }\n  .mb-sm-n2 {\n    margin-bottom: -0.5rem !important; }\n  .mb-sm-n3 {\n    margin-bottom: -1rem !important; }\n  .mb-sm-n4 {\n    margin-bottom: -1.5rem !important; }\n  .mb-sm-n5 {\n    margin-bottom: -3rem !important; }\n  .ms-sm-n1 {\n    margin-left: -0.25rem !important; }\n  .ms-sm-n2 {\n    margin-left: -0.5rem !important; }\n  .ms-sm-n3 {\n    margin-left: -1rem !important; }\n  .ms-sm-n4 {\n    margin-left: -1.5rem !important; }\n  .ms-sm-n5 {\n    margin-left: -3rem !important; }\n  .p-sm-0 {\n    padding: 0 !important; }\n  .p-sm-1 {\n    padding: 0.25rem !important; }\n  .p-sm-2 {\n    padding: 0.5rem !important; }\n  .p-sm-3 {\n    padding: 1rem !important; }\n  .p-sm-4 {\n    padding: 1.5rem !important; }\n  .p-sm-5 {\n    padding: 3rem !important; }\n  .px-sm-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important; }\n  .px-sm-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important; }\n  .px-sm-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important; }\n  .px-sm-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important; }\n  .px-sm-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important; }\n  .px-sm-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important; }\n  .py-sm-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important; }\n  .py-sm-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important; }\n  .py-sm-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important; }\n  .py-sm-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important; }\n  .py-sm-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important; }\n  .py-sm-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important; }\n  .pt-sm-0 {\n    padding-top: 0 !important; }\n  .pt-sm-1 {\n    padding-top: 0.25rem !important; }\n  .pt-sm-2 {\n    padding-top: 0.5rem !important; }\n  .pt-sm-3 {\n    padding-top: 1rem !important; }\n  .pt-sm-4 {\n    padding-top: 1.5rem !important; }\n  .pt-sm-5 {\n    padding-top: 3rem !important; }\n  .pe-sm-0 {\n    padding-right: 0 !important; }\n  .pe-sm-1 {\n    padding-right: 0.25rem !important; }\n  .pe-sm-2 {\n    padding-right: 0.5rem !important; }\n  .pe-sm-3 {\n    padding-right: 1rem !important; }\n  .pe-sm-4 {\n    padding-right: 1.5rem !important; }\n  .pe-sm-5 {\n    padding-right: 3rem !important; }\n  .pb-sm-0 {\n    padding-bottom: 0 !important; }\n  .pb-sm-1 {\n    padding-bottom: 0.25rem !important; }\n  .pb-sm-2 {\n    padding-bottom: 0.5rem !important; }\n  .pb-sm-3 {\n    padding-bottom: 1rem !important; }\n  .pb-sm-4 {\n    padding-bottom: 1.5rem !important; }\n  .pb-sm-5 {\n    padding-bottom: 3rem !important; }\n  .ps-sm-0 {\n    padding-left: 0 !important; }\n  .ps-sm-1 {\n    padding-left: 0.25rem !important; }\n  .ps-sm-2 {\n    padding-left: 0.5rem !important; }\n  .ps-sm-3 {\n    padding-left: 1rem !important; }\n  .ps-sm-4 {\n    padding-left: 1.5rem !important; }\n  .ps-sm-5 {\n    padding-left: 3rem !important; }\n  .gap-sm-0 {\n    gap: 0 !important; }\n  .gap-sm-1 {\n    gap: 0.25rem !important; }\n  .gap-sm-2 {\n    gap: 0.5rem !important; }\n  .gap-sm-3 {\n    gap: 1rem !important; }\n  .gap-sm-4 {\n    gap: 1.5rem !important; }\n  .gap-sm-5 {\n    gap: 3rem !important; }\n  .row-gap-sm-0 {\n    row-gap: 0 !important; }\n  .row-gap-sm-1 {\n    row-gap: 0.25rem !important; }\n  .row-gap-sm-2 {\n    row-gap: 0.5rem !important; }\n  .row-gap-sm-3 {\n    row-gap: 1rem !important; }\n  .row-gap-sm-4 {\n    row-gap: 1.5rem !important; }\n  .row-gap-sm-5 {\n    row-gap: 3rem !important; }\n  .column-gap-sm-0 {\n    column-gap: 0 !important; }\n  .column-gap-sm-1 {\n    column-gap: 0.25rem !important; }\n  .column-gap-sm-2 {\n    column-gap: 0.5rem !important; }\n  .column-gap-sm-3 {\n    column-gap: 1rem !important; }\n  .column-gap-sm-4 {\n    column-gap: 1.5rem !important; }\n  .column-gap-sm-5 {\n    column-gap: 3rem !important; }\n  .text-sm-start {\n    text-align: left !important; }\n  .text-sm-end {\n    text-align: right !important; }\n  .text-sm-center {\n    text-align: center !important; } }\n\n@media (min-width: 768px) {\n  .float-md-start {\n    float: left !important; }\n  .float-md-end {\n    float: right !important; }\n  .float-md-none {\n    float: none !important; }\n  .object-fit-md-contain {\n    object-fit: contain !important; }\n  .object-fit-md-cover {\n    object-fit: cover !important; }\n  .object-fit-md-fill {\n    object-fit: fill !important; }\n  .object-fit-md-scale {\n    object-fit: scale-down !important; }\n  .object-fit-md-none {\n    object-fit: none !important; }\n  .d-md-inline {\n    display: inline !important; }\n  .d-md-inline-block {\n    display: inline-block !important; }\n  .d-md-block {\n    display: block !important; }\n  .d-md-grid {\n    display: grid !important; }\n  .d-md-inline-grid {\n    display: inline-grid !important; }\n  .d-md-table {\n    display: table !important; }\n  .d-md-table-row {\n    display: table-row !important; }\n  .d-md-table-cell {\n    display: table-cell !important; }\n  .d-md-flex {\n    display: flex !important; }\n  .d-md-inline-flex {\n    display: inline-flex !important; }\n  .d-md-none {\n    display: none !important; }\n  .flex-md-fill {\n    flex: 1 1 auto !important; }\n  .flex-md-row {\n    flex-direction: row !important; }\n  .flex-md-column {\n    flex-direction: column !important; }\n  .flex-md-row-reverse {\n    flex-direction: row-reverse !important; }\n  .flex-md-column-reverse {\n    flex-direction: column-reverse !important; }\n  .flex-md-grow-0 {\n    flex-grow: 0 !important; }\n  .flex-md-grow-1 {\n    flex-grow: 1 !important; }\n  .flex-md-shrink-0 {\n    flex-shrink: 0 !important; }\n  .flex-md-shrink-1 {\n    flex-shrink: 1 !important; }\n  .flex-md-wrap {\n    flex-wrap: wrap !important; }\n  .flex-md-nowrap {\n    flex-wrap: nowrap !important; }\n  .flex-md-wrap-reverse {\n    flex-wrap: wrap-reverse !important; }\n  .justify-content-md-start {\n    justify-content: flex-start !important; }\n  .justify-content-md-end {\n    justify-content: flex-end !important; }\n  .justify-content-md-center {\n    justify-content: center !important; }\n  .justify-content-md-between {\n    justify-content: space-between !important; }\n  .justify-content-md-around {\n    justify-content: space-around !important; }\n  .justify-content-md-evenly {\n    justify-content: space-evenly !important; }\n  .align-items-md-start {\n    align-items: flex-start !important; }\n  .align-items-md-end {\n    align-items: flex-end !important; }\n  .align-items-md-center {\n    align-items: center !important; }\n  .align-items-md-baseline {\n    align-items: baseline !important; }\n  .align-items-md-stretch {\n    align-items: stretch !important; }\n  .align-content-md-start {\n    align-content: flex-start !important; }\n  .align-content-md-end {\n    align-content: flex-end !important; }\n  .align-content-md-center {\n    align-content: center !important; }\n  .align-content-md-between {\n    align-content: space-between !important; }\n  .align-content-md-around {\n    align-content: space-around !important; }\n  .align-content-md-stretch {\n    align-content: stretch !important; }\n  .align-self-md-auto {\n    align-self: auto !important; }\n  .align-self-md-start {\n    align-self: flex-start !important; }\n  .align-self-md-end {\n    align-self: flex-end !important; }\n  .align-self-md-center {\n    align-self: center !important; }\n  .align-self-md-baseline {\n    align-self: baseline !important; }\n  .align-self-md-stretch {\n    align-self: stretch !important; }\n  .order-md-first {\n    order: -1 !important; }\n  .order-md-0 {\n    order: 0 !important; }\n  .order-md-1 {\n    order: 1 !important; }\n  .order-md-2 {\n    order: 2 !important; }\n  .order-md-3 {\n    order: 3 !important; }\n  .order-md-4 {\n    order: 4 !important; }\n  .order-md-5 {\n    order: 5 !important; }\n  .order-md-last {\n    order: 6 !important; }\n  .m-md-0 {\n    margin: 0 !important; }\n  .m-md-1 {\n    margin: 0.25rem !important; }\n  .m-md-2 {\n    margin: 0.5rem !important; }\n  .m-md-3 {\n    margin: 1rem !important; }\n  .m-md-4 {\n    margin: 1.5rem !important; }\n  .m-md-5 {\n    margin: 3rem !important; }\n  .m-md-auto {\n    margin: auto !important; }\n  .mx-md-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important; }\n  .mx-md-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important; }\n  .mx-md-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important; }\n  .mx-md-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important; }\n  .mx-md-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important; }\n  .mx-md-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important; }\n  .mx-md-auto {\n    margin-right: auto !important;\n    margin-left: auto !important; }\n  .my-md-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important; }\n  .my-md-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important; }\n  .my-md-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important; }\n  .my-md-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important; }\n  .my-md-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important; }\n  .my-md-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important; }\n  .my-md-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important; }\n  .mt-md-0 {\n    margin-top: 0 !important; }\n  .mt-md-1 {\n    margin-top: 0.25rem !important; }\n  .mt-md-2 {\n    margin-top: 0.5rem !important; }\n  .mt-md-3 {\n    margin-top: 1rem !important; }\n  .mt-md-4 {\n    margin-top: 1.5rem !important; }\n  .mt-md-5 {\n    margin-top: 3rem !important; }\n  .mt-md-auto {\n    margin-top: auto !important; }\n  .me-md-0 {\n    margin-right: 0 !important; }\n  .me-md-1 {\n    margin-right: 0.25rem !important; }\n  .me-md-2 {\n    margin-right: 0.5rem !important; }\n  .me-md-3 {\n    margin-right: 1rem !important; }\n  .me-md-4 {\n    margin-right: 1.5rem !important; }\n  .me-md-5 {\n    margin-right: 3rem !important; }\n  .me-md-auto {\n    margin-right: auto !important; }\n  .mb-md-0 {\n    margin-bottom: 0 !important; }\n  .mb-md-1 {\n    margin-bottom: 0.25rem !important; }\n  .mb-md-2 {\n    margin-bottom: 0.5rem !important; }\n  .mb-md-3 {\n    margin-bottom: 1rem !important; }\n  .mb-md-4 {\n    margin-bottom: 1.5rem !important; }\n  .mb-md-5 {\n    margin-bottom: 3rem !important; }\n  .mb-md-auto {\n    margin-bottom: auto !important; }\n  .ms-md-0 {\n    margin-left: 0 !important; }\n  .ms-md-1 {\n    margin-left: 0.25rem !important; }\n  .ms-md-2 {\n    margin-left: 0.5rem !important; }\n  .ms-md-3 {\n    margin-left: 1rem !important; }\n  .ms-md-4 {\n    margin-left: 1.5rem !important; }\n  .ms-md-5 {\n    margin-left: 3rem !important; }\n  .ms-md-auto {\n    margin-left: auto !important; }\n  .m-md-n1 {\n    margin: -0.25rem !important; }\n  .m-md-n2 {\n    margin: -0.5rem !important; }\n  .m-md-n3 {\n    margin: -1rem !important; }\n  .m-md-n4 {\n    margin: -1.5rem !important; }\n  .m-md-n5 {\n    margin: -3rem !important; }\n  .mx-md-n1 {\n    margin-right: -0.25rem !important;\n    margin-left: -0.25rem !important; }\n  .mx-md-n2 {\n    margin-right: -0.5rem !important;\n    margin-left: -0.5rem !important; }\n  .mx-md-n3 {\n    margin-right: -1rem !important;\n    margin-left: -1rem !important; }\n  .mx-md-n4 {\n    margin-right: -1.5rem !important;\n    margin-left: -1.5rem !important; }\n  .mx-md-n5 {\n    margin-right: -3rem !important;\n    margin-left: -3rem !important; }\n  .my-md-n1 {\n    margin-top: -0.25rem !important;\n    margin-bottom: -0.25rem !important; }\n  .my-md-n2 {\n    margin-top: -0.5rem !important;\n    margin-bottom: -0.5rem !important; }\n  .my-md-n3 {\n    margin-top: -1rem !important;\n    margin-bottom: -1rem !important; }\n  .my-md-n4 {\n    margin-top: -1.5rem !important;\n    margin-bottom: -1.5rem !important; }\n  .my-md-n5 {\n    margin-top: -3rem !important;\n    margin-bottom: -3rem !important; }\n  .mt-md-n1 {\n    margin-top: -0.25rem !important; }\n  .mt-md-n2 {\n    margin-top: -0.5rem !important; }\n  .mt-md-n3 {\n    margin-top: -1rem !important; }\n  .mt-md-n4 {\n    margin-top: -1.5rem !important; }\n  .mt-md-n5 {\n    margin-top: -3rem !important; }\n  .me-md-n1 {\n    margin-right: -0.25rem !important; }\n  .me-md-n2 {\n    margin-right: -0.5rem !important; }\n  .me-md-n3 {\n    margin-right: -1rem !important; }\n  .me-md-n4 {\n    margin-right: -1.5rem !important; }\n  .me-md-n5 {\n    margin-right: -3rem !important; }\n  .mb-md-n1 {\n    margin-bottom: -0.25rem !important; }\n  .mb-md-n2 {\n    margin-bottom: -0.5rem !important; }\n  .mb-md-n3 {\n    margin-bottom: -1rem !important; }\n  .mb-md-n4 {\n    margin-bottom: -1.5rem !important; }\n  .mb-md-n5 {\n    margin-bottom: -3rem !important; }\n  .ms-md-n1 {\n    margin-left: -0.25rem !important; }\n  .ms-md-n2 {\n    margin-left: -0.5rem !important; }\n  .ms-md-n3 {\n    margin-left: -1rem !important; }\n  .ms-md-n4 {\n    margin-left: -1.5rem !important; }\n  .ms-md-n5 {\n    margin-left: -3rem !important; }\n  .p-md-0 {\n    padding: 0 !important; }\n  .p-md-1 {\n    padding: 0.25rem !important; }\n  .p-md-2 {\n    padding: 0.5rem !important; }\n  .p-md-3 {\n    padding: 1rem !important; }\n  .p-md-4 {\n    padding: 1.5rem !important; }\n  .p-md-5 {\n    padding: 3rem !important; }\n  .px-md-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important; }\n  .px-md-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important; }\n  .px-md-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important; }\n  .px-md-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important; }\n  .px-md-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important; }\n  .px-md-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important; }\n  .py-md-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important; }\n  .py-md-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important; }\n  .py-md-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important; }\n  .py-md-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important; }\n  .py-md-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important; }\n  .py-md-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important; }\n  .pt-md-0 {\n    padding-top: 0 !important; }\n  .pt-md-1 {\n    padding-top: 0.25rem !important; }\n  .pt-md-2 {\n    padding-top: 0.5rem !important; }\n  .pt-md-3 {\n    padding-top: 1rem !important; }\n  .pt-md-4 {\n    padding-top: 1.5rem !important; }\n  .pt-md-5 {\n    padding-top: 3rem !important; }\n  .pe-md-0 {\n    padding-right: 0 !important; }\n  .pe-md-1 {\n    padding-right: 0.25rem !important; }\n  .pe-md-2 {\n    padding-right: 0.5rem !important; }\n  .pe-md-3 {\n    padding-right: 1rem !important; }\n  .pe-md-4 {\n    padding-right: 1.5rem !important; }\n  .pe-md-5 {\n    padding-right: 3rem !important; }\n  .pb-md-0 {\n    padding-bottom: 0 !important; }\n  .pb-md-1 {\n    padding-bottom: 0.25rem !important; }\n  .pb-md-2 {\n    padding-bottom: 0.5rem !important; }\n  .pb-md-3 {\n    padding-bottom: 1rem !important; }\n  .pb-md-4 {\n    padding-bottom: 1.5rem !important; }\n  .pb-md-5 {\n    padding-bottom: 3rem !important; }\n  .ps-md-0 {\n    padding-left: 0 !important; }\n  .ps-md-1 {\n    padding-left: 0.25rem !important; }\n  .ps-md-2 {\n    padding-left: 0.5rem !important; }\n  .ps-md-3 {\n    padding-left: 1rem !important; }\n  .ps-md-4 {\n    padding-left: 1.5rem !important; }\n  .ps-md-5 {\n    padding-left: 3rem !important; }\n  .gap-md-0 {\n    gap: 0 !important; }\n  .gap-md-1 {\n    gap: 0.25rem !important; }\n  .gap-md-2 {\n    gap: 0.5rem !important; }\n  .gap-md-3 {\n    gap: 1rem !important; }\n  .gap-md-4 {\n    gap: 1.5rem !important; }\n  .gap-md-5 {\n    gap: 3rem !important; }\n  .row-gap-md-0 {\n    row-gap: 0 !important; }\n  .row-gap-md-1 {\n    row-gap: 0.25rem !important; }\n  .row-gap-md-2 {\n    row-gap: 0.5rem !important; }\n  .row-gap-md-3 {\n    row-gap: 1rem !important; }\n  .row-gap-md-4 {\n    row-gap: 1.5rem !important; }\n  .row-gap-md-5 {\n    row-gap: 3rem !important; }\n  .column-gap-md-0 {\n    column-gap: 0 !important; }\n  .column-gap-md-1 {\n    column-gap: 0.25rem !important; }\n  .column-gap-md-2 {\n    column-gap: 0.5rem !important; }\n  .column-gap-md-3 {\n    column-gap: 1rem !important; }\n  .column-gap-md-4 {\n    column-gap: 1.5rem !important; }\n  .column-gap-md-5 {\n    column-gap: 3rem !important; }\n  .text-md-start {\n    text-align: left !important; }\n  .text-md-end {\n    text-align: right !important; }\n  .text-md-center {\n    text-align: center !important; } }\n\n@media (min-width: 992px) {\n  .float-lg-start {\n    float: left !important; }\n  .float-lg-end {\n    float: right !important; }\n  .float-lg-none {\n    float: none !important; }\n  .object-fit-lg-contain {\n    object-fit: contain !important; }\n  .object-fit-lg-cover {\n    object-fit: cover !important; }\n  .object-fit-lg-fill {\n    object-fit: fill !important; }\n  .object-fit-lg-scale {\n    object-fit: scale-down !important; }\n  .object-fit-lg-none {\n    object-fit: none !important; }\n  .d-lg-inline {\n    display: inline !important; }\n  .d-lg-inline-block {\n    display: inline-block !important; }\n  .d-lg-block {\n    display: block !important; }\n  .d-lg-grid {\n    display: grid !important; }\n  .d-lg-inline-grid {\n    display: inline-grid !important; }\n  .d-lg-table {\n    display: table !important; }\n  .d-lg-table-row {\n    display: table-row !important; }\n  .d-lg-table-cell {\n    display: table-cell !important; }\n  .d-lg-flex {\n    display: flex !important; }\n  .d-lg-inline-flex {\n    display: inline-flex !important; }\n  .d-lg-none {\n    display: none !important; }\n  .flex-lg-fill {\n    flex: 1 1 auto !important; }\n  .flex-lg-row {\n    flex-direction: row !important; }\n  .flex-lg-column {\n    flex-direction: column !important; }\n  .flex-lg-row-reverse {\n    flex-direction: row-reverse !important; }\n  .flex-lg-column-reverse {\n    flex-direction: column-reverse !important; }\n  .flex-lg-grow-0 {\n    flex-grow: 0 !important; }\n  .flex-lg-grow-1 {\n    flex-grow: 1 !important; }\n  .flex-lg-shrink-0 {\n    flex-shrink: 0 !important; }\n  .flex-lg-shrink-1 {\n    flex-shrink: 1 !important; }\n  .flex-lg-wrap {\n    flex-wrap: wrap !important; }\n  .flex-lg-nowrap {\n    flex-wrap: nowrap !important; }\n  .flex-lg-wrap-reverse {\n    flex-wrap: wrap-reverse !important; }\n  .justify-content-lg-start {\n    justify-content: flex-start !important; }\n  .justify-content-lg-end {\n    justify-content: flex-end !important; }\n  .justify-content-lg-center {\n    justify-content: center !important; }\n  .justify-content-lg-between {\n    justify-content: space-between !important; }\n  .justify-content-lg-around {\n    justify-content: space-around !important; }\n  .justify-content-lg-evenly {\n    justify-content: space-evenly !important; }\n  .align-items-lg-start {\n    align-items: flex-start !important; }\n  .align-items-lg-end {\n    align-items: flex-end !important; }\n  .align-items-lg-center {\n    align-items: center !important; }\n  .align-items-lg-baseline {\n    align-items: baseline !important; }\n  .align-items-lg-stretch {\n    align-items: stretch !important; }\n  .align-content-lg-start {\n    align-content: flex-start !important; }\n  .align-content-lg-end {\n    align-content: flex-end !important; }\n  .align-content-lg-center {\n    align-content: center !important; }\n  .align-content-lg-between {\n    align-content: space-between !important; }\n  .align-content-lg-around {\n    align-content: space-around !important; }\n  .align-content-lg-stretch {\n    align-content: stretch !important; }\n  .align-self-lg-auto {\n    align-self: auto !important; }\n  .align-self-lg-start {\n    align-self: flex-start !important; }\n  .align-self-lg-end {\n    align-self: flex-end !important; }\n  .align-self-lg-center {\n    align-self: center !important; }\n  .align-self-lg-baseline {\n    align-self: baseline !important; }\n  .align-self-lg-stretch {\n    align-self: stretch !important; }\n  .order-lg-first {\n    order: -1 !important; }\n  .order-lg-0 {\n    order: 0 !important; }\n  .order-lg-1 {\n    order: 1 !important; }\n  .order-lg-2 {\n    order: 2 !important; }\n  .order-lg-3 {\n    order: 3 !important; }\n  .order-lg-4 {\n    order: 4 !important; }\n  .order-lg-5 {\n    order: 5 !important; }\n  .order-lg-last {\n    order: 6 !important; }\n  .m-lg-0 {\n    margin: 0 !important; }\n  .m-lg-1 {\n    margin: 0.25rem !important; }\n  .m-lg-2 {\n    margin: 0.5rem !important; }\n  .m-lg-3 {\n    margin: 1rem !important; }\n  .m-lg-4 {\n    margin: 1.5rem !important; }\n  .m-lg-5 {\n    margin: 3rem !important; }\n  .m-lg-auto {\n    margin: auto !important; }\n  .mx-lg-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important; }\n  .mx-lg-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important; }\n  .mx-lg-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important; }\n  .mx-lg-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important; }\n  .mx-lg-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important; }\n  .mx-lg-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important; }\n  .mx-lg-auto {\n    margin-right: auto !important;\n    margin-left: auto !important; }\n  .my-lg-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important; }\n  .my-lg-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important; }\n  .my-lg-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important; }\n  .my-lg-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important; }\n  .my-lg-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important; }\n  .my-lg-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important; }\n  .my-lg-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important; }\n  .mt-lg-0 {\n    margin-top: 0 !important; }\n  .mt-lg-1 {\n    margin-top: 0.25rem !important; }\n  .mt-lg-2 {\n    margin-top: 0.5rem !important; }\n  .mt-lg-3 {\n    margin-top: 1rem !important; }\n  .mt-lg-4 {\n    margin-top: 1.5rem !important; }\n  .mt-lg-5 {\n    margin-top: 3rem !important; }\n  .mt-lg-auto {\n    margin-top: auto !important; }\n  .me-lg-0 {\n    margin-right: 0 !important; }\n  .me-lg-1 {\n    margin-right: 0.25rem !important; }\n  .me-lg-2 {\n    margin-right: 0.5rem !important; }\n  .me-lg-3 {\n    margin-right: 1rem !important; }\n  .me-lg-4 {\n    margin-right: 1.5rem !important; }\n  .me-lg-5 {\n    margin-right: 3rem !important; }\n  .me-lg-auto {\n    margin-right: auto !important; }\n  .mb-lg-0 {\n    margin-bottom: 0 !important; }\n  .mb-lg-1 {\n    margin-bottom: 0.25rem !important; }\n  .mb-lg-2 {\n    margin-bottom: 0.5rem !important; }\n  .mb-lg-3 {\n    margin-bottom: 1rem !important; }\n  .mb-lg-4 {\n    margin-bottom: 1.5rem !important; }\n  .mb-lg-5 {\n    margin-bottom: 3rem !important; }\n  .mb-lg-auto {\n    margin-bottom: auto !important; }\n  .ms-lg-0 {\n    margin-left: 0 !important; }\n  .ms-lg-1 {\n    margin-left: 0.25rem !important; }\n  .ms-lg-2 {\n    margin-left: 0.5rem !important; }\n  .ms-lg-3 {\n    margin-left: 1rem !important; }\n  .ms-lg-4 {\n    margin-left: 1.5rem !important; }\n  .ms-lg-5 {\n    margin-left: 3rem !important; }\n  .ms-lg-auto {\n    margin-left: auto !important; }\n  .m-lg-n1 {\n    margin: -0.25rem !important; }\n  .m-lg-n2 {\n    margin: -0.5rem !important; }\n  .m-lg-n3 {\n    margin: -1rem !important; }\n  .m-lg-n4 {\n    margin: -1.5rem !important; }\n  .m-lg-n5 {\n    margin: -3rem !important; }\n  .mx-lg-n1 {\n    margin-right: -0.25rem !important;\n    margin-left: -0.25rem !important; }\n  .mx-lg-n2 {\n    margin-right: -0.5rem !important;\n    margin-left: -0.5rem !important; }\n  .mx-lg-n3 {\n    margin-right: -1rem !important;\n    margin-left: -1rem !important; }\n  .mx-lg-n4 {\n    margin-right: -1.5rem !important;\n    margin-left: -1.5rem !important; }\n  .mx-lg-n5 {\n    margin-right: -3rem !important;\n    margin-left: -3rem !important; }\n  .my-lg-n1 {\n    margin-top: -0.25rem !important;\n    margin-bottom: -0.25rem !important; }\n  .my-lg-n2 {\n    margin-top: -0.5rem !important;\n    margin-bottom: -0.5rem !important; }\n  .my-lg-n3 {\n    margin-top: -1rem !important;\n    margin-bottom: -1rem !important; }\n  .my-lg-n4 {\n    margin-top: -1.5rem !important;\n    margin-bottom: -1.5rem !important; }\n  .my-lg-n5 {\n    margin-top: -3rem !important;\n    margin-bottom: -3rem !important; }\n  .mt-lg-n1 {\n    margin-top: -0.25rem !important; }\n  .mt-lg-n2 {\n    margin-top: -0.5rem !important; }\n  .mt-lg-n3 {\n    margin-top: -1rem !important; }\n  .mt-lg-n4 {\n    margin-top: -1.5rem !important; }\n  .mt-lg-n5 {\n    margin-top: -3rem !important; }\n  .me-lg-n1 {\n    margin-right: -0.25rem !important; }\n  .me-lg-n2 {\n    margin-right: -0.5rem !important; }\n  .me-lg-n3 {\n    margin-right: -1rem !important; }\n  .me-lg-n4 {\n    margin-right: -1.5rem !important; }\n  .me-lg-n5 {\n    margin-right: -3rem !important; }\n  .mb-lg-n1 {\n    margin-bottom: -0.25rem !important; }\n  .mb-lg-n2 {\n    margin-bottom: -0.5rem !important; }\n  .mb-lg-n3 {\n    margin-bottom: -1rem !important; }\n  .mb-lg-n4 {\n    margin-bottom: -1.5rem !important; }\n  .mb-lg-n5 {\n    margin-bottom: -3rem !important; }\n  .ms-lg-n1 {\n    margin-left: -0.25rem !important; }\n  .ms-lg-n2 {\n    margin-left: -0.5rem !important; }\n  .ms-lg-n3 {\n    margin-left: -1rem !important; }\n  .ms-lg-n4 {\n    margin-left: -1.5rem !important; }\n  .ms-lg-n5 {\n    margin-left: -3rem !important; }\n  .p-lg-0 {\n    padding: 0 !important; }\n  .p-lg-1 {\n    padding: 0.25rem !important; }\n  .p-lg-2 {\n    padding: 0.5rem !important; }\n  .p-lg-3 {\n    padding: 1rem !important; }\n  .p-lg-4 {\n    padding: 1.5rem !important; }\n  .p-lg-5 {\n    padding: 3rem !important; }\n  .px-lg-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important; }\n  .px-lg-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important; }\n  .px-lg-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important; }\n  .px-lg-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important; }\n  .px-lg-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important; }\n  .px-lg-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important; }\n  .py-lg-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important; }\n  .py-lg-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important; }\n  .py-lg-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important; }\n  .py-lg-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important; }\n  .py-lg-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important; }\n  .py-lg-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important; }\n  .pt-lg-0 {\n    padding-top: 0 !important; }\n  .pt-lg-1 {\n    padding-top: 0.25rem !important; }\n  .pt-lg-2 {\n    padding-top: 0.5rem !important; }\n  .pt-lg-3 {\n    padding-top: 1rem !important; }\n  .pt-lg-4 {\n    padding-top: 1.5rem !important; }\n  .pt-lg-5 {\n    padding-top: 3rem !important; }\n  .pe-lg-0 {\n    padding-right: 0 !important; }\n  .pe-lg-1 {\n    padding-right: 0.25rem !important; }\n  .pe-lg-2 {\n    padding-right: 0.5rem !important; }\n  .pe-lg-3 {\n    padding-right: 1rem !important; }\n  .pe-lg-4 {\n    padding-right: 1.5rem !important; }\n  .pe-lg-5 {\n    padding-right: 3rem !important; }\n  .pb-lg-0 {\n    padding-bottom: 0 !important; }\n  .pb-lg-1 {\n    padding-bottom: 0.25rem !important; }\n  .pb-lg-2 {\n    padding-bottom: 0.5rem !important; }\n  .pb-lg-3 {\n    padding-bottom: 1rem !important; }\n  .pb-lg-4 {\n    padding-bottom: 1.5rem !important; }\n  .pb-lg-5 {\n    padding-bottom: 3rem !important; }\n  .ps-lg-0 {\n    padding-left: 0 !important; }\n  .ps-lg-1 {\n    padding-left: 0.25rem !important; }\n  .ps-lg-2 {\n    padding-left: 0.5rem !important; }\n  .ps-lg-3 {\n    padding-left: 1rem !important; }\n  .ps-lg-4 {\n    padding-left: 1.5rem !important; }\n  .ps-lg-5 {\n    padding-left: 3rem !important; }\n  .gap-lg-0 {\n    gap: 0 !important; }\n  .gap-lg-1 {\n    gap: 0.25rem !important; }\n  .gap-lg-2 {\n    gap: 0.5rem !important; }\n  .gap-lg-3 {\n    gap: 1rem !important; }\n  .gap-lg-4 {\n    gap: 1.5rem !important; }\n  .gap-lg-5 {\n    gap: 3rem !important; }\n  .row-gap-lg-0 {\n    row-gap: 0 !important; }\n  .row-gap-lg-1 {\n    row-gap: 0.25rem !important; }\n  .row-gap-lg-2 {\n    row-gap: 0.5rem !important; }\n  .row-gap-lg-3 {\n    row-gap: 1rem !important; }\n  .row-gap-lg-4 {\n    row-gap: 1.5rem !important; }\n  .row-gap-lg-5 {\n    row-gap: 3rem !important; }\n  .column-gap-lg-0 {\n    column-gap: 0 !important; }\n  .column-gap-lg-1 {\n    column-gap: 0.25rem !important; }\n  .column-gap-lg-2 {\n    column-gap: 0.5rem !important; }\n  .column-gap-lg-3 {\n    column-gap: 1rem !important; }\n  .column-gap-lg-4 {\n    column-gap: 1.5rem !important; }\n  .column-gap-lg-5 {\n    column-gap: 3rem !important; }\n  .text-lg-start {\n    text-align: left !important; }\n  .text-lg-end {\n    text-align: right !important; }\n  .text-lg-center {\n    text-align: center !important; } }\n\n@media (min-width: 1200px) {\n  .float-xl-start {\n    float: left !important; }\n  .float-xl-end {\n    float: right !important; }\n  .float-xl-none {\n    float: none !important; }\n  .object-fit-xl-contain {\n    object-fit: contain !important; }\n  .object-fit-xl-cover {\n    object-fit: cover !important; }\n  .object-fit-xl-fill {\n    object-fit: fill !important; }\n  .object-fit-xl-scale {\n    object-fit: scale-down !important; }\n  .object-fit-xl-none {\n    object-fit: none !important; }\n  .d-xl-inline {\n    display: inline !important; }\n  .d-xl-inline-block {\n    display: inline-block !important; }\n  .d-xl-block {\n    display: block !important; }\n  .d-xl-grid {\n    display: grid !important; }\n  .d-xl-inline-grid {\n    display: inline-grid !important; }\n  .d-xl-table {\n    display: table !important; }\n  .d-xl-table-row {\n    display: table-row !important; }\n  .d-xl-table-cell {\n    display: table-cell !important; }\n  .d-xl-flex {\n    display: flex !important; }\n  .d-xl-inline-flex {\n    display: inline-flex !important; }\n  .d-xl-none {\n    display: none !important; }\n  .flex-xl-fill {\n    flex: 1 1 auto !important; }\n  .flex-xl-row {\n    flex-direction: row !important; }\n  .flex-xl-column {\n    flex-direction: column !important; }\n  .flex-xl-row-reverse {\n    flex-direction: row-reverse !important; }\n  .flex-xl-column-reverse {\n    flex-direction: column-reverse !important; }\n  .flex-xl-grow-0 {\n    flex-grow: 0 !important; }\n  .flex-xl-grow-1 {\n    flex-grow: 1 !important; }\n  .flex-xl-shrink-0 {\n    flex-shrink: 0 !important; }\n  .flex-xl-shrink-1 {\n    flex-shrink: 1 !important; }\n  .flex-xl-wrap {\n    flex-wrap: wrap !important; }\n  .flex-xl-nowrap {\n    flex-wrap: nowrap !important; }\n  .flex-xl-wrap-reverse {\n    flex-wrap: wrap-reverse !important; }\n  .justify-content-xl-start {\n    justify-content: flex-start !important; }\n  .justify-content-xl-end {\n    justify-content: flex-end !important; }\n  .justify-content-xl-center {\n    justify-content: center !important; }\n  .justify-content-xl-between {\n    justify-content: space-between !important; }\n  .justify-content-xl-around {\n    justify-content: space-around !important; }\n  .justify-content-xl-evenly {\n    justify-content: space-evenly !important; }\n  .align-items-xl-start {\n    align-items: flex-start !important; }\n  .align-items-xl-end {\n    align-items: flex-end !important; }\n  .align-items-xl-center {\n    align-items: center !important; }\n  .align-items-xl-baseline {\n    align-items: baseline !important; }\n  .align-items-xl-stretch {\n    align-items: stretch !important; }\n  .align-content-xl-start {\n    align-content: flex-start !important; }\n  .align-content-xl-end {\n    align-content: flex-end !important; }\n  .align-content-xl-center {\n    align-content: center !important; }\n  .align-content-xl-between {\n    align-content: space-between !important; }\n  .align-content-xl-around {\n    align-content: space-around !important; }\n  .align-content-xl-stretch {\n    align-content: stretch !important; }\n  .align-self-xl-auto {\n    align-self: auto !important; }\n  .align-self-xl-start {\n    align-self: flex-start !important; }\n  .align-self-xl-end {\n    align-self: flex-end !important; }\n  .align-self-xl-center {\n    align-self: center !important; }\n  .align-self-xl-baseline {\n    align-self: baseline !important; }\n  .align-self-xl-stretch {\n    align-self: stretch !important; }\n  .order-xl-first {\n    order: -1 !important; }\n  .order-xl-0 {\n    order: 0 !important; }\n  .order-xl-1 {\n    order: 1 !important; }\n  .order-xl-2 {\n    order: 2 !important; }\n  .order-xl-3 {\n    order: 3 !important; }\n  .order-xl-4 {\n    order: 4 !important; }\n  .order-xl-5 {\n    order: 5 !important; }\n  .order-xl-last {\n    order: 6 !important; }\n  .m-xl-0 {\n    margin: 0 !important; }\n  .m-xl-1 {\n    margin: 0.25rem !important; }\n  .m-xl-2 {\n    margin: 0.5rem !important; }\n  .m-xl-3 {\n    margin: 1rem !important; }\n  .m-xl-4 {\n    margin: 1.5rem !important; }\n  .m-xl-5 {\n    margin: 3rem !important; }\n  .m-xl-auto {\n    margin: auto !important; }\n  .mx-xl-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important; }\n  .mx-xl-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important; }\n  .mx-xl-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important; }\n  .mx-xl-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important; }\n  .mx-xl-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important; }\n  .mx-xl-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important; }\n  .mx-xl-auto {\n    margin-right: auto !important;\n    margin-left: auto !important; }\n  .my-xl-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important; }\n  .my-xl-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important; }\n  .my-xl-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important; }\n  .my-xl-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important; }\n  .my-xl-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important; }\n  .my-xl-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important; }\n  .my-xl-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important; }\n  .mt-xl-0 {\n    margin-top: 0 !important; }\n  .mt-xl-1 {\n    margin-top: 0.25rem !important; }\n  .mt-xl-2 {\n    margin-top: 0.5rem !important; }\n  .mt-xl-3 {\n    margin-top: 1rem !important; }\n  .mt-xl-4 {\n    margin-top: 1.5rem !important; }\n  .mt-xl-5 {\n    margin-top: 3rem !important; }\n  .mt-xl-auto {\n    margin-top: auto !important; }\n  .me-xl-0 {\n    margin-right: 0 !important; }\n  .me-xl-1 {\n    margin-right: 0.25rem !important; }\n  .me-xl-2 {\n    margin-right: 0.5rem !important; }\n  .me-xl-3 {\n    margin-right: 1rem !important; }\n  .me-xl-4 {\n    margin-right: 1.5rem !important; }\n  .me-xl-5 {\n    margin-right: 3rem !important; }\n  .me-xl-auto {\n    margin-right: auto !important; }\n  .mb-xl-0 {\n    margin-bottom: 0 !important; }\n  .mb-xl-1 {\n    margin-bottom: 0.25rem !important; }\n  .mb-xl-2 {\n    margin-bottom: 0.5rem !important; }\n  .mb-xl-3 {\n    margin-bottom: 1rem !important; }\n  .mb-xl-4 {\n    margin-bottom: 1.5rem !important; }\n  .mb-xl-5 {\n    margin-bottom: 3rem !important; }\n  .mb-xl-auto {\n    margin-bottom: auto !important; }\n  .ms-xl-0 {\n    margin-left: 0 !important; }\n  .ms-xl-1 {\n    margin-left: 0.25rem !important; }\n  .ms-xl-2 {\n    margin-left: 0.5rem !important; }\n  .ms-xl-3 {\n    margin-left: 1rem !important; }\n  .ms-xl-4 {\n    margin-left: 1.5rem !important; }\n  .ms-xl-5 {\n    margin-left: 3rem !important; }\n  .ms-xl-auto {\n    margin-left: auto !important; }\n  .m-xl-n1 {\n    margin: -0.25rem !important; }\n  .m-xl-n2 {\n    margin: -0.5rem !important; }\n  .m-xl-n3 {\n    margin: -1rem !important; }\n  .m-xl-n4 {\n    margin: -1.5rem !important; }\n  .m-xl-n5 {\n    margin: -3rem !important; }\n  .mx-xl-n1 {\n    margin-right: -0.25rem !important;\n    margin-left: -0.25rem !important; }\n  .mx-xl-n2 {\n    margin-right: -0.5rem !important;\n    margin-left: -0.5rem !important; }\n  .mx-xl-n3 {\n    margin-right: -1rem !important;\n    margin-left: -1rem !important; }\n  .mx-xl-n4 {\n    margin-right: -1.5rem !important;\n    margin-left: -1.5rem !important; }\n  .mx-xl-n5 {\n    margin-right: -3rem !important;\n    margin-left: -3rem !important; }\n  .my-xl-n1 {\n    margin-top: -0.25rem !important;\n    margin-bottom: -0.25rem !important; }\n  .my-xl-n2 {\n    margin-top: -0.5rem !important;\n    margin-bottom: -0.5rem !important; }\n  .my-xl-n3 {\n    margin-top: -1rem !important;\n    margin-bottom: -1rem !important; }\n  .my-xl-n4 {\n    margin-top: -1.5rem !important;\n    margin-bottom: -1.5rem !important; }\n  .my-xl-n5 {\n    margin-top: -3rem !important;\n    margin-bottom: -3rem !important; }\n  .mt-xl-n1 {\n    margin-top: -0.25rem !important; }\n  .mt-xl-n2 {\n    margin-top: -0.5rem !important; }\n  .mt-xl-n3 {\n    margin-top: -1rem !important; }\n  .mt-xl-n4 {\n    margin-top: -1.5rem !important; }\n  .mt-xl-n5 {\n    margin-top: -3rem !important; }\n  .me-xl-n1 {\n    margin-right: -0.25rem !important; }\n  .me-xl-n2 {\n    margin-right: -0.5rem !important; }\n  .me-xl-n3 {\n    margin-right: -1rem !important; }\n  .me-xl-n4 {\n    margin-right: -1.5rem !important; }\n  .me-xl-n5 {\n    margin-right: -3rem !important; }\n  .mb-xl-n1 {\n    margin-bottom: -0.25rem !important; }\n  .mb-xl-n2 {\n    margin-bottom: -0.5rem !important; }\n  .mb-xl-n3 {\n    margin-bottom: -1rem !important; }\n  .mb-xl-n4 {\n    margin-bottom: -1.5rem !important; }\n  .mb-xl-n5 {\n    margin-bottom: -3rem !important; }\n  .ms-xl-n1 {\n    margin-left: -0.25rem !important; }\n  .ms-xl-n2 {\n    margin-left: -0.5rem !important; }\n  .ms-xl-n3 {\n    margin-left: -1rem !important; }\n  .ms-xl-n4 {\n    margin-left: -1.5rem !important; }\n  .ms-xl-n5 {\n    margin-left: -3rem !important; }\n  .p-xl-0 {\n    padding: 0 !important; }\n  .p-xl-1 {\n    padding: 0.25rem !important; }\n  .p-xl-2 {\n    padding: 0.5rem !important; }\n  .p-xl-3 {\n    padding: 1rem !important; }\n  .p-xl-4 {\n    padding: 1.5rem !important; }\n  .p-xl-5 {\n    padding: 3rem !important; }\n  .px-xl-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important; }\n  .px-xl-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important; }\n  .px-xl-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important; }\n  .px-xl-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important; }\n  .px-xl-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important; }\n  .px-xl-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important; }\n  .py-xl-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important; }\n  .py-xl-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important; }\n  .py-xl-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important; }\n  .py-xl-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important; }\n  .py-xl-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important; }\n  .py-xl-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important; }\n  .pt-xl-0 {\n    padding-top: 0 !important; }\n  .pt-xl-1 {\n    padding-top: 0.25rem !important; }\n  .pt-xl-2 {\n    padding-top: 0.5rem !important; }\n  .pt-xl-3 {\n    padding-top: 1rem !important; }\n  .pt-xl-4 {\n    padding-top: 1.5rem !important; }\n  .pt-xl-5 {\n    padding-top: 3rem !important; }\n  .pe-xl-0 {\n    padding-right: 0 !important; }\n  .pe-xl-1 {\n    padding-right: 0.25rem !important; }\n  .pe-xl-2 {\n    padding-right: 0.5rem !important; }\n  .pe-xl-3 {\n    padding-right: 1rem !important; }\n  .pe-xl-4 {\n    padding-right: 1.5rem !important; }\n  .pe-xl-5 {\n    padding-right: 3rem !important; }\n  .pb-xl-0 {\n    padding-bottom: 0 !important; }\n  .pb-xl-1 {\n    padding-bottom: 0.25rem !important; }\n  .pb-xl-2 {\n    padding-bottom: 0.5rem !important; }\n  .pb-xl-3 {\n    padding-bottom: 1rem !important; }\n  .pb-xl-4 {\n    padding-bottom: 1.5rem !important; }\n  .pb-xl-5 {\n    padding-bottom: 3rem !important; }\n  .ps-xl-0 {\n    padding-left: 0 !important; }\n  .ps-xl-1 {\n    padding-left: 0.25rem !important; }\n  .ps-xl-2 {\n    padding-left: 0.5rem !important; }\n  .ps-xl-3 {\n    padding-left: 1rem !important; }\n  .ps-xl-4 {\n    padding-left: 1.5rem !important; }\n  .ps-xl-5 {\n    padding-left: 3rem !important; }\n  .gap-xl-0 {\n    gap: 0 !important; }\n  .gap-xl-1 {\n    gap: 0.25rem !important; }\n  .gap-xl-2 {\n    gap: 0.5rem !important; }\n  .gap-xl-3 {\n    gap: 1rem !important; }\n  .gap-xl-4 {\n    gap: 1.5rem !important; }\n  .gap-xl-5 {\n    gap: 3rem !important; }\n  .row-gap-xl-0 {\n    row-gap: 0 !important; }\n  .row-gap-xl-1 {\n    row-gap: 0.25rem !important; }\n  .row-gap-xl-2 {\n    row-gap: 0.5rem !important; }\n  .row-gap-xl-3 {\n    row-gap: 1rem !important; }\n  .row-gap-xl-4 {\n    row-gap: 1.5rem !important; }\n  .row-gap-xl-5 {\n    row-gap: 3rem !important; }\n  .column-gap-xl-0 {\n    column-gap: 0 !important; }\n  .column-gap-xl-1 {\n    column-gap: 0.25rem !important; }\n  .column-gap-xl-2 {\n    column-gap: 0.5rem !important; }\n  .column-gap-xl-3 {\n    column-gap: 1rem !important; }\n  .column-gap-xl-4 {\n    column-gap: 1.5rem !important; }\n  .column-gap-xl-5 {\n    column-gap: 3rem !important; }\n  .text-xl-start {\n    text-align: left !important; }\n  .text-xl-end {\n    text-align: right !important; }\n  .text-xl-center {\n    text-align: center !important; } }\n\n@media (min-width: 1400px) {\n  .float-xxl-start {\n    float: left !important; }\n  .float-xxl-end {\n    float: right !important; }\n  .float-xxl-none {\n    float: none !important; }\n  .object-fit-xxl-contain {\n    object-fit: contain !important; }\n  .object-fit-xxl-cover {\n    object-fit: cover !important; }\n  .object-fit-xxl-fill {\n    object-fit: fill !important; }\n  .object-fit-xxl-scale {\n    object-fit: scale-down !important; }\n  .object-fit-xxl-none {\n    object-fit: none !important; }\n  .d-xxl-inline {\n    display: inline !important; }\n  .d-xxl-inline-block {\n    display: inline-block !important; }\n  .d-xxl-block {\n    display: block !important; }\n  .d-xxl-grid {\n    display: grid !important; }\n  .d-xxl-inline-grid {\n    display: inline-grid !important; }\n  .d-xxl-table {\n    display: table !important; }\n  .d-xxl-table-row {\n    display: table-row !important; }\n  .d-xxl-table-cell {\n    display: table-cell !important; }\n  .d-xxl-flex {\n    display: flex !important; }\n  .d-xxl-inline-flex {\n    display: inline-flex !important; }\n  .d-xxl-none {\n    display: none !important; }\n  .flex-xxl-fill {\n    flex: 1 1 auto !important; }\n  .flex-xxl-row {\n    flex-direction: row !important; }\n  .flex-xxl-column {\n    flex-direction: column !important; }\n  .flex-xxl-row-reverse {\n    flex-direction: row-reverse !important; }\n  .flex-xxl-column-reverse {\n    flex-direction: column-reverse !important; }\n  .flex-xxl-grow-0 {\n    flex-grow: 0 !important; }\n  .flex-xxl-grow-1 {\n    flex-grow: 1 !important; }\n  .flex-xxl-shrink-0 {\n    flex-shrink: 0 !important; }\n  .flex-xxl-shrink-1 {\n    flex-shrink: 1 !important; }\n  .flex-xxl-wrap {\n    flex-wrap: wrap !important; }\n  .flex-xxl-nowrap {\n    flex-wrap: nowrap !important; }\n  .flex-xxl-wrap-reverse {\n    flex-wrap: wrap-reverse !important; }\n  .justify-content-xxl-start {\n    justify-content: flex-start !important; }\n  .justify-content-xxl-end {\n    justify-content: flex-end !important; }\n  .justify-content-xxl-center {\n    justify-content: center !important; }\n  .justify-content-xxl-between {\n    justify-content: space-between !important; }\n  .justify-content-xxl-around {\n    justify-content: space-around !important; }\n  .justify-content-xxl-evenly {\n    justify-content: space-evenly !important; }\n  .align-items-xxl-start {\n    align-items: flex-start !important; }\n  .align-items-xxl-end {\n    align-items: flex-end !important; }\n  .align-items-xxl-center {\n    align-items: center !important; }\n  .align-items-xxl-baseline {\n    align-items: baseline !important; }\n  .align-items-xxl-stretch {\n    align-items: stretch !important; }\n  .align-content-xxl-start {\n    align-content: flex-start !important; }\n  .align-content-xxl-end {\n    align-content: flex-end !important; }\n  .align-content-xxl-center {\n    align-content: center !important; }\n  .align-content-xxl-between {\n    align-content: space-between !important; }\n  .align-content-xxl-around {\n    align-content: space-around !important; }\n  .align-content-xxl-stretch {\n    align-content: stretch !important; }\n  .align-self-xxl-auto {\n    align-self: auto !important; }\n  .align-self-xxl-start {\n    align-self: flex-start !important; }\n  .align-self-xxl-end {\n    align-self: flex-end !important; }\n  .align-self-xxl-center {\n    align-self: center !important; }\n  .align-self-xxl-baseline {\n    align-self: baseline !important; }\n  .align-self-xxl-stretch {\n    align-self: stretch !important; }\n  .order-xxl-first {\n    order: -1 !important; }\n  .order-xxl-0 {\n    order: 0 !important; }\n  .order-xxl-1 {\n    order: 1 !important; }\n  .order-xxl-2 {\n    order: 2 !important; }\n  .order-xxl-3 {\n    order: 3 !important; }\n  .order-xxl-4 {\n    order: 4 !important; }\n  .order-xxl-5 {\n    order: 5 !important; }\n  .order-xxl-last {\n    order: 6 !important; }\n  .m-xxl-0 {\n    margin: 0 !important; }\n  .m-xxl-1 {\n    margin: 0.25rem !important; }\n  .m-xxl-2 {\n    margin: 0.5rem !important; }\n  .m-xxl-3 {\n    margin: 1rem !important; }\n  .m-xxl-4 {\n    margin: 1.5rem !important; }\n  .m-xxl-5 {\n    margin: 3rem !important; }\n  .m-xxl-auto {\n    margin: auto !important; }\n  .mx-xxl-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important; }\n  .mx-xxl-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important; }\n  .mx-xxl-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important; }\n  .mx-xxl-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important; }\n  .mx-xxl-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important; }\n  .mx-xxl-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important; }\n  .mx-xxl-auto {\n    margin-right: auto !important;\n    margin-left: auto !important; }\n  .my-xxl-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important; }\n  .my-xxl-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important; }\n  .my-xxl-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important; }\n  .my-xxl-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important; }\n  .my-xxl-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important; }\n  .my-xxl-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important; }\n  .my-xxl-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important; }\n  .mt-xxl-0 {\n    margin-top: 0 !important; }\n  .mt-xxl-1 {\n    margin-top: 0.25rem !important; }\n  .mt-xxl-2 {\n    margin-top: 0.5rem !important; }\n  .mt-xxl-3 {\n    margin-top: 1rem !important; }\n  .mt-xxl-4 {\n    margin-top: 1.5rem !important; }\n  .mt-xxl-5 {\n    margin-top: 3rem !important; }\n  .mt-xxl-auto {\n    margin-top: auto !important; }\n  .me-xxl-0 {\n    margin-right: 0 !important; }\n  .me-xxl-1 {\n    margin-right: 0.25rem !important; }\n  .me-xxl-2 {\n    margin-right: 0.5rem !important; }\n  .me-xxl-3 {\n    margin-right: 1rem !important; }\n  .me-xxl-4 {\n    margin-right: 1.5rem !important; }\n  .me-xxl-5 {\n    margin-right: 3rem !important; }\n  .me-xxl-auto {\n    margin-right: auto !important; }\n  .mb-xxl-0 {\n    margin-bottom: 0 !important; }\n  .mb-xxl-1 {\n    margin-bottom: 0.25rem !important; }\n  .mb-xxl-2 {\n    margin-bottom: 0.5rem !important; }\n  .mb-xxl-3 {\n    margin-bottom: 1rem !important; }\n  .mb-xxl-4 {\n    margin-bottom: 1.5rem !important; }\n  .mb-xxl-5 {\n    margin-bottom: 3rem !important; }\n  .mb-xxl-auto {\n    margin-bottom: auto !important; }\n  .ms-xxl-0 {\n    margin-left: 0 !important; }\n  .ms-xxl-1 {\n    margin-left: 0.25rem !important; }\n  .ms-xxl-2 {\n    margin-left: 0.5rem !important; }\n  .ms-xxl-3 {\n    margin-left: 1rem !important; }\n  .ms-xxl-4 {\n    margin-left: 1.5rem !important; }\n  .ms-xxl-5 {\n    margin-left: 3rem !important; }\n  .ms-xxl-auto {\n    margin-left: auto !important; }\n  .m-xxl-n1 {\n    margin: -0.25rem !important; }\n  .m-xxl-n2 {\n    margin: -0.5rem !important; }\n  .m-xxl-n3 {\n    margin: -1rem !important; }\n  .m-xxl-n4 {\n    margin: -1.5rem !important; }\n  .m-xxl-n5 {\n    margin: -3rem !important; }\n  .mx-xxl-n1 {\n    margin-right: -0.25rem !important;\n    margin-left: -0.25rem !important; }\n  .mx-xxl-n2 {\n    margin-right: -0.5rem !important;\n    margin-left: -0.5rem !important; }\n  .mx-xxl-n3 {\n    margin-right: -1rem !important;\n    margin-left: -1rem !important; }\n  .mx-xxl-n4 {\n    margin-right: -1.5rem !important;\n    margin-left: -1.5rem !important; }\n  .mx-xxl-n5 {\n    margin-right: -3rem !important;\n    margin-left: -3rem !important; }\n  .my-xxl-n1 {\n    margin-top: -0.25rem !important;\n    margin-bottom: -0.25rem !important; }\n  .my-xxl-n2 {\n    margin-top: -0.5rem !important;\n    margin-bottom: -0.5rem !important; }\n  .my-xxl-n3 {\n    margin-top: -1rem !important;\n    margin-bottom: -1rem !important; }\n  .my-xxl-n4 {\n    margin-top: -1.5rem !important;\n    margin-bottom: -1.5rem !important; }\n  .my-xxl-n5 {\n    margin-top: -3rem !important;\n    margin-bottom: -3rem !important; }\n  .mt-xxl-n1 {\n    margin-top: -0.25rem !important; }\n  .mt-xxl-n2 {\n    margin-top: -0.5rem !important; }\n  .mt-xxl-n3 {\n    margin-top: -1rem !important; }\n  .mt-xxl-n4 {\n    margin-top: -1.5rem !important; }\n  .mt-xxl-n5 {\n    margin-top: -3rem !important; }\n  .me-xxl-n1 {\n    margin-right: -0.25rem !important; }\n  .me-xxl-n2 {\n    margin-right: -0.5rem !important; }\n  .me-xxl-n3 {\n    margin-right: -1rem !important; }\n  .me-xxl-n4 {\n    margin-right: -1.5rem !important; }\n  .me-xxl-n5 {\n    margin-right: -3rem !important; }\n  .mb-xxl-n1 {\n    margin-bottom: -0.25rem !important; }\n  .mb-xxl-n2 {\n    margin-bottom: -0.5rem !important; }\n  .mb-xxl-n3 {\n    margin-bottom: -1rem !important; }\n  .mb-xxl-n4 {\n    margin-bottom: -1.5rem !important; }\n  .mb-xxl-n5 {\n    margin-bottom: -3rem !important; }\n  .ms-xxl-n1 {\n    margin-left: -0.25rem !important; }\n  .ms-xxl-n2 {\n    margin-left: -0.5rem !important; }\n  .ms-xxl-n3 {\n    margin-left: -1rem !important; }\n  .ms-xxl-n4 {\n    margin-left: -1.5rem !important; }\n  .ms-xxl-n5 {\n    margin-left: -3rem !important; }\n  .p-xxl-0 {\n    padding: 0 !important; }\n  .p-xxl-1 {\n    padding: 0.25rem !important; }\n  .p-xxl-2 {\n    padding: 0.5rem !important; }\n  .p-xxl-3 {\n    padding: 1rem !important; }\n  .p-xxl-4 {\n    padding: 1.5rem !important; }\n  .p-xxl-5 {\n    padding: 3rem !important; }\n  .px-xxl-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important; }\n  .px-xxl-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important; }\n  .px-xxl-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important; }\n  .px-xxl-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important; }\n  .px-xxl-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important; }\n  .px-xxl-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important; }\n  .py-xxl-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important; }\n  .py-xxl-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important; }\n  .py-xxl-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important; }\n  .py-xxl-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important; }\n  .py-xxl-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important; }\n  .py-xxl-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important; }\n  .pt-xxl-0 {\n    padding-top: 0 !important; }\n  .pt-xxl-1 {\n    padding-top: 0.25rem !important; }\n  .pt-xxl-2 {\n    padding-top: 0.5rem !important; }\n  .pt-xxl-3 {\n    padding-top: 1rem !important; }\n  .pt-xxl-4 {\n    padding-top: 1.5rem !important; }\n  .pt-xxl-5 {\n    padding-top: 3rem !important; }\n  .pe-xxl-0 {\n    padding-right: 0 !important; }\n  .pe-xxl-1 {\n    padding-right: 0.25rem !important; }\n  .pe-xxl-2 {\n    padding-right: 0.5rem !important; }\n  .pe-xxl-3 {\n    padding-right: 1rem !important; }\n  .pe-xxl-4 {\n    padding-right: 1.5rem !important; }\n  .pe-xxl-5 {\n    padding-right: 3rem !important; }\n  .pb-xxl-0 {\n    padding-bottom: 0 !important; }\n  .pb-xxl-1 {\n    padding-bottom: 0.25rem !important; }\n  .pb-xxl-2 {\n    padding-bottom: 0.5rem !important; }\n  .pb-xxl-3 {\n    padding-bottom: 1rem !important; }\n  .pb-xxl-4 {\n    padding-bottom: 1.5rem !important; }\n  .pb-xxl-5 {\n    padding-bottom: 3rem !important; }\n  .ps-xxl-0 {\n    padding-left: 0 !important; }\n  .ps-xxl-1 {\n    padding-left: 0.25rem !important; }\n  .ps-xxl-2 {\n    padding-left: 0.5rem !important; }\n  .ps-xxl-3 {\n    padding-left: 1rem !important; }\n  .ps-xxl-4 {\n    padding-left: 1.5rem !important; }\n  .ps-xxl-5 {\n    padding-left: 3rem !important; }\n  .gap-xxl-0 {\n    gap: 0 !important; }\n  .gap-xxl-1 {\n    gap: 0.25rem !important; }\n  .gap-xxl-2 {\n    gap: 0.5rem !important; }\n  .gap-xxl-3 {\n    gap: 1rem !important; }\n  .gap-xxl-4 {\n    gap: 1.5rem !important; }\n  .gap-xxl-5 {\n    gap: 3rem !important; }\n  .row-gap-xxl-0 {\n    row-gap: 0 !important; }\n  .row-gap-xxl-1 {\n    row-gap: 0.25rem !important; }\n  .row-gap-xxl-2 {\n    row-gap: 0.5rem !important; }\n  .row-gap-xxl-3 {\n    row-gap: 1rem !important; }\n  .row-gap-xxl-4 {\n    row-gap: 1.5rem !important; }\n  .row-gap-xxl-5 {\n    row-gap: 3rem !important; }\n  .column-gap-xxl-0 {\n    column-gap: 0 !important; }\n  .column-gap-xxl-1 {\n    column-gap: 0.25rem !important; }\n  .column-gap-xxl-2 {\n    column-gap: 0.5rem !important; }\n  .column-gap-xxl-3 {\n    column-gap: 1rem !important; }\n  .column-gap-xxl-4 {\n    column-gap: 1.5rem !important; }\n  .column-gap-xxl-5 {\n    column-gap: 3rem !important; }\n  .text-xxl-start {\n    text-align: left !important; }\n  .text-xxl-end {\n    text-align: right !important; }\n  .text-xxl-center {\n    text-align: center !important; } }\n\n@media (min-width: 1200px) {\n  .fs-1 {\n    font-size: 2.5rem !important; }\n  .fs-2 {\n    font-size: 2rem !important; }\n  .fs-3 {\n    font-size: 1.75rem !important; }\n  .fs-4 {\n    font-size: 1.5rem !important; } }\n\n@media print {\n  .d-print-inline {\n    display: inline !important; }\n  .d-print-inline-block {\n    display: inline-block !important; }\n  .d-print-block {\n    display: block !important; }\n  .d-print-grid {\n    display: grid !important; }\n  .d-print-inline-grid {\n    display: inline-grid !important; }\n  .d-print-table {\n    display: table !important; }\n  .d-print-table-row {\n    display: table-row !important; }\n  .d-print-table-cell {\n    display: table-cell !important; }\n  .d-print-flex {\n    display: flex !important; }\n  .d-print-inline-flex {\n    display: inline-flex !important; }\n  .d-print-none {\n    display: none !important; } }\n\n/* jost-regular - latin */\n@font-face {\n  font-family: Jost;\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  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\");\n  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ }\n\n/* jost-500 - latin */\n@font-face {\n  font-family: Jost;\n  font-style: normal;\n  font-weight: 500;\n  font-display: swap;\n  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\");\n  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ }\n\n/* jost-700 - latin */\n@font-face {\n  font-family: Jost;\n  font-style: normal;\n  font-weight: 700;\n  font-display: swap;\n  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\");\n  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ }\n\n/* jost-italic - latin */\n@font-face {\n  font-family: Jost;\n  font-style: italic;\n  font-weight: 400;\n  font-display: swap;\n  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\");\n  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ }\n\n/* jost-500italic - latin */\n@font-face {\n  font-family: Jost;\n  font-style: italic;\n  font-weight: 500;\n  font-display: swap;\n  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\");\n  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ }\n\n/* jost-700italic - latin */\n@font-face {\n  font-family: Jost;\n  font-style: italic;\n  font-weight: 700;\n  font-display: swap;\n  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\");\n  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ }\n\n/* Show the sun icon if the bs theme is dark */\nhtml[data-bs-theme=\"dark\"] .icon-tabler-sun {\n  display: block; }\n\nhtml[data-bs-theme=\"dark\"] .icon-tabler-moon {\n  display: none; }\n\n/* Show the moon icon if the bs theme is light */\nhtml[data-bs-theme=\"light\"] .icon-tabler-sun {\n  display: none; }\n\nhtml[data-bs-theme=\"light\"] .icon-tabler-moon {\n  display: block; }\n\n/*\n.section:not(body.section) {\n  padding-top: 5rem;\n  padding-bottom: 5rem;\n}\n\n.section-lg {\n  padding-top: 7rem;\n  padding-bottom: 7rem;\n}\n*/\n/*\n.highlight .chroma {\n  padding: 1rem;\n  border-radius: var(--bs-border-radius);\n}\n*/\n.privacy .content,\n.terms .content,\n.about .content,\n.contributors .content,\n.blog .content,\n.page .content,\n.error404 .content,\n.docs.list .content,\n.tutorial.list .content,\n.showcase.list .content,\n.categories.list .content,\n.tags.list .content,\n.list.section .content {\n  padding-top: 1rem;\n  padding-bottom: 3rem; }\n\n.content img {\n  max-width: 100%; }\n\nh6,\n.h6,\nh5,\n.h5,\nh4,\n.h4,\nh3,\n.h3,\nh2,\n.h2,\nh1,\n.h1 {\n  margin-top: 2rem;\n  margin-bottom: 1rem; }\n\n/*\nbody.docs,\nbody.blog {\n  padding-top: 0;\n  padding-bottom: 0;\n}\n*/\n@media (min-width: 768px) {\n  body {\n    font-size: 1.125rem;\n    /*\n    padding-top: 4rem !important;\n    */ }\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6,\n  .h1,\n  .h2,\n  .h3,\n  .h4,\n  .h5,\n  .h6 {\n    margin-bottom: 1.125rem; } }\n\n.home h1, .home .h1 {\n  /* font-size: calc(1.375rem + 1.5vw); */\n  font-size: calc(1.875rem + 1.5vw);\n  margin-top: -1rem; }\n\na:hover,\na:focus {\n  text-decoration: underline; }\n\n.docs-navigation .card {\n  transition: transform 0.3s; }\n\n.docs-navigation .card:hover {\n  transform: scale(1.025); }\n\na.btn:hover, .search-form a.search-submit:hover,\na.btn:focus,\n.search-form a.search-submit:focus {\n  text-decoration: none; }\n\n.section {\n  padding-top: 5rem;\n  padding-bottom: 5rem; }\n\nbody.section {\n  padding-top: 0;\n  padding-bottom: 0; }\n\n.section-md {\n  padding-top: 3rem;\n  padding-bottom: 3rem; }\n\n.section-sm {\n  padding-top: 1rem;\n  padding-bottom: 1rem; }\n\n/*\n.section svg {\n  display: inline-block;\n  width: 2rem;\n  height: 2rem;\n  vertical-align: text-top;\n}\n*/\n/*\nbody {\n  padding-top: 3.5625rem;\n}\n*/\n.docs-sidebar {\n  order: 2; }\n\n@media (min-width: 992px) {\n  .docs-sidebar {\n    order: 0;\n    border-right: 1px solid #e9ecef; }\n  @supports (position: -webkit-sticky) or (position: sticky) {\n    .docs-sidebar {\n      position: -webkit-sticky;\n      position: sticky;\n      top: 4.25rem;\n      z-index: 1000;\n      height: calc(100vh - 4.25rem); }\n    .docs-sidebar-offset {\n      top: 4.5rem;\n      height: calc(100vh - 4.5rem); }\n    .docs-sidebar-top {\n      position: static; } } }\n\n@media (min-width: 1200px) {\n  .docs-sidebar {\n    flex: 0 1 320px; } }\n\n.docs-links {\n  padding-bottom: 5rem; }\n\n@media (min-width: 992px) {\n  @supports (position: -webkit-sticky) or (position: sticky) {\n    .docs-links {\n      max-height: calc(100vh - 4rem);\n      overflow-y: scroll; } } }\n\n@media (min-width: 992px) {\n  .docs-links {\n    display: block;\n    width: auto;\n    margin-right: -1.5rem;\n    padding-bottom: 4rem; } }\n\n.docs-toc {\n  order: 2; }\n\n@supports (position: -webkit-sticky) or (position: sticky) {\n  .docs-toc {\n    position: -webkit-sticky;\n    position: sticky;\n    top: 4.25rem;\n    height: calc(100vh - 4.25rem);\n    overflow-y: auto; }\n  .docs-toc-offset {\n    top: 4.5rem;\n    height: calc(100vh - 4.5rem); }\n  .docs-toc-top {\n    position: static; } }\n\n.docs-content {\n  padding-bottom: 3rem;\n  order: 1; }\n\n.docs-navigation {\n  border-top: 1px solid #e9ecef;\n  margin-top: 2rem;\n  margin-bottom: 0;\n  padding-top: 2rem; }\n\n.docs-navigation a {\n  font-size: 0.9rem; }\n\n@media (min-width: 992px) {\n  .docs-navigation {\n    margin-bottom: -1rem; }\n  .docs-navigation a {\n    font-size: 1rem; } }\n\n.docs-navigation a:hover,\n.docs-navigation a:focus {\n  text-decoration: none; }\n\n.navbar a:hover,\n.navbar a:focus {\n  text-decoration: none; }\n\n#TableOfContents ul,\n#toc ul {\n  padding-left: 0;\n  list-style: none; }\n\n#toc a.active {\n  color: #4f46e5;\n  font-weight: 500; }\n\n.section-features {\n  padding-top: 2rem; }\n\n.bg-dots {\n  background-image: radial-gradient(#dee2e6 15%, transparent 15%);\n  background-position: 0 0;\n  background-size: 1rem 1rem;\n  -webkit-mask: linear-gradient(to top, #fff, transparent);\n  mask: linear-gradient(to top, #fff, transparent);\n  width: 100%;\n  height: 11rem;\n  margin-top: -10rem;\n  z-index: -1; }\n\n.bg-dots-md {\n  margin-top: -11rem; }\n\n.bg-dots-lg {\n  margin-top: -12rem; }\n\n.gradient-text {\n  background-color: #4f46e5;\n  background-image: linear-gradient(90deg, #4f46e5, #b3c7ff 50%, var(--sl-color-blue));\n  background-size: 100%;\n  background-repeat: repeat;\n  -webkit-background-clip: text;\n  -moz-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  -moz-text-fill-color: transparent; }\n\n.katex {\n  font-size: 1.125rem; }\n\n.card-bar {\n  border-top: 4px solid;\n  border-image-source: linear-gradient(90deg, #4f46e5, #b3c7ff 50%, var(--sl-color-blue));\n  border-image-slice: 1; }\n\n.modal-backdrop {\n  background-color: #fff; }\n\n.modal-backdrop.show {\n  opacity: 0.7; }\n\n@media (min-width: 768px) {\n  .modal-backdrop.show {\n    opacity: 0; } }\n\nsup[id] {\n  scroll-margin-top: 4.5rem; }\n\ndiv.footnotes {\n  font-size: 0.875rem; }\n\na.footnote-backref {\n  text-decoration: none; }\n\nli input[type=\"checkbox\"] {\n  margin: 0.25rem;\n  border: 1px solid #ced4da; }\n\nli input[type=\"checkbox\"]:disabled {\n  pointer-events: none;\n  filter: none;\n  opacity: 1; }\n\nli input[type=\"checkbox\"]:checked {\n  background-color: #5d2f86;\n  border-color: #5d2f86; }\n\n[data-bs-theme=\"dark\"] li input[type=\"checkbox\"] {\n  border: 1px solid #6c757d; }\n\n[data-bs-theme=\"dark\"] li input[type=\"checkbox\"]:checked {\n  background-color: #b3c7ff;\n  border-color: #b3c7ff;\n  --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\"); }\n\n.content .svg-inline {\n  margin-bottom: 1.5rem; }\n\n.content .svg-inline:not(.svg-inline-custom) {\n  height: 1.875rem;\n  width: 1.875rem;\n  stroke-width: 1.5; }\n\n/*\n.content .alert .icon {\n  stroke-width: 1;\n  margin-bottom: 0;\n  margin-right: 0.5rem;\n}\n*/\n.logo-netlify-large-fullcolor-darkmode {\n  display: none; }\n\n[data-bs-theme=\"dark\"] .logo-netlify-large-fullcolor-lightmode {\n  display: none; }\n\n[data-bs-theme=\"dark\"] .logo-netlify-large-fullcolor-darkmode {\n  display: block; }\n\n.svg-lightmode {\n  display: block; }\n\n.svg-darkmode {\n  display: none; }\n\n.svg-monochrome path {\n  fill: #1d2d35; }\n\n[data-bs-theme=\"dark\"] .svg-lightmode {\n  display: none; }\n\n[data-bs-theme=\"dark\"] .svg-darkmode {\n  display: block; }\n\n[data-bs-theme=\"dark\"] .netlify-logo path,\n[data-bs-theme=\"dark\"] .netlify-monogram path {\n  fill: #fff; }\n\n[data-bs-theme=\"dark\"] .svg-monochrome path {\n  fill: var(--sl-color-gray-1); }\n\nhr {\n  border-color: #808080; }\n\n[data-bs-theme=\"dark\"] hr {\n  border-color: var(--sl-color-gray-3); }\n\n.container-fw {\n  min-width: 0; }\n\n.card-nav {\n  column-gap: 1rem; }\n\n.card-nav .card {\n  margin: 0.5rem 0; }\n\n.card-nav .card:hover {\n  border: 1px solid #d9d9d9;\n  background-color: var(--sl-color-gray-7); }\n\n[data-bs-theme=\"dark\"] .card-nav .card {\n  border: 1px solid #353841; }\n\n[data-bs-theme=\"dark\"] .card-nav .card:hover {\n  border: 1px solid #888c96;\n  background-color: var(--sl-color-gray-6); }\n\n.highlight > .chroma {\n  border: 1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%); }\n\n/* Background */\n.bg {\n  background-color: var(--sl-color-gray-7); }\n\n/* PreWrapper */\n.chroma {\n  background-color: var(--sl-color-gray-7); }\n\n/* Other */\n/* Error */\n.chroma .err {\n  color: inherit; }\n\n/* CodeLine */\n/* LineLink */\n.chroma .lnlinks {\n  outline: none;\n  text-decoration: none;\n  color: inherit; }\n\n/* LineTableTD */\n.chroma .lntd {\n  vertical-align: top;\n  padding: 0;\n  margin: 0;\n  border: 0; }\n\n/* LineTable */\n.chroma .lntable {\n  border-spacing: 0;\n  padding: 0;\n  margin: 0;\n  border: 0; }\n\n/* LineHighlight */\n.chroma .hl {\n  background-color: #0000001a; }\n\n.chroma .hl {\n  border-inline-start: 0.15rem solid #00000055;\n  margin-left: -1rem;\n  margin-right: -1rem;\n  padding-left: 1rem;\n  padding-right: 1rem; }\n  .chroma .hl .ln {\n    margin-left: -0.15rem; }\n\n/* LineNumbersTable */\n.chroma .lnt {\n  white-space: pre;\n  -webkit-user-select: none;\n  user-select: none;\n  margin-right: 0.4em;\n  padding: 0 0.4em 0 0.4em;\n  color: #7f7f7f; }\n\n/* LineNumbers */\n.chroma .ln {\n  white-space: pre;\n  -webkit-user-select: none;\n  user-select: none;\n  margin-right: 0.4em;\n  padding: 0 0.4em 0 0.4em;\n  color: #7f7f7f; }\n\n/* Line */\n.chroma .line {\n  display: flex; }\n\n/* Keyword */\n.chroma .k {\n  color: #000000;\n  font-weight: bold; }\n\n/* KeywordConstant */\n.chroma .kc {\n  color: #000000;\n  font-weight: bold; }\n\n/* KeywordDeclaration */\n.chroma .kd {\n  color: #000000;\n  font-weight: bold; }\n\n/* KeywordNamespace */\n.chroma .kn {\n  color: #000000;\n  font-weight: bold; }\n\n/* KeywordPseudo */\n.chroma .kp {\n  color: #000000;\n  font-weight: bold; }\n\n/* KeywordReserved */\n.chroma .kr {\n  color: #000000;\n  font-weight: bold; }\n\n/* KeywordType */\n.chroma .kt {\n  color: #445588;\n  font-weight: bold; }\n\n/* Name */\n/* NameAttribute */\n.chroma .na {\n  color: #008080; }\n\n/* NameBuiltin */\n.chroma .nb {\n  color: #0086b3; }\n\n/* NameBuiltinPseudo */\n.chroma .bp {\n  color: #999999; }\n\n/* NameClass */\n.chroma .nc {\n  color: #445588;\n  font-weight: bold; }\n\n/* NameConstant */\n.chroma .no {\n  color: #008080; }\n\n/* NameDecorator */\n.chroma .nd {\n  color: #3c5d5d;\n  font-weight: bold; }\n\n/* NameEntity */\n.chroma .ni {\n  color: #800080; }\n\n/* NameException */\n.chroma .ne {\n  color: #990000;\n  font-weight: bold; }\n\n/* NameFunction */\n.chroma .nf {\n  color: #990000;\n  font-weight: bold; }\n\n/* NameFunctionMagic */\n/* NameLabel */\n.chroma .nl {\n  color: #990000;\n  font-weight: bold; }\n\n/* NameNamespace */\n.chroma .nn {\n  color: #555555; }\n\n/* NameOther */\n/* NameProperty */\n/* NameTag */\n.chroma .nt {\n  color: #000080; }\n\n/* NameVariable */\n.chroma .nv {\n  color: #008080; }\n\n/* NameVariableClass */\n.chroma .vc {\n  color: #008080; }\n\n/* NameVariableGlobal */\n.chroma .vg {\n  color: #008080; }\n\n/* NameVariableInstance */\n.chroma .vi {\n  color: #008080; }\n\n/* NameVariableMagic */\n/* Literal */\n/* LiteralDate */\n/* LiteralString */\n.chroma .s {\n  color: #dd1144; }\n\n/* LiteralStringAffix */\n.chroma .sa {\n  color: #dd1144; }\n\n/* LiteralStringBacktick */\n.chroma .sb {\n  color: #dd1144; }\n\n/* LiteralStringChar */\n.chroma .sc {\n  color: #dd1144; }\n\n/* LiteralStringDelimiter */\n.chroma .dl {\n  color: #dd1144; }\n\n/* LiteralStringDoc */\n.chroma .sd {\n  color: #dd1144; }\n\n/* LiteralStringDouble */\n.chroma .s2 {\n  color: #dd1144; }\n\n/* LiteralStringEscape */\n.chroma .se {\n  color: #dd1144; }\n\n/* LiteralStringHeredoc */\n.chroma .sh {\n  color: #dd1144; }\n\n/* LiteralStringInterpol */\n.chroma .si {\n  color: #dd1144; }\n\n/* LiteralStringOther */\n.chroma .sx {\n  color: #dd1144; }\n\n/* LiteralStringRegex */\n.chroma .sr {\n  color: #009926; }\n\n/* LiteralStringSingle */\n.chroma .s1 {\n  color: #dd1144; }\n\n/* LiteralStringSymbol */\n.chroma .ss {\n  color: #990073; }\n\n/* LiteralNumber */\n.chroma .m {\n  color: #009999; }\n\n/* LiteralNumberBin */\n.chroma .mb {\n  color: #009999; }\n\n/* LiteralNumberFloat */\n.chroma .mf {\n  color: #009999; }\n\n/* LiteralNumberHex */\n.chroma .mh {\n  color: #009999; }\n\n/* LiteralNumberInteger */\n.chroma .mi {\n  color: #009999; }\n\n/* LiteralNumberIntegerLong */\n.chroma .il {\n  color: #009999; }\n\n/* LiteralNumberOct */\n.chroma .mo {\n  color: #009999; }\n\n/* Operator */\n.chroma .o {\n  color: #000000;\n  font-weight: bold; }\n\n/* OperatorWord */\n.chroma .ow {\n  color: #000000;\n  font-weight: bold; }\n\n/* Punctuation */\n/* Comment */\n.chroma .c {\n  color: #999988;\n  font-style: italic; }\n\n/* CommentHashbang */\n.chroma .ch {\n  color: #999988;\n  font-style: italic; }\n\n/* CommentMultiline */\n.chroma .cm {\n  color: #999988;\n  font-style: italic; }\n\n/* CommentSingle */\n.chroma .c1 {\n  color: #999988;\n  font-style: italic; }\n\n/* CommentSpecial */\n.chroma .cs {\n  color: #999999;\n  font-weight: bold;\n  font-style: italic; }\n\n/* CommentPreproc */\n.chroma .cp {\n  color: #999999;\n  font-weight: bold;\n  font-style: italic; }\n\n/* CommentPreprocFile */\n.chroma .cpf {\n  color: #999999;\n  font-weight: bold;\n  font-style: italic; }\n\n/* Generic */\n/* GenericDeleted */\n.chroma .gd {\n  color: #000000;\n  background-color: #ffdddd; }\n\n/* GenericEmph */\n.chroma .ge {\n  color: inherit;\n  font-style: italic; }\n\n/* GenericError */\n.chroma .gr {\n  color: #aa0000; }\n\n/* GenericHeading */\n.chroma .gh {\n  color: #999999; }\n\n/* GenericInserted */\n.chroma .gi {\n  color: #000000;\n  background-color: #ddffdd; }\n\n/* GenericOutput */\n.chroma .go {\n  color: #888888; }\n\n/* GenericPrompt */\n.chroma .gp {\n  color: #555555; }\n\n/* GenericStrong */\n.chroma .gs {\n  font-weight: bold; }\n\n/* GenericSubheading */\n.chroma .gu {\n  color: #aaaaaa; }\n\n/* GenericTraceback */\n.chroma .gt {\n  color: #aa0000; }\n\n/* GenericUnderline */\n.chroma .gl {\n  text-decoration: underline; }\n\n/* TextWhitespace */\n.chroma .w {\n  color: #bbbbbb; }\n\n[data-bs-theme=\"dark\"] {\n  /* Background */\n  /* PreWrapper */\n  /* Other */\n  /* Error */\n  /* CodeLine */\n  /* LineLink */\n  /* LineTableTD */\n  /* LineTable */\n  /* LineHighlight */\n  /* LineNumbersTable */\n  /* LineNumbers */\n  /* Line */\n  /* Keyword */\n  /* KeywordConstant */\n  /* KeywordDeclaration */\n  /* KeywordNamespace */\n  /* KeywordPseudo */\n  /* KeywordReserved */\n  /* KeywordType */\n  /* Name */\n  /* NameAttribute */\n  /* NameBuiltin */\n  /* NameBuiltinPseudo */\n  /* NameClass */\n  /* NameConstant */\n  /* NameDecorator */\n  /* NameEntity */\n  /* NameException */\n  /* NameFunction */\n  /* NameFunctionMagic */\n  /* NameLabel */\n  /* NameNamespace */\n  /* NameOther */\n  /* NameProperty */\n  /* NameTag */\n  /* NameVariable */\n  /* NameVariableClass */\n  /* NameVariableGlobal */\n  /* NameVariableInstance */\n  /* NameVariableMagic */\n  /* Literal */\n  /* LiteralDate */\n  /* LiteralString */\n  /* LiteralStringAffix */\n  /* LiteralStringBacktick */\n  /* LiteralStringChar */\n  /* LiteralStringDelimiter */\n  /* LiteralStringDoc */\n  /* LiteralStringDouble */\n  /* LiteralStringEscape */\n  /* LiteralStringHeredoc */\n  /* LiteralStringInterpol */\n  /* LiteralStringOther */\n  /* LiteralStringRegex */\n  /* LiteralStringSingle */\n  /* LiteralStringSymbol */\n  /* LiteralNumber */\n  /* LiteralNumberBin */\n  /* LiteralNumberFloat */\n  /* LiteralNumberHex */\n  /* LiteralNumberInteger */\n  /* LiteralNumberIntegerLong */\n  /* LiteralNumberOct */\n  /* Operator */\n  /* OperatorWord */\n  /* Punctuation */\n  /* Comment */\n  /* CommentHashbang */\n  /* CommentMultiline */\n  /* CommentSingle */\n  /* CommentSpecial */\n  /* CommentPreproc */\n  /* CommentPreprocFile */\n  /* Generic */\n  /* GenericDeleted */\n  /* GenericEmph */\n  /* GenericError */\n  /* GenericHeading */\n  /* GenericInserted */\n  /* GenericOutput */\n  /* GenericPrompt */\n  /* GenericStrong */\n  /* GenericSubheading */\n  /* GenericTraceback */\n  /* GenericUnderline */\n  /* TextWhitespace */ }\n  [data-bs-theme=\"dark\"] .highlight > .chroma {\n    border: 1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%); }\n  [data-bs-theme=\"dark\"] .bg {\n    color: #c9d1d9;\n    background-color: var(--sl-color-gray-6); }\n  [data-bs-theme=\"dark\"] .chroma {\n    color: #c9d1d9;\n    background-color: var(--sl-color-gray-6); }\n  [data-bs-theme=\"dark\"] .chroma .err {\n    color: inherit; }\n  [data-bs-theme=\"dark\"] .chroma .lnlinks {\n    outline: none;\n    text-decoration: none;\n    color: inherit; }\n  [data-bs-theme=\"dark\"] .chroma .lntd {\n    vertical-align: top;\n    padding: 0;\n    margin: 0;\n    border: 0; }\n  [data-bs-theme=\"dark\"] .chroma .lntable {\n    border-spacing: 0;\n    padding: 0;\n    margin: 0;\n    border: 0; }\n  [data-bs-theme=\"dark\"] .chroma .hl {\n    background-color: #ffffff17; }\n  [data-bs-theme=\"dark\"] .chroma .hl {\n    border-inline-start: 0.15rem solid #ffffff40;\n    margin-left: -1rem;\n    margin-right: -1rem;\n    padding-left: 1rem;\n    padding-right: 1rem; }\n    [data-bs-theme=\"dark\"] .chroma .hl .ln {\n      margin-left: -0.15rem; }\n  [data-bs-theme=\"dark\"] .chroma .lnt {\n    white-space: pre;\n    -webkit-user-select: none;\n    user-select: none;\n    margin-right: 0.4em;\n    padding: 0 0.4em 0 0.4em;\n    color: #64686c; }\n  [data-bs-theme=\"dark\"] .chroma .ln {\n    white-space: pre;\n    -webkit-user-select: none;\n    user-select: none;\n    margin-right: 0.4em;\n    padding: 0 0.4em 0 0.4em;\n    color: #6e7681; }\n  [data-bs-theme=\"dark\"] .chroma .line {\n    display: flex; }\n  [data-bs-theme=\"dark\"] .chroma .k {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .kc {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .kd {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .kn {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .kp {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .kr {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .kt {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .na {\n    color: #d2a8ff; }\n  [data-bs-theme=\"dark\"] .chroma .nc {\n    color: #f0883e;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .no {\n    color: #79c0ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .nd {\n    color: #d2a8ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .ni {\n    color: #ffa657; }\n  [data-bs-theme=\"dark\"] .chroma .ne {\n    color: #f0883e;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .nf {\n    color: #d2a8ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .nl {\n    color: #79c0ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .nn {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .py {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .nt {\n    color: #7ee787; }\n  [data-bs-theme=\"dark\"] .chroma .nv {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .l {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .ld {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .s {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .sa {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .sb {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .sc {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .dl {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .sd {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .s2 {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .se {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .sh {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .si {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .sx {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .sr {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .s1 {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .ss {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .m {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .mb {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .mf {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .mh {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .mi {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .il {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .mo {\n    color: #a5d6ff; }\n  [data-bs-theme=\"dark\"] .chroma .o {\n    color: inherit;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .ow {\n    color: #ff7b72;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .c {\n    color: #8b949e;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .ch {\n    color: #8b949e;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .cm {\n    color: #8b949e;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .c1 {\n    color: #8b949e;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .cs {\n    color: #8b949e;\n    font-weight: bold;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .cp {\n    color: #8b949e;\n    font-weight: bold;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .cpf {\n    color: #8b949e;\n    font-weight: bold;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .gd {\n    color: #ffa198;\n    background-color: #490202; }\n  [data-bs-theme=\"dark\"] .chroma .ge {\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .gr {\n    color: #ffa198; }\n  [data-bs-theme=\"dark\"] .chroma .gh {\n    color: #79c0ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .gi {\n    color: #56d364;\n    background-color: #0f5323; }\n  [data-bs-theme=\"dark\"] .chroma .go {\n    color: #8b949e; }\n  [data-bs-theme=\"dark\"] .chroma .gp {\n    color: #8b949e; }\n  [data-bs-theme=\"dark\"] .chroma .gs {\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .gu {\n    color: #79c0ff; }\n  [data-bs-theme=\"dark\"] .chroma .gt {\n    color: #ff7b72; }\n  [data-bs-theme=\"dark\"] .chroma .gl {\n    text-decoration: underline; }\n  [data-bs-theme=\"dark\"] .chroma .w {\n    color: #6e7681; }\n\n/** Theme styles */\n[data-bs-theme=\"dark\"] {\n  /*\n.dropdown-menu {\n  @extend .dropdown-menu-dark;\n}\n*/\n  /*\n.navbar-light .navbar-brand {\n  color: $navbar-dark-color !important;\n}\n*/\n  /*\n.navbar-form::after {\n  color: $gray-600;\n  border: 1px solid $gray-800;\n}\n*/\n  /*\npre code::-webkit-scrollbar-thumb {\n  background: $gray-400;\n}\n\ncode:not(.hljs) {\n  background: $body-overlay-dark;\n  color: $body-color-dark;\n}\n\npre code:hover {\n  scrollbar-width: thin;\n  scrollbar-color: $border-dark transparent;\n}\n\npre code::-webkit-scrollbar-thumb:hover {\n  background: $gray-500;\n}\n*/\n  /*\n.dropdown-toggle:focus,\n.doks-sidebar-toggle:focus {\n  box-shadow: 0 0 0 0.2rem $focus-color-dark;\n}\n*/\n  /*\n@include media-breakpoint-up(md) {\n  .alert-dismissible .btn-close {\n    background-size: 1.25rem;\n  }\n}\n*/\n  /*\n.btn-close:focus {\n  box-shadow: 0 0 0 0.2rem $focus-color-dark;\n}\n*/ }\n  [data-bs-theme=\"dark\"] h1, [data-bs-theme=\"dark\"] .h1,\n  [data-bs-theme=\"dark\"] h2,\n  [data-bs-theme=\"dark\"] .h2,\n  [data-bs-theme=\"dark\"] h3,\n  [data-bs-theme=\"dark\"] .h3,\n  [data-bs-theme=\"dark\"] h4,\n  [data-bs-theme=\"dark\"] .h4 {\n    color: white; }\n  [data-bs-theme=\"dark\"] body {\n    background: #17181c;\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] a {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .callout a {\n    color: inherit; }\n  [data-bs-theme=\"dark\"] a.text- {\n    color: #c1c3c8 !important; }\n  [data-bs-theme=\"dark\"] .btn-primary {\n    --bs-btn-color: #000;\n    --bs-btn-bg: #b3c7ff;\n    --bs-btn-border-color: #b3c7ff;\n    --bs-btn-hover-color: #000;\n    --bs-btn-hover-bg: #becfff;\n    --bs-btn-hover-border-color: #bacdff;\n    --bs-btn-focus-shadow-rgb: 152, 169, 217;\n    --bs-btn-active-color: #000;\n    --bs-btn-active-bg: #c2d2ff;\n    --bs-btn-active-border-color: #bacdff;\n    --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n    --bs-btn-disabled-color: #000;\n    --bs-btn-disabled-bg: #b3c7ff;\n    --bs-btn-disabled-border-color: #b3c7ff;\n    color: #17181c; }\n  [data-bs-theme=\"dark\"] .btn-outline-primary {\n    --bs-btn-color: #b3c7ff;\n    --bs-btn-border-color: #b3c7ff;\n    --bs-btn-hover-color: #b3c7ff;\n    --bs-btn-hover-bg: #b3c7ff;\n    --bs-btn-hover-border-color: #b3c7ff;\n    --bs-btn-focus-shadow-rgb: 178.5, 198.9, 255;\n    --bs-btn-active-color: #000;\n    --bs-btn-active-bg: #b3c7ff;\n    --bs-btn-active-border-color: #b3c7ff;\n    --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n    --bs-btn-disabled-color: #b3c7ff;\n    --bs-btn-disabled-bg: transparent;\n    --bs-btn-disabled-border-color: #b3c7ff;\n    --bs-gradient: none;\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .btn-outline-primary:hover {\n    color: #17181c; }\n  [data-bs-theme=\"dark\"] .btn-doks-light {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .show > .btn-doks-light,\n  [data-bs-theme=\"dark\"] .btn-doks-light:hover,\n  [data-bs-theme=\"dark\"] .btn-doks-light:active {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .btn-menu svg {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .doks-sidebar-toggle {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .btn-menu:hover,\n  [data-bs-theme=\"dark\"] .btn-doks-light:hover,\n  [data-bs-theme=\"dark\"] .doks-sidebar-toggle:hover {\n    background: transparent; }\n  [data-bs-theme=\"dark\"] .navbar,\n  [data-bs-theme=\"dark\"] .doks-subnavbar {\n    background-color: rgba(23, 24, 28, 0.95);\n    border-bottom: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] body.home .navbar {\n    border-bottom: 0; }\n  [data-bs-theme=\"dark\"] .offcanvas-header {\n    border-bottom: 1px solid #343a40; }\n  [data-bs-theme=\"dark\"] .offcanvas .nav-link, [data-bs-theme=\"dark\"] .offcanvas .banner .nav a, .banner .nav [data-bs-theme=\"dark\"] .offcanvas a {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .offcanvas .nav-link:hover, [data-bs-theme=\"dark\"] .offcanvas .banner .nav a:hover, .banner .nav [data-bs-theme=\"dark\"] .offcanvas a:hover,\n  [data-bs-theme=\"dark\"] .offcanvas .nav-link:focus,\n  [data-bs-theme=\"dark\"] .offcanvas .banner .nav a:focus,\n  .banner .nav [data-bs-theme=\"dark\"] .offcanvas a:focus {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .offcanvas .nav-link.active, [data-bs-theme=\"dark\"] .offcanvas .banner .nav a.active, .banner .nav [data-bs-theme=\"dark\"] .offcanvas a.active {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .nav-link, [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav a, .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav a {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .nav-link:hover, [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav a:hover, .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav a:hover,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .nav-link:focus,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav a:focus,\n  .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav a:focus {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .nav-link.disabled, [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav a.disabled, .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav a.disabled {\n    color: rgba(255, 255, 255, 0.25); }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .show > .nav-link, [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav .show > a, .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .show > a,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .active > .nav-link,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav .active > a,\n  .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .active > a,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .nav-link.show,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav a.show,\n  .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav a.show,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .nav-link.active,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-nav .banner .nav a.active,\n  .banner .nav [data-bs-theme=\"dark\"] .navbar-light .navbar-nav a.active {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-text {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .alert-primary a {\n    color: #17181c; }\n  [data-bs-theme=\"dark\"] .alert-doks {\n    background: #23262f;\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .alert-doks a {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .page-links a {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .btn-toggle-nav a {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .showcase-meta a {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .showcase-meta a:hover,\n  [data-bs-theme=\"dark\"] .showcase-meta a:focus {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .docs-link:hover,\n  [data-bs-theme=\"dark\"] .docs-link.active,\n  [data-bs-theme=\"dark\"] .page-links a:hover {\n    text-decoration: none;\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .btn-toggle {\n    color: #c1c3c8;\n    background-color: transparent;\n    border: 0; }\n  [data-bs-theme=\"dark\"] .btn-toggle:hover,\n  [data-bs-theme=\"dark\"] .btn-toggle:focus {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .btn-toggle::before {\n    width: 1.25em;\n    line-height: 0;\n    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\");\n    transition: transform 0.35s ease;\n    transform-origin: 0.5em 50%;\n    margin-bottom: 0.125rem; }\n  [data-bs-theme=\"dark\"] .btn-toggle[aria-expanded=\"true\"] {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .btn-toggle[aria-expanded=\"true\"]::before {\n    transform: rotate(90deg); }\n  [data-bs-theme=\"dark\"] .btn-toggle-nav a:hover,\n  [data-bs-theme=\"dark\"] .btn-toggle-nav a:focus {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .btn-toggle-nav a.active {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-text a {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .docs-links h3.sidebar-link a, [data-bs-theme=\"dark\"] .docs-links .sidebar-link.h3 a,\n  [data-bs-theme=\"dark\"] .page-links h3.sidebar-link a,\n  [data-bs-theme=\"dark\"] .page-links .sidebar-link.h3 a {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-text a:hover,\n  [data-bs-theme=\"dark\"] .navbar-light .navbar-text a:focus {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .navbar .btn-link {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .content .btn-link {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .content .btn-link:hover {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .content img[src^=\"https://latex.codecogs.com/svg.latex\"] {\n    filter: invert(1); }\n  [data-bs-theme=\"dark\"] .navbar .btn-link:hover {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .navbar .btn-link:active {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .form-control.is-search, [data-bs-theme=\"dark\"] .search-form .is-search.search-field, .search-form [data-bs-theme=\"dark\"] .is-search.search-field, [data-bs-theme=\"dark\"] .comment-form input.is-search[type=\"text\"], .comment-form [data-bs-theme=\"dark\"] input.is-search[type=\"text\"],\n  [data-bs-theme=\"dark\"] .comment-form input.is-search[type=\"email\"],\n  .comment-form [data-bs-theme=\"dark\"] input.is-search[type=\"email\"],\n  [data-bs-theme=\"dark\"] .comment-form input.is-search[type=\"url\"],\n  .comment-form [data-bs-theme=\"dark\"] input.is-search[type=\"url\"],\n  [data-bs-theme=\"dark\"] .comment-form textarea.is-search,\n  .comment-form [data-bs-theme=\"dark\"] textarea.is-search {\n    background: #23262f;\n    border: 1px solid transparent;\n    color: #dee2e6;\n    /*\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-search'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  background-position: right calc(0.375em + 0.1875rem) center;\n  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n  */ }\n  [data-bs-theme=\"dark\"] .form-control.is-search:focus, [data-bs-theme=\"dark\"] .search-form .is-search.search-field:focus, .search-form [data-bs-theme=\"dark\"] .is-search.search-field:focus, [data-bs-theme=\"dark\"] .comment-form input.is-search[type=\"text\"]:focus, .comment-form [data-bs-theme=\"dark\"] input.is-search[type=\"text\"]:focus,\n  [data-bs-theme=\"dark\"] .comment-form input.is-search[type=\"email\"]:focus,\n  .comment-form [data-bs-theme=\"dark\"] input.is-search[type=\"email\"]:focus,\n  [data-bs-theme=\"dark\"] .comment-form input.is-search[type=\"url\"]:focus,\n  .comment-form [data-bs-theme=\"dark\"] input.is-search[type=\"url\"]:focus,\n  [data-bs-theme=\"dark\"] .comment-form textarea.is-search:focus,\n  .comment-form [data-bs-theme=\"dark\"] textarea.is-search:focus {\n    border: 1px solid #b3c7ff; }\n  [data-bs-theme=\"dark\"] .doks-search::after {\n    color: #dee2e6;\n    border: 1px solid #495057; }\n  [data-bs-theme=\"dark\"] .text-dark {\n    color: #c1c3c8 !important; }\n  [data-bs-theme=\"dark\"] .form-control, [data-bs-theme=\"dark\"] .search-form .search-field, .search-form [data-bs-theme=\"dark\"] .search-field, [data-bs-theme=\"dark\"] .comment-form input[type=\"text\"], .comment-form [data-bs-theme=\"dark\"] input[type=\"text\"],\n  [data-bs-theme=\"dark\"] .comment-form input[type=\"email\"],\n  .comment-form [data-bs-theme=\"dark\"] input[type=\"email\"],\n  [data-bs-theme=\"dark\"] .comment-form input[type=\"url\"],\n  .comment-form [data-bs-theme=\"dark\"] input[type=\"url\"],\n  [data-bs-theme=\"dark\"] .comment-form textarea,\n  .comment-form [data-bs-theme=\"dark\"] textarea {\n    color: #dee2e6; }\n  [data-bs-theme=\"dark\"] .form-control::placeholder, [data-bs-theme=\"dark\"] .search-form .search-field::placeholder, .search-form [data-bs-theme=\"dark\"] .search-field::placeholder, [data-bs-theme=\"dark\"] .comment-form input[type=\"text\"]::placeholder, .comment-form [data-bs-theme=\"dark\"] input[type=\"text\"]::placeholder,\n  [data-bs-theme=\"dark\"] .comment-form input[type=\"email\"]::placeholder,\n  .comment-form [data-bs-theme=\"dark\"] input[type=\"email\"]::placeholder,\n  [data-bs-theme=\"dark\"] .comment-form input[type=\"url\"]::placeholder,\n  .comment-form [data-bs-theme=\"dark\"] input[type=\"url\"]::placeholder,\n  [data-bs-theme=\"dark\"] .comment-form textarea::placeholder,\n  .comment-form [data-bs-theme=\"dark\"] textarea::placeholder {\n    color: #ced4da;\n    opacity: 1; }\n  [data-bs-theme=\"dark\"] .border-top {\n    border-top: 1px solid #23262f !important; }\n  @media (min-width: 992px) {\n    [data-bs-theme=\"dark\"] .docs-sidebar {\n      order: 0;\n      border-right: 1px solid #23262f; } }\n  [data-bs-theme=\"dark\"] .docs-navigation {\n    border-top: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] blockquote {\n    border-left: 3px solid #23262f; }\n  [data-bs-theme=\"dark\"] .footer {\n    border-top: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] .docs-links,\n  [data-bs-theme=\"dark\"] .docs-toc {\n    scrollbar-width: thin;\n    scrollbar-color: #17181c #17181c; }\n  [data-bs-theme=\"dark\"] .docs-links::-webkit-scrollbar,\n  [data-bs-theme=\"dark\"] .docs-toc::-webkit-scrollbar {\n    width: 5px; }\n  [data-bs-theme=\"dark\"] .docs-links::-webkit-scrollbar-track,\n  [data-bs-theme=\"dark\"] .docs-toc::-webkit-scrollbar-track {\n    background: #17181c; }\n  [data-bs-theme=\"dark\"] .docs-links::-webkit-scrollbar-thumb,\n  [data-bs-theme=\"dark\"] .docs-toc::-webkit-scrollbar-thumb {\n    background: #17181c; }\n  [data-bs-theme=\"dark\"] .docs-links:hover,\n  [data-bs-theme=\"dark\"] .docs-toc:hover {\n    scrollbar-width: thin;\n    scrollbar-color: #23262f #17181c; }\n  [data-bs-theme=\"dark\"] .docs-links:hover::-webkit-scrollbar-thumb,\n  [data-bs-theme=\"dark\"] .docs-toc:hover::-webkit-scrollbar-thumb {\n    background: #23262f; }\n  [data-bs-theme=\"dark\"] .docs-links::-webkit-scrollbar-thumb:hover,\n  [data-bs-theme=\"dark\"] .docs-toc::-webkit-scrollbar-thumb:hover {\n    background: #23262f; }\n  [data-bs-theme=\"dark\"] .docs-links h3:not(:first-child), [data-bs-theme=\"dark\"] .docs-links .h3:not(:first-child) {\n    border-top: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] a.docs-link {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .page-links li:not(:first-child) {\n    border-top: 1px dashed #23262f; }\n  [data-bs-theme=\"dark\"] .card {\n    background: #17181c;\n    border: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] .card.bg-light {\n    background: #23262f !important; }\n  [data-bs-theme=\"dark\"] .navbar .menu-icon .navicon {\n    background: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .navbar .menu-icon .navicon::before,\n  [data-bs-theme=\"dark\"] .navbar .menu-icon .navicon::after {\n    background: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .logo-light {\n    display: none !important; }\n  [data-bs-theme=\"dark\"] .logo-dark {\n    display: inline-block !important; }\n  [data-bs-theme=\"dark\"] .bg-light {\n    background: #141518 !important; }\n  [data-bs-theme=\"dark\"] .bg-dots {\n    background-image: radial-gradient(#414349 15%, transparent 15%); }\n  [data-bs-theme=\"dark\"] .text-muted {\n    color: #adafb6 !important; }\n  [data-bs-theme=\"dark\"] .alert-primary {\n    background: #b3c7ff;\n    color: #17181c; }\n  [data-bs-theme=\"dark\"] .figure-caption {\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .copy-status::after {\n    content: \"Copy\";\n    display: block;\n    color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .copy-status:hover::after {\n    content: \"Copy\";\n    display: block;\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .copy-status:focus::after,\n  [data-bs-theme=\"dark\"] .copy-status:active::after {\n    content: \"Copied\";\n    display: block;\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .offcanvas {\n    background-color: #17181c; }\n  [data-bs-theme=\"dark\"] .alert-dismissible .btn-close {\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNkZWUyZTYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLXgiPjxsaW5lIHgxPSIxOCIgeTE9IjYiIHgyPSI2IiB5Mj0iMTgiPjwvbGluZT48bGluZSB4MT0iNiIgeTE9IjYiIHgyPSIxOCIgeTI9IjE4Ij48L2xpbmU+PC9zdmc+\");\n    background-size: 1.5rem; }\n  [data-bs-theme=\"dark\"] .dropdown-item {\n    color: #17181c; }\n  [data-bs-theme=\"dark\"] hr.text-black-50 {\n    color: #6c757d !important; }\n  [data-bs-theme=\"dark\"] .email-form .form-control, [data-bs-theme=\"dark\"] .email-form .search-form .search-field, .search-form [data-bs-theme=\"dark\"] .email-form .search-field, [data-bs-theme=\"dark\"] .email-form .comment-form input[type=\"text\"], .comment-form [data-bs-theme=\"dark\"] .email-form input[type=\"text\"],\n  [data-bs-theme=\"dark\"] .email-form .comment-form input[type=\"email\"],\n  .comment-form [data-bs-theme=\"dark\"] .email-form input[type=\"email\"],\n  [data-bs-theme=\"dark\"] .email-form .comment-form input[type=\"url\"],\n  .comment-form [data-bs-theme=\"dark\"] .email-form input[type=\"url\"],\n  [data-bs-theme=\"dark\"] .email-form .comment-form textarea,\n  .comment-form [data-bs-theme=\"dark\"] .email-form textarea {\n    background: #23262f;\n    border: 1px solid transparent; }\n  [data-bs-theme=\"dark\"] .email-form .form-control:focus, [data-bs-theme=\"dark\"] .email-form .search-form .search-field:focus, .search-form [data-bs-theme=\"dark\"] .email-form .search-field:focus, [data-bs-theme=\"dark\"] .email-form .comment-form input[type=\"text\"]:focus, .comment-form [data-bs-theme=\"dark\"] .email-form input[type=\"text\"]:focus,\n  [data-bs-theme=\"dark\"] .email-form .comment-form input[type=\"email\"]:focus,\n  .comment-form [data-bs-theme=\"dark\"] .email-form input[type=\"email\"]:focus,\n  [data-bs-theme=\"dark\"] .email-form .comment-form input[type=\"url\"]:focus,\n  .comment-form [data-bs-theme=\"dark\"] .email-form input[type=\"url\"]:focus,\n  [data-bs-theme=\"dark\"] .email-form .comment-form textarea:focus,\n  .comment-form [data-bs-theme=\"dark\"] .email-form textarea:focus {\n    border: 1px solid #b3c7ff; }\n  [data-bs-theme=\"dark\"] .page-link {\n    color: #b3c7ff;\n    background-color: transparent;\n    border: var(--bs-border-width) solid #23262f; }\n    [data-bs-theme=\"dark\"] .page-link:hover {\n      color: #17181c;\n      background-color: #c1c3c8;\n      border-color: #c1c3c8; }\n    [data-bs-theme=\"dark\"] .page-link:focus {\n      color: #17181c;\n      background-color: #c1c3c8; }\n  [data-bs-theme=\"dark\"] .page-item.active .page-link {\n    color: #17181c;\n    background-color: #b3c7ff;\n    border-color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .page-item.disabled .page-link {\n    color: var(--bs-secondary-color);\n    background-color: #23262f;\n    border-color: #23262f; }\n  [data-bs-theme=\"dark\"] .dropdown-menu {\n    background: #23262f; }\n  [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item {\n    color: #c1c3c8; }\n    [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item.untranslated {\n      color: #6c757d;\n      text-decoration: line-through; }\n      [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item.untranslated:focus-visible, [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item.untranslated:hover {\n        background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-home' width='24' height='24' viewBox='0 0 24 24' stroke-width='2' stroke='%23b3c7ff' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M5 12l-2 0l9 -9l9 9l-2 0' /%3E%3Cpath d='M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7' /%3E%3Cpath d='M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6' /%3E%3C/svg%3E\");\n        background-repeat: no-repeat;\n        background-position: right 1rem top 0.6rem;\n        background-size: 0.9rem 0.9rem;\n        text-decoration: unset; }\n  [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item:hover {\n    color: #b3c7ff;\n    background: #17181c; }\n  [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item.active,\n  [data-bs-theme=\"dark\"] .dropdown-menu .dropdown-item:focus {\n    color: #b3c7ff;\n    background: #17181c; }\n  [data-bs-theme=\"dark\"] .navbar .dropdown-item.current,\n  [data-bs-theme=\"dark\"] .doks-subnavbar .dropdown-item.current {\n    background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23dee2e6' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n    background-repeat: no-repeat;\n    background-position: right 1rem top 0.6rem;\n    background-size: 0.75rem 0.75rem; }\n  [data-bs-theme=\"dark\"] details {\n    border: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] summary:hover {\n    background: #23262f; }\n  [data-bs-theme=\"dark\"] details[open] > summary {\n    border-bottom: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] details summary::after {\n    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\"); }\n  [data-bs-theme=\"dark\"] #toc a.active {\n    color: #b3c7ff; }\n  [data-bs-theme=\"dark\"] .btn-light {\n    color: #b3c7ff;\n    background: #23262f;\n    border: 1px solid #23262f; }\n  [data-bs-theme=\"dark\"] table th {\n    color: white; }\n  [data-bs-theme=\"dark\"] .table-dark, [data-bs-theme=\"dark\"] table,\n  [data-bs-theme=\"dark\"] [data-bs-theme=\"dark\"] table {\n    --bs-table-color: inherit;\n    --bs-table-bg: $body-bg-dark;\n    background: #17181c;\n    border-color: #23262f; }\n\n.alert {\n  font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  font-size: 0.875rem; }\n\n.alert-icon {\n  margin-right: 0.75rem; }\n\n.docs main .alert {\n  margin: 2rem -1.5rem; }\n\n.alert .alert-link {\n  text-decoration: underline; }\n\n.alert-doks {\n  background: #fbf7f0;\n  color: #1d2d35; }\n\n/*\n.alert-light {\n  color: #215888;\n  background: linear-gradient(-45deg, rgb(212, 245, 255), rgb(234, 250, 255), rgb(234, 250, 255), #d3f6ef);\n}\n\n.alert-light .alert-link {\n  color: #215888;\n}\n*/\n.alert-white {\n  background-color: rgba(255, 255, 255, 0.95); }\n\n.alert-primary {\n  color: #fff;\n  background-color: #4f46e5; }\n\n.alert a {\n  text-decoration: underline; }\n\n.alert-primary .alert-link {\n  color: #fff; }\n\n/*\n.alert-primary {\n  color: #084298;\n  background-color: #cfe2ff;\n  border-color: #b6d4fe;\n}\n\n.alert-primary .alert-link {\n  color: #06357a;\n}\n*/\n.alert-secondary {\n  color: #41464b;\n  background-color: #e2e3e5;\n  border-color: #d3d6d8; }\n\n.alert-secondary .alert-link {\n  color: #34383c; }\n\n.alert-success {\n  color: #0f5132;\n  background-color: #d1e7dd;\n  border-color: #badbcc; }\n\n.alert-success .alert-link {\n  color: #0c4128; }\n\n.alert-info {\n  color: #055160;\n  background-color: #cff4fc;\n  border-color: #b6effb; }\n\n.alert-info .alert-link {\n  color: #04414d; }\n\n.alert-warning {\n  color: #664d03;\n  background-color: #fff3cd;\n  border-color: #ffecb5; }\n\n.alert-warning .alert-link {\n  color: #523e02; }\n\n.alert-danger {\n  color: #842029;\n  background-color: #f8d7da;\n  border-color: #f5c2c7; }\n\n.alert-danger .alert-link {\n  color: #6a1a21; }\n\n.alert-light {\n  color: #636464;\n  background-color: #fefefe;\n  border-color: #fdfdfe; }\n\n.alert-light .alert-link {\n  color: #4f5050; }\n\n.alert-dark {\n  color: #141619;\n  background-color: #d3d3d4;\n  border-color: #bcbebf; }\n\n.alert-dark .alert-link {\n  color: #101214; }\n\n.alert .alert-link:hover,\n.alert .alert-link:focus {\n  text-decoration: none; }\n\n.alert-text {\n  margin-right: -3rem;\n  font-size: 1rem; }\n\n.alert-dismissible .btn-close {\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n  right: 1rem;\n  z-index: 2;\n  padding: 0.5rem;\n  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\");\n  background-size: 1.5rem;\n  filter: invert(1) grayscale(100%) brightness(200%); }\n\n.btn-close:focus,\n.btn-close:active {\n  outline: none;\n  box-shadow: none; }\n\n@media (min-width: 768px) {\n  .alert-dismissible .btn-close {\n    background-size: 1.5rem; } }\n\n[data-global-alert=\"closed\"] #announcement {\n  display: none; }\n\n.alert code {\n  background: #f6ecdc;\n  color: #1d2d35;\n  padding: 0.25rem 0.5rem; }\n\n.navbar .btn-link {\n  color: rgba(var(--bs-emphasis-color-rgb), 0.65);\n  padding: 0.4375rem 0; }\n\n#mode {\n  padding: 0.5rem; }\n\n.btn-link:focus {\n  outline: 0;\n  box-shadow: none; }\n\n#navigation {\n  margin-left: 1.25rem; }\n\n@media (min-width: 992px) {\n  #mode {\n    margin-left: 0.5rem;\n    margin-right: 0.25rem; }\n  .navbar .btn-link {\n    padding: 0.5625em 0.25rem 0.5rem 0.125rem; } }\n\n.navbar .btn-link:hover {\n  color: rgba(var(--bs-emphasis-color-rgb), 0.8); }\n\n.navbar .btn-link:active {\n  color: rgba(var(--bs-emphasis-color-rgb), 1); }\n\nbody .toggle-dark {\n  display: block; }\n\nbody .toggle-light {\n  display: none; }\n\n[data-dark-mode] body .toggle-light {\n  display: block; }\n\n[data-dark-mode] body .toggle-dark {\n  display: none; }\n\n.collapsible-sidebar {\n  margin: 2.125rem 0; }\n\n.btn-toggle {\n  display: inline-flex;\n  align-items: center;\n  padding: 0.25rem 0.5rem 0.25rem 0;\n  font-weight: 700;\n  font-size: 1rem;\n  text-transform: uppercase;\n  color: #1d2d35;\n  background-color: transparent;\n  border: 0; }\n\n.btn-toggle:hover,\n.btn-toggle:focus {\n  color: #1d2d35;\n  background-color: transparent;\n  outline: 0;\n  box-shadow: none; }\n\n.btn-toggle::before {\n  width: 1.25em;\n  line-height: 0;\n  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\");\n  transition: transform 0.35s ease;\n  transform-origin: 0.5em 50%;\n  margin-bottom: 0.125rem; }\n\n.btn-toggle[aria-expanded=\"true\"] {\n  color: #1d2d35; }\n\n.btn-toggle[aria-expanded=\"true\"]::before {\n  transform: rotate(90deg); }\n\n.btn-toggle-nav a {\n  display: inline-flex;\n  padding: 0.1875rem 0.5rem;\n  margin-top: 0.125rem;\n  margin-left: 1.25rem;\n  text-decoration: none; }\n\n.btn-toggle-nav a:hover,\n.btn-toggle-nav a:focus {\n  background-color: transparent;\n  color: #4f46e5; }\n\n.btn-toggle-nav a.active {\n  color: #4f46e5; }\n\n@media (max-width: 991.98px) {\n  .dropdown-menu {\n    width: 100%;\n    position: static; } }\n\n/*\n@include media-breakpoint-up(lg) {\n  .dropdown-menu {\n    width: auto;\n  }\n}\n*/\n.btn-dropdown {\n  border: 0; }\n\n@media (max-width: 991.98px) {\n  .btn-dropdown {\n    width: 100%;\n    text-align: left;\n    padding-left: 0;\n    padding-right: 0; } }\n\n.navbar .dropdown-item.current {\n  font-weight: 600;\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23292b2c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat;\n  background-position: right 1rem top 0.6rem;\n  background-size: 0.75rem 0.75rem; }\n  @media (max-width: 991.98px) {\n    .navbar .dropdown-item.current {\n      background-position: right 0.375rem top 0.6rem; } }\n.btn-close {\n  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\");\n  background-size: 1.5rem; }\n\n.offcanvas-header .btn-close {\n  margin-right: 0 !important; }\n\n.dropdown-toggle::after {\n  display: none; }\n\n.dropdown-caret {\n  margin-left: -0.1875rem; }\n\n.dropdown-menu .dropdown-item.untranslated {\n  color: #6c757d;\n  text-decoration: line-through; }\n  .dropdown-menu .dropdown-item.untranslated:focus-visible, .dropdown-menu .dropdown-item.untranslated:hover {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-home' width='24' height='24' viewBox='0 0 24 24' stroke-width='2' stroke='%234f46e5' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M5 12l-2 0l9 -9l9 9l-2 0' /%3E%3Cpath d='M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7' /%3E%3Cpath d='M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6' /%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    background-position: right 1rem top 0.6rem;\n    background-size: 0.9rem 0.9rem;\n    text-decoration: unset; }\n\n.dropdown-menu .dropdown-item:hover {\n  color: #4f46e5; }\n\n.dropdown-menu span.dropdown-item.current:hover {\n  color: unset; }\n\n.clipboard {\n  position: relative;\n  float: right; }\n\n.btn-clipboard {\n  transition: opacity 0.25s ease-in-out;\n  opacity: 0;\n  position: absolute;\n  right: 0.5rem;\n  top: 0.5rem;\n  line-height: 1;\n  padding: 0.3125rem 0.3125rem 0.1875rem;\n  background-color: transparent;\n  border-color: transparent; }\n  @media (max-width: 767.98px) {\n    .btn-clipboard {\n      position: absolute;\n      right: -0.5rem;\n      top: 0.5rem; } }\n.btn-clipboard::after {\n  width: 22px;\n  height: 22px;\n  display: inline-block;\n  content: \"\";\n  -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%;\n  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%;\n  -webkit-mask-size: cover;\n  mask-size: cover;\n  background-color: #495057; }\n\n.btn-clipboard:hover {\n  border-color: transparent; }\n\n.btn-clipboard:hover::after {\n  width: 22px;\n  height: 22px;\n  display: inline-block;\n  content: \"\";\n  -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%;\n  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%;\n  -webkit-mask-size: cover;\n  mask-size: cover;\n  background-color: #212529; }\n\n.btn-clipboard:focus,\n.btn-clipboard:active {\n  border-color: transparent !important; }\n\n.btn-clipboard:focus::after,\n.btn-clipboard:active::after {\n  width: 22px;\n  height: 22px;\n  display: inline-block;\n  content: \"\";\n  -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%;\n  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%;\n  -webkit-mask-size: cover;\n  mask-size: cover;\n  background-color: #212529; }\n\n[data-bs-theme=\"dark\"] .btn-clipboard {\n  background-color: transparent;\n  border-color: transparent; }\n\n[data-bs-theme=\"dark\"] .btn-clipboard::after {\n  background-color: #ced4da; }\n\n[data-bs-theme=\"dark\"] .btn-clipboard:hover {\n  border-color: transparent; }\n\n[data-bs-theme=\"dark\"] .btn-clipboard:hover::after {\n  background-color: #e9ecef; }\n\n[data-bs-theme=\"dark\"] .btn-clipboard:focus,\n[data-bs-theme=\"dark\"] .btn-clipboard:active {\n  border-color: transparent; }\n\n[data-bs-theme=\"dark\"] .btn-clipboard:focus::after,\n[data-bs-theme=\"dark\"] .btn-clipboard:active::after {\n  background-color: #e9ecef; }\n\n.highlight {\n  position: relative; }\n\n@media (min-width: 768px) {\n  .highlight:hover .btn-clipboard {\n    opacity: 1; } }\n\n#toTop {\n  opacity: 0;\n  transition: opacity 0.3s ease-in-out; }\n\n#toTop.fade {\n  opacity: 1; }\n\n.btn-cta {\n  padding-left: 2rem;\n  padding-right: 2rem; }\n\n.callout {\n  --bs-link-color-rgb: var(--callout-link);\n  --bs-code-color: var(--callout-code-color);\n  color: var(--callout-color, inherit);\n  background-color: var(--callout-bg, var(--bs-gray-100));\n  border-left: 0.25rem solid var(--callout-border, var(--bs-gray-300));\n  border-radius: 0;\n  /*\n  code {\n    background: transparent;\n    color: inherit;\n  }\n  */ }\n  .callout a {\n    text-decoration: underline; }\n  .callout .highlight {\n    background-color: rgba(0, 0, 0, 0.05); }\n  .callout .callout-icon.svg-inline {\n    flex-shrink: 0;\n    height: calc(1.5 * 1.125rem); }\n  .callout .callout-title {\n    font-weight: 700; }\n\n.callout-content {\n  min-width: 0; }\n\n.callout.callout-note {\n  border-color: var(--sl-color-blue);\n  background-color: var(--sl-color-blue-high);\n  /*\n  code:not(:where(.not-content *)) {\n    background: tint-color($info, 80%);\n  }\n  */ }\n  .callout.callout-note .callout-icon,\n  .callout.callout-note .callout-title,\n  .callout.callout-note .callout-body a {\n    color: var(--sl-color-blue-low); }\n  .callout.callout-note .callout-body,\n  .callout.callout-note .callout-body a:hover,\n  .callout.callout-note .callout-body a:active {\n    color: var(--sl-color-white); }\n\n.callout.callout-tip {\n  border-color: var(--sl-color-purple);\n  background-color: var(--sl-color-purple-high);\n  /*\n  code:not(:where(.not-content *)) {\n    background: tint-color($purple, 80%);\n  }\n  */ }\n  .callout.callout-tip .callout-icon,\n  .callout.callout-tip .callout-title,\n  .callout.callout-tip .callout-body a {\n    color: var(--sl-color-purple-low); }\n  .callout.callout-tip .callout-body,\n  .callout.callout-tip .callout-body a:hover,\n  .callout.callout-tip .callout-body a:active {\n    color: var(--sl-color-white); }\n\n.callout.callout-caution {\n  border-color: var(--sl-color-orange);\n  background-color: var(--sl-color-orange-high);\n  /*\n  code:not(:where(.not-content *)) {\n    background: tint-color($yellow, 80%);\n  }\n  */ }\n  .callout.callout-caution .callout-icon,\n  .callout.callout-caution .callout-title,\n  .callout.callout-caution .callout-body a {\n    color: var(--sl-color-orange-low); }\n  .callout.callout-caution .callout-body,\n  .callout.callout-caution .callout-body a:hover,\n  .callout.callout-caution .callout-body a:active {\n    color: var(--sl-color-white); }\n\n.callout.callout-danger {\n  border-color: var(--sl-color-red);\n  background-color: var(--sl-color-red-high);\n  /*\n  code:not(:where(.not-content *)) {\n    background: tint-color($red, 80%);\n  }\n  */ }\n  .callout.callout-danger .callout-icon,\n  .callout.callout-danger .callout-title,\n  .callout.callout-danger .callout-body a {\n    color: var(--sl-color-red-low); }\n  .callout.callout-danger .callout-body,\n  .callout.callout-danger .callout-body a:hover,\n  .callout.callout-danger .callout-body a:active {\n    color: var(--sl-color-white); }\n\n/*\n.callout.callout-light code {\n  background: var(--sl-color-gray-1);\n}\n*/\n[data-bs-theme=\"dark\"] .callout {\n  color: var(--sl-color-gray-1); }\n\n[data-bs-theme=\"dark\"] .callout.callout-note {\n  border-color: var(--sl-color-blue);\n  background-color: var(--sl-color-blue-low); }\n  [data-bs-theme=\"dark\"] .callout.callout-note .callout-icon,\n  [data-bs-theme=\"dark\"] .callout.callout-note .callout-title,\n  [data-bs-theme=\"dark\"] .callout.callout-note .callout-body a {\n    color: var(--sl-color-blue-high); }\n  [data-bs-theme=\"dark\"] .callout.callout-note .callout-body,\n  [data-bs-theme=\"dark\"] .callout.callout-note .callout-body a:hover,\n  [data-bs-theme=\"dark\"] .callout.callout-note .callout-body a:active {\n    color: var(--sl-color-white); }\n  [data-bs-theme=\"dark\"] .callout.callout-note code:not(:where(.not-content *)) {\n    color: var(--ec-codeFg); }\n\n[data-bs-theme=\"dark\"] .callout.callout-tip {\n  border-color: var(--sl-color-purple);\n  background-color: var(--sl-color-purple-low); }\n  [data-bs-theme=\"dark\"] .callout.callout-tip .callout-icon,\n  [data-bs-theme=\"dark\"] .callout.callout-tip .callout-title,\n  [data-bs-theme=\"dark\"] .callout.callout-tip .callout-body a {\n    color: var(--sl-color-purple-high); }\n  [data-bs-theme=\"dark\"] .callout.callout-tip .callout-body,\n  [data-bs-theme=\"dark\"] .callout.callout-tip .callout-body a:hover,\n  [data-bs-theme=\"dark\"] .callout.callout-tip .callout-body a:active {\n    color: var(--sl-color-white); }\n  [data-bs-theme=\"dark\"] .callout.callout-tip code:not(:where(.not-content *)) {\n    color: var(--ec-codeFg); }\n\n[data-bs-theme=\"dark\"] .callout.callout-caution {\n  border-color: var(--sl-color-orange);\n  background-color: var(--sl-color-orange-low); }\n  [data-bs-theme=\"dark\"] .callout.callout-caution .callout-icon,\n  [data-bs-theme=\"dark\"] .callout.callout-caution .callout-title,\n  [data-bs-theme=\"dark\"] .callout.callout-caution .callout-body a {\n    color: var(--sl-color-orange-high); }\n  [data-bs-theme=\"dark\"] .callout.callout-caution .callout-body,\n  [data-bs-theme=\"dark\"] .callout.callout-caution .callout-body a:hover,\n  [data-bs-theme=\"dark\"] .callout.callout-caution .callout-body a:active {\n    color: var(--sl-color-white); }\n  [data-bs-theme=\"dark\"] .callout.callout-caution code:not(:where(.not-content *)) {\n    color: var(--ec-codeFg); }\n\n[data-bs-theme=\"dark\"] .callout.callout-danger {\n  border-color: var(--sl-color-red);\n  background-color: var(--sl-color-red-low); }\n  [data-bs-theme=\"dark\"] .callout.callout-danger .callout-icon,\n  [data-bs-theme=\"dark\"] .callout.callout-danger .callout-title,\n  [data-bs-theme=\"dark\"] .callout.callout-danger .callout-body a {\n    color: var(--sl-color-red-high); }\n  [data-bs-theme=\"dark\"] .callout.callout-danger .callout-body,\n  [data-bs-theme=\"dark\"] .callout.callout-danger .callout-body a:hover,\n  [data-bs-theme=\"dark\"] .callout.callout-danger .callout-body a:active {\n    color: var(--sl-color-white); }\n  [data-bs-theme=\"dark\"] .callout.callout-danger code:not(:where(.not-content *)) {\n    color: var(--ec-codeFg); }\n\n.expressive-code {\n  font-family: var(--ec-uiFontFml);\n  font-size: var(--ec-uiFontSize);\n  line-height: var(--ec-uiLineHt);\n  text-size-adjust: none;\n  -webkit-text-size-adjust: none;\n  margin: 1.5rem 0; }\n\n.expressive-code *:not(path) {\n  all: revert;\n  box-sizing: border-box; }\n\n.expressive-code pre {\n  display: flex;\n  margin: 0;\n  padding: 0;\n  border: var(--ec-brdWd) solid var(--ec-brdCol);\n  border-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));\n  background: var(--ec-codeBg); }\n\n.expressive-code pre:focus-visible {\n  outline: 3px solid var(--ec-focusBrd);\n  outline-offset: -3px; }\n\n.expressive-code pre > code {\n  all: unset;\n  display: block;\n  flex: 1 0 100%;\n  padding: var(--ec-codePadBlk) 0;\n  color: var(--ec-codeFg);\n  font-family: var(--ec-codeFontFml);\n  font-size: var(--ec-codeFontSize);\n  line-height: var(--ec-codeLineHt); }\n\n.expressive-code pre {\n  overflow-x: auto; }\n\n.expressive-code pre::-webkit-scrollbar,\n.expressive-code pre::-webkit-scrollbar-track {\n  background-color: inherit;\n  border-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));\n  border-top-left-radius: 0;\n  border-top-right-radius: 0; }\n\n.expressive-code pre::-webkit-scrollbar-thumb {\n  background-color: var(--ec-sbThumbCol);\n  border: 4px solid transparent;\n  background-clip: content-box;\n  border-radius: 10px; }\n\n.expressive-code pre::-webkit-scrollbar-thumb:hover {\n  background-color: var(--ec-sbThumbHoverCol); }\n\n.expressive-code .ec-line {\n  padding-inline: var(--ec-codePadInl);\n  padding-inline-end: calc(2rem + var(--ec-codePadInl));\n  direction: ltr;\n  unicode-bidi: isolate; }\n\n.expressive-code .sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0; }\n\n.expressive-code .ec-line.mark {\n  --tmLineBgCol: var(--ec-tm-markBg);\n  --tmLineBrdCol: var(--ec-tm-markBrdCol); }\n\n.expressive-code .ec-line.ins {\n  --tmLineBgCol: var(--ec-tm-insBg);\n  --tmLineBrdCol: var(--ec-tm-insBrdCol); }\n\n.expressive-code .ec-line.ins::before {\n  content: var(--ec-tm-insDiffIndContent);\n  color: var(--ec-tm-insDiffIndCol); }\n\n.expressive-code .ec-line.del {\n  --tmLineBgCol: var(--ec-tm-delBg);\n  --tmLineBrdCol: var(--ec-tm-delBrdCol); }\n\n.expressive-code .ec-line.del::before {\n  content: var(--ec-tm-delDiffIndContent);\n  color: var(--ec-tm-delDiffIndCol); }\n\n.expressive-code .ec-line.mark,\n.expressive-code .ec-line.ins,\n.expressive-code .ec-line.del {\n  position: relative;\n  background: var(--tmLineBgCol);\n  min-width: calc(100% - var(--ec-tm-lineMarkerAccentMarg));\n  margin-inline-start: var(--ec-tm-lineMarkerAccentMarg);\n  border-inline-start: var(--ec-tm-lineMarkerAccentWd) solid var(--tmLineBrdCol);\n  padding-inline-start: calc(var(--ec-codePadInl) - var(--ec-tm-lineMarkerAccentMarg) - var(--ec-tm-lineMarkerAccentWd)) !important; }\n\n.expressive-code .ec-line.mark::before,\n.expressive-code .ec-line.ins::before,\n.expressive-code .ec-line.del::before {\n  position: absolute;\n  left: var(--ec-tm-lineDiffIndMargLeft); }\n\n.expressive-code .ec-line mark, .expressive-code .ec-line .mark {\n  --tmInlineBgCol: var(--ec-tm-markBg);\n  --tmInlineBrdCol: var(--ec-tm-markBrdCol); }\n\n.expressive-code .ec-line ins {\n  --tmInlineBgCol: var(--ec-tm-insBg);\n  --tmInlineBrdCol: var(--ec-tm-insBrdCol); }\n\n.expressive-code .ec-line del {\n  --tmInlineBgCol: var(--ec-tm-delBg);\n  --tmInlineBrdCol: var(--ec-tm-delBrdCol); }\n\n.expressive-code .ec-line mark, .expressive-code .ec-line .mark,\n.expressive-code .ec-line ins,\n.expressive-code .ec-line del {\n  all: unset;\n  display: inline-block;\n  position: relative;\n  --tmBrdL: var(--ec-tm-inlMarkerBrdWd);\n  --tmBrdR: var(--ec-tm-inlMarkerBrdWd);\n  --tmRadL: var(--ec-tm-inlMarkerBrdRad);\n  --tmRadR: var(--ec-tm-inlMarkerBrdRad);\n  margin-inline: 0.025rem;\n  padding-inline: var(--ec-tm-inlMarkerPad);\n  border-radius: var(--tmRadL) var(--tmRadR) var(--tmRadR) var(--tmRadL);\n  background: var(--tmInlineBgCol);\n  background-clip: padding-box; }\n\n.expressive-code .ec-line mark.open-start, .expressive-code .ec-line .open-start.mark,\n.expressive-code .ec-line ins.open-start,\n.expressive-code .ec-line del.open-start {\n  margin-inline-start: 0;\n  padding-inline-start: 0;\n  --tmBrdL: 0px;\n  --tmRadL: 0; }\n\n.expressive-code .ec-line mark.open-end, .expressive-code .ec-line .open-end.mark,\n.expressive-code .ec-line ins.open-end,\n.expressive-code .ec-line del.open-end {\n  margin-inline-end: 0;\n  padding-inline-end: 0;\n  --tmBrdR: 0px;\n  --tmRadR: 0; }\n\n.expressive-code .ec-line mark::before, .expressive-code .ec-line .mark::before,\n.expressive-code .ec-line ins::before,\n.expressive-code .ec-line del::before {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  display: inline-block;\n  inset: 0;\n  border-radius: var(--tmRadL) var(--tmRadR) var(--tmRadR) var(--tmRadL);\n  border: var(--ec-tm-inlMarkerBrdWd) solid var(--tmInlineBrdCol);\n  border-inline-width: var(--tmBrdL) var(--tmBrdR); }\n\n.expressive-code .frame {\n  all: unset;\n  position: relative;\n  display: block;\n  --header-border-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));\n  --tab-border-radius: calc(var(--ec-frm-edTabBrdRad) + var(--ec-brdWd));\n  --button-spacing: 0.4rem;\n  --code-background: var(--ec-frm-edBg);\n  border-radius: var(--header-border-radius);\n  box-shadow: var(--ec-frm-frameBoxShdCssVal); }\n\n.expressive-code .frame .header {\n  display: none;\n  z-index: 1;\n  position: relative;\n  border-radius: var(--header-border-radius) var(--header-border-radius) 0 0; }\n\n.expressive-code .frame.has-title pre,\n.expressive-code .frame.has-title code,\n.expressive-code .frame.is-terminal pre,\n.expressive-code .frame.is-terminal code {\n  border-top: none;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0; }\n\n.expressive-code .frame .title:empty:before {\n  content: \"\\a0\"; }\n\n.expressive-code .frame.has-title:not(.is-terminal) {\n  --button-spacing: calc(1.9rem + 2 * (var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt))); }\n\n.expressive-code .frame.has-title:not(.is-terminal) .title {\n  position: relative;\n  color: var(--ec-frm-edActTabFg);\n  background: var(--ec-frm-edActTabBg);\n  background-clip: padding-box;\n  margin-block-start: var(--ec-frm-edTabsMargBlkStart);\n  padding: calc(var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt)) var(--ec-uiPadInl);\n  border: var(--ec-brdWd) solid var(--ec-frm-edActTabBrdCol);\n  border-radius: var(--tab-border-radius) var(--tab-border-radius) 0 0;\n  border-bottom: none;\n  overflow: hidden; }\n\n.expressive-code .frame.has-title:not(.is-terminal) .title::after {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  inset: 0;\n  border-top: var(--ec-frm-edActTabIndHt) solid var(--ec-frm-edActTabIndTopCol);\n  border-bottom: var(--ec-frm-edActTabIndHt) solid var(--ec-frm-edActTabIndBtmCol); }\n\n.expressive-code .frame.has-title:not(.is-terminal) .header {\n  display: flex;\n  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));\n  background-repeat: no-repeat;\n  padding-inline-start: var(--ec-frm-edTabsMargInlStart); }\n\n.expressive-code .frame.has-title:not(.is-terminal) .header::before {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  inset: 0;\n  border: var(--ec-brdWd) solid var(--ec-frm-edTabBarBrdCol);\n  border-radius: inherit;\n  border-bottom: none; }\n\n.expressive-code .frame.is-terminal {\n  --button-spacing: calc(1.9rem + var(--ec-brdWd) + 2 * var(--ec-uiPadBlk));\n  --code-background: var(--ec-frm-trmBg); }\n\n.expressive-code .frame.is-terminal .header {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding-block: var(--ec-uiPadBlk);\n  padding-block-end: calc(var(--ec-uiPadBlk) + var(--ec-brdWd));\n  position: relative;\n  font-weight: 500;\n  letter-spacing: 0.025ch;\n  color: var(--ec-frm-trmTtbFg);\n  background: var(--ec-frm-trmTtbBg);\n  border: var(--ec-brdWd) solid var(--ec-brdCol);\n  border-bottom: none; }\n\n.expressive-code .frame.is-terminal .header::before {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  left: var(--ec-uiPadInl);\n  width: 2.1rem;\n  height: 0.56rem;\n  line-height: 0;\n  background-color: var(--ec-frm-trmTtbDotsFg);\n  opacity: var(--ec-frm-trmTtbDotsOpa);\n  -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\");\n  -webkit-mask-repeat: no-repeat;\n  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\");\n  mask-repeat: no-repeat; }\n\n.expressive-code .frame.is-terminal .header::after {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  inset: 0;\n  border-bottom: var(--ec-brdWd) solid var(--ec-frm-trmTtbBrdBtmCol); }\n\n.expressive-code .frame pre {\n  background: var(--code-background); }\n\n.expressive-code .copy {\n  display: flex;\n  gap: 0.25rem;\n  flex-direction: row;\n  position: absolute;\n  inset-block-start: calc(var(--ec-brdWd) + var(--button-spacing));\n  inset-inline-end: calc(var(--ec-brdWd) + var(--ec-uiPadInl) / 2);\n  direction: ltr;\n  unicode-bidi: isolate; }\n\n.expressive-code .copy button {\n  position: relative;\n  align-self: flex-end;\n  margin: 0;\n  padding: 0;\n  border: none;\n  border-radius: 0.2rem;\n  z-index: 1;\n  cursor: pointer;\n  transition-property: opacity, background, border-color;\n  transition-duration: 0.2s;\n  transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);\n  width: 2.5rem;\n  height: 2.5rem;\n  background: var(--code-background);\n  opacity: 0.75; }\n\n.expressive-code .copy button div {\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  background: var(--ec-frm-inlBtnBg);\n  opacity: var(--ec-frm-inlBtnBgIdleOpa);\n  transition-property: inherit;\n  transition-duration: inherit;\n  transition-timing-function: inherit; }\n\n.expressive-code .copy button::before {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  inset: 0;\n  border-radius: inherit;\n  border: var(--ec-brdWd) solid var(--ec-frm-inlBtnBrd);\n  opacity: var(--ec-frm-inlBtnBrdOpa); }\n\n.expressive-code .copy button::after {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  inset: 0;\n  background-color: var(--ec-frm-inlBtnFg);\n  -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\");\n  -webkit-mask-repeat: no-repeat;\n  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\");\n  mask-repeat: no-repeat;\n  margin: 0.475rem;\n  line-height: 0; }\n\n.expressive-code .copy button:focus::after,\n.expressive-code .copy button:active::after {\n  display: inline-block;\n  content: \"\";\n  -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%;\n  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%;\n  -webkit-mask-size: cover;\n  mask-size: cover;\n  margin: 0.2375rem; }\n\n.expressive-code .copy button:hover,\n.expressive-code .copy button:focus:focus-visible {\n  opacity: 1; }\n\n.expressive-code .copy button:hover div,\n.expressive-code .copy button:focus:focus-visible div {\n  opacity: var(--ec-frm-inlBtnBgHoverOrFocusOpa); }\n\n.expressive-code .copy button:active {\n  opacity: 1; }\n\n.expressive-code .copy button:active div {\n  opacity: var(--ec-frm-inlBtnBgActOpa); }\n\n.expressive-code .copy .feedback {\n  --tooltip-arrow-size: 0.35rem;\n  --tooltip-bg: var(--ec-frm-tooltipSuccessBg);\n  color: var(--ec-frm-tooltipSuccessFg);\n  pointer-events: none;\n  user-select: none;\n  -webkit-user-select: none;\n  position: relative;\n  align-self: center;\n  background-color: var(--tooltip-bg);\n  z-index: 99;\n  padding: 0.125rem 0.75rem;\n  border-radius: 0.2rem;\n  margin-inline-end: var(--tooltip-arrow-size);\n  opacity: 0;\n  transition-property: opacity, transform;\n  transition-duration: 0.2s;\n  transition-timing-function: ease-in-out;\n  transform: translate3d(0, 0.25rem, 0); }\n\n.expressive-code .copy .feedback::after {\n  content: \"\";\n  position: absolute;\n  pointer-events: none;\n  top: calc(50% - var(--tooltip-arrow-size));\n  inset-inline-end: calc(-2 * (var(--tooltip-arrow-size) - 0.5px));\n  border: var(--tooltip-arrow-size) solid transparent;\n  border-inline-start-color: var(--tooltip-bg); }\n\n.expressive-code .copy .feedback.show {\n  opacity: 1;\n  transform: translate3d(0, 0, 0); }\n\n@media (hover: hover) {\n  .expressive-code .copy button {\n    opacity: 0;\n    width: 2rem;\n    height: 2rem; }\n  .expressive-code .frame:hover .copy button:not(:hover),\n  .expressive-code .frame:focus-within :focus-visible ~ .copy button:not(:hover),\n  .expressive-code .frame .copy .feedback.show ~ button:not(:hover) {\n    opacity: 0.75; } }\n\n:root {\n  --ec-brdRad: 0px;\n  --ec-brdWd: 1px;\n  --ec-brdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-codeFontFml: var(--__sl-font-mono);\n  --ec-codeFontSize: var(--sl-text-code);\n  --ec-codeFontWg: 400;\n  --ec-codeLineHt: var(--sl-line-height);\n  --ec-codePadBlk: 0;\n  --ec-codePadInl: 1rem;\n  --ec-codeBg: #011627;\n  --ec-codeFg: #d6deeb;\n  --ec-codeSelBg: #1d3b53;\n  --ec-uiFontFml: var(--__sl-font);\n  --ec-uiFontSize: 0.9rem;\n  --ec-uiFontWg: 400;\n  --ec-uiLineHt: 1.65;\n  --ec-uiPadBlk: 0.25rem;\n  --ec-uiPadInl: 1rem;\n  --ec-uiSelBg: #234d708c;\n  --ec-uiSelFg: #ffffff;\n  --ec-focusBrd: #122d42;\n  --ec-sbThumbCol: #ffffff17;\n  --ec-sbThumbHoverCol: #ffffff49;\n  --ec-tm-lineMarkerAccentMarg: 0rem;\n  --ec-tm-lineMarkerAccentWd: 0.15rem;\n  --ec-tm-lineDiffIndMargLeft: 0.25rem;\n  --ec-tm-inlMarkerBrdWd: 1.5px;\n  --ec-tm-inlMarkerBrdRad: 0.2rem;\n  --ec-tm-inlMarkerPad: 0.15rem;\n  --ec-tm-insDiffIndContent: \"+\";\n  --ec-tm-delDiffIndContent: \"-\";\n  --ec-tm-markBg: #ffffff17;\n  --ec-tm-markBrdCol: #ffffff40;\n  --ec-tm-insBg: #1e571599;\n  --ec-tm-insBrdCol: #487f3bd0;\n  --ec-tm-insDiffIndCol: #79b169d0;\n  --ec-tm-delBg: #862d2799;\n  --ec-tm-delBrdCol: #b4554bd0;\n  --ec-tm-delDiffIndCol: #ed8779d0;\n  --ec-frm-shdCol: #011627;\n  --ec-frm-frameBoxShdCssVal: none;\n  --ec-frm-edActTabBg: var(--sl-color-gray-6);\n  --ec-frm-edActTabFg: var(--sl-color-text);\n  --ec-frm-edActTabBrdCol: transparent;\n  --ec-frm-edActTabIndHt: 1px;\n  --ec-frm-edActTabIndTopCol: var(--sl-color-accent-high);\n  --ec-frm-edActTabIndBtmCol: transparent;\n  --ec-frm-edTabsMargInlStart: 0;\n  --ec-frm-edTabsMargBlkStart: 0;\n  --ec-frm-edTabBrdRad: 0px;\n  --ec-frm-edTabBarBg: var(--sl-color-black);\n  --ec-frm-edTabBarBrdCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-edBg: var(--sl-color-gray-6);\n  --ec-frm-trmTtbDotsFg: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-trmTtbDotsOpa: 0.75;\n  --ec-frm-trmTtbBg: var(--sl-color-black);\n  --ec-frm-trmTtbFg: var(--sl-color-text);\n  --ec-frm-trmTtbBrdBtmCol: color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  --ec-frm-trmBg: var(--sl-color-gray-6);\n  --ec-frm-inlBtnFg: var(--sl-color-text);\n  --ec-frm-inlBtnBg: var(--sl-color-text);\n  --ec-frm-inlBtnBgIdleOpa: 0;\n  --ec-frm-inlBtnBgHoverOrFocusOpa: 0.2;\n  --ec-frm-inlBtnBgActOpa: 0.3;\n  --ec-frm-inlBtnBrd: var(--sl-color-text);\n  --ec-frm-inlBtnBrdOpa: 0.4;\n  --ec-frm-tooltipSuccessBg: #158744;\n  --ec-frm-tooltipSuccessFg: white; }\n\n.expressive-code .ec-line span[style^=\"--\"]:not([class]) {\n  color: var(0, inherit);\n  font-style: var(0fs, inherit);\n  font-weight: var(0fw, inherit);\n  text-decoration: var(0td, inherit); }\n\n@media (prefers-color-scheme: light) {\n  :root:not([data-bs-theme=\"dark\"]) {\n    --ec-codeBg: #fbfbfb;\n    --ec-codeFg: #403f53;\n    --ec-codeSelBg: #e0e0e0;\n    --ec-uiSelBg: #d3e8f8;\n    --ec-uiSelFg: #403f53;\n    --ec-focusBrd: #93a1a1;\n    --ec-sbThumbCol: #0000001a;\n    --ec-sbThumbHoverCol: #0000005c;\n    --ec-tm-markBg: #0000001a;\n    --ec-tm-markBrdCol: #00000055;\n    --ec-tm-insBg: #8ec77d99;\n    --ec-tm-insDiffIndCol: #336a28d0;\n    --ec-tm-delBg: #ff9c8e99;\n    --ec-tm-delDiffIndCol: #9d4138d0;\n    --ec-frm-shdCol: #d9d9d9;\n    --ec-frm-edActTabBg: var(--sl-color-gray-7);\n    --ec-frm-edActTabIndTopCol: #5d2f86;\n    --ec-frm-edTabBarBg: var(--sl-color-gray-6);\n    --ec-frm-edBg: var(--sl-color-gray-7);\n    --ec-frm-trmTtbBg: var(--sl-color-gray-6);\n    --ec-frm-trmBg: var(--sl-color-gray-7);\n    --ec-frm-tooltipSuccessBg: #078662; }\n  :root:not([data-bs-theme=\"dark\"]) .expressive-code .ec-line span[style^=\"--\"]:not([class]) {\n    color: var(1, inherit);\n    font-style: var(1fs, inherit);\n    font-weight: var(1fw, inherit);\n    text-decoration: var(1td, inherit); } }\n\n:root[data-bs-theme=\"light\"] .expressive-code,\n.expressive-code[data-bs-theme=\"light\"] {\n  --ec-codeBg: #fbfbfb;\n  --ec-codeFg: #403f53;\n  --ec-codeSelBg: #e0e0e0;\n  --ec-uiSelBg: #d3e8f8;\n  --ec-uiSelFg: #403f53;\n  --ec-focusBrd: #93a1a1;\n  --ec-sbThumbCol: #0000001a;\n  --ec-sbThumbHoverCol: #0000005c;\n  --ec-tm-markBg: #0000001a;\n  --ec-tm-markBrdCol: #00000055;\n  --ec-tm-insBg: #8ec77d99;\n  --ec-tm-insDiffIndCol: #336a28d0;\n  --ec-tm-delBg: #ff9c8e99;\n  --ec-tm-delDiffIndCol: #9d4138d0;\n  --ec-frm-shdCol: #d9d9d9;\n  --ec-frm-edActTabBg: var(--sl-color-gray-7);\n  --ec-frm-edActTabIndTopCol: #5d2f86;\n  --ec-frm-edTabBarBg: var(--sl-color-gray-6);\n  --ec-frm-edBg: var(--sl-color-gray-7);\n  --ec-frm-trmTtbBg: var(--sl-color-gray-6);\n  --ec-frm-trmBg: var(--sl-color-gray-7);\n  --ec-frm-tooltipSuccessBg: #078662; }\n\n:root[data-bs-theme=\"light\"] .expressive-code .ec-line span[style^=\"--\"]:not([class]),\n.expressive-code[data-bs-theme=\"light\"] .ec-line span[style^=\"--\"]:not([class]) {\n  color: var(1, inherit);\n  font-style: var(1fs, inherit);\n  font-weight: var(1fw, inherit);\n  text-decoration: var(1td, inherit); }\n\npre,\ncode,\nkbd,\nsamp {\n  font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  font-size: 0.875rem; }\n\ncode:not(:where(.not-content *)) {\n  background-color: var(--sl-color-gray-6);\n  margin-block: -0.125rem;\n  padding: 0.125rem 0.375rem;\n  color: inherit; }\n\n[data-bs-theme=\"dark\"] code:not(:where(.not-content *)) {\n  background-color: var(--sl-color-gray-5); }\n\n/*\ncode {\n  background: $db-khaki-100;\n\n  // background: $db-gray-200;\n  color: $db-bluishCyan-100;\n  padding: 0.25rem 0.5rem;\n}\n\npre {\n  margin: 2rem 0;\n}\n\npre code {\n  display: block;\n  overflow-x: auto;\n  line-height: $line-height-base;\n  padding: 1.25rem 1.5rem;\n  tab-size: 4;\n  scrollbar-width: thin;\n  scrollbar-color: transparent transparent;\n}\n\n.hljs {\n  padding: 1.5rem !important;\n}\n\n@include media-breakpoint-down(sm) {\n  pre,\n  code,\n  kbd,\n  samp {\n    border-radius: 0;\n  }\n\n  pre {\n    margin: 2rem -1.5rem;\n  }\n}\n\npre code::-webkit-scrollbar {\n  height: 5px;\n}\n\npre code::-webkit-scrollbar-thumb {\n  background: $gray-400;\n}\n\npre code:hover {\n  scrollbar-width: thin;\n  scrollbar-color: $gray-500 transparent;\n}\n\npre code::-webkit-scrollbar-thumb:hover {\n  background: $gray-500;\n}\n\ncode.language-mermaid {\n  background: none;\n}\n\n.line .ln {\n  margin-right: 1rem;\n}\n\n.line.hl {\n  color: var(--sl-color-blue);\n}\n\n@include color-mode(dark) {\n  .line.hl {\n    color: $yellow-100;\n  }\n}\n*/\n.math-block {\n  display: block;\n  margin: 2rem 0;\n  overflow-x: auto; }\n\n.math-inline {\n  display: inline; }\n\n[data-bs-theme=\"dark\"] .math-inline img,\n[data-bs-theme=\"dark\"] .math-block img {\n  filter: invert(1); }\n\nimg.diagram {\n  height: auto;\n  width: 100%;\n  margin: 1rem 0 2rem; }\n\nimg.diagram-kroki-mermaid {\n  background: #fff; }\n\n/* Applies when there are no line numbers, or when line numbers are inline. */\n.highlight > pre {\n  padding: 0.875rem 1rem; }\n\n/* Applies when line numbers are in a table cell. */\n.highlight div {\n  padding: 0; }\n\n/* Applies to all. */\n.highlight > .chroma {\n  overflow-x: auto;\n  border: 1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%);\n  /* add border-radius and box-shadow here */ }\n\n/* Applies when line numbers are inline */\n.chroma .ln {\n  padding: 0 0.5rem 0 0; }\n\n.chroma .hl {\n  border-inline-start: 0.15rem solid #0005;\n  margin-left: -1rem;\n  margin-right: -1rem;\n  padding-left: 1rem;\n  padding-right: 1rem; }\n  .chroma .hl .ln {\n    margin-left: -0.15rem; }\n\n/* Applies when using an external style sheet */\n.highlight .chroma .lntable .lnt,\n.highlight .chroma .lntable .hl {\n  display: flex; }\n\n/* Applies when highlihting using table */\n.chroma .lntd:first-child {\n  padding: 0; }\n  .chroma .lntd:first-child .lnt {\n    padding-left: 1rem; }\n\n.chroma .lntd:nth-child(2) {\n  padding: 0; }\n\n/* Applies when using an external style sheet */\n.highlight .chroma .lntable .lntd + .lntd {\n  width: 100%; }\n\n[data-bs-theme=\"dark\"] .chroma .ln {\n  padding: 0 0.5em 0 0; }\n\n/* LineTableTD */\n.chroma .lntd pre {\n  padding: 1rem 0;\n  margin-bottom: 0; }\n\n.highlight > .chroma::-webkit-scrollbar,\n.highlight > .chroma::-webkit-scrollbar-track {\n  background-color: inherit;\n  border-radius: 1px;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0; }\n\n.highlight > .chroma::-webkit-scrollbar-thumb {\n  background-color: #dddee0;\n  border: 4px solid transparent;\n  background-clip: content-box;\n  border-radius: 10px; }\n\n.highlight > .chroma::-webkit-scrollbar-thumb:hover {\n  background-color: #9d9e9f; }\n\n[data-bs-theme=\"dark\"] .highlight > .chroma::-webkit-scrollbar-thumb {\n  background-color: #ffffff17; }\n\n[data-bs-theme=\"dark\"] .highlight > .chroma::-webkit-scrollbar-thumb:hover {\n  background-color: #ffffff49; }\n\n/*\n.chroma .hl {\n  background-color: #0000001a\n}\n*/\n[data-bs-theme=\"dark\"] {\n  /*\n  .chroma .hl {\n    background-color: #ffffff17;\n  }\n  */ }\n  [data-bs-theme=\"dark\"] .highlight > .chroma {\n    border: 1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%); }\n  [data-bs-theme=\"dark\"] .chroma .hl {\n    border-inline-start: 0.15rem solid #ffffff40;\n    margin-left: -1rem;\n    margin-right: -1rem;\n    padding-left: 1rem;\n    padding-right: 1rem; }\n    [data-bs-theme=\"dark\"] .chroma .hl .ln {\n      margin-left: -0.15rem; }\n\n.comment-list ol {\n  list-style: none; }\n\nblockquote {\n  margin-bottom: 1rem;\n  font-size: 1.25rem;\n  border-left: 3px solid #dee2e6;\n  padding-left: 1rem; }\n\ndetails {\n  display: block;\n  position: relative;\n  border: 1px solid #e9ecef;\n  border-radius: 0.25rem;\n  padding: 0.5rem 1rem 0;\n  margin: 0.5rem 0; }\n\n/*\ndetails summary {\n  &::marker {\n    content: \"\";\n  }\n}\n*/\nsummary {\n  list-style: none;\n  display: inline-block;\n  width: calc(100% + 2rem);\n  margin: -0.5rem -1rem 0;\n  padding: 0.5rem 1rem; }\n\nsummary::-webkit-details-marker {\n  display: none; }\n\nsummary:hover {\n  background: #f8f9fa; }\n\ndetails summary::after {\n  display: inline-block;\n  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\");\n  transition: transform 0.35s ease;\n  transform-origin: center center;\n  position: absolute;\n  right: 1rem; }\n\ndetails[open] > summary::after {\n  transform: rotate(90deg); }\n\n/*\ndetails summary > * {\n  display: inline-block;\n}\n*/\ndetails[open] {\n  padding: 0.5rem 1rem; }\n\ndetails[open] > summary {\n  border-bottom: 1px solid #dee2e6;\n  margin-bottom: 0.5rem; }\n\ndetails h2, details .h2,\ndetails h3,\ndetails .h3,\ndetails h4,\ndetails .h4 {\n  margin: 1rem 0 0.5rem; }\n\ndetails p:last-child {\n  margin-bottom: 0; }\n\ndetails ul,\ndetails ol {\n  margin-bottom: 0; }\n\ndetails pre {\n  margin: 0 0 1rem; }\n\n/** Search form */\n.search-form label {\n  font-weight: normal; }\n\nimg {\n  max-width: 100%;\n  height: auto; }\n\nimg[data-sizes=\"auto\"] {\n  display: block; }\n\nimg,\npicture {\n  font-size: 0; }\n\nfigcaption {\n  font-size: 1rem;\n  margin-top: 0.5rem;\n  font-style: italic; }\n\n.content .gitpod-mark-monochrome.icon {\n  margin-bottom: 0.125rem;\n  margin-right: 0.5rem; }\n\n.blur-up {\n  filter: blur(5px);\n  transition: filter 400ms; }\n\n.blur-up.lazyloaded {\n  filter: unset; }\n\n.mermaid {\n  margin: 1.5rem 0;\n  padding: 1.5rem; }\n\n.mermaid svg {\n  height: auto; }\n\n.search-form .form-control:focus, .search-form .comment-form input[type=\"text\"]:focus, .comment-form .search-form input[type=\"text\"]:focus,\n.search-form .comment-form input[type=\"email\"]:focus,\n.comment-form .search-form input[type=\"email\"]:focus,\n.search-form .comment-form input[type=\"url\"]:focus,\n.comment-form .search-form input[type=\"url\"]:focus,\n.search-form .comment-form textarea:focus,\n.comment-form .search-form textarea:focus, .search-form .search-field:focus {\n  border: 2px solid #4f46e5; }\n\n[data-bs-theme=\"dark\"] .search-form .form-control:focus, [data-bs-theme=\"dark\"] .search-form .comment-form input[type=\"text\"]:focus, .comment-form [data-bs-theme=\"dark\"] .search-form input[type=\"text\"]:focus,\n[data-bs-theme=\"dark\"] .search-form .comment-form input[type=\"email\"]:focus,\n.comment-form [data-bs-theme=\"dark\"] .search-form input[type=\"email\"]:focus,\n[data-bs-theme=\"dark\"] .search-form .comment-form input[type=\"url\"]:focus,\n.comment-form [data-bs-theme=\"dark\"] .search-form input[type=\"url\"]:focus,\n[data-bs-theme=\"dark\"] .search-form .comment-form textarea:focus,\n.comment-form [data-bs-theme=\"dark\"] .search-form textarea:focus, [data-bs-theme=\"dark\"] .search-form .search-field:focus {\n  border: 2px solid #b3c7ff; }\n\n[data-bs-theme=\"dark\"] .search-form .btn-link {\n  color: #b3c7ff; }\n\n.search-form .btn-link,\n.modal-body p.message,\n.modal-footer {\n  font-size: 0.875rem; }\n\n.modal-body::-webkit-scrollbar {\n  width: 0.25rem; }\n\n.modal-body::-webkit-scrollbar-track {\n  background-color: #f1f1f1; }\n\n.modal-body::-webkit-scrollbar-thumb {\n  background-color: #c1c1c1; }\n\n[data-bs-theme=\"dark\"] .modal-body::-webkit-scrollbar-track {\n  background-color: #424242; }\n\n[data-bs-theme=\"dark\"] .modal-body::-webkit-scrollbar-thumb {\n  background-color: #686868; }\n\n@media (min-width: 768px) {\n  #searchModal .modal-dialog {\n    max-height: 40rem; } }\n\n.search-result h2, .search-result .h2 {\n  margin-top: 0; }\n\n.search-result a:focus {\n  /*\n  border-color: transparent;\n  box-shadow: 0;\n  */\n  outline: 0 none; }\n\n.search-result .content {\n  margin-top: 0.5rem;\n  padding-top: 0 !important;\n  padding-bottom: 0 !important; }\n\n.search-result .card .content p {\n  margin-bottom: 0; }\n\n.search-result .card .content a {\n  position: relative;\n  z-index: 1; }\n\n.search-result:hover .card,\n.search-result.selected .card {\n  background-color: #4f46e5;\n  color: #fff; }\n  .search-result:hover .card .content a,\n  .search-result.selected .card .content a {\n    color: #fff;\n    text-decoration: underline; }\n\n[data-bs-theme=\"dark\"] .search-result:hover .card,\n[data-bs-theme=\"dark\"] .search-result.selected .card {\n  background-color: #b3c7ff;\n  color: #23262f; }\n  [data-bs-theme=\"dark\"] .search-result:hover .card .content a,\n  [data-bs-theme=\"dark\"] .search-result.selected .card .content a {\n    color: #23262f;\n    text-decoration: underline; }\n\n[data-bs-theme=\"dark\"] .search-result:hover .card h2, [data-bs-theme=\"dark\"] .search-result:hover .card .h2,\n[data-bs-theme=\"dark\"] .search-result.selected .card h2,\n[data-bs-theme=\"dark\"] .search-result.selected .card .h2 {\n  color: #17181c; }\n\n.search-result .submitted {\n  font-size: 0.875rem;\n  margin-top: 0.5rem; }\n\n.navbar-form {\n  position: relative; }\n\n#suggestions {\n  position: absolute;\n  right: 0;\n  margin-top: 0.5rem;\n  width: calc(100vw - 3rem);\n  max-width: calc(400px - 3rem);\n  z-index: 1000; }\n  @media (min-width: 768px) {\n    #suggestions {\n      right: -2rem; } }\n  @media (min-width: 992px) {\n    #suggestions {\n      right: 0; } }\n#suggestions a,\n.suggestion__no-results {\n  padding: 0.75rem;\n  margin: 0 0.5rem; }\n\n#suggestions a {\n  display: block;\n  text-decoration: none; }\n\n#suggestions a:focus {\n  background: #f8f9fa;\n  outline: 0; }\n\n#suggestions div:not(:first-child) {\n  border-top: 1px dashed #e9ecef; }\n\n#suggestions div:first-child {\n  margin-top: 0.5rem; }\n\n#suggestions div:last-child {\n  margin-bottom: 0.5rem; }\n\n#suggestions a:hover {\n  background: #f8f9fa; }\n\n#suggestions span {\n  display: flex;\n  font-size: 1rem; }\n\n.suggestion__title {\n  font-weight: 700;\n  color: #b3c7ff; }\n\n.suggestion__description,\n.suggestion__no-results {\n  color: #495057; }\n\n@media (min-width: 992px) {\n  #suggestions {\n    width: 31.125rem;\n    max-width: 31.125rem; }\n  #suggestions a {\n    display: flex; }\n  .suggestion__title {\n    width: 9rem;\n    padding-right: 1rem;\n    border-right: 1px solid #e9ecef;\n    display: inline-block;\n    text-align: right; }\n  .suggestion__description {\n    width: 19rem;\n    padding-left: 1rem; } }\n\n.section-nav {\n  padding-top: 2rem; }\n  .section-nav details {\n    border: 0;\n    padding: 0;\n    margin: 0.5rem 0; }\n  .section-nav details[open] {\n    padding: 0; }\n  .section-nav summary {\n    width: 100%;\n    padding: 0;\n    margin: 0;\n    font-weight: 700; }\n  .section-nav summary:hover {\n    background: none; }\n  .section-nav details[open] > summary {\n    border-bottom: 0;\n    margin-bottom: 0; }\n  .section-nav ul.list-nested details {\n    padding-left: 1rem;\n    margin-top: 0.5rem; }\n  .section-nav ul.list-nested li {\n    margin: 0; }\n  .section-nav a {\n    display: block;\n    margin: 0.5rem 0;\n    color: #1d2d35;\n    font-size: 1rem;\n    text-decoration: none; }\n  .section-nav a:hover,\n  .section-nav a:active {\n    color: #4f46e5; }\n  .section-nav li.active a {\n    color: #4f46e5;\n    font-weight: 500; }\n  .section-nav ul.list-nested li a {\n    padding-left: 1rem; }\n  .section-nav ul.list-nested {\n    border-left: 1px solid #e9ecef; }\n\n[data-bs-theme=\"dark\"] .section-nav ul.list-nested {\n  border-left: 1px solid #23262f; }\n\n[data-bs-theme=\"dark\"] .section-nav a {\n  color: #c1c3c8; }\n\n[data-bs-theme=\"dark\"] .section-nav a:hover,\n[data-bs-theme=\"dark\"] .section-nav a:active {\n  color: var(--sl-color-text-accent); }\n\n[data-bs-theme=\"dark\"] .section-nav li.active a {\n  color: var(--sl-color-text-accent);\n  font-weight: 500; }\n\n[data-bs-theme=\"dark\"] .section-nav summary {\n  color: #fff; }\n\ntable {\n  margin: 3rem 0; }\n\n.nav-tabs {\n  border-bottom: 0.0625rem solid #d8dee4;\n  margin-bottom: 1rem; }\n\n.nav-tabs .nav-link, .nav-tabs .banner .nav a, .banner .nav .nav-tabs a {\n  margin-bottom: -0.0625rem !important;\n  background: none;\n  border: 0;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  color: inherit; }\n\n.nav-tabs .nav-link:hover, .nav-tabs .banner .nav a:hover, .banner .nav .nav-tabs a:hover,\n.nav-tabs .nav-link:focus,\n.nav-tabs .banner .nav a:focus,\n.banner .nav .nav-tabs a:focus {\n  isolation: isolate;\n  border-color: transparent;\n  color: var(--bs-emphasis-color); }\n\n.nav-tabs .nav-link.active, .nav-tabs .banner .nav a.active, .banner .nav .nav-tabs a.active,\n.nav-tabs .nav-item.show .nav-link,\n.nav-tabs .nav-item.show .banner .nav a,\n.banner .nav .nav-tabs .nav-item.show a,\n.nav-tabs .banner .nav li.show .nav-link,\n.nav-tabs .banner .nav li.show a,\n.banner .nav .nav-tabs li.show .nav-link,\n.banner .nav .nav-tabs li.show a {\n  background-color: transparent;\n  border-color: transparent;\n  border-bottom: 0.125rem solid #4f46e5; }\n\n[data-bs-theme=\"dark\"] .nav-tabs {\n  border-bottom: 0.0625rem solid #343a40; }\n\n[data-bs-theme=\"dark\"] .nav-tabs .nav-link.active, [data-bs-theme=\"dark\"] .nav-tabs .banner .nav a.active, .banner .nav [data-bs-theme=\"dark\"] .nav-tabs a.active,\n[data-bs-theme=\"dark\"] .nav-tabs .nav-item.show .nav-link,\n[data-bs-theme=\"dark\"] .nav-tabs .nav-item.show .banner .nav a,\n.banner .nav [data-bs-theme=\"dark\"] .nav-tabs .nav-item.show a,\n[data-bs-theme=\"dark\"] .nav-tabs .banner .nav li.show .nav-link,\n[data-bs-theme=\"dark\"] .nav-tabs .banner .nav li.show a,\n.banner .nav [data-bs-theme=\"dark\"] .nav-tabs li.show .nav-link,\n.banner .nav [data-bs-theme=\"dark\"] .nav-tabs li.show a {\n  border-bottom: 0.125rem solid #b3c7ff; }\n\n.footer {\n  border-top: 1px solid #e9ecef;\n  padding-top: 1.125rem;\n  padding-bottom: 1.125rem; }\n  .footer ul {\n    margin-bottom: 0; }\n  .footer li {\n    font-size: 0.875rem;\n    margin-bottom: 0; }\n  .footer .list-inline-item:not(:last-child) {\n    margin-right: 1rem; }\n\n@media (max-width: 991.98px) {\n  .footer .col-lg-8 {\n    margin-top: 0.25rem;\n    margin-bottom: 0.25rem; } }\n\n@media (min-width: 768px) {\n  .footer li {\n    font-size: 1rem; } }\n\n.fixed-bottom-right {\n  position: fixed;\n  right: 0;\n  bottom: 0;\n  z-index: 1000; }\n\n.navbar-text {\n  margin-left: 1rem; }\n\n.navbar-brand {\n  font-weight: 700; }\n\n.navbar-brand svg {\n  margin-right: 0.25rem; }\n\n[data-bs-theme=\"dark\"] .navbar-brand {\n  color: inherit; }\n\n/*\n.navbar-light .navbar-brand,\n.navbar-light .navbar-brand:hover,\n.navbar-light .navbar-brand:active {\n  color: $body-color;\n}\n\n.navbar-light .navbar-nav .active .nav-link {\n  color: $primary;\n}\n*/\n.navbar {\n  z-index: 1000;\n  background-color: rgba(255, 255, 255, 0.95);\n  border-bottom: 1px solid #e9ecef;\n  /*\n  margin-top: 4px;\n  */ }\n\n@media (min-width: 992px) {\n  .navbar {\n    z-index: 1025;\n    /*\n    padding-top: 0.25rem;\n    padding-bottom: 0.25rem;\n    */ } }\n\n@media (min-width: 768px) {\n  .navbar-brand {\n    font-size: 1.375rem; }\n  .navbar-text {\n    margin-left: 1.25rem; } }\n\n/*\n.navbar-nav {\n  flex-direction: row;\n}\n*/\n.nav-item, .banner .nav li {\n  margin-left: 0; }\n\n@media (max-width: 991.98px) {\n  .navbar .icon-tabler-chevron-down {\n    display: block;\n    float: right;\n    transform: rotate(270deg);\n    transition: transform 0.35s ease; }\n  .navbar .dropdown-toggle[aria-expanded=\"true\"] .icon-tabler-chevron-down {\n    transform: rotate(360deg); }\n  .navbar-nav .dropdown-menu {\n    border: 0; }\n  /*\n  .navbar-nav .nav-item {\n    border-bottom: 1px solid rgba(52, 56, 65, 0.5);\n    font-family: $headings-font-family;\n    padding-top: 0.75rem;\n    padding-bottom: 0.75rem;\n  }\n  */\n  .navbar-nav .nav-link, .navbar-nav .banner .nav a, .banner .nav .navbar-nav a {\n    font-weight: 400; }\n  .navbar-nav .dropdown-item {\n    font-weight: 300; }\n  .dropdown-toggle svg {\n    margin-top: 0.25rem;\n    margin-left: 0; } }\n\n@media (min-width: 768px) {\n  .nav-item, .banner .nav li {\n    margin-left: 0.5rem; } }\n\n/*\n@include media-breakpoint-down(sm) {\n  .nav-item:first-child {\n    margin-left: 0;\n  }\n}\n*/\n/*\n@include media-breakpoint-down(md) {\n  .navbar .container {\n    padding-left: 1.5rem;\n    padding-right: 1.5rem;\n  }\n}\n*/\n.break {\n  flex-basis: 100%;\n  height: 0; }\n\nspan#doks-language-current {\n  margin-left: 0.1rem; }\n\nbutton#doks-languages {\n  margin: 0.25rem 0 0; }\n  @media (min-width: 992px) {\n    button#doks-languages {\n      margin: 0.25rem 0.5rem 0 0.25rem; } }\nbutton#doks-versions {\n  margin: 0.25rem 0 0; }\n  @media (min-width: 992px) {\n    button#doks-versions {\n      margin: 0.25rem 0.5rem 0 0.25rem; } }\n@media (max-width: 575.98px) {\n  .navbar .offcanvas.offcanvas-start,\n  .navbar .offcanvas.offcanvas-end {\n    width: 80vw; } }\n\n.offcanvas-header {\n  border-bottom: 1px solid #dee2e6;\n  padding-top: 1.0625rem;\n  padding-bottom: 0.8125rem; }\n\nh5.offcanvas-title, .offcanvas-title.h5 {\n  margin: 0;\n  color: inherit; }\n\n.offcanvas .nav-link, .offcanvas .banner .nav a, .banner .nav .offcanvas a {\n  color: #1d2d35; }\n\n/*\n.doks-subnavbar {\n  background-color: rgba(255, 255, 255, 0.95);\n  border-bottom: 1px solid $gray-200;\n}\n\n.doks-subnavbar .nav-link {\n  padding: 0.5rem 1.5rem 0.5rem 0;\n}\n\n.doks-subnavbar .nav-link:first-child {\n  padding: 0.5rem 1.5rem 0.5rem 0;\n}\n*/\n.offcanvas .nav-link:hover, .offcanvas .banner .nav a:hover, .banner .nav .offcanvas a:hover,\n.offcanvas .nav-link:focus,\n.offcanvas .banner .nav a:focus,\n.banner .nav .offcanvas a:focus {\n  color: #4f46e5; }\n\n.offcanvas .nav-link.active, .offcanvas .banner .nav a.active, .banner .nav .offcanvas a.active {\n  color: #4f46e5; }\n\n/*\n.navbar {\n  background-color: rgba(255, 255, 255, 0.95);\n  border-bottom: 1px solid $gray-200;\n  margin-top: 4px;\n}\n*/\n.header-bar {\n  border-top: 4px solid;\n  border-image-source: linear-gradient(83.21deg, #ffe000 0%, #e55235 100%);\n  border-image-slice: 1; }\n\n[data-bs-theme=\"dark\"] .header-bar {\n  border-top: 4px solid;\n  border-image-source: linear-gradient(83.21deg, var(--sl-color-accent) 0%, var(--sl-color-green) 100%);\n  border-image-slice: 1; }\n\n.offcanvas .header-bar {\n  margin-bottom: -4px; }\n\n.home .navbar {\n  border-bottom: 0; }\n\n/*\n.navbar-form {\n  position: relative;\n  margin-top: 0.25rem;\n}\n*/\n@media (min-width: 992px) {\n  .navbar-brand {\n    margin-right: 0.75rem !important; }\n  .main-nav .nav-item:first-child .nav-link, .main-nav .banner .nav li:first-child .nav-link, .banner .nav .main-nav li:first-child .nav-link, .main-nav .nav-item:first-child .banner .nav a, .banner .nav .main-nav .nav-item:first-child a, .main-nav .banner .nav li:first-child a, .banner .nav .main-nav li:first-child a,\n  .social-nav .nav-item:first-child .nav-link,\n  .social-nav .banner .nav li:first-child .nav-link,\n  .banner .nav .social-nav li:first-child .nav-link,\n  .social-nav .nav-item:first-child .banner .nav a,\n  .banner .nav .social-nav .nav-item:first-child a,\n  .social-nav .banner .nav li:first-child a,\n  .banner .nav .social-nav li:first-child a {\n    padding-left: 0; }\n  .main-nav .nav-item:last-child .nav-link, .main-nav .banner .nav li:last-child .nav-link, .banner .nav .main-nav li:last-child .nav-link, .main-nav .nav-item:last-child .banner .nav a, .banner .nav .main-nav .nav-item:last-child a, .main-nav .banner .nav li:last-child a, .banner .nav .main-nav li:last-child a,\n  .social-nav .nav-item:last-child .nav-link,\n  .social-nav .banner .nav li:last-child .nav-link,\n  .banner .nav .social-nav li:last-child .nav-link,\n  .social-nav .nav-item:last-child .banner .nav a,\n  .banner .nav .social-nav .nav-item:last-child a,\n  .social-nav .banner .nav li:last-child a,\n  .banner .nav .social-nav li:last-child a {\n    padding-right: 0; }\n  /*\n  .doks-search {\n    max-width: 20rem;\n    margin-top: 0.125rem;\n    margin-bottom: 0.125rem;\n  }\n  */\n  /*\n  .navbar-form {\n    margin-top: 0;\n    margin-left: 6rem;\n    margin-right: 1.5rem;\n  }\n  */ }\n\n.form-control.is-search, .comment-form input.is-search[type=\"text\"],\n.comment-form input.is-search[type=\"email\"],\n.comment-form input.is-search[type=\"url\"],\n.comment-form textarea.is-search, .search-form .is-search.search-field {\n  padding-right: 4rem;\n  border: 1px solid transparent;\n  background: #f8f9fa; }\n  @media (min-width: 768px) {\n    .form-control.is-search, .comment-form input.is-search[type=\"text\"],\n    .comment-form input.is-search[type=\"email\"],\n    .comment-form input.is-search[type=\"url\"],\n    .comment-form textarea.is-search, .search-form .is-search.search-field {\n      width: calc(100% + 2rem); } }\n  @media (min-width: 992px) {\n    .form-control.is-search, .comment-form input.is-search[type=\"text\"],\n    .comment-form input.is-search[type=\"email\"],\n    .comment-form input.is-search[type=\"url\"],\n    .comment-form textarea.is-search, .search-form .is-search.search-field {\n      width: 100%; } }\n.form-control.is-search:focus, .comment-form input.is-search[type=\"text\"]:focus,\n.comment-form input.is-search[type=\"email\"]:focus,\n.comment-form input.is-search[type=\"url\"]:focus,\n.comment-form textarea.is-search:focus, .search-form .is-search.search-field:focus {\n  border: 1px solid #4f46e5; }\n\n/*\n.doks-search::after {\n  position: absolute;\n  top: 0.4625rem;\n  right: 0.5375rem;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 1.5rem;\n  padding-right: 0.3125rem;\n  padding-left: 0.3125rem;\n  font-size: $font-size-base * 0.75;\n  color: $gray-700;\n  content: \"Ctrl + /\";\n  border: 1px solid $gray-300;\n  border-radius: 0.25rem;\n\n  @include media-breakpoint-up(md) {\n    right: -1.4625rem;\n  }\n\n  @include media-breakpoint-up(lg) {\n    right: 0.3125rem;\n  }\n}\n*/\n/*\n@include media-breakpoint-up(lg) {\n  .navbar-form {\n    margin-left: 15rem;\n  }\n}\n\n@include media-breakpoint-up(xl) {\n  .navbar-form {\n    margin-left: 30rem;\n  }\n}\n*/\n/*\n.form-control.is-search {\n*/\n/*\n  padding-right: calc(1.5em + 0.75rem);\n  */\n/*\n  padding-right: 2.5rem;\n  background: $gray-100;\n  border: 0;\n  */\n/*\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-search'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  background-position: right calc(0.375em + 0.1875rem) center;\n  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n  */\n/*\n}\n*/\n/*\n.navbar-form::after {\n  position: absolute;\n  top: 0.4625rem;\n  right: 0.5375rem;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 1.5rem;\n  padding-right: 0.4375rem;\n  padding-left: 0.4375rem;\n  font-size: $font-size-base * 0.75;\n  color: $gray-700;\n  content: \"/\";\n  border: 1px solid $gray-300;\n  border-radius: 0.25rem;\n}\n*/\n/*! purgecss start ignore */\n/*\n.algolia-autocomplete {\n  display: flex !important;\n}\n\n.algolia-autocomplete .ds-dropdown-menu {\n  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n@include media-breakpoint-down(sm) {\n  .algolia-autocomplete .ds-dropdown-menu {\n    max-width: 512px !important;\n    min-width: 312px !important;\n    width: auto !important;\n  }\n\n  .algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column {\n    font-weight: normal;\n  }\n\n  .algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column::after {\n    content: \"/\";\n    margin-right: 0.25rem;\n  }\n}\n\n.algolia-autocomplete .algolia-docsearch-suggestion--category-header {\n  color: $link-color-dark;\n}\n\n.algolia-autocomplete .algolia-docsearch-suggestion--title {\n  margin-bottom: 0;\n}\n\n.algolia-autocomplete .algolia-docsearch-suggestion--highlight {\n  padding: 0 0.05em;\n}\n\n.algolia-autocomplete .algolia-docsearch-footer {\n  margin-top: 1rem;\n  margin-right: 0.5rem;\n  margin-bottom: 0.5rem;\n}\n*/\n/*! purgecss end ignore */\n/*\n * Source: https://medium.com/creative-technology-concepts-code/responsive-mobile-dropdown-navigation-using-css-only-7218e4498a99\n*/\n/* Style the menu icon for the dropdown */\n.navbar .menu-icon {\n  cursor: pointer;\n  /* display: inline-block; */\n  /* float: right; */\n  padding: 1.125rem 0.625rem;\n  margin: 0 0 0 -0.625rem;\n  /* position: relative; */\n  user-select: none; }\n\n.navbar .menu-icon .navicon {\n  background: rgba(var(--bs-emphasis-color-rgb), 0.65);\n  display: block;\n  height: 2px;\n  position: relative;\n  transition: background 0.2s ease-out;\n  width: 18px; }\n\n.navbar .menu-icon .navicon::before,\n.navbar .menu-icon .navicon::after {\n  background: rgba(var(--bs-emphasis-color-rgb), 0.65);\n  content: \"\";\n  display: block;\n  height: 100%;\n  position: absolute;\n  transition: all 0.2s ease-out;\n  width: 100%; }\n\n.navbar .menu-icon .navicon::before {\n  top: 5px; }\n\n.navbar .menu-icon .navicon::after {\n  top: -5px; }\n\n/* Add the icon and menu animations when the checkbox is clicked */\n.navbar .menu-btn {\n  display: none; }\n\n.navbar .menu-btn:checked ~ .navbar-collapse {\n  display: block;\n  max-height: 100vh; }\n\n.navbar .menu-btn:checked ~ .menu-icon .navicon {\n  background: transparent; }\n\n.navbar .menu-btn:checked ~ .menu-icon .navicon::before {\n  transform: rotate(-45deg); }\n\n.navbar .menu-btn:checked ~ .menu-icon .navicon::after {\n  transform: rotate(45deg); }\n\n.navbar .menu-btn:checked ~ .menu-icon:not(.steps) .navicon::before,\n.navbar .menu-btn:checked ~ .menu-icon:not(.steps) .navicon::after {\n  top: 0; }\n\n.btn-menu {\n  margin-left: 1rem;\n  border: transparent; }\n\n.btn-doks-light {\n  border: transparent; }\n\n.btn-menu,\n.doks-sidebar-toggle {\n  padding-right: 0.25rem;\n  padding-left: 0.25rem;\n  margin-right: -0.5rem; }\n\n.btn-menu:hover,\n.btn-doks-light:hover,\n.doks-sidebar-toggle:hover {\n  background: transparent;\n  border: transparent; }\n\n.btn-menu:focus,\n.btn-doks-light:focus,\n.doks-sidebar-toggle:focus,\n.doks-mode-toggle:focus {\n  outline: 0;\n  border: transparent; }\n\n.doks-sidebar-toggle .doks-collapse,\n.doks-toc-toggle .doks-collapse {\n  display: none; }\n\n.doks-sidebar-toggle:not(.collapsed) .doks-expand,\n.doks-toc-toggle:not(.collapsed) .doks-expand {\n  display: none; }\n\n.doks-sidebar-toggle:not(.collapsed) .doks-collapse,\n.doks-toc-toggle:not(.collapsed) .doks-collapse {\n  display: inline-block; }\n\n.navbar-light .navbar-brand,\n.navbar-light .navbar-brand:hover,\n.navbar-light .navbar-brand:active {\n  color: #1d2d35; }\n\n.navbar-light .navbar-nav .active .nav-link, .navbar-light .navbar-nav .active .banner .nav a, .banner .nav .navbar-light .navbar-nav .active a {\n  color: #4f46e5; }\n\n.dropdown-divider {\n  border-top: 1px dashed #e9ecef; }\n\n.dropdown-item:hover {\n  background: #f8f9fa; }\n\n.dropdown-item:active {\n  color: inherit; }\n\n.social-link {\n  padding-right: 0.375rem;\n  padding-left: 0.375rem; }\n\n@media (max-width: 991.98px) {\n  #buttonColorMode {\n    margin: 0.5rem 0; }\n  #socialMenu {\n    margin: 0.5rem 0 0.5rem -0.25rem; }\n  .navbar-nav {\n    margin-top: 1rem; }\n  .dropdown-menu {\n    box-shadow: none !important;\n    background: transparent !important;\n    border-radius: 0 !important;\n    padding: 0;\n    margin-bottom: 0.25rem; }\n  .dropdown-item {\n    padding: 0.375rem 1rem 0.375rem 0; }\n  .nav-item .nav-link, .banner .nav li .nav-link, .nav-item .banner .nav a, .banner .nav .nav-item a, .banner .nav li a {\n    font-weight: 400;\n    font-size: 1.125rem; }\n  .btn-dropdown {\n    font-weight: 400;\n    font-size: 1.125rem; } }\n\n/*\n@include media-breakpoint-up(lg) {\n  // Source: https://bootstrap-menu.com/detail-basic-hover.html\n  .navbar .nav-item .dropdown-menu {\n    display: none;\n  }\n\n  .navbar .nav-item:hover .dropdown-menu {\n    display: block;\n  }\n}\n*/\n.modal-backdrop,\n.offcanvas-backdrop {\n  visibility: hidden;\n  background: rgba(23, 24, 28, 0.5);\n  opacity: 0; }\n\n[data-bs-theme=\"dark\"] .modal-backdrop,\n[data-bs-theme=\"dark\"] .offcanvas-backdrop {\n  visibility: hidden;\n  background: rgba(23, 24, 28, 0.5);\n  opacity: 0; }\n\n.modal-backdrop.show,\n.offcanvas-backdrop.show {\n  visibility: visible;\n  opacity: 1;\n  -webkit-backdrop-filter: blur(8px);\n  backdrop-filter: blur(8px); }\n\n.showing,\n.hiding {\n  -webkit-transition: none;\n  transition: none;\n  display: none; }\n\n.offcanvas-top.h-auto {\n  bottom: initial; }\n\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n  padding-right: 0.75rem; }\n\n.docs-content > h2[id]::before, .docs-content > [id].h2::before,\n.docs-content > h3[id]::before,\n.docs-content > [id].h3::before,\n.docs-content > h4[id]::before,\n.docs-content > [id].h4::before {\n  display: block;\n  height: 6rem;\n  margin-top: -6rem;\n  content: \"\"; }\n\n.docs-content ul,\n.docs-content ol {\n  margin-bottom: 1rem; }\n\n.anchor {\n  visibility: hidden;\n  margin-left: 0.375rem; }\n\n.edit-page a,\n.last-modified a {\n  color: var(--sl-color-gray-3); }\n\nh1:hover a, .h1:hover a,\nh2:hover a,\n.h2:hover a,\nh3:hover a,\n.h3:hover a,\nh4:hover a,\n.h4:hover a {\n  visibility: visible;\n  text-decoration: none; }\n\n.card-list {\n  margin-top: 2.25rem; }\n\n.page-footer-meta {\n  margin-top: 2rem;\n  margin-bottom: 2rem; }\n\n.edit-page,\n.last-modified {\n  font-size: 0.875rem;\n  margin-top: 0.25rem;\n  margin-bottom: 0.25rem; }\n\n@media (min-width: 768px) {\n  .edit-page,\n  .last-modified {\n    font-size: 1rem;\n    margin-top: 0.75rem;\n    margin-bottom: 0.25rem; } }\n\n.edit-page a:hover,\n.last-modified a:hover {\n  color: var(--sl-color-gray-4);\n  text-decoration: none; }\n\n[data-bs-theme=\"dark\"] .edit-page a:hover,\n[data-bs-theme=\"dark\"] .last-modified a:hover {\n  color: var(--sl-color-gray-2); }\n\n.edit-page svg,\n.last-modified svg {\n  margin-right: 0.25rem;\n  margin-bottom: 0.25rem; }\n\np.meta {\n  margin-top: 0.5rem;\n  font-size: 1rem; }\n\n.breadcrumb {\n  margin-top: 2.25rem;\n  font-size: 1rem; }\n\n.toc-mobile {\n  margin-top: 2rem;\n  margin-bottom: 2rem; }\n\n.page-link:hover {\n  text-decoration: none; }\n\n.row-about {\n  padding-top: 5rem;\n  padding-bottom: 5rem; }\n  @media (min-width: 992px) {\n    .row-about {\n      padding-top: 7rem;\n      padding-bottom: 7rem; } }\n.row-about h1, .row-about .h1 {\n  margin-top: 1rem; }\n\nul li {\n  margin: 0.25rem 0; }\n\n.list-contributors {\n  margin-left: 1.25rem; }\n\n.list-contributors li {\n  margin: 0.25rem 0 0.25rem -1.5rem;\n  padding: 0.25rem;\n  background-color: #fff;\n  border-radius: 50%; }\n\n[data-bs-theme=\"dark\"] .list-contributors li {\n  background-color: #212529; }\n\nul.list-toolbox li {\n  position: relative;\n  margin: 0.25rem 0; }\n  ul.list-toolbox li::before {\n    background: none;\n    content: \"🧰\";\n    height: 1rem;\n    width: 1rem;\n    position: absolute;\n    left: -2rem;\n    top: 0; }\n\nul.list-books li {\n  position: relative;\n  margin: 0.25rem 0; }\n  ul.list-books li::before {\n    background: none;\n    content: \"📚\";\n    height: 1rem;\n    width: 1rem;\n    position: absolute;\n    left: -2rem;\n    top: 0; }\n\nul.list-speech-balloon li {\n  position: relative;\n  margin: 0.25rem 0; }\n  ul.list-speech-balloon li::before {\n    background: none;\n    content: \"💬\";\n    height: 1rem;\n    width: 1rem;\n    position: absolute;\n    left: -2rem;\n    top: 0; }\n\nul.list-package li {\n  position: relative;\n  margin: 0.25rem 0; }\n  ul.list-package li::before {\n    background: none;\n    content: \"📦\";\n    height: 1rem;\n    width: 1rem;\n    position: absolute;\n    left: -2rem;\n    top: 0; }\n\nul.list-star li {\n  position: relative;\n  margin: 0.25rem 0; }\n  ul.list-star li::before {\n    background: none;\n    content: \"⭐\";\n    height: 1rem;\n    width: 1rem;\n    position: absolute;\n    left: -2rem;\n    top: 0; }\n\n.page-nav .card .icon-tabler-arrow-left {\n  margin-right: 0.75rem; }\n\n.page-nav .card .icon-tabler-arrow-right {\n  margin-left: 0.75rem; }\n\n.page-nav .card:hover {\n  border: 1px solid #d9d9d9; }\n\n[data-bs-theme=\"dark\"] .page-nav .card {\n  border: 1px solid #353841; }\n\n[data-bs-theme=\"dark\"] .page-nav .card:hover {\n  border: 1px solid #888c96; }\n\n.container-fw {\n  max-width: 1200px; }\n  .container-fw .docs-toc {\n    margin-left: 3rem; }\n\n.home .card,\n.contributors.list .card,\n.blog.list .card,\n.blog.single .card,\n.categories.list .card,\n.tags.list .card {\n  margin-top: 2rem;\n  margin-bottom: 2rem;\n  transition: transform 0.3s; }\n\n.home .content .card:hover,\n.contributors.list .content .card:hover,\n.blog.list .content .card:hover,\n.blog.single .content .card:hover,\n.categories.list .content .card:hover,\n.tags.list .content .card:hover {\n  transform: scale(1.025); }\n\n.contributors.list .card.card-terms:hover,\n.categories.list .card.card-terms:hover,\n.tags.list .card.card-terms:hover {\n  transform: none; }\n\n.home .content .card-body,\n.contributors.list .content .card-body,\n.blog.list .content .card-body,\n.blog.single .content .card-body,\n.categories.list .content .card-body,\n.tags.list .content .card-body {\n  padding: 0 2rem 1rem; }\n\n.contributors.list .card-terms .card-body,\n.categories.list .card-terms .card-body,\n.tags.list .card-terms .card-body {\n  padding: 1rem; }\n\n.blog-header {\n  text-align: center;\n  margin-bottom: 2rem; }\n\n.blog-footer {\n  text-align: center; }\n\n.related-posts {\n  margin-top: 4rem; }\n\nh2.section-title, .section-title.h2 {\n  margin-bottom: 1.25rem; }\n\n.img-post-single {\n  margin-bottom: 2rem; }\n\n.pagination {\n  display: flex;\n  justify-content: center; }\n\n.page-item:first-child,\n.page-item:last-child,\n.page-item.disabled {\n  display: none; }\n\n.page-item a {\n  margin-left: 0.5rem;\n  margin-right: 0.5rem;\n  padding-left: 0.875rem;\n  padding-right: 0.875rem; }\n\n.page-item a[aria-label=\"Previous\"],\n.page-item a[aria-label=\"Next\"] {\n  border-radius: 50%; }\n\n.tag-list-single {\n  margin-top: 3rem;\n  margin-bottom: 1rem; }\n\n.section-related {\n  padding-top: 1.5rem;\n  padding-bottom: 1.5rem; }\n\n.contributor-image {\n  text-align: center;\n  margin-top: 2.5rem; }\n\nspan.reading-time {\n  margin-left: 2rem; }\n  span.reading-time svg {\n    margin-right: 0.3rem;\n    vertical-align: -0.4rem; }\n\n.docs-links,\n.docs-toc {\n  scrollbar-width: thin;\n  scrollbar-color: #fff #fff; }\n\n.docs-links::-webkit-scrollbar,\n.docs-toc::-webkit-scrollbar {\n  width: 5px; }\n\n.docs-links::-webkit-scrollbar-track,\n.docs-toc::-webkit-scrollbar-track {\n  background: #fff; }\n\n.docs-links::-webkit-scrollbar-thumb,\n.docs-toc::-webkit-scrollbar-thumb {\n  background: #fff; }\n\n.docs-links:hover,\n.docs-toc:hover {\n  scrollbar-width: thin;\n  scrollbar-color: #e9ecef #fff; }\n\n.docs-links:hover::-webkit-scrollbar-thumb,\n.docs-toc:hover::-webkit-scrollbar-thumb {\n  background: #e9ecef; }\n\n.docs-links::-webkit-scrollbar-thumb:hover,\n.docs-toc::-webkit-scrollbar-thumb:hover {\n  background: #e9ecef; }\n\n.docs-links h3, .docs-links .h3,\n.page-links h3,\n.page-links .h3 {\n  font-size: 1.125rem;\n  margin: 1.25rem 0 0.5rem;\n  padding: 1.5rem 0 0; }\n\n@media (min-width: 992px) {\n  .docs-links h3, .docs-links .h3,\n  .page-links h3,\n  .page-links .h3 {\n    margin: 1.125rem 1.5rem 0.75rem 0;\n    padding: 1.375rem 0 0; } }\n\n.docs-links h3:not(:first-child), .docs-links .h3:not(:first-child) {\n  border-top: 1px solid #e9ecef; }\n\na.docs-link {\n  color: #1d2d35;\n  display: block;\n  padding: 0.125rem 0;\n  font-size: 1rem; }\n\n.page-links li {\n  margin-top: 0.375rem;\n  padding-top: 0.375rem; }\n\n.page-links li ul li {\n  border-top: none;\n  padding-left: 1rem;\n  margin-top: 0.125rem;\n  padding-top: 0.125rem; }\n\n.page-links li:not(:first-child) {\n  border-top: 1px dashed #e9ecef; }\n\n.page-links a {\n  color: #1d2d35;\n  display: block;\n  padding: 0.125rem 0;\n  font-size: 0.9375rem;\n  text-decoration: none; }\n\n.docs-link:hover,\n.docs-link.active,\n.page-links a:hover,\n.page-links a.active {\n  text-decoration: none;\n  color: #4f46e5; }\n\n.nav-link.active, .banner .nav a.active,\n.dropdown-menu-main .dropdown-item.active,\n.docs-link.active {\n  font-weight: 500; }\n\n.docs-links h3.sidebar-link, .docs-links .sidebar-link.h3,\n.page-links h3.sidebar-link,\n.page-links .sidebar-link.h3 {\n  text-transform: none;\n  font-size: 1.125rem;\n  font-weight: normal; }\n\n.docs-links h3.sidebar-link a, .docs-links .sidebar-link.h3 a,\n.page-links h3.sidebar-link a,\n.page-links .sidebar-link.h3 a {\n  color: #1d2d35; }\n\n.docs-links h3.sidebar-link a:hover, .docs-links .sidebar-link.h3 a:hover,\n.page-links h3.sidebar-link a:hover,\n.page-links .sidebar-link.h3 a:hover {\n  text-decoration: underline; }\n\n/*\nbody {\n  background-color: {{ site.Params.doks.backGround }};\n}\n*/\n* {\n  -webkit-font-smoothing: antialiased; }\n\nh1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, .navbar-brand {\n  font-family: Quicksand, sans-serif;\n  font-weight: 700; }\n\n[data-bs-theme=\"dark\"] .only-light {\n  display: none; }\n\n[data-bs-theme=\"light\"] .only-dark {\n  display: none; }\n\n.homepage-features .row {\n  max-width: 1400px;\n  margin: 0 auto; }\n\n@media (min-width: 992px) {\n  .text-center.text-lg-start {\n    padding-right: 2rem; } }\n\n.learn-container {\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: 3rem 1rem; }\n\n.learn-header {\n  text-align: center;\n  margin-bottom: 4rem; }\n\n.learn-main-title {\n  font-size: 3rem;\n  font-weight: 700;\n  margin-bottom: 1rem;\n  color: var(--bs-body-color); }\n\n.learn-subtitle {\n  font-size: 1.25rem;\n  color: var(--bs-body-color-secondary);\n  max-width: 600px;\n  margin: 0 auto;\n  line-height: 1.6; }\n\n.learn-section {\n  margin-bottom: 4rem; }\n\n.learn-section:last-child {\n  margin-bottom: 0; }\n\n.learn-section-title {\n  text-align: center;\n  font-size: 2rem;\n  font-weight: 600;\n  margin-bottom: 3rem;\n  color: var(--bs-body-color); }\n\n.learn-row {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 2rem;\n  justify-items: center; }\n\n.learn-card {\n  display: flex;\n  flex-direction: column;\n  padding: 2rem;\n  border-radius: 0.75rem;\n  transition: all 0.3s ease;\n  text-decoration: none;\n  background: var(--bs-body-bg);\n  border: 1px solid var(--bs-border-color);\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n  min-height: 280px;\n  width: 100%;\n  max-width: 350px;\n  text-align: center;\n  color: inherit;\n  cursor: pointer;\n  position: relative;\n  overflow: hidden; }\n\n.learn-card:hover {\n  transform: translateY(-2px);\n  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);\n  text-decoration: none;\n  color: inherit;\n  border-color: var(--bs-primary); }\n\n.learn-card:focus {\n  outline: 2px solid var(--bs-primary);\n  outline-offset: 2px; }\n\n.learn-card-icon {\n  margin: 0 auto 0;\n  color: var(--bs-primary);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100px;\n  height: 100px; }\n\n.learn-card h3, .learn-card .h3 {\n  font-size: 1.25rem;\n  font-weight: 600;\n  margin-bottom: 0.5rem;\n  color: var(--bs-body-color);\n  margin-top: 0; }\n\n.learn-card-secondary {\n  display: block;\n  font-size: 0.9rem;\n  font-weight: 600;\n  color: var(--bs-gray-600);\n  margin-bottom: 1rem; }\n\n.learn-card p {\n  font-size: 0.95rem;\n  line-height: 1.6;\n  margin-bottom: 2rem;\n  flex-grow: 1;\n  color: var(--bs-body-color-secondary); }\n\n[data-bs-theme=\"dark\"] .learn-card {\n  background: var(--bs-body-bg);\n  border-color: var(--bs-border-color); }\n\n[data-bs-theme=\"dark\"] .learn-card:hover {\n  border-color: var(--bs-primary); }\n\n@media (max-width: 767.98px) {\n  .learn-container {\n    padding: 2rem 1rem; }\n  .learn-header {\n    margin-bottom: 3rem; }\n  .learn-main-title {\n    font-size: 2.5rem; }\n  .learn-subtitle {\n    font-size: 1.1rem; }\n  .learn-section-title {\n    font-size: 1.5rem;\n    margin-bottom: 2rem; }\n  .learn-row {\n    grid-template-columns: 1fr;\n    gap: 1.5rem; }\n  .learn-card {\n    padding: 1.5rem;\n    min-height: 240px;\n    max-width: none; } }\n\n@media (min-width: 768px) and (max-width: 1199.98px) {\n  .learn-row {\n    grid-template-columns: repeat(2, 1fr); }\n  .learn-section:first-child .learn-row {\n    grid-template-columns: repeat(2, 1fr); }\n  .learn-section:first-child .learn-row .learn-card:nth-child(3) {\n    grid-column: 1 / -1;\n    justify-self: center; } }\n\n.quickstart-container {\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 3rem 1rem; }\n\n.quickstart-header {\n  text-align: center;\n  margin-bottom: 3rem; }\n\n.quickstart-main-title {\n  font-size: 3rem;\n  font-weight: 700;\n  margin-bottom: 1rem;\n  color: var(--bs-body-color); }\n\n.quickstart-subtitle {\n  font-size: 1.25rem;\n  color: var(--bs-body-color-secondary);\n  max-width: 600px;\n  margin: 0 auto;\n  line-height: 1.6; }\n\n.quickstart-content {\n  font-size: 1.1rem;\n  line-height: 1.8; }\n\nbody.quickstart .event-driven-banner {\n  display: none; }\n\n@media (max-width: 767.98px) {\n  .quickstart-container {\n    padding: 2rem 1rem; }\n  .quickstart-header {\n    margin-bottom: 2rem; }\n  .quickstart-main-title {\n    font-size: 2.5rem; }\n  .quickstart-subtitle {\n    font-size: 1.1rem; } }\n\n@media (max-width: 991.98px) {\n  .display-5 {\n    font-size: 2rem; }\n  .text-center.text-lg-start {\n    text-align: center !important;\n    padding-right: 0; } }\n\n.pubsub-logos-container {\n  padding: 2rem 0; }\n\n.pubsub-logos-grid {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 2rem;\n  max-width: 900px;\n  margin: 0 auto;\n  align-items: center;\n  justify-content: center; }\n\n.pubsub-logo-item {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: center;\n  transition: all 0.3s ease;\n  padding: 1rem;\n  border-radius: 0.75rem;\n  text-decoration: none;\n  color: inherit;\n  flex: 0 0 120px; }\n  .pubsub-logo-item img {\n    width: 60px;\n    height: 60px;\n    margin-bottom: 0.75rem;\n    transition: all 0.3s ease; }\n  .pubsub-logo-item span {\n    font-size: 0.9rem;\n    font-weight: 600;\n    color: var(--bs-body-color-secondary);\n    transition: all 0.3s ease; }\n  .pubsub-logo-item:hover {\n    background: rgba(var(--bs-primary-rgb), 0.05);\n    text-decoration: none;\n    color: inherit; }\n    .pubsub-logo-item:hover span {\n      color: var(--bs-primary); }\n  .pubsub-logo-item:focus {\n    outline: 2px solid var(--bs-primary);\n    outline-offset: 2px;\n    text-decoration: none;\n    color: inherit; }\n\n[data-bs-theme=\"dark\"] .pubsub-logo-item:hover {\n  background: rgba(var(--bs-primary-rgb), 0.1); }\n\n@media (max-width: 767.98px) {\n  .pubsub-logos-grid {\n    grid-template-columns: repeat(3, 1fr);\n    gap: 1.5rem;\n    max-width: 100%; }\n  .pubsub-logo-item {\n    padding: 0.75rem; }\n    .pubsub-logo-item img {\n      width: 50px;\n      height: 50px;\n      margin-bottom: 0.5rem; }\n    .pubsub-logo-item span {\n      font-size: 0.8rem; } }\n\n@media (max-width: 575.98px) {\n  .pubsub-logos-grid {\n    grid-template-columns: repeat(2, 1fr);\n    gap: 1rem; }\n  .pubsub-logo-item img {\n    width: 45px;\n    height: 45px; }\n  .pubsub-logo-item span {\n    font-size: 0.75rem; } }\n\n.homepage-code-block .frame {\n  border-radius: 12px !important;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06) !important;\n  border: 1px solid var(--bs-border-color) !important;\n  overflow: hidden;\n  overflow-x: auto; }\n  .homepage-code-block .frame * {\n    border-radius: inherit !important; }\n  .homepage-code-block .frame pre, .homepage-code-block .frame code, .homepage-code-block .frame .expressive-code-block {\n    border-radius: 12px !important;\n    border: none !important; }\n  .homepage-code-block .frame .highlight {\n    border-radius: 12px !important;\n    border: none !important; }\n\n[data-bs-theme=\"light\"] .homepage-code-block .frame pre,\n[data-bs-theme=\"light\"] .homepage-code-block .frame code,\n[data-bs-theme=\"light\"] .homepage-code-block .frame .expressive-code-block {\n  background: white !important; }\n\n@media (max-width: 767.98px) {\n  .homepage-code-block {\n    margin-left: -45px;\n    margin-right: -45px; } }\n\n[data-bs-theme=\"dark\"] .homepage-code-block .frame {\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2) !important;\n  border: 1px solid var(--bs-border-color) !important; }\n\n.gradient-text-1 {\n  background: linear-gradient(to right, #6366f1, #8b5cf6, #3b82f6);\n  -webkit-background-clip: text;\n  background-clip: text;\n  -webkit-text-fill-color: transparent;\n  color: transparent; }\n\n.homepage-section-gradient {\n  position: relative; }\n  .homepage-section-gradient::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 50%;\n    transform: translateX(-50%);\n    width: 100vw;\n    height: 100%;\n    border-top: 1px solid rgba(0, 0, 0, 0.08);\n    pointer-events: none;\n    z-index: -1; }\n\n.homepage-section-gradient::before {\n  background: linear-gradient(180deg, rgba(79, 70, 229, 0.03) 0%, transparent 100%); }\n\n[data-bs-theme=\"dark\"] .homepage-section-gradient::before {\n  background: linear-gradient(180deg, rgba(79, 70, 229, 0.06) 0%, transparent 100%);\n  border-top: 1px solid rgba(255, 255, 255, 0.08); }\n\n[data-bs-theme=\"dark\"], [data-bs-theme=\"light\"] {\n  /* Background */\n  /* PreWrapper */\n  /* Other */\n  /* Error */\n  /* CodeLine */\n  /* LineLink */\n  /* LineTableTD */\n  /* LineTable */\n  /* LineHighlight */\n  /* LineNumbersTable */\n  /* LineNumbers */\n  /* Line */\n  /* Keyword */\n  /* KeywordConstant */\n  /* KeywordDeclaration */\n  /* KeywordNamespace */\n  /* KeywordPseudo */\n  /* KeywordReserved */\n  /* KeywordType */\n  /* Name */\n  /* NameAttribute */\n  /* NameBuiltin */\n  /* NameBuiltinPseudo */\n  /* NameClass */\n  /* NameConstant */\n  /* NameDecorator */\n  /* NameEntity */\n  /* NameException */\n  /* NameFunction */\n  /* NameFunctionMagic */\n  /* NameLabel */\n  /* NameNamespace */\n  /* NameOther */\n  /* NameProperty */\n  /* NameTag */\n  /* NameVariable */\n  /* NameVariableClass */\n  /* NameVariableGlobal */\n  /* NameVariableInstance */\n  /* NameVariableMagic */\n  /* Literal */\n  /* LiteralDate */\n  /* LiteralString */\n  /* LiteralStringAffix */\n  /* LiteralStringBacktick */\n  /* LiteralStringChar */\n  /* LiteralStringDelimiter */\n  /* LiteralStringDoc */\n  /* LiteralStringDouble */\n  /* LiteralStringEscape */\n  /* LiteralStringHeredoc */\n  /* LiteralStringInterpol */\n  /* LiteralStringOther */\n  /* LiteralStringRegex */\n  /* LiteralStringSingle */\n  /* LiteralStringSymbol */\n  /* LiteralNumber */\n  /* LiteralNumberBin */\n  /* LiteralNumberFloat */\n  /* LiteralNumberHex */\n  /* LiteralNumberInteger */\n  /* LiteralNumberIntegerLong */\n  /* LiteralNumberOct */\n  /* Operator */\n  /* OperatorWord */\n  /* Punctuation */\n  /* Comment */\n  /* CommentHashbang */\n  /* CommentMultiline */\n  /* CommentSingle */\n  /* CommentSpecial */\n  /* CommentPreproc */\n  /* CommentPreprocFile */\n  /* Generic */\n  /* GenericDeleted */\n  /* GenericEmph */\n  /* GenericError */\n  /* GenericHeading */\n  /* GenericInserted */\n  /* GenericOutput */\n  /* GenericPrompt */\n  /* GenericStrong */\n  /* GenericSubheading */\n  /* GenericTraceback */\n  /* GenericUnderline */\n  /* TextWhitespace */ }\n  [data-bs-theme=\"dark\"] .bg, [data-bs-theme=\"light\"] .bg {\n    color: #c9c9c9;\n    background-color: #282c34; }\n  [data-bs-theme=\"dark\"] .chroma, [data-bs-theme=\"light\"] .chroma {\n    color: #c9c9c9;\n    background-color: #282c34; }\n  [data-bs-theme=\"dark\"] .chroma .err, [data-bs-theme=\"light\"] .chroma .err {\n    color: #cf5967; }\n  [data-bs-theme=\"dark\"] .chroma .lnlinks, [data-bs-theme=\"light\"] .chroma .lnlinks {\n    outline: none;\n    text-decoration: none;\n    color: inherit; }\n  [data-bs-theme=\"dark\"] .chroma .lntd, [data-bs-theme=\"light\"] .chroma .lntd {\n    vertical-align: top;\n    padding: 0;\n    margin: 0;\n    border: 0; }\n  [data-bs-theme=\"dark\"] .chroma .lntable, [data-bs-theme=\"light\"] .chroma .lntable {\n    border-spacing: 0;\n    padding: 0;\n    margin: 0;\n    border: 0; }\n  [data-bs-theme=\"dark\"] .chroma .hl, [data-bs-theme=\"light\"] .chroma .hl {\n    background-color: #3d4148; }\n  [data-bs-theme=\"dark\"] .chroma .lnt, [data-bs-theme=\"light\"] .chroma .lnt {\n    white-space: pre;\n    -webkit-user-select: none;\n    user-select: none;\n    margin-right: 0.4em;\n    padding: 0 0.4em 0 0.4em;\n    color: #7f7f7f; }\n  [data-bs-theme=\"dark\"] .chroma .ln, [data-bs-theme=\"light\"] .chroma .ln {\n    white-space: pre;\n    -webkit-user-select: none;\n    user-select: none;\n    margin-right: 0.4em;\n    padding: 0 0.4em 0 0.4em;\n    color: #7f7f7f; }\n  [data-bs-theme=\"dark\"] .chroma .line, [data-bs-theme=\"light\"] .chroma .line {\n    display: flex; }\n  [data-bs-theme=\"dark\"] .chroma .k, [data-bs-theme=\"light\"] .chroma .k {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .kc, [data-bs-theme=\"light\"] .chroma .kc {\n    color: #cf5967; }\n  [data-bs-theme=\"dark\"] .chroma .kd, [data-bs-theme=\"light\"] .chroma .kd {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .kn, [data-bs-theme=\"light\"] .chroma .kn {\n    color: #bc74c4; }\n  [data-bs-theme=\"dark\"] .chroma .kp, [data-bs-theme=\"light\"] .chroma .kp {\n    color: #bc74c4; }\n  [data-bs-theme=\"dark\"] .chroma .kr, [data-bs-theme=\"light\"] .chroma .kr {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .kt, [data-bs-theme=\"light\"] .chroma .kt {\n    color: #57c7ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .na, [data-bs-theme=\"light\"] .chroma .na {\n    color: #bc74c4; }\n  [data-bs-theme=\"dark\"] .chroma .nb, [data-bs-theme=\"light\"] .chroma .nb {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .bp, [data-bs-theme=\"light\"] .chroma .bp {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .nc, [data-bs-theme=\"light\"] .chroma .nc {\n    color: #ecbe7b; }\n  [data-bs-theme=\"dark\"] .chroma .no, [data-bs-theme=\"light\"] .chroma .no {\n    color: #ecbe7b; }\n  [data-bs-theme=\"dark\"] .chroma .nd, [data-bs-theme=\"light\"] .chroma .nd {\n    color: #ecbe7b; }\n  [data-bs-theme=\"dark\"] .chroma .ne, [data-bs-theme=\"light\"] .chroma .ne {\n    color: #cf5967; }\n  [data-bs-theme=\"dark\"] .chroma .nf, [data-bs-theme=\"light\"] .chroma .nf {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .nl, [data-bs-theme=\"light\"] .chroma .nl {\n    color: #cf5967; }\n  [data-bs-theme=\"dark\"] .chroma .nt, [data-bs-theme=\"light\"] .chroma .nt {\n    color: #bc74c4; }\n  [data-bs-theme=\"dark\"] .chroma .nv, [data-bs-theme=\"light\"] .chroma .nv {\n    color: #bc74c4;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .vc, [data-bs-theme=\"light\"] .chroma .vc {\n    color: #57c7ff;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .vg, [data-bs-theme=\"light\"] .chroma .vg {\n    color: #ecbe7b; }\n  [data-bs-theme=\"dark\"] .chroma .vi, [data-bs-theme=\"light\"] .chroma .vi {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .ld, [data-bs-theme=\"light\"] .chroma .ld {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .s, [data-bs-theme=\"light\"] .chroma .s {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .sa, [data-bs-theme=\"light\"] .chroma .sa {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .sb, [data-bs-theme=\"light\"] .chroma .sb {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .sc, [data-bs-theme=\"light\"] .chroma .sc {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .dl, [data-bs-theme=\"light\"] .chroma .dl {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .sd, [data-bs-theme=\"light\"] .chroma .sd {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .s2, [data-bs-theme=\"light\"] .chroma .s2 {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .se, [data-bs-theme=\"light\"] .chroma .se {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .sh, [data-bs-theme=\"light\"] .chroma .sh {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .si, [data-bs-theme=\"light\"] .chroma .si {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .sx, [data-bs-theme=\"light\"] .chroma .sx {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .sr, [data-bs-theme=\"light\"] .chroma .sr {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .s1, [data-bs-theme=\"light\"] .chroma .s1 {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .ss, [data-bs-theme=\"light\"] .chroma .ss {\n    color: #82cc6a; }\n  [data-bs-theme=\"dark\"] .chroma .m, [data-bs-theme=\"light\"] .chroma .m {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .mb, [data-bs-theme=\"light\"] .chroma .mb {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .mf, [data-bs-theme=\"light\"] .chroma .mf {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .mh, [data-bs-theme=\"light\"] .chroma .mh {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .mi, [data-bs-theme=\"light\"] .chroma .mi {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .il, [data-bs-theme=\"light\"] .chroma .il {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .mo, [data-bs-theme=\"light\"] .chroma .mo {\n    color: #57c7ff; }\n  [data-bs-theme=\"dark\"] .chroma .o, [data-bs-theme=\"light\"] .chroma .o {\n    color: #bc74c4; }\n  [data-bs-theme=\"dark\"] .chroma .ow, [data-bs-theme=\"light\"] .chroma .ow {\n    color: #bc74c4; }\n  [data-bs-theme=\"dark\"] .chroma .p, [data-bs-theme=\"light\"] .chroma .p {\n    color: #56b6c2; }\n  [data-bs-theme=\"dark\"] .chroma .c, [data-bs-theme=\"light\"] .chroma .c {\n    color: #3e4460; }\n  [data-bs-theme=\"dark\"] .chroma .ch, [data-bs-theme=\"light\"] .chroma .ch {\n    color: #3e4460;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .cm, [data-bs-theme=\"light\"] .chroma .cm {\n    color: #3e4460; }\n  [data-bs-theme=\"dark\"] .chroma .c1, [data-bs-theme=\"light\"] .chroma .c1 {\n    color: #3e4460; }\n  [data-bs-theme=\"dark\"] .chroma .cs, [data-bs-theme=\"light\"] .chroma .cs {\n    color: #bc74c4;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .cp, [data-bs-theme=\"light\"] .chroma .cp {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .cpf, [data-bs-theme=\"light\"] .chroma .cpf {\n    color: #7fbaf5; }\n  [data-bs-theme=\"dark\"] .chroma .gd, [data-bs-theme=\"light\"] .chroma .gd {\n    color: #cf5967; }\n  [data-bs-theme=\"dark\"] .chroma .ge, [data-bs-theme=\"light\"] .chroma .ge {\n    text-decoration: underline; }\n  [data-bs-theme=\"dark\"] .chroma .gr, [data-bs-theme=\"light\"] .chroma .gr {\n    color: #cf5967;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .gh, [data-bs-theme=\"light\"] .chroma .gh {\n    color: #ecbe7b;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .gi, [data-bs-theme=\"light\"] .chroma .gi {\n    color: #ecbe7b; }\n  [data-bs-theme=\"dark\"] .chroma .go, [data-bs-theme=\"light\"] .chroma .go {\n    color: #43454f; }\n  [data-bs-theme=\"dark\"] .chroma .gs, [data-bs-theme=\"light\"] .chroma .gs {\n    color: #cf5967;\n    font-weight: bold; }\n  [data-bs-theme=\"dark\"] .chroma .gu, [data-bs-theme=\"light\"] .chroma .gu {\n    color: #cf5967;\n    font-style: italic; }\n  [data-bs-theme=\"dark\"] .chroma .gl, [data-bs-theme=\"light\"] .chroma .gl {\n    text-decoration: underline; }\n\n/*# sourceMappingURL=main.css.map */"
  },
  {
    "path": "docs/resources/_gen/assets/scss/app.scss_901a6e181e810c5c7347a10d84f037ab.json",
    "content": "{\"Target\":\"main.83702c5537fa0c04c34adb61a7648af280d09c019220547885486514a58362197cb2edbc80454e2ef087a4e5604abe50e4672cec25bf6db782f47ea6df8beff2.css\",\"MediaType\":\"text/css\",\"Data\":{\"Integrity\":\"sha512-g3AsVTf6DATDStthp2SK8oDQnAGSIFR4hUhlFKWDYhl8su28gEVOLvCHpOVgSr5Q5Gcs7CW/bbeC9H6m34vv8g==\"}}"
  },
  {
    "path": "docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.content",
    "content": ":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}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-5{font-size:3rem}}.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%}.col-xl-10{flex:0 0 auto;width:62.5%}}.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}.position-relative{position:relative !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-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !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}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-4{padding-right:1.5rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-5{padding-bottom:3rem !important}.ps-3{padding-left:1rem !important}.fs-5{font-size:1.25rem !important}.fw-bold{font-weight:700 !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}.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}.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\")}.content .svg-inline{margin-bottom:1.5rem}.content .svg-inline:not(.svg-inline-custom){height:1.875rem;width:1.875rem;stroke-width:1.5}.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 .callout-icon.svg-inline{flex-shrink:0;height:calc(1.5 * 1.125rem)}.callout .callout-title{font-weight:700}.callout-content{min-width:0}.callout.callout-note{border-color:var(--sl-color-blue);background-color:var(--sl-color-blue-high)}.callout.callout-note .callout-icon,.callout.callout-note .callout-title,.callout.callout-note .callout-body a{color:var(--sl-color-blue-low)}.callout.callout-note .callout-body,.callout.callout-note .callout-body a:hover,.callout.callout-note .callout-body a:active{color:var(--sl-color-white)}.callout.callout-danger{border-color:var(--sl-color-red);background-color:var(--sl-color-red-high)}.callout.callout-danger .callout-icon,.callout.callout-danger .callout-title,.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-note{border-color:var(--sl-color-blue);background-color:var(--sl-color-blue-low)}[data-bs-theme=\"dark\"] .callout.callout-note .callout-icon,[data-bs-theme=\"dark\"] .callout.callout-note .callout-title,[data-bs-theme=\"dark\"] .callout.callout-note .callout-body a{color:var(--sl-color-blue-high)}[data-bs-theme=\"dark\"] .callout.callout-note .callout-body,[data-bs-theme=\"dark\"] .callout.callout-note .callout-body a:hover,[data-bs-theme=\"dark\"] .callout.callout-note .callout-body a:active{color:var(--sl-color-white)}[data-bs-theme=\"dark\"] .callout.callout-note code:not(:where(.not-content *)){color:var(--ec-codeFg)}[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-icon,[data-bs-theme=\"dark\"] .callout.callout-danger .callout-title,[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}.homepage-features .row{max-width:1400px;margin:0 auto}@media (min-width: 992px){.text-center.text-lg-start{padding-right:2rem}}.learn-container{max-width:1200px;margin:0 auto;padding:3rem 1rem}.learn-header{text-align:center;margin-bottom:4rem}.learn-main-title{font-size:3rem;font-weight:700;margin-bottom:1rem;color:var(--bs-body-color)}.learn-subtitle{font-size:1.25rem;color:var(--bs-body-color-secondary);max-width:600px;margin:0 auto;line-height:1.6}.learn-section{margin-bottom:4rem}.learn-section:last-child{margin-bottom:0}.learn-section-title{text-align:center;font-size:2rem;font-weight:600;margin-bottom:3rem;color:var(--bs-body-color)}.learn-row{display:grid;grid-template-columns:repeat(3, 1fr);gap:2rem;justify-items:center}.learn-card{display:flex;flex-direction:column;padding:2rem;border-radius:0.75rem;transition:all 0.3s ease;text-decoration:none;background:var(--bs-body-bg);border:1px solid var(--bs-border-color);box-shadow:0 2px 4px rgba(0,0,0,0.05);min-height:280px;width:100%;max-width:350px;text-align:center;color:inherit;cursor:pointer;position:relative;overflow:hidden}.learn-card:hover{transform:translateY(-2px);box-shadow:0 8px 16px rgba(0,0,0,0.1);text-decoration:none;color:inherit;border-color:var(--bs-primary)}.learn-card:focus{outline:2px solid var(--bs-primary);outline-offset:2px}.learn-card-icon{margin:0 auto 0;color:var(--bs-primary);display:flex;justify-content:center;align-items:center;width:100px;height:100px}.learn-card h3,.learn-card .h3{font-size:1.25rem;font-weight:600;margin-bottom:0.5rem;color:var(--bs-body-color);margin-top:0}.learn-card-secondary{display:block;font-size:0.9rem;font-weight:600;color:var(--bs-gray-600);margin-bottom:1rem}.learn-card p{font-size:0.95rem;line-height:1.6;margin-bottom:2rem;flex-grow:1;color:var(--bs-body-color-secondary)}[data-bs-theme=\"dark\"] .learn-card{background:var(--bs-body-bg);border-color:var(--bs-border-color)}[data-bs-theme=\"dark\"] .learn-card:hover{border-color:var(--bs-primary)}@media (max-width: 767.98px){.learn-container{padding:2rem 1rem}.learn-header{margin-bottom:3rem}.learn-main-title{font-size:2.5rem}.learn-subtitle{font-size:1.1rem}.learn-section-title{font-size:1.5rem;margin-bottom:2rem}.learn-row{grid-template-columns:1fr;gap:1.5rem}.learn-card{padding:1.5rem;min-height:240px;max-width:none}}@media (min-width: 768px) and (max-width: 1199.98px){.learn-row{grid-template-columns:repeat(2, 1fr)}.learn-section:first-child .learn-row{grid-template-columns:repeat(2, 1fr)}.learn-section:first-child .learn-row .learn-card:nth-child(3){grid-column:1 / -1;justify-self:center}}.quickstart-container{max-width:800px;margin:0 auto;padding:3rem 1rem}.quickstart-header{text-align:center;margin-bottom:3rem}.quickstart-main-title{font-size:3rem;font-weight:700;margin-bottom:1rem;color:var(--bs-body-color)}.quickstart-subtitle{font-size:1.25rem;color:var(--bs-body-color-secondary);max-width:600px;margin:0 auto;line-height:1.6}.quickstart-content{font-size:1.1rem;line-height:1.8}body.quickstart .event-driven-banner{display:none}@media (max-width: 767.98px){.quickstart-container{padding:2rem 1rem}.quickstart-header{margin-bottom:2rem}.quickstart-main-title{font-size:2.5rem}.quickstart-subtitle{font-size:1.1rem}}@media (max-width: 991.98px){.display-5{font-size:2rem}.text-center.text-lg-start{text-align:center !important;padding-right:0}}.pubsub-logos-container{padding:2rem 0}.pubsub-logos-grid{display:flex;flex-wrap:wrap;gap:2rem;max-width:900px;margin:0 auto;align-items:center;justify-content:center}.pubsub-logo-item{display:flex;flex-direction:column;align-items:center;text-align:center;transition:all 0.3s ease;padding:1rem;border-radius:0.75rem;text-decoration:none;color:inherit;flex:0 0 120px}.pubsub-logo-item img{width:60px;height:60px;margin-bottom:0.75rem;transition:all 0.3s ease}.pubsub-logo-item span{font-size:0.9rem;font-weight:600;color:var(--bs-body-color-secondary);transition:all 0.3s ease}.pubsub-logo-item:hover{background:rgba(var(--bs-primary-rgb), 0.05);text-decoration:none;color:inherit}.pubsub-logo-item:hover span{color:var(--bs-primary)}.pubsub-logo-item:focus{outline:2px solid var(--bs-primary);outline-offset:2px;text-decoration:none;color:inherit}[data-bs-theme=\"dark\"] .pubsub-logo-item:hover{background:rgba(var(--bs-primary-rgb), 0.1)}@media (max-width: 767.98px){.pubsub-logos-grid{grid-template-columns:repeat(3, 1fr);gap:1.5rem;max-width:100%}.pubsub-logo-item{padding:0.75rem}.pubsub-logo-item img{width:50px;height:50px;margin-bottom:0.5rem}.pubsub-logo-item span{font-size:0.8rem}}@media (max-width: 575.98px){.pubsub-logos-grid{grid-template-columns:repeat(2, 1fr);gap:1rem}.pubsub-logo-item img{width:45px;height:45px}.pubsub-logo-item span{font-size:0.75rem}}.homepage-code-block .frame{border-radius:12px !important;box-shadow:0 4px 12px rgba(0,0,0,0.1),0 2px 4px rgba(0,0,0,0.06) !important;border:1px solid var(--bs-border-color) !important;overflow:hidden;overflow-x:auto}.homepage-code-block .frame *{border-radius:inherit !important}.homepage-code-block .frame pre,.homepage-code-block .frame code,.homepage-code-block .frame .expressive-code-block{border-radius:12px !important;border:none !important}.homepage-code-block .frame .highlight{border-radius:12px !important;border:none !important}[data-bs-theme=\"light\"] .homepage-code-block .frame pre,[data-bs-theme=\"light\"] .homepage-code-block .frame code,[data-bs-theme=\"light\"] .homepage-code-block .frame .expressive-code-block{background:white !important}@media (max-width: 767.98px){.homepage-code-block{margin-left:-45px;margin-right:-45px}}[data-bs-theme=\"dark\"] .homepage-code-block .frame{box-shadow:0 4px 12px rgba(0,0,0,0.3),0 2px 4px rgba(0,0,0,0.2) !important;border:1px solid var(--bs-border-color) !important}.gradient-text-1{background:linear-gradient(to right, #6366f1, #8b5cf6, #3b82f6);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}.homepage-section-gradient{position:relative}.homepage-section-gradient::before{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);width:100vw;height:100%;border-top:1px solid rgba(0,0,0,0.08);pointer-events:none;z-index:-1}.homepage-section-gradient::before{background:linear-gradient(180deg, rgba(79,70,229,0.03) 0%, transparent 100%)}[data-bs-theme=\"dark\"] .homepage-section-gradient::before{background:linear-gradient(180deg, rgba(79,70,229,0.06) 0%, transparent 100%);border-top:1px solid rgba(255,255,255,0.08)}[data-bs-theme=\"dark\"] .bg,[data-bs-theme=\"light\"] .bg{color:#c9c9c9;background-color:#282c34}[data-bs-theme=\"dark\"] .chroma,[data-bs-theme=\"light\"] .chroma{color:#c9c9c9;background-color:#282c34}[data-bs-theme=\"dark\"] .chroma .err,[data-bs-theme=\"light\"] .chroma .err{color:#cf5967}[data-bs-theme=\"dark\"] .chroma .lnlinks,[data-bs-theme=\"light\"] .chroma .lnlinks{outline:none;text-decoration:none;color:inherit}[data-bs-theme=\"dark\"] .chroma .lntd,[data-bs-theme=\"light\"] .chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}[data-bs-theme=\"dark\"] .chroma .lntable,[data-bs-theme=\"light\"] .chroma .lntable{border-spacing:0;padding:0;margin:0;border:0}[data-bs-theme=\"dark\"] .chroma .hl,[data-bs-theme=\"light\"] .chroma .hl{background-color:#3d4148}[data-bs-theme=\"dark\"] .chroma .lnt,[data-bs-theme=\"light\"] .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}[data-bs-theme=\"dark\"] .chroma .ln,[data-bs-theme=\"light\"] .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}[data-bs-theme=\"dark\"] .chroma .line,[data-bs-theme=\"light\"] .chroma .line{display:flex}[data-bs-theme=\"dark\"] .chroma .k,[data-bs-theme=\"light\"] .chroma .k{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .kc,[data-bs-theme=\"light\"] .chroma .kc{color:#cf5967}[data-bs-theme=\"dark\"] .chroma .kd,[data-bs-theme=\"light\"] .chroma .kd{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .kn,[data-bs-theme=\"light\"] .chroma .kn{color:#bc74c4}[data-bs-theme=\"dark\"] .chroma .kp,[data-bs-theme=\"light\"] .chroma .kp{color:#bc74c4}[data-bs-theme=\"dark\"] .chroma .kr,[data-bs-theme=\"light\"] .chroma .kr{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .kt,[data-bs-theme=\"light\"] .chroma .kt{color:#57c7ff;font-weight:bold}[data-bs-theme=\"dark\"] .chroma .na,[data-bs-theme=\"light\"] .chroma .na{color:#bc74c4}[data-bs-theme=\"dark\"] .chroma .nb,[data-bs-theme=\"light\"] .chroma .nb{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .bp,[data-bs-theme=\"light\"] .chroma .bp{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .nc,[data-bs-theme=\"light\"] .chroma .nc{color:#ecbe7b}[data-bs-theme=\"dark\"] .chroma .no,[data-bs-theme=\"light\"] .chroma .no{color:#ecbe7b}[data-bs-theme=\"dark\"] .chroma .nd,[data-bs-theme=\"light\"] .chroma .nd{color:#ecbe7b}[data-bs-theme=\"dark\"] .chroma .ne,[data-bs-theme=\"light\"] .chroma .ne{color:#cf5967}[data-bs-theme=\"dark\"] .chroma .nf,[data-bs-theme=\"light\"] .chroma .nf{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .nl,[data-bs-theme=\"light\"] .chroma .nl{color:#cf5967}[data-bs-theme=\"dark\"] .chroma .nt,[data-bs-theme=\"light\"] .chroma .nt{color:#bc74c4}[data-bs-theme=\"dark\"] .chroma .nv,[data-bs-theme=\"light\"] .chroma .nv{color:#bc74c4;font-style:italic}[data-bs-theme=\"dark\"] .chroma .vc,[data-bs-theme=\"light\"] .chroma .vc{color:#57c7ff;font-weight:bold}[data-bs-theme=\"dark\"] .chroma .vg,[data-bs-theme=\"light\"] .chroma .vg{color:#ecbe7b}[data-bs-theme=\"dark\"] .chroma .vi,[data-bs-theme=\"light\"] .chroma .vi{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .ld,[data-bs-theme=\"light\"] .chroma .ld{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .s,[data-bs-theme=\"light\"] .chroma .s{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .sa,[data-bs-theme=\"light\"] .chroma .sa{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .sb,[data-bs-theme=\"light\"] .chroma .sb{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .sc,[data-bs-theme=\"light\"] .chroma .sc{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .dl,[data-bs-theme=\"light\"] .chroma .dl{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .sd,[data-bs-theme=\"light\"] .chroma .sd{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .s2,[data-bs-theme=\"light\"] .chroma .s2{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .se,[data-bs-theme=\"light\"] .chroma .se{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .sh,[data-bs-theme=\"light\"] .chroma .sh{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .si,[data-bs-theme=\"light\"] .chroma .si{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .sx,[data-bs-theme=\"light\"] .chroma .sx{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .sr,[data-bs-theme=\"light\"] .chroma .sr{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .s1,[data-bs-theme=\"light\"] .chroma .s1{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .ss,[data-bs-theme=\"light\"] .chroma .ss{color:#82cc6a}[data-bs-theme=\"dark\"] .chroma .m,[data-bs-theme=\"light\"] .chroma .m{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .mb,[data-bs-theme=\"light\"] .chroma .mb{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .mf,[data-bs-theme=\"light\"] .chroma .mf{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .mh,[data-bs-theme=\"light\"] .chroma .mh{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .mi,[data-bs-theme=\"light\"] .chroma .mi{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .il,[data-bs-theme=\"light\"] .chroma .il{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .mo,[data-bs-theme=\"light\"] .chroma .mo{color:#57c7ff}[data-bs-theme=\"dark\"] .chroma .o,[data-bs-theme=\"light\"] .chroma .o{color:#bc74c4}[data-bs-theme=\"dark\"] .chroma .ow,[data-bs-theme=\"light\"] .chroma .ow{color:#bc74c4}[data-bs-theme=\"dark\"] .chroma .p,[data-bs-theme=\"light\"] .chroma .p{color:#56b6c2}[data-bs-theme=\"dark\"] .chroma .c,[data-bs-theme=\"light\"] .chroma .c{color:#3e4460}[data-bs-theme=\"dark\"] .chroma .ch,[data-bs-theme=\"light\"] .chroma .ch{color:#3e4460;font-style:italic}[data-bs-theme=\"dark\"] .chroma .cm,[data-bs-theme=\"light\"] .chroma .cm{color:#3e4460}[data-bs-theme=\"dark\"] .chroma .c1,[data-bs-theme=\"light\"] .chroma .c1{color:#3e4460}[data-bs-theme=\"dark\"] .chroma .cs,[data-bs-theme=\"light\"] .chroma .cs{color:#bc74c4;font-style:italic}[data-bs-theme=\"dark\"] .chroma .cp,[data-bs-theme=\"light\"] .chroma .cp{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .cpf,[data-bs-theme=\"light\"] .chroma .cpf{color:#7fbaf5}[data-bs-theme=\"dark\"] .chroma .gd,[data-bs-theme=\"light\"] .chroma .gd{color:#cf5967}[data-bs-theme=\"dark\"] .chroma .ge,[data-bs-theme=\"light\"] .chroma .ge{text-decoration:underline}[data-bs-theme=\"dark\"] .chroma .gr,[data-bs-theme=\"light\"] .chroma .gr{color:#cf5967;font-weight:bold}[data-bs-theme=\"dark\"] .chroma .gh,[data-bs-theme=\"light\"] .chroma .gh{color:#ecbe7b;font-weight:bold}[data-bs-theme=\"dark\"] .chroma .gi,[data-bs-theme=\"light\"] .chroma .gi{color:#ecbe7b}[data-bs-theme=\"dark\"] .chroma .go,[data-bs-theme=\"light\"] .chroma .go{color:#43454f}[data-bs-theme=\"dark\"] .chroma .gs,[data-bs-theme=\"light\"] .chroma .gs{color:#cf5967;font-weight:bold}[data-bs-theme=\"dark\"] .chroma .gu,[data-bs-theme=\"light\"] .chroma .gu{color:#cf5967;font-style:italic}[data-bs-theme=\"dark\"] .chroma .gl,[data-bs-theme=\"light\"] .chroma .gl{text-decoration:underline}\n"
  },
  {
    "path": "docs/resources/_gen/assets/scss/app.scss_cdf9d7c9eb97e4550ded64a8776dd9e8.json",
    "content": "{\"Target\":\"main.c9aa351ca37dda2041352b32354756af1febb20a2d4a399b26dde00035e7b7022e9af6d4482f9f9f9d29aacdc8f99a6a6a01884a63bb53d6e3974dd6629034c5.css\",\"MediaType\":\"text/css\",\"Data\":{\"Integrity\":\"sha512-yao1HKN92iBBNSsyNUdWrx/rsgotSjmbJt3gADXntwIumvbUSC+fn50pqs3I+ZpqagGISmO7U9bjl03WYpA0xQ==\"}}"
  },
  {
    "path": "docs/static/.gitkeep",
    "content": ""
  },
  {
    "path": "go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill\n\ngo 1.23.0\n\ntoolchain go1.24.3\n\nrequire (\n\tgithub.com/cenkalti/backoff/v5 v5.0.3\n\tgithub.com/go-chi/chi/v5 v5.2.2\n\tgithub.com/gogo/protobuf v1.3.2\n\tgithub.com/golang/protobuf v1.5.4\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7\n\tgithub.com/oklog/ulid v1.3.1\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/prometheus/client_golang v1.23.0\n\tgithub.com/sony/gobreaker v1.0.0\n\tgithub.com/stretchr/testify v1.11.0\n\tgoogle.golang.org/protobuf v1.36.8\n)\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.65.0 // indirect\n\tgithub.com/prometheus/procfs v0.17.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=\ngithub.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=\ngithub.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=\ngithub.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=\ngithub.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=\ngithub.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=\ngithub.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=\ngithub.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=\ngithub.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=\ngithub.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=\ngithub.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=\ngithub.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=\ngithub.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=\ngithub.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=\ngithub.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=\ngithub.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=\ngithub.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=\ngithub.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=\ngithub.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=\ngithub.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=\ngolang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/channel.go",
    "content": "package internal\n\n// IsChannelClosed returns true if provided `chan struct{}` is closed.\n// IsChannelClosed panics if message is sent to this channel.\nfunc IsChannelClosed(channel chan struct{}) bool {\n\tselect {\n\tcase _, ok := <-channel:\n\t\tif ok {\n\t\t\tpanic(\"received unexpected message\")\n\t\t}\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/channel_test.go",
    "content": "package internal_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/internal\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsChannelClosed(t *testing.T) {\n\tclosed := make(chan struct{})\n\tclose(closed)\n\n\twithSentValue := make(chan struct{}, 1)\n\twithSentValue <- struct{}{}\n\n\ttestCases := []struct {\n\t\tName           string\n\t\tChannel        chan struct{}\n\t\tExpectedPanic  bool\n\t\tExpectedClosed bool\n\t}{\n\t\t{\n\t\t\tName:           \"not_closed\",\n\t\t\tChannel:        make(chan struct{}),\n\t\t\tExpectedPanic:  false,\n\t\t\tExpectedClosed: false,\n\t\t},\n\t\t{\n\t\t\tName:           \"closed\",\n\t\t\tChannel:        closed,\n\t\t\tExpectedPanic:  false,\n\t\t\tExpectedClosed: true,\n\t\t},\n\t\t{\n\t\t\tName:           \"with_sent_value\",\n\t\t\tChannel:        withSentValue,\n\t\t\tExpectedPanic:  true,\n\t\t\tExpectedClosed: false,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\ttestFunc := func() {\n\t\t\t\tclosed := internal.IsChannelClosed(c.Channel)\n\t\t\t\tassert.EqualValues(t, c.ExpectedClosed, closed)\n\t\t\t}\n\n\t\t\tif c.ExpectedPanic {\n\t\t\t\tassert.Panics(t, testFunc)\n\t\t\t} else {\n\t\t\t\tassert.NotPanics(t, testFunc)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/name.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// StructName returns a normalized name of the passed structure.\nfunc StructName(v interface{}) string {\n\tif s, ok := v.(fmt.Stringer); ok {\n\t\treturn s.String()\n\t}\n\n\ts := fmt.Sprintf(\"%T\", v)\n\t// trim the pointer marker, if any\n\treturn strings.TrimLeft(s, \"*\")\n}\n"
  },
  {
    "path": "internal/name_test.go",
    "content": "package internal_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/internal\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype testStruct struct{}\n\ntype stringerStruct struct{}\n\nfunc (stringerStruct) String() string {\n\treturn \"stringer\"\n}\n\nfunc TestStructName(t *testing.T) {\n\ttestCases := []struct {\n\t\tName         string\n\t\tStruct       interface{}\n\t\tExpectedName string\n\t}{\n\t\t{\n\t\t\tName:         \"simple_struct\",\n\t\t\tStruct:       testStruct{},\n\t\t\tExpectedName: \"internal_test.testStruct\",\n\t\t},\n\t\t{\n\t\t\tName:         \"pointer_struct\",\n\t\t\tStruct:       &testStruct{},\n\t\t\tExpectedName: \"internal_test.testStruct\",\n\t\t},\n\t\t{\n\t\t\tName:         \"stringer\",\n\t\t\tStruct:       stringerStruct{},\n\t\t\tExpectedName: \"stringer\",\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\ts := internal.StructName(c.Struct)\n\t\t\tassert.Equal(t, c.ExpectedName, s)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/norace.go",
    "content": "//go:build !race\n// +build !race\n\npackage internal\n\nconst RaceEnabled = false\n"
  },
  {
    "path": "internal/publisher/errors.go",
    "content": "package publisher\n\nimport (\n\t\"strings\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype ErrCouldNotPublish struct {\n\treasons map[string]error\n}\n\nfunc (e *ErrCouldNotPublish) addMsg(msg *message.Message, reason error) {\n\te.reasons[msg.UUID] = reason\n}\n\nfunc NewErrCouldNotPublish() *ErrCouldNotPublish {\n\treturn &ErrCouldNotPublish{make(map[string]error)}\n}\n\nfunc (e ErrCouldNotPublish) Len() int {\n\treturn len(e.reasons)\n}\n\nfunc (e ErrCouldNotPublish) Error() string {\n\tif len(e.reasons) == 0 {\n\t\treturn \"\"\n\t}\n\tb := strings.Builder{}\n\tb.WriteString(\"Could not publish the messages:\\n\")\n\tfor uuid, reason := range e.reasons {\n\t\tb.WriteString(uuid + \" : \" + reason.Error() + \"\\n\")\n\t}\n\treturn b.String()\n}\n\nfunc (e ErrCouldNotPublish) Reasons() map[string]error {\n\treturn e.reasons\n}\n"
  },
  {
    "path": "internal/publisher/retry.go",
    "content": "package publisher\n\nimport (\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\n\t\"github.com/pkg/errors\"\n)\n\nvar (\n\tErrNonPositiveNumberOfRetries  = errors.New(\"number of retries should be positive\")\n\tErrNonPositiveTimeToFirstRetry = errors.New(\"time to first retry should be positive\")\n)\n\ntype RetryPublisherConfig struct {\n\tMaxRetries int\n\t// each subsequent retry doubles the time to next retry.\n\tTimeToFirstRetry time.Duration\n\tLogger           watermill.LoggerAdapter\n}\n\nfunc (c *RetryPublisherConfig) setDefaults() {\n\tif c.MaxRetries == 0 {\n\t\tc.MaxRetries = 5\n\t}\n\n\tif c.TimeToFirstRetry == 0 {\n\t\tc.TimeToFirstRetry = time.Second\n\t}\n\n\tif c.Logger == nil {\n\t\tc.Logger = watermill.NopLogger{}\n\t}\n}\n\nfunc (c RetryPublisherConfig) validate() error {\n\tif c.MaxRetries <= 0 {\n\t\treturn ErrNonPositiveNumberOfRetries\n\t}\n\tif c.TimeToFirstRetry <= 0 {\n\t\treturn ErrNonPositiveTimeToFirstRetry\n\t}\n\n\treturn nil\n}\n\n// RetryPublisher is a decorator for a publisher that retries message publishing after a failure.\ntype RetryPublisher struct {\n\tpub    message.Publisher\n\tconfig RetryPublisherConfig\n}\n\nfunc NewRetryPublisher(pub message.Publisher, config RetryPublisherConfig) (*RetryPublisher, error) {\n\tconfig.setDefaults()\n\n\tif err := config.validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid RetryPublisher config\")\n\t}\n\n\treturn &RetryPublisher{\n\t\tpub,\n\t\tconfig,\n\t}, nil\n}\n\nfunc (p RetryPublisher) Publish(topic string, messages ...*message.Message) error {\n\tfailedMessages := NewErrCouldNotPublish()\n\n\t// todo: do some parallel processing maybe? this is a very basic implementation\n\tfor _, msg := range messages {\n\t\terr := p.send(topic, msg)\n\t\tif err != nil {\n\t\t\tfailedMessages.addMsg(msg, err)\n\t\t}\n\t}\n\n\tif failedMessages.Len() > 0 {\n\t\treturn failedMessages\n\t}\n\n\treturn nil\n}\n\nfunc (p RetryPublisher) Close() error {\n\treturn p.pub.Close()\n}\n\n// send sends one message at a time to prevent sending a successful message more than once.\nfunc (p RetryPublisher) send(topic string, msg *message.Message) error {\n\tvar err error\n\ttimeToNextRetry := p.config.TimeToFirstRetry\n\n\tfor i := 0; i < p.config.MaxRetries; i++ {\n\t\terr = p.pub.Publish(topic, msg)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tp.config.Logger.Info(\"Publish failed, retrying in \"+timeToNextRetry.String(), watermill.LogFields{\n\t\t\t\"error\": err,\n\t\t})\n\t\ttime.Sleep(timeToNextRetry)\n\t\ttimeToNextRetry *= 2\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/publisher/retry_test.go",
    "content": "package publisher_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/internal/publisher\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\nvar errCouldNotPublish = errors.New(\"could not publish, try again\")\n\n// FailingPublisher mocks a publisher that fails a specific number of time for a message, then succeeds.\ntype FailingPublisher struct {\n\thowManyFails     map[string]int\n\thowManyPublished map[string]int\n}\n\nfunc (p *FailingPublisher) Publish(topic string, messages ...*message.Message) error {\n\tfor _, msg := range messages {\n\t\thowManyFails, ok := p.howManyFails[msg.UUID]\n\t\tif !ok || howManyFails <= 0 {\n\t\t\tp.publish(msg)\n\t\t\tcontinue\n\t\t}\n\t\tp.howManyFails[msg.UUID]--\n\t\treturn errCouldNotPublish\n\t}\n\n\treturn nil\n}\n\nfunc (p *FailingPublisher) publish(msg *message.Message) {\n\tif _, ok := p.howManyPublished[msg.UUID]; !ok {\n\t\tp.howManyPublished[msg.UUID] = 1\n\t\treturn\n\t}\n\tp.howManyPublished[msg.UUID]++\n}\n\nfunc (p *FailingPublisher) Close() error {\n\treturn nil\n}\n\nfunc TestRetryPublisher_Publish_after_retries(t *testing.T) {\n\tmsg := message.NewMessage(\"uuid\", []byte{})\n\tpub := FailingPublisher{\n\t\thowManyFails: map[string]int{\n\t\t\tmsg.UUID: 4,\n\t\t},\n\t\thowManyPublished: map[string]int{},\n\t}\n\tconf := publisher.RetryPublisherConfig{\n\t\tMaxRetries:       5,\n\t\tTimeToFirstRetry: time.Millisecond,\n\t\tLogger:           watermill.NopLogger{},\n\t}\n\tretryPub, err := publisher.NewRetryPublisher(&pub, conf)\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.True(t, pub.howManyFails[msg.UUID] < conf.MaxRetries, \"Publisher must fail less than MaxRetries times\")\n\n\t// when\n\terr = retryPub.Publish(\"topic\", msg)\n\n\t// then\n\trequire.NoError(t, err)\n\tassert.Contains(t, pub.howManyPublished, msg.UUID)\n\tassert.Equal(t, pub.howManyPublished[msg.UUID], 1, \"Expected msg to be published exactly once\")\n}\n\nfunc TestRetryPublisher_Publish_too_many_retries(t *testing.T) {\n\tmsg := message.NewMessage(\"uuid\", []byte{})\n\tpub := FailingPublisher{\n\t\thowManyFails: map[string]int{\n\t\t\tmsg.UUID: 5,\n\t\t},\n\t\thowManyPublished: map[string]int{},\n\t}\n\tconf := publisher.RetryPublisherConfig{\n\t\tMaxRetries:       5,\n\t\tTimeToFirstRetry: time.Millisecond,\n\t\tLogger:           watermill.NopLogger{},\n\t}\n\tretryPub, err := publisher.NewRetryPublisher(&pub, conf)\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.True(t, pub.howManyFails[msg.UUID] >= conf.MaxRetries, \"Publisher must fail at least MaxRetries times\")\n\n\t// when\n\terr = retryPub.Publish(\"topic\", msg)\n\n\t// then\n\trequire.Error(t, err)\n\n\tcnpErr, ok := err.(*publisher.ErrCouldNotPublish)\n\trequire.True(t, ok, \"expected the ErrCouldNotPublish composite error type\")\n\n\tassert.Equal(t, 1, cnpErr.Len(), \"attempted to publish one message, expecting one error\")\n\tassert.Equal(t, errCouldNotPublish, errors.Cause(cnpErr.Reasons()[msg.UUID]))\n\n\tassert.NotContains(t, pub.howManyPublished, msg.UUID, \"expected msg to not be published\")\n}\n\nfunc TestPublishEachMessageOnlyOnce(t *testing.T) {\n\tmsg1 := message.NewMessage(\"uuid1\", []byte{})\n\tmsg2 := message.NewMessage(\"uuid2\", []byte{})\n\n\tpub := FailingPublisher{\n\t\thowManyFails: map[string]int{\n\t\t\tmsg1.UUID: 2,\n\t\t\tmsg2.UUID: 4,\n\t\t},\n\t\thowManyPublished: map[string]int{},\n\t}\n\tconf := publisher.RetryPublisherConfig{\n\t\tMaxRetries:       5,\n\t\tTimeToFirstRetry: time.Millisecond,\n\t\tLogger:           watermill.NopLogger{},\n\t}\n\tretryPub, err := publisher.NewRetryPublisher(&pub, conf)\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.True(t, pub.howManyFails[msg1.UUID] < conf.MaxRetries, \"Publisher must fail less than MaxRetries times for msg1\")\n\trequire.True(t, pub.howManyFails[msg2.UUID] < conf.MaxRetries, \"Publisher must fail less than MaxRetries times for msg2\")\n\trequire.True(t, pub.howManyFails[msg1.UUID] < pub.howManyFails[msg2.UUID], \"Publisher must fail less times for msg1 than msg2\")\n\n\t// when\n\terr = retryPub.Publish(\"topic\", msg1, msg2)\n\n\t// then\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, pub.howManyPublished[msg1.UUID], 1, \"expected msg1 to be published only once\")\n\tassert.Equal(t, pub.howManyPublished[msg2.UUID], 1, \"expected msg2 to be published only once\")\n}\n\nvar ErrCouldNotClose = errors.New(\"this publisher fails on Close()\")\n\n// ClosingPublisher mocks a publisher that may fail on closing so we may check if the Close() call propagated correctly.\ntype ClosingPublisher struct {\n\tclosed      bool\n\tfailOnClose bool\n}\n\nfunc (ClosingPublisher) Publish(topic string, messages ...*message.Message) error {\n\treturn nil\n}\n\nfunc (p *ClosingPublisher) Close() error {\n\tif p.failOnClose {\n\t\treturn ErrCouldNotClose\n\t}\n\tp.closed = true\n\treturn nil\n}\n\nfunc TestRetryPublisher_Close(t *testing.T) {\n\tpub := ClosingPublisher{}\n\tretryPub, err := publisher.NewRetryPublisher(&pub, publisher.RetryPublisherConfig{})\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.False(t, pub.closed)\n\n\t// when\n\terr = retryPub.Close()\n\n\t// then\n\trequire.NoError(t, err)\n\tassert.True(t, pub.closed)\n}\n\nfunc TestRetryPublisher_Close_failed(t *testing.T) {\n\tpub := ClosingPublisher{failOnClose: true}\n\tretryPub, err := publisher.NewRetryPublisher(&pub, publisher.RetryPublisherConfig{})\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.False(t, pub.closed)\n\n\t// when\n\terr = retryPub.Close()\n\n\t// then\n\trequire.Error(t, err)\n\tassert.Equal(t, ErrCouldNotClose, errors.Cause(err))\n}\n"
  },
  {
    "path": "internal/race.go",
    "content": "//go:build race\n// +build race\n\npackage internal\n\nconst RaceEnabled = true\n"
  },
  {
    "path": "internal/subscriber/multiplier.go",
    "content": "package subscriber\n\nimport (\n\t\"context\"\n\tstdErrors \"errors\"\n\t\"sync\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// Constructor is a function that creates a subscriber.\ntype Constructor func() (message.Subscriber, error)\n\ntype multiplier struct {\n\tsubscriberConstructor func() (message.Subscriber, error)\n\tsubscribersCount      int\n\tsubscribers           []message.Subscriber\n}\n\n// NewMultiplier returns multiplier subscriber decorator,\n// which under the hood is calling subscribe multiple times to increase throughput.\nfunc NewMultiplier(constructor Constructor, subscribersCount int) message.Subscriber {\n\treturn &multiplier{\n\t\tsubscriberConstructor: constructor,\n\t\tsubscribersCount:      subscribersCount,\n\t}\n}\n\nfunc (s *multiplier) Subscribe(ctx context.Context, topic string) (msgs <-chan *message.Message, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif closeErr := s.Close(); closeErr != nil {\n\t\t\t\terr = stdErrors.Join(err, closeErr)\n\t\t\t}\n\t\t}\n\t}()\n\n\tout := make(chan *message.Message)\n\n\tsubWg := sync.WaitGroup{}\n\tsubWg.Add(s.subscribersCount)\n\n\tfor i := 0; i < s.subscribersCount; i++ {\n\t\tsub, err := s.subscriberConstructor()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot create subscriber\")\n\t\t}\n\n\t\ts.subscribers = append(s.subscribers, sub)\n\n\t\tmsgs, err := sub.Subscribe(ctx, topic)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cannot subscribe\")\n\t\t}\n\n\t\tgo func() {\n\t\t\tfor msg := range msgs {\n\t\t\t\tout <- msg\n\t\t\t}\n\t\t\tsubWg.Done()\n\t\t}()\n\t}\n\n\tgo func() {\n\t\tsubWg.Wait()\n\t\tclose(out)\n\t}()\n\n\treturn out, nil\n}\n\nfunc (s *multiplier) Close() error {\n\tvar err error\n\n\tfor _, sub := range s.subscribers {\n\t\tif closeErr := sub.Close(); closeErr != nil {\n\t\t\terr = stdErrors.Join(err, closeErr)\n\t\t}\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "log.go",
    "content": "package watermill\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"maps\"\n\t\"os\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// LogFields is the logger's key-value list of fields.\ntype LogFields map[string]interface{}\n\n// Add adds new fields to the list of LogFields.\nfunc (l LogFields) Add(newFields LogFields) LogFields {\n\tresultFields := make(LogFields, len(l)+len(newFields))\n\n\tmaps.Copy(resultFields, l)\n\tmaps.Copy(resultFields, newFields)\n\n\treturn resultFields\n}\n\n// Copy copies the LogFields.\nfunc (l LogFields) Copy() LogFields {\n\tcpy := make(LogFields, len(l))\n\tmaps.Copy(cpy, l)\n\n\treturn cpy\n}\n\n// LoggerAdapter is an interface, that you need to implement to support Watermill logging.\n// You can use StdLoggerAdapter as a reference implementation.\ntype LoggerAdapter interface {\n\tError(msg string, err error, fields LogFields)\n\tInfo(msg string, fields LogFields)\n\tDebug(msg string, fields LogFields)\n\tTrace(msg string, fields LogFields)\n\tWith(fields LogFields) LoggerAdapter\n}\n\n// NopLogger is a logger which discards all logs.\ntype NopLogger struct{}\n\nfunc (NopLogger) Error(msg string, err error, fields LogFields) {}\nfunc (NopLogger) Info(msg string, fields LogFields)             {}\nfunc (NopLogger) Debug(msg string, fields LogFields)            {}\nfunc (NopLogger) Trace(msg string, fields LogFields)            {}\nfunc (l NopLogger) With(fields LogFields) LoggerAdapter         { return l }\n\n// StdLoggerAdapter is a logger implementation, which sends all logs to provided standard output.\ntype StdLoggerAdapter struct {\n\tErrorLogger *log.Logger\n\tInfoLogger  *log.Logger\n\tDebugLogger *log.Logger\n\tTraceLogger *log.Logger\n\n\tfields LogFields\n}\n\n// NewStdLogger creates StdLoggerAdapter which sends all logs to stderr.\nfunc NewStdLogger(debug, trace bool) LoggerAdapter {\n\treturn NewStdLoggerWithOut(os.Stderr, debug, trace)\n}\n\n// NewStdLoggerWithOut creates StdLoggerAdapter which sends all logs to provided io.Writer.\nfunc NewStdLoggerWithOut(out io.Writer, debug bool, trace bool) LoggerAdapter {\n\tl := log.New(out, \"[watermill] \", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)\n\ta := &StdLoggerAdapter{InfoLogger: l, ErrorLogger: l}\n\n\tif debug {\n\t\ta.DebugLogger = l\n\t}\n\tif trace {\n\t\ta.TraceLogger = l\n\t}\n\n\treturn a\n}\n\nfunc (l *StdLoggerAdapter) Error(msg string, err error, fields LogFields) {\n\tl.log(l.ErrorLogger, \"ERROR\", msg, fields.Add(LogFields{\"err\": err}))\n}\n\nfunc (l *StdLoggerAdapter) Info(msg string, fields LogFields) {\n\tl.log(l.InfoLogger, \"INFO \", msg, fields)\n}\n\nfunc (l *StdLoggerAdapter) Debug(msg string, fields LogFields) {\n\tl.log(l.DebugLogger, \"DEBUG\", msg, fields)\n}\n\nfunc (l *StdLoggerAdapter) Trace(msg string, fields LogFields) {\n\tl.log(l.TraceLogger, \"TRACE\", msg, fields)\n}\n\nfunc (l *StdLoggerAdapter) With(fields LogFields) LoggerAdapter {\n\treturn &StdLoggerAdapter{\n\t\tErrorLogger: l.ErrorLogger,\n\t\tInfoLogger:  l.InfoLogger,\n\t\tDebugLogger: l.DebugLogger,\n\t\tTraceLogger: l.TraceLogger,\n\t\tfields:      l.fields.Add(fields),\n\t}\n}\n\nfunc (l *StdLoggerAdapter) log(logger *log.Logger, level string, msg string, fields LogFields) {\n\tif logger == nil {\n\t\treturn\n\t}\n\n\tfieldsStr := \"\"\n\n\tallFields := l.fields.Add(fields)\n\n\tkeys := make([]string, len(allFields))\n\ti := 0\n\tfor field := range allFields {\n\t\tkeys[i] = field\n\t\ti++\n\t}\n\n\tsort.Strings(keys)\n\n\tfor _, key := range keys {\n\t\tvar valueStr string\n\t\tvalue := allFields[key]\n\n\t\tif stringer, ok := value.(fmt.Stringer); ok {\n\t\t\tvalueStr = stringer.String()\n\t\t} else {\n\t\t\tvalueStr = fmt.Sprintf(\"%v\", value)\n\t\t}\n\n\t\tif strings.Contains(valueStr, \" \") {\n\t\t\tvalueStr = `\"` + valueStr + `\"`\n\t\t}\n\n\t\tfieldsStr += key + \"=\" + valueStr + \" \"\n\t}\n\n\t_ = logger.Output(3, fmt.Sprintf(\"\\t\"+`level=%s msg=\"%s\" %s`, level, msg, fieldsStr))\n}\n\ntype LogLevel uint\n\nconst (\n\tTraceLogLevel LogLevel = iota + 1\n\tDebugLogLevel\n\tInfoLogLevel\n\tErrorLogLevel\n)\n\ntype CapturedMessage struct {\n\tLevel  LogLevel\n\tTime   time.Time\n\tFields LogFields\n\tMsg    string\n\tErr    error\n}\n\nfunc (c CapturedMessage) ContentEquals(other CapturedMessage) bool {\n\treturn c.Level == other.Level &&\n\t\treflect.DeepEqual(c.Fields, other.Fields) &&\n\t\tc.Msg == other.Msg &&\n\t\terrors.Is(c.Err, other.Err)\n}\n\n// CaptureLoggerAdapter is a logger which captures all logs.\n// This logger is mostly useful for testing logging.\ntype CaptureLoggerAdapter struct {\n\tcaptured map[LogLevel][]CapturedMessage\n\tfields   LogFields\n\tlock     *sync.Mutex\n}\n\nfunc NewCaptureLogger() *CaptureLoggerAdapter {\n\treturn &CaptureLoggerAdapter{\n\t\tcaptured: map[LogLevel][]CapturedMessage{},\n\t\tlock:     &sync.Mutex{},\n\t}\n}\n\nfunc (c *CaptureLoggerAdapter) With(fields LogFields) LoggerAdapter {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\treturn &CaptureLoggerAdapter{\n\t\tcaptured: c.captured, // we are passing the same map, so we'll capture logs from this instance as well\n\t\tfields:   c.fields.Copy().Add(fields),\n\t\tlock:     c.lock,\n\t}\n}\n\nfunc (c *CaptureLoggerAdapter) capture(level LogLevel, msg string, err error, fields LogFields) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\tlogMsg := CapturedMessage{\n\t\tLevel:  level,\n\t\tTime:   time.Now(),\n\t\tFields: c.fields.Add(fields),\n\t\tMsg:    msg,\n\t\tErr:    err,\n\t}\n\n\tc.captured[level] = append(c.captured[level], logMsg)\n}\n\nfunc (c *CaptureLoggerAdapter) Captured() map[LogLevel][]CapturedMessage {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\treturn c.captured\n}\n\ntype Logfer interface {\n\tLogf(format string, a ...interface{})\n}\n\nfunc (c *CaptureLoggerAdapter) PrintCaptured(t Logfer) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\tfor level, messages := range c.captured {\n\t\tfor _, msg := range messages {\n\t\t\tt.Logf(\"%s %d %s %v\", msg.Time.Format(\"15:04:05.999999999\"), level, msg.Msg, msg.Fields)\n\t\t}\n\t}\n}\n\nfunc (c *CaptureLoggerAdapter) Has(msg CapturedMessage) bool {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\treturn slices.ContainsFunc(c.captured[msg.Level], msg.ContentEquals)\n}\n\nfunc (c *CaptureLoggerAdapter) HasError(err error) bool {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\tfor _, capturedMsg := range c.captured[ErrorLogLevel] {\n\t\tif errors.Is(err, capturedMsg.Err) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *CaptureLoggerAdapter) Error(msg string, err error, fields LogFields) {\n\tc.capture(ErrorLogLevel, msg, err, fields)\n}\n\nfunc (c *CaptureLoggerAdapter) Info(msg string, fields LogFields) {\n\tc.capture(InfoLogLevel, msg, nil, fields)\n}\n\nfunc (c *CaptureLoggerAdapter) Debug(msg string, fields LogFields) {\n\tc.capture(DebugLogLevel, msg, nil, fields)\n}\n\nfunc (c *CaptureLoggerAdapter) Trace(msg string, fields LogFields) {\n\tc.capture(TraceLogLevel, msg, nil, fields)\n}\n"
  },
  {
    "path": "log_test.go",
    "content": "package watermill_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n)\n\nfunc TestLogFields_Copy(t *testing.T) {\n\tfields1 := watermill.LogFields{\"foo\": \"bar\"}\n\n\tfields2 := fields1.Copy()\n\tfields2[\"foo\"] = \"baz\"\n\n\tassert.Equal(t, fields1[\"foo\"], \"bar\")\n\tassert.Equal(t, fields2[\"foo\"], \"baz\")\n}\n\nfunc TestStdLogger_with(t *testing.T) {\n\tbuf := bytes.NewBuffer([]byte{})\n\tcleanLogger := watermill.NewStdLoggerWithOut(buf, true, true)\n\n\twithLogFieldsLogger := cleanLogger.With(watermill.LogFields{\"foo\": \"1\"})\n\n\tfor name, logger := range map[string]watermill.LoggerAdapter{\"clean\": cleanLogger, \"with\": withLogFieldsLogger} {\n\t\tlogger.Error(name, nil, watermill.LogFields{\"bar\": \"2\"})\n\t\tlogger.Info(name, watermill.LogFields{\"bar\": \"2\"})\n\t\tlogger.Debug(name, watermill.LogFields{\"bar\": \"2\"})\n\t\tlogger.Trace(name, watermill.LogFields{\"bar\": \"2\"})\n\t}\n\n\tcleanLoggerOut := buf.String()\n\tassert.Contains(t, cleanLoggerOut, `level=ERROR msg=\"clean\" bar=2 err=<nil>`)\n\tassert.Contains(t, cleanLoggerOut, `level=INFO  msg=\"clean\" bar=2`)\n\tassert.Contains(t, cleanLoggerOut, `level=TRACE msg=\"clean\" bar=2`)\n\n\tassert.Contains(t, cleanLoggerOut, `level=ERROR msg=\"with\" bar=2 err=<nil> foo=1`)\n\tassert.Contains(t, cleanLoggerOut, `level=INFO  msg=\"with\" bar=2 foo=1`)\n\tassert.Contains(t, cleanLoggerOut, `level=TRACE msg=\"with\" bar=2 foo=1`)\n}\n\ntype stringer struct{}\n\nfunc (s stringer) String() string {\n\treturn \"stringer\"\n}\n\nfunc TestStdLoggerAdapter_stringer_field(t *testing.T) {\n\tbuf := bytes.NewBuffer([]byte{})\n\tlogger := watermill.NewStdLoggerWithOut(buf, true, true)\n\n\tlogger.Info(\"foo\", watermill.LogFields{\"foo\": stringer{}})\n\n\tout := buf.String()\n\tassert.Contains(t, out, `foo=stringer`)\n}\n\nfunc TestStdLoggerAdapter_field_with_space(t *testing.T) {\n\tbuf := bytes.NewBuffer([]byte{})\n\tlogger := watermill.NewStdLoggerWithOut(buf, true, true)\n\n\tlogger.Info(\"foo\", watermill.LogFields{\"foo\": `bar baz`})\n\n\tout := buf.String()\n\tassert.Contains(t, out, `foo=\"bar baz\"`)\n}\n\nfunc TestCaptureLoggerAdapter(t *testing.T) {\n\tvar logger watermill.LoggerAdapter = watermill.NewCaptureLogger()\n\n\terr := errors.New(\"error\")\n\n\tlogger = logger.With(watermill.LogFields{\"default\": \"field\"})\n\tlogger.Error(\"error\", err, watermill.LogFields{\"bar\": \"2\"})\n\tlogger.Info(\"info\", watermill.LogFields{\"bar\": \"2\"})\n\tlogger.Debug(\"debug\", watermill.LogFields{\"bar\": \"2\"})\n\tlogger.Trace(\"trace\", watermill.LogFields{\"bar\": \"2\"})\n\n\texpectedLogs := map[watermill.LogLevel][]watermill.CapturedMessage{\n\t\twatermill.TraceLogLevel: {\n\t\t\twatermill.CapturedMessage{\n\t\t\t\tLevel:  watermill.TraceLogLevel,\n\t\t\t\tFields: watermill.LogFields{\"bar\": \"2\", \"default\": \"field\"},\n\t\t\t\tMsg:    \"trace\",\n\t\t\t\tErr:    error(nil),\n\t\t\t},\n\t\t},\n\t\twatermill.DebugLogLevel: {\n\t\t\twatermill.CapturedMessage{\n\t\t\t\tLevel:  watermill.DebugLogLevel,\n\t\t\t\tFields: watermill.LogFields{\"default\": \"field\", \"bar\": \"2\"},\n\t\t\t\tMsg:    \"debug\",\n\t\t\t\tErr:    error(nil),\n\t\t\t},\n\t\t},\n\t\twatermill.InfoLogLevel: {\n\t\t\twatermill.CapturedMessage{\n\t\t\t\tLevel:  watermill.InfoLogLevel,\n\t\t\t\tFields: watermill.LogFields{\"default\": \"field\", \"bar\": \"2\"},\n\t\t\t\tMsg:    \"info\",\n\t\t\t\tErr:    error(nil),\n\t\t\t},\n\t\t},\n\t\twatermill.ErrorLogLevel: {\n\t\t\twatermill.CapturedMessage{\n\t\t\t\tLevel:  watermill.ErrorLogLevel,\n\t\t\t\tFields: watermill.LogFields{\"default\": \"field\", \"bar\": \"2\"},\n\t\t\t\tMsg:    \"error\",\n\t\t\t\tErr:    err,\n\t\t\t},\n\t\t},\n\t}\n\n\tcapturedLogger := logger.(*watermill.CaptureLoggerAdapter)\n\n\tassert.Equal(t, len(expectedLogs), len(capturedLogger.Captured()))\n\tfor _, logs := range expectedLogs {\n\t\tfor _, log := range logs {\n\t\t\tassert.True(t, capturedLogger.Has(log))\n\t\t}\n\t}\n\n\tassert.False(t, capturedLogger.Has(watermill.CapturedMessage{\n\t\tLevel:  0,\n\t\tFields: nil,\n\t\tMsg:    \"\",\n\t\tErr:    nil,\n\t}))\n\n\tassert.True(t, capturedLogger.HasError(err))\n\tassert.False(t, capturedLogger.HasError(errors.New(\"foo\")))\n}\n"
  },
  {
    "path": "message/decorator.go",
    "content": "package message\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\n// MessageTransformSubscriberDecorator creates a subscriber decorator that calls transform\n// on each message that passes through the subscriber.\nfunc MessageTransformSubscriberDecorator(transform func(*Message)) SubscriberDecorator {\n\tif transform == nil {\n\t\tpanic(\"transform function is nil\")\n\t}\n\treturn func(sub Subscriber) (Subscriber, error) {\n\t\treturn &messageTransformSubscriberDecorator{\n\t\t\tsub:       sub,\n\t\t\ttransform: transform,\n\t\t}, nil\n\t}\n}\n\n// MessageTransformPublisherDecorator creates a publisher decorator that calls transform\n// on each message that passes through the publisher.\nfunc MessageTransformPublisherDecorator(transform func(*Message)) PublisherDecorator {\n\tif transform == nil {\n\t\tpanic(\"transform function is nil\")\n\t}\n\treturn func(pub Publisher) (Publisher, error) {\n\t\treturn &messageTransformPublisherDecorator{\n\t\t\tPublisher: pub,\n\t\t\ttransform: transform,\n\t\t}, nil\n\t}\n}\n\ntype messageTransformSubscriberDecorator struct {\n\tsub Subscriber\n\n\ttransform   func(*Message)\n\tsubscribeWg sync.WaitGroup\n}\n\nfunc (t *messageTransformSubscriberDecorator) Subscribe(ctx context.Context, topic string) (<-chan *Message, error) {\n\tin, err := t.sub.Subscribe(ctx, topic)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout := make(chan *Message)\n\tt.subscribeWg.Add(1)\n\tgo func() {\n\t\tfor msg := range in {\n\t\t\tt.transform(msg)\n\t\t\tout <- msg\n\t\t}\n\t\tclose(out)\n\t\tt.subscribeWg.Done()\n\t}()\n\n\treturn out, nil\n}\n\nfunc (t *messageTransformSubscriberDecorator) Close() error {\n\terr := t.sub.Close()\n\n\tt.subscribeWg.Wait()\n\treturn err\n}\n\ntype messageTransformPublisherDecorator struct {\n\tPublisher\n\ttransform func(*Message)\n}\n\n// Publish applies the transform to each message and returns the underlying Publisher's result.\nfunc (d messageTransformPublisherDecorator) Publish(topic string, messages ...*Message) error {\n\tfor i := range messages {\n\t\td.transform(messages[i])\n\t}\n\treturn d.Publisher.Publish(topic, messages...)\n}\n"
  },
  {
    "path": "message/decorator_bench_test.go",
    "content": "package message_test\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype benchSubscriber struct {\n\tmsgCh   chan *message.Message\n\tcloseCh chan struct{}\n\trunning sync.Mutex\n}\n\nfunc (b *benchSubscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {\n\treturn b.msgCh, nil\n}\n\nfunc (b *benchSubscriber) Close() error {\n\tclose(b.closeCh)\n\tb.running.Lock()\n\tclose(b.msgCh)\n\tb.running.Unlock()\n\treturn nil\n}\n\nfunc newBenchSubscriber() *benchSubscriber {\n\tsub := &benchSubscriber{\n\t\tmsgCh:   make(chan *message.Message, 1),\n\t\tcloseCh: make(chan struct{}),\n\t}\n\n\t// continually produce messages until Close()\n\tgo func() {\n\t\tsub.running.Lock()\n\t\tmsg := message.NewMessage(\"\", []byte{})\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-sub.closeCh:\n\t\t\t\tsub.running.Unlock()\n\t\t\t\treturn\n\t\t\tcase sub.msgCh <- msg:\n\t\t\t\t// the buffer limit is 1, so this will block until someone consumes\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn sub\n}\n\nfunc BenchmarkMessageTransformSubscriberDecorator(b *testing.B) {\n\tb.ReportAllocs()\n\tb.Run(\"no_decorator\", benchmarkNoDecorator)\n\tb.Run(\"message_transform_decorator\", benchmarkMessageTransformSubscriberDecorator)\n}\n\nfunc benchmarkNoDecorator(b *testing.B) {\n\tsub := newBenchSubscriber()\n\n\tin, err := sub.Subscribe(context.Background(), \"\")\n\trequire.NoError(b, err)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t// consume one message\n\t\t<-in\n\t}\n}\n\nfunc benchmarkMessageTransformSubscriberDecorator(b *testing.B) {\n\tsub := newBenchSubscriber()\n\n\tnoopDecorator := message.MessageTransformSubscriberDecorator(func(*message.Message) {})\n\tdecoratedSub, err := noopDecorator(sub)\n\trequire.NoError(b, err)\n\n\tin, err := decoratedSub.Subscribe(context.Background(), \"\")\n\trequire.NoError(b, err)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t// consume one message\n\t\t<-in\n\t}\n}\n"
  },
  {
    "path": "message/decorator_test.go",
    "content": "package message_test\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/tests\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nvar noop = func(*message.Message) {}\nvar closingErr = errors.New(\"mock error on close\")\n\ntype mockSubscriber struct {\n\tch chan *message.Message\n}\n\nfunc (m mockSubscriber) Subscribe(context.Context, string) (<-chan *message.Message, error) {\n\treturn m.ch, nil\n}\n\nfunc (m mockSubscriber) Close() error {\n\tclose(m.ch)\n\treturn nil\n}\n\nfunc TestMessageTransformSubscriberDecorator_transparent(t *testing.T) {\n\tsub := mockSubscriber{make(chan *message.Message)}\n\tdecorated, err := message.MessageTransformSubscriberDecorator(noop)(sub)\n\trequire.NoError(t, err)\n\n\tmessages, err := decorated.Subscribe(context.Background(), \"topic\")\n\trequire.NoError(t, err)\n\n\trichMessage := message.NewMessage(\"uuid\", []byte(\"serious payloads\"))\n\trichMessage.Metadata.Set(\"k1\", \"v1\")\n\trichMessage.Metadata.Set(\"k2\", \"v2\")\n\n\tgo func() {\n\t\tsub.ch <- richMessage\n\t}()\n\n\treceived, all := subscriber.BulkRead(messages, 1, time.Second)\n\trequire.True(t, all)\n\n\tassert.True(t, received[0].Equals(richMessage), \"expected the message to pass unchanged through decorator\")\n}\n\ntype closingSubscriber struct {\n\tclosed bool\n}\n\nfunc (closingSubscriber) Subscribe(context.Context, string) (<-chan *message.Message, error) {\n\treturn nil, nil\n}\n\nfunc (c *closingSubscriber) Close() error {\n\tc.closed = true\n\treturn closingErr\n}\n\nfunc TestMessageTransformSubscriberDecorator_Close(t *testing.T) {\n\tcs := &closingSubscriber{}\n\n\tdecoratedSub, err := message.MessageTransformSubscriberDecorator(noop)(cs)\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.False(t, cs.closed)\n\n\t// when\n\tdecoratedCloseErr := decoratedSub.Close()\n\n\t// then\n\tassert.True(\n\t\tt,\n\t\tcs.closed,\n\t\t\"expected the Close() call to propagate to decorated subscriber\",\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tclosingErr,\n\t\tdecoratedCloseErr,\n\t\t\"expected the decorator to propagate the closing error from underlying subscriber\",\n\t)\n}\n\nfunc TestMessageTransformSubscriberDecorator_Subscribe(t *testing.T) {\n\tnumMessages := 1000\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, watermill.NewStdLogger(true, true))\n\n\tonMessage := func(msg *message.Message) {\n\t\tmsg.Metadata.Set(\"key\", \"value\")\n\t}\n\tdecorator := message.MessageTransformSubscriberDecorator(onMessage)\n\n\tdecoratedSub, err := decorator(pubSub)\n\trequire.NoError(t, err)\n\n\tmessages, err := decoratedSub.Subscribe(context.Background(), \"topic\")\n\trequire.NoError(t, err)\n\n\tsent := message.Messages{}\n\n\tgo func() {\n\t\tfor i := 0; i < numMessages; i++ {\n\t\t\tmsg := message.NewMessage(strconv.Itoa(i), []byte{})\n\t\t\tsent = append(sent, msg)\n\n\t\t\terr = pubSub.Publish(\"topic\", msg)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t}()\n\n\treceived, all := subscriber.BulkRead(messages, numMessages, time.Second)\n\trequire.True(t, all)\n\ttests.AssertAllMessagesReceived(t, sent, received)\n\n\tfor _, msg := range received {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\t\"value\",\n\t\t\tmsg.Metadata.Get(\"key\"),\n\t\t\t\"expected onMessage callback to have set metadata\",\n\t\t)\n\t}\n}\n\ntype mockPublisher struct {\n\tpublished message.Messages\n}\n\nfunc (m *mockPublisher) Publish(topic string, messages ...*message.Message) error {\n\tm.published = append(m.published, messages...)\n\treturn nil\n}\n\nfunc (m mockPublisher) Close() error {\n\treturn nil\n}\n\nfunc TestMessageTransformPublisherDecorator_transparent(t *testing.T) {\n\tpub := &mockPublisher{message.Messages{}}\n\tdecorated, err := message.MessageTransformPublisherDecorator(noop)(pub)\n\trequire.NoError(t, err)\n\n\trichMessage := message.NewMessage(\"uuid\", []byte(\"serious payloads\"))\n\trichMessage.Metadata.Set(\"k1\", \"v1\")\n\trichMessage.Metadata.Set(\"k2\", \"v2\")\n\n\trequire.NoError(t, decorated.Publish(\"topic\", richMessage))\n\n\trequire.Len(t, pub.published, 1)\n\tassert.True(t, pub.published[0].Equals(richMessage), \"expected the message to pass unchanged through decorator\")\n}\n\ntype closingPublisher struct {\n\tclosed bool\n}\n\nfunc (c *closingPublisher) Publish(topic string, messages ...*message.Message) error {\n\treturn nil\n}\n\nfunc (c *closingPublisher) Close() error {\n\tc.closed = true\n\treturn closingErr\n}\n\nfunc TestMessageTransformPublisherDecorator_Close(t *testing.T) {\n\tcp := &closingPublisher{}\n\n\tdecoratedPub, err := message.MessageTransformPublisherDecorator(noop)(cp)\n\trequire.NoError(t, err)\n\n\t// given\n\trequire.False(t, cp.closed)\n\n\t// when\n\tdecoratedCloseErr := decoratedPub.Close()\n\n\t// then\n\tassert.True(\n\t\tt,\n\t\tcp.closed,\n\t\t\"expected the Close() call to propagate to decorated publisher\",\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tclosingErr,\n\t\tdecoratedCloseErr,\n\t\t\"expected the decorator to propagate the closing error from underlying publisher\",\n\t)\n}\n\nfunc TestMessageTransformPublisherDecorator_Subscribe(t *testing.T) {\n\tnumMessages := 1000\n\tpub := &mockPublisher{}\n\n\tonMessage := func(msg *message.Message) {\n\t\tmsg.Metadata.Set(\"key\", \"value\")\n\t}\n\tdecorator := message.MessageTransformPublisherDecorator(onMessage)\n\tdecoratedPub, err := decorator(pub)\n\trequire.NoError(t, err)\n\n\tfor i := 0; i < numMessages; i++ {\n\t\tmsg := message.NewMessage(strconv.Itoa(i), []byte{})\n\t\trequire.NoError(t, decoratedPub.Publish(\"topic\", msg))\n\t}\n\n\tfor i, msg := range pub.published {\n\t\tassert.Equal(t, strconv.Itoa(i), msg.UUID, \"expected messages to arrive in unchanged order\")\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\t\"value\",\n\t\t\tmsg.Metadata.Get(\"key\"),\n\t\t\t\"expected onMessage callback to have set metadata\",\n\t\t)\n\t}\n}\n\nfunc TestMessageTransformer_nil_panics(t *testing.T) {\n\trequire.Panics(\n\t\tt,\n\t\tfunc() {\n\t\t\t_ = message.MessageTransformSubscriberDecorator(nil)\n\t\t},\n\t\t\"expected to panic if transform is nil\",\n\t)\n\trequire.Panics(\n\t\tt,\n\t\tfunc() {\n\t\t\t_ = message.MessageTransformPublisherDecorator(nil)\n\t\t},\n\t\t\"expected to panic if transform is nil\",\n\t)\n}\n"
  },
  {
    "path": "message/message.go",
    "content": "package message\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"sync\"\n)\n\nvar closedchan = make(chan struct{})\n\nfunc init() {\n\tclose(closedchan)\n}\n\n// Payload is the Message's payload.\ntype Payload []byte\n\n// Message is the basic transfer unit.\n// Messages are emitted by Publishers and received by Subscribers.\n//\n// A publisher can modify the message during publishing, e.g. can alter the metadata.\n// Avoid modifying the message in parallel with publishing, as it can lead to data races.\n// In general, a message should be passed to a single Publish and then considered immutable.\n// If needed, use the Copy method to create a new message.\ntype Message struct {\n\t// UUID is a unique identifier of the message.\n\t//\n\t// It is only used by Watermill for debugging.\n\t// UUID can be empty.\n\tUUID string\n\n\t// Metadata contains the message metadata.\n\t//\n\t// Can be used to store data which doesn't require unmarshalling the entire payload.\n\t// It is something similar to HTTP request's headers.\n\t//\n\t// Metadata is marshaled and will be saved to the PubSub.\n\tMetadata Metadata\n\n\t// Payload is the message's payload.\n\tPayload Payload\n\n\t// ack is closed when acknowledge is received.\n\tack chan struct{}\n\t// noAck is closed when negative acknowledge is received.\n\tnoAck chan struct{}\n\n\tackMutex    sync.Mutex\n\tackSentType ackType\n\n\tctx context.Context\n}\n\n// NewMessage creates a new Message with given uuid and payload.\nfunc NewMessage(uuid string, payload Payload) *Message {\n\treturn &Message{\n\t\tUUID:     uuid,\n\t\tMetadata: make(map[string]string),\n\t\tPayload:  payload,\n\t\tack:      make(chan struct{}),\n\t\tnoAck:    make(chan struct{}),\n\t}\n}\n\n// NewMessageWithContext creates a new Message with given uuid, payload, and context.\nfunc NewMessageWithContext(ctx context.Context, uuid string, payload Payload) *Message {\n\tmsg := NewMessage(uuid, payload)\n\tmsg.SetContext(ctx)\n\treturn msg\n}\n\ntype ackType int\n\nconst (\n\tnoAckSent ackType = iota\n\tack\n\tnack\n)\n\n// Equals compare, that two messages are equal. Acks/Nacks are not compared.\nfunc (m *Message) Equals(toCompare *Message) bool {\n\tif m.UUID != toCompare.UUID {\n\t\treturn false\n\t}\n\tif len(m.Metadata) != len(toCompare.Metadata) {\n\t\treturn false\n\t}\n\tfor key, value := range m.Metadata {\n\t\tif value != toCompare.Metadata[key] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn bytes.Equal(m.Payload, toCompare.Payload)\n}\n\n// Ack sends message's acknowledgement.\n//\n// Ack is not blocking.\n// Ack is idempotent.\n// False is returned, if Nack is already sent.\nfunc (m *Message) Ack() bool {\n\tm.ackMutex.Lock()\n\tdefer m.ackMutex.Unlock()\n\n\tif m.ackSentType == nack {\n\t\treturn false\n\t}\n\tif m.ackSentType != noAckSent {\n\t\treturn true\n\t}\n\n\tm.ackSentType = ack\n\tif m.ack == nil {\n\t\tm.ack = closedchan\n\t} else {\n\t\tclose(m.ack)\n\t}\n\n\treturn true\n}\n\n// Nack sends message's negative acknowledgement.\n//\n// Nack is not blocking.\n// Nack is idempotent.\n// False is returned, if Ack is already sent.\nfunc (m *Message) Nack() bool {\n\tm.ackMutex.Lock()\n\tdefer m.ackMutex.Unlock()\n\n\tif m.ackSentType == ack {\n\t\treturn false\n\t}\n\tif m.ackSentType != noAckSent {\n\t\treturn true\n\t}\n\n\tm.ackSentType = nack\n\n\tif m.noAck == nil {\n\t\tm.noAck = closedchan\n\t} else {\n\t\tclose(m.noAck)\n\t}\n\n\treturn true\n}\n\n// Acked returns channel which is closed when acknowledgement is sent.\n//\n// Usage:\n//\n//\tselect {\n//\tcase <-message.Acked():\n//\t\t// ack received\n//\tcase <-message.Nacked():\n//\t\t// nack received\n//\t}\nfunc (m *Message) Acked() <-chan struct{} {\n\treturn m.ack\n}\n\n// Nacked returns channel which is closed when negative acknowledgement is sent.\n//\n// Usage:\n//\n//\tselect {\n//\tcase <-message.Acked():\n//\t\t// ack received\n//\tcase <-message.Nacked():\n//\t\t// nack received\n//\t}\nfunc (m *Message) Nacked() <-chan struct{} {\n\treturn m.noAck\n}\n\n// Context returns the message's context. To change the context, use\n// SetContext.\n//\n// The returned context is always non-nil; it defaults to the\n// background context.\nfunc (m *Message) Context() context.Context {\n\tif m.ctx != nil {\n\t\treturn m.ctx\n\t}\n\treturn context.Background()\n}\n\n// SetContext sets provided context to the message.\nfunc (m *Message) SetContext(ctx context.Context) {\n\tm.ctx = ctx\n}\n\n// Copy copies all message without Acks/Nacks.\n// The context is not propagated to the copy.\nfunc (m *Message) Copy() *Message {\n\tmsg := NewMessage(m.UUID, m.Payload)\n\tfor k, v := range m.Metadata {\n\t\tmsg.Metadata.Set(k, v)\n\t}\n\treturn msg\n}\n\n// CopyWithContext copies all message without Acks/Nacks.\n// The context is also propagated to the copy.\nfunc (m *Message) CopyWithContext() *Message {\n\tmsg := m.Copy()\n\tmsg.ctx = m.ctx\n\treturn msg\n}\n"
  },
  {
    "path": "message/message_test.go",
    "content": "package message_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMessage_Equals(t *testing.T) {\n\twithMetadata := func(msg *message.Message, metadata message.Metadata) *message.Message {\n\t\tmsg.Metadata = metadata\n\t\treturn msg\n\t}\n\n\ttestCases := []struct {\n\t\tName   string\n\t\tMsg1   *message.Message\n\t\tMsg2   *message.Message\n\t\tEquals bool\n\t}{\n\t\t{\n\t\t\tName:   \"equal\",\n\t\t\tMsg1:   message.NewMessage(\"1\", []byte(\"foo\")),\n\t\t\tMsg2:   message.NewMessage(\"1\", []byte(\"foo\")),\n\t\t\tEquals: true,\n\t\t},\n\t\t{\n\t\t\tName:   \"different_uuid\",\n\t\t\tMsg1:   message.NewMessage(\"1\", []byte(\"foo\")),\n\t\t\tMsg2:   message.NewMessage(\"2\", []byte(\"foo\")),\n\t\t\tEquals: false,\n\t\t},\n\t\t{\n\t\t\tName:   \"different_payload\",\n\t\t\tMsg1:   message.NewMessage(\"1\", []byte(\"foo\")),\n\t\t\tMsg2:   message.NewMessage(\"1\", []byte(\"bar\")),\n\t\t\tEquals: false,\n\t\t},\n\t\t{\n\t\t\tName:   \"different_metadata\",\n\t\t\tMsg1:   withMetadata(message.NewMessage(\"1\", []byte(\"foo\")), map[string]string{\"foo\": \"1\"}),\n\t\t\tMsg2:   withMetadata(message.NewMessage(\"1\", []byte(\"foo\")), map[string]string{\"foo\": \"2\"}),\n\t\t\tEquals: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.Equals, tc.Msg1.Equals(tc.Msg2))\n\t\t\tassert.Equal(t, tc.Equals, tc.Msg2.Equals(tc.Msg1))\n\t\t})\n\t}\n}\n\nfunc TestMessage_Ack(t *testing.T) {\n\tmsg := &message.Message{}\n\trequire.True(t, msg.Ack())\n\n\tassertAcked(t, msg)\n\tassertNoNack(t, msg)\n}\n\nfunc TestMessage_Ack_idempotent(t *testing.T) {\n\tmsg := &message.Message{}\n\trequire.True(t, msg.Ack())\n\trequire.True(t, msg.Ack())\n\n\tassertAcked(t, msg)\n}\n\nfunc TestMessage_Ack_already_Nack(t *testing.T) {\n\tmsg := &message.Message{}\n\trequire.True(t, msg.Nack())\n\n\tassert.False(t, msg.Ack())\n}\n\nfunc TestMessage_Nack(t *testing.T) {\n\tmsg := &message.Message{}\n\trequire.True(t, msg.Nack())\n\n\tassertNoAck(t, msg)\n\tassertNacked(t, msg)\n}\n\nfunc TestMessage_Nack_idempotent(t *testing.T) {\n\tmsg := &message.Message{}\n\trequire.True(t, msg.Nack())\n\trequire.True(t, msg.Nack())\n\n\tassertNacked(t, msg)\n}\n\nfunc TestMessage_Nack_already_Ack(t *testing.T) {\n\tmsg := &message.Message{}\n\trequire.True(t, msg.Ack())\n\n\tassert.False(t, msg.Nack())\n}\n\nfunc TestMessage_Copy(t *testing.T) {\n\tmsg := message.NewMessage(\"1\", []byte(\"foo\"))\n\tmsgCopy := msg.Copy()\n\n\trequire.True(t, msg.Ack())\n\n\tassertAcked(t, msg)\n\tassertNoAck(t, msgCopy)\n\tassert.True(t, msg.Equals(msgCopy))\n}\n\ntype ctxKey string\n\nfunc TestMessage_CopyWithContext(t *testing.T) {\n\tmsg := message.NewMessage(\"1\", []byte(\"foo\"))\n\ttestCtx := context.Background()\n\ttestCtx = context.WithValue(testCtx, ctxKey(\"foo\"), \"bar\")\n\tmsg.SetContext(testCtx)\n\n\tmsgCopy := msg.CopyWithContext()\n\tcopyMsgCtx := msgCopy.Context()\n\tassert.True(t, copyMsgCtx.Value(ctxKey(\"foo\")) == \"bar\", \"expected context not being copied\")\n\tassert.False(t, copyMsgCtx.Value(ctxKey(\"abc\")) == \"def\", \"non-expected context being copied\")\n\tassert.True(t, msg.Equals(msgCopy))\n}\n\nfunc TestMessage_CopyWithContextAndMetadata(t *testing.T) {\n\tmsg := message.NewMessage(\"1\", []byte(\"foo\"))\n\ttestCtx := context.Background()\n\ttestCtx = context.WithValue(testCtx, ctxKey(\"foo\"), \"bar\")\n\tmsg.SetContext(testCtx)\n\tmsg.Metadata.Set(\"foo\", \"bar\")\n\tmsgCopy := msg.CopyWithContext()\n\n\tmsg.Metadata.Set(\"foo\", \"baz\")\n\n\tcopyMsgCtx := msgCopy.Context()\n\tassert.True(t, copyMsgCtx.Value(ctxKey(\"foo\")) == \"bar\", \"expected context not being copied\")\n\tassert.Equal(t, msgCopy.Metadata.Get(\"foo\"), \"bar\", \"did not expect changing source message's metadata to alter copy's metadata\")\n}\n\nfunc TestMessage_CopyMetadata(t *testing.T) {\n\tmsg := message.NewMessage(\"1\", []byte(\"foo\"))\n\tmsg.Metadata.Set(\"foo\", \"bar\")\n\tmsgCopy := msg.Copy()\n\n\tmsg.Metadata.Set(\"foo\", \"baz\")\n\n\tassert.Equal(t, msgCopy.Metadata.Get(\"foo\"), \"bar\", \"did not expect changing source message's metadata to alter copy's metadata\")\n}\n\nfunc assertAcked(t *testing.T, msg *message.Message) {\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tdefault:\n\t\tt.Fatal(\"no ack received\")\n\t}\n}\n\nfunc assertNacked(t *testing.T, msg *message.Message) {\n\tselect {\n\tcase <-msg.Nacked():\n\t\t// ok\n\tdefault:\n\t\tt.Fatal(\"no ack received\")\n\t}\n}\n\nfunc assertNoAck(t *testing.T, msg *message.Message) {\n\tselect {\n\tcase <-msg.Acked():\n\t\tt.Fatal(\"nack should be not sent\")\n\tdefault:\n\t\t// ok\n\t}\n}\n\nfunc assertNoNack(t *testing.T, msg *message.Message) {\n\tselect {\n\tcase <-msg.Nacked():\n\t\tt.Fatal(\"nack should be not sent\")\n\tdefault:\n\t\t// ok\n\t}\n}\n"
  },
  {
    "path": "message/messages.go",
    "content": "package message\n\n// Messages is a slice of messages.\ntype Messages []*Message\n\n// IDs returns a slice of Messages' IDs.\nfunc (m Messages) IDs() []string {\n\tids := make([]string, len(m))\n\n\tfor i, msg := range m {\n\t\tids[i] = msg.UUID\n\t}\n\n\treturn ids\n}\n"
  },
  {
    "path": "message/messages_test.go",
    "content": "package message_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMessages_IDs(t *testing.T) {\n\tmsgs := message.Messages{\n\t\tmessage.NewMessage(\"1\", nil),\n\t\tmessage.NewMessage(\"2\", nil),\n\t\tmessage.NewMessage(\"3\", nil),\n\t}\n\n\tassert.Equal(t, []string{\"1\", \"2\", \"3\"}, msgs.IDs())\n}\n"
  },
  {
    "path": "message/metadata.go",
    "content": "package message\n\n// Metadata is sent with every message to provide extra context without unmarshaling the message payload.\ntype Metadata map[string]string\n\n// Get returns the metadata value for the given key. If the key is not found, an empty string is returned.\nfunc (m Metadata) Get(key string) string {\n\tif v, ok := m[key]; ok {\n\t\treturn v\n\t}\n\n\treturn \"\"\n}\n\n// Set sets the metadata key to value.\nfunc (m Metadata) Set(key, value string) {\n\tm[key] = value\n}\n"
  },
  {
    "path": "message/pubsub.go",
    "content": "package message\n\nimport (\n\t\"context\"\n)\n\n// Publisher is the emitting part of a Pub/Sub.\ntype Publisher interface {\n\t// Publish publishes provided messages to the given topic.\n\t//\n\t// Publish can be synchronous or asynchronous - it depends on the implementation.\n\t//\n\t// Most publisher implementations don't support atomic publishing of messages.\n\t// This means that if publishing one of the messages fails, the next messages will not be published.\n\t//\n\t// Publish does not work with a single Context.\n\t// Use the Context() method of each message instead.\n\t//\n\t// Publish must be thread safe.\n\tPublish(topic string, messages ...*Message) error\n\t// Close should flush unsent messages if publisher is async.\n\tClose() error\n}\n\n// Subscriber is the consuming part of the Pub/Sub.\ntype Subscriber interface {\n\t// Subscribe returns an output channel with messages from the provided topic.\n\t// The channel is closed after Close() is called on the subscriber.\n\t//\n\t// To receive the next message, `Ack()` must be called on the received message.\n\t// If message processing fails and the message should be redelivered `Nack()` should be called instead.\n\t//\n\t// When the provided ctx is canceled, the subscriber closes the subscription and the output channel.\n\t// The provided ctx is passed to all produced messages.\n\t// When Nack or Ack is called on the message, the context of the message is canceled.\n\tSubscribe(ctx context.Context, topic string) (<-chan *Message, error)\n\t// Close closes all subscriptions with their output channels and flushes offsets etc. when needed.\n\tClose() error\n}\n\n// SubscribeInitializer is used to initialize subscribers.\ntype SubscribeInitializer interface {\n\t// SubscribeInitialize can be called to initialize subscribe before consume.\n\t// When calling Subscribe before Publish, SubscribeInitialize should be not required.\n\t//\n\t// Not every Pub/Sub requires this initialization, and it may be optional for performance improvements etc.\n\t// For detailed SubscribeInitialize functionality, please check Pub/Subs godoc.\n\t//\n\t// Implementing SubscribeInitialize is not obligatory.\n\tSubscribeInitialize(topic string) error\n}\n"
  },
  {
    "path": "message/router/middleware/circuit_breaker.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/sony/gobreaker\"\n)\n\n// CircuitBreaker is a middleware that wraps the handler in a circuit breaker.\n// Based on the configuration, the circuit breaker will fail fast if the handler keeps returning errors.\n// This is useful for preventing cascading failures.\ntype CircuitBreaker struct {\n\tcb *gobreaker.CircuitBreaker\n}\n\n// NewCircuitBreaker returns a new CircuitBreaker middleware.\n// Refer to the gobreaker documentation for the available settings.\nfunc NewCircuitBreaker(settings gobreaker.Settings) CircuitBreaker {\n\treturn CircuitBreaker{\n\t\tcb: gobreaker.NewCircuitBreaker(settings),\n\t}\n}\n\n// Middleware returns the CircuitBreaker middleware.\nfunc (c CircuitBreaker) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\tout, err := c.cb.Execute(func() (interface{}, error) {\n\t\t\treturn h(msg)\n\t\t})\n\n\t\tvar result []*message.Message\n\t\tif out != nil {\n\t\t\tresult = out.([]*message.Message)\n\t\t}\n\n\t\treturn result, err\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/circuit_breaker_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/sony/gobreaker\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCircuitBreaker(t *testing.T) {\n\tt.Parallel()\n\n\tcount := 0\n\tfailing := true\n\n\th := middleware.NewCircuitBreaker(\n\t\tgobreaker.Settings{\n\t\t\tName:    \"test\",\n\t\t\tTimeout: time.Millisecond * 50,\n\t\t},\n\t).Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\tcount++\n\n\t\tif failing {\n\t\t\treturn nil, errors.New(\"test error\")\n\t\t}\n\n\t\treturn nil, nil\n\t})\n\n\tmsg := message.NewMessage(\"1\", nil)\n\n\t// The first 6 calls should fail and increment the count\n\tfor i := 0; i < 6; i++ {\n\t\t_, err := h(msg)\n\t\tassert.Error(t, err)\n\t}\n\n\tassert.Equal(t, 6, count)\n\n\t// The next calls should fail and not increment the count (the circuit breaker is open)\n\tfor i := 0; i < 4; i++ {\n\t\t_, err := h(msg)\n\t\tassert.Error(t, err)\n\t}\n\tassert.Equal(t, 6, count)\n\n\ttime.Sleep(time.Millisecond * 100)\n\tfailing = false\n\n\t// After a timeout, the Circuit Breaker is closed again\n\tfor i := 0; i < 4; i++ {\n\t\t_, err := h(msg)\n\t\tassert.NoError(t, err)\n\t}\n\tassert.Equal(t, 10, count)\n}\n"
  },
  {
    "path": "message/router/middleware/correlation.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// CorrelationIDMetadataKey is used to store the correlation ID in metadata.\nconst CorrelationIDMetadataKey = \"correlation_id\"\n\n// SetCorrelationID sets a correlation ID for the message.\n//\n// SetCorrelationID should be called when the message enters the system.\n// When message is produced in a request (for example HTTP),\n// message correlation ID should be the same as the request's correlation ID.\nfunc SetCorrelationID(id string, msg *message.Message) {\n\tif MessageCorrelationID(msg) != \"\" {\n\t\treturn\n\t}\n\n\tmsg.Metadata.Set(CorrelationIDMetadataKey, id)\n}\n\n// MessageCorrelationID returns correlation ID from the message.\nfunc MessageCorrelationID(message *message.Message) string {\n\treturn message.Metadata.Get(CorrelationIDMetadataKey)\n}\n\n// CorrelationID adds correlation ID to all messages produced by the handler.\n// ID is based on ID from message received by handler.\n//\n// To make CorrelationID working correctly, SetCorrelationID must be called to first message entering the system.\nfunc CorrelationID(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(message *message.Message) ([]*message.Message, error) {\n\t\tproducedMessages, err := h(message)\n\n\t\tcorrelationID := MessageCorrelationID(message)\n\t\tfor _, msg := range producedMessages {\n\t\t\tSetCorrelationID(correlationID, msg)\n\t\t}\n\n\t\treturn producedMessages, err\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/correlation_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc TestCorrelationID(t *testing.T) {\n\thandlerErr := errors.New(\"foo\")\n\n\thandler := middleware.CorrelationID(func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{message.NewMessage(\"2\", nil)}, handlerErr\n\t})\n\n\tmsg := message.NewMessage(\"1\", nil)\n\tmiddleware.SetCorrelationID(\"correlation_id\", msg)\n\n\tproducedMsgs, err := handler(msg)\n\n\tassert.Equal(t, \"2\", producedMsgs[0].UUID)\n\tassert.Equal(t, middleware.MessageCorrelationID(producedMsgs[0]), \"correlation_id\")\n\tassert.Equal(t, handlerErr, err)\n}\n\nfunc TestSetCorrelationID_already_set(t *testing.T) {\n\tmsg := message.NewMessage(\"\", nil)\n\n\tmiddleware.SetCorrelationID(\"foo\", msg)\n\tmiddleware.SetCorrelationID(\"bar\", msg)\n\n\tassert.Equal(t, \"foo\", middleware.MessageCorrelationID(msg))\n}\n"
  },
  {
    "path": "message/router/middleware/deduplicator.go",
    "content": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"hash/adler32\"\n\t\"io\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// MessageHasherReadLimitMinimum specifies the least number\n// of bytes of a [message.Message] are used for calculating\n// their hash values using a [MessageHasher].\nconst MessageHasherReadLimitMinimum = 64\n\n// ExpiringKeyRepository is a state container for checking the\n// existence of a key in a certain time window.\n// All operations must be safe for concurrent use.\ntype ExpiringKeyRepository interface {\n\t// IsDuplicate returns `true` if the key\n\t// was not checked in recent past.\n\t// The key must expire in a certain time window.\n\tIsDuplicate(ctx context.Context, key string) (ok bool, err error)\n}\n\n// MessageHasher returns a short tag that describes\n// a message. The tag should be unique per message,\n// but avoiding hash collisions entirely is not practical\n// for performance reasons. Used for powering [Deduplicator]s.\ntype MessageHasher func(*message.Message) (string, error)\n\n// Deduplicator drops similar messages if they are present\n// in a [ExpiringKeyRepository]. The similarity is determined\n// by a [MessageHasher]. Time out is applied to repository\n// operations using [context.WithTimeout].\n//\n// Call [Deduplicator.Middleware] for a new middleware\n// or [Deduplicator.Decorator] for a [message.PublisherDecorator].\n//\n// KeyFactory defaults to [NewMessageHasherAdler32] with read\n// limit  set to [math.MaxInt64] for fast tagging.\n// Use [NewMessageHasherSHA256] for minimal collisions.\n//\n// Repository defaults to [NewMapExpiringKeyRepository] with one\n// minute retention window. This default setting is performant\n// but **does not support distributed operations**. If you\n// implement a [ExpiringKeyRepository] backed by Redis,\n// please submit a pull request.\n//\n// Timeout defaults to one minute. If lower than\n// five milliseconds, it is set to five milliseconds.\n//\n// [ExpiringKeyRepository] must expire values\n// in a certain time window. If there is no expiration, only one\n// unique message will be ever delivered as long as the repository\n// keeps its state.\ntype Deduplicator struct {\n\tKeyFactory MessageHasher\n\tRepository ExpiringKeyRepository\n\tTimeout    time.Duration\n}\n\n// IsDuplicate returns true if the message hash tag calculated\n// using a [MessageHasher] was seen in deduplication time window.\nfunc (d *Deduplicator) IsDuplicate(m *message.Message) (bool, error) {\n\tkey, err := d.KeyFactory(m)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tctx, cancel := context.WithTimeout(m.Context(), d.Timeout)\n\tdefer cancel()\n\treturn d.Repository.IsDuplicate(ctx, key)\n}\n\nfunc applyDefaultsToDeduplicator(d *Deduplicator) *Deduplicator {\n\tif d == nil {\n\t\tkr, err := NewMapExpiringKeyRepository(time.Minute)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn &Deduplicator{\n\t\t\tKeyFactory: NewMessageHasherAdler32(math.MaxInt64),\n\t\t\tRepository: kr,\n\t\t\tTimeout:    time.Minute,\n\t\t}\n\t}\n\tif d.KeyFactory == nil {\n\t\td.KeyFactory = NewMessageHasherAdler32(math.MaxInt64)\n\t}\n\tif d.Repository == nil {\n\t\tkr, err := NewMapExpiringKeyRepository(time.Minute)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\td.Repository = kr\n\t}\n\tif d.Timeout < time.Millisecond*5 {\n\t\td.Timeout = time.Millisecond * 5\n\t}\n\treturn d\n}\n\n// Middleware returns the [message.HandlerMiddleware]\n// that drops similar messages in a given time window.\nfunc (d *Deduplicator) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\td = applyDefaultsToDeduplicator(d)\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\tisDuplicate, err := d.IsDuplicate(msg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif isDuplicate {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn h(msg)\n\t}\n}\n\ntype mapExpiringKeyRepository struct {\n\twindow time.Duration\n\tmu     *sync.Mutex\n\ttags   map[string]time.Time\n}\n\n// NewMapExpiringKeyRepository returns a memory store\n// backed by a regular hash map protected by\n// a [sync.Mutex]. The state **cannot be shared or synchronized\n// between instances** by design for performance.\n//\n// If you need to drop duplicate messages by orchestration,\n// implement [ExpiringKeyRepository] interface backed by Redis\n// or similar.\n//\n// Window specifies the minimum duration of how long the\n// duplicate tags are remembered for. Real duration can\n// extend up to 50% longer because it depends on the\n// clean up cycle.\nfunc NewMapExpiringKeyRepository(window time.Duration) (ExpiringKeyRepository, error) {\n\tif window < time.Millisecond {\n\t\treturn nil, errors.New(\"deduplication window of less than a millisecond is impractical\")\n\t}\n\n\tkr := &mapExpiringKeyRepository{\n\t\twindow: window,\n\t\tmu:     &sync.Mutex{},\n\t\ttags:   make(map[string]time.Time),\n\t}\n\tticker := time.NewTicker(window / 2)\n\n\tgo kr.cleanOutLoop(context.Background(), ticker)\n\treturn kr, nil\n}\n\nfunc (kr *mapExpiringKeyRepository) IsDuplicate(\n\tctx context.Context,\n\tkey string,\n) (bool, error) {\n\tkr.mu.Lock()\n\t_, alreadySeen := kr.tags[key]\n\tif alreadySeen {\n\t\t// NOTE: could also check if key expires.After(t)\n\t\t// and remove it for exact expiration\n\t\t// instead of fuzzy until-next clean up expiration\n\t\t// but this should not be needed for most use cases.\n\t\tkr.mu.Unlock()\n\t\treturn true, nil\n\t}\n\tkr.tags[key] = time.Now().Add(kr.window)\n\tkr.mu.Unlock()\n\treturn false, nil\n}\n\nfunc (kr *mapExpiringKeyRepository) cleanOutLoop(ctx context.Context, ticker *time.Ticker) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn // execution ended, part the go routine\n\t\tcase tagsBefore := <-ticker.C:\n\t\t\tkr.cleanOut(tagsBefore)\n\t\t}\n\t}\n}\n\nfunc (kr *mapExpiringKeyRepository) cleanOut(tagsBefore time.Time) {\n\tkr.mu.Lock()\n\tdefer kr.mu.Unlock()\n\n\tfor hash, expires := range kr.tags {\n\t\tif expires.Before(tagsBefore) {\n\t\t\tdelete(kr.tags, hash)\n\t\t}\n\t}\n}\n\n// Len returns the number of known tags that have not been\n// cleaned out yet.\nfunc (kr *mapExpiringKeyRepository) Len() (count int) {\n\tkr.mu.Lock()\n\tcount = len(kr.tags)\n\tkr.mu.Unlock()\n\treturn\n}\n\n// NewMessageHasherAdler32 generates message hashes using a fast\n// Adler-32 checksum of the [message.Message] body. Read\n// limit specifies how many bytes of the message are\n// used for calculating the hash.\n//\n// Lower limit improves performance but results in more false\n// positives. Read limit must be greater than\n// [MessageHasherReadLimitMinimum].\nfunc NewMessageHasherAdler32(readLimit int64) MessageHasher {\n\tif readLimit < MessageHasherReadLimitMinimum {\n\t\treadLimit = MessageHasherReadLimitMinimum\n\t}\n\treturn func(m *message.Message) (string, error) {\n\t\th := adler32.New()\n\t\t_, err := io.CopyN(h, bytes.NewReader(m.Payload), readLimit)\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn string(h.Sum(nil)), nil\n\t}\n}\n\n// NewMessageHasherSHA256 generates message hashes using a slower\n// but more resilient hashing of the [message.Message] body. Read\n// limit specifies how many bytes of the message are\n// used for calculating the hash.\n//\n// Lower limit improves performance but results in more false\n// positives. Read limit must be greater than\n// [MessageHasherReadLimitMinimum].\nfunc NewMessageHasherSHA256(readLimit int64) MessageHasher {\n\tif readLimit < MessageHasherReadLimitMinimum {\n\t\treadLimit = MessageHasherReadLimitMinimum\n\t}\n\n\treturn func(m *message.Message) (string, error) {\n\t\th := sha256.New()\n\t\t_, err := io.CopyN(h, bytes.NewReader(m.Payload), readLimit)\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn string(h.Sum(nil)), nil\n\t}\n}\n\n// NewMessageHasherFromMetadataField looks for a hash value\n// inside message metadata instead of calculating a new one.\n// Useful if a [MessageHasher] was applied in a previous\n// [message.HandlerFunc].\nfunc NewMessageHasherFromMetadataField(field string) MessageHasher {\n\treturn func(m *message.Message) (string, error) {\n\t\tfromMetadata, ok := m.Metadata[field]\n\t\tif ok {\n\t\t\treturn fromMetadata, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"cannot recover hash value from metadata of message #%s: field %q is absent\", m.UUID, field)\n\t}\n}\n\ntype deduplicatingPublisherDecorator struct {\n\tmessage.Publisher\n\tdeduplicator *Deduplicator\n}\n\nfunc (d *deduplicatingPublisherDecorator) Publish(\n\ttopic string,\n\tmessages ...*message.Message,\n) (err error) {\n\tnotRecent := make([]*message.Message, 0, len(messages))\n\tisDuplicate := false\n\n\tfor _, m := range messages {\n\t\tisDuplicate, err = d.deduplicator.IsDuplicate(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif isDuplicate {\n\t\t\tm.Ack() // acknowledge and ignore\n\t\t\tcontinue\n\t\t}\n\t\tnotRecent = append(notRecent, m)\n\t}\n\treturn d.Publisher.Publish(topic, notRecent...)\n}\n\n// PublisherDecorator returns a decorator that\n// acknowledges and drops every [message.Message] that\n// was recognized by a [Deduplicator].\n//\n// The returned decorator provides the same functionality\n// to a [message.Publisher] as [Deduplicator.Middleware]\n// to a [message.Router].\nfunc (d *Deduplicator) PublisherDecorator() message.PublisherDecorator {\n\treturn func(pub message.Publisher) (message.Publisher, error) {\n\t\tif pub == nil {\n\t\t\treturn nil, errors.New(\"cannot decorate a <nil> publisher\")\n\t\t}\n\n\t\treturn &deduplicatingPublisherDecorator{\n\t\t\tPublisher:    pub,\n\t\t\tdeduplicator: applyDefaultsToDeduplicator(d),\n\t\t}, nil\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/deduplicator_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDeduplicatorMiddleware(t *testing.T) {\n\tt.Parallel()\n\n\tcount := 0\n\td := &middleware.Deduplicator{\n\t\tKeyFactory: middleware.NewMessageHasherAdler32(1024),\n\t\t// KeyFactory: middleware.NewMessageHasherSHA256(1024),\n\t\tTimeout: time.Second,\n\t}\n\th := d.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\tcount++\n\t\treturn nil, nil\n\t})\n\n\tfor i := 0; i < 6; i++ { // only one should go through\n\t\tmsg := message.NewMessage(\n\t\t\tfmt.Sprintf(\"first%d\", i),\n\t\t\t[]byte(\"1\"),\n\t\t)\n\t\t_, err := h(msg)\n\t\tassert.NoError(t, err)\n\t}\n\n\tfor i := 0; i < 2; i++ { // only one should go through\n\t\tmsg := message.NewMessage(\n\t\t\tfmt.Sprintf(\"second%d\", i),\n\t\t\t[]byte(\"2\"),\n\t\t)\n\t\t_, err := h(msg)\n\t\tassert.NoError(t, err)\n\t}\n\n\tassert.Equal(t, 2, count)\n}\n\nfunc TestDeduplicatorPublisherDecorator(t *testing.T) {\n\tt.Parallel()\n\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{\n\t\tOutputChannelBuffer: 100,\n\t\tPersistent:          true,\n\t}, nil)\n\tdefer pubSub.Close()\n\n\tconst testDedupeTopic = \"testTopic\"\n\tctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)\n\tdefer cancel()\n\n\td := &middleware.Deduplicator{\n\t\tKeyFactory: middleware.NewMessageHasherAdler32(1024),\n\t\t// KeyFactory: middleware.NewMessageHasherSHA256(1024),\n\t\tTimeout: time.Second,\n\t}\n\tdecorated, err := d.PublisherDecorator()(pubSub)\n\tassert.NoError(t, err)\n\n\tfor i := 0; i < 6; i++ { // only one should go through\n\t\tmsg := message.NewMessage(\n\t\t\tfmt.Sprintf(\"first%d\", i),\n\t\t\t[]byte(\"1\"),\n\t\t)\n\t\terr := decorated.Publish(testDedupeTopic, msg)\n\t\tassert.NoError(t, err)\n\t}\n\n\tfor i := 0; i < 2; i++ { // only one should go through\n\t\tmsg := message.NewMessage(\n\t\t\tfmt.Sprintf(\"second%d\", i),\n\t\t\t[]byte(\"2\"),\n\t\t)\n\t\terr := decorated.Publish(testDedupeTopic, msg)\n\t\tassert.NoError(t, err)\n\t}\n\n\tgot, err := pubSub.Subscribe(ctx, testDedupeTopic)\n\tassert.NoError(t, err)\n\tcount := 0\n\tfor m := range got {\n\t\tcount++\n\t\tm.Ack()\n\t\tt.Log(\"got message:\", m.UUID)\n\t}\n\tassert.Equal(t, 2, count)\n}\n\nfunc TestMessageHasherAdler32(t *testing.T) {\n\tt.Parallel()\n\n\tshort := middleware.NewMessageHasherAdler32(0)\n\tfull := middleware.NewMessageHasherAdler32(middleware.MessageHasherReadLimitMinimum)\n\n\tmsg := message.NewMessage(\"adlerTest\", []byte(\"some random data\"))\n\th1, err := short(msg)\n\tassert.NoError(t, err)\n\th2, err := full(msg)\n\tassert.NoError(t, err)\n\n\tif h1 != h2 {\n\t\tt.Fatal(\"MessageHasherReadLimitMinimum did not apply to Adler32 message hasher\")\n\t}\n}\n\nfunc TestMessageHasherSHA256(t *testing.T) {\n\tt.Parallel()\n\n\tshort := middleware.NewMessageHasherSHA256(0)\n\tfull := middleware.NewMessageHasherSHA256(middleware.MessageHasherReadLimitMinimum)\n\n\tmsg := message.NewMessage(\"adlerTest\", []byte(\"some random data\"))\n\th1, err := short(msg)\n\tassert.NoError(t, err)\n\th2, err := full(msg)\n\tassert.NoError(t, err)\n\n\tif h1 != h2 {\n\t\tt.Fatal(\"MessageHasherReadLimitMinimum did not apply to SHA256 message hasher\")\n\t}\n}\n\nfunc TestMessageHasherFromMetadataField(t *testing.T) {\n\tt.Parallel()\n\n\tfield := \"hash\"\n\tvalue := \"someHash\"\n\tmsg := message.NewMessage(\"one\", []byte(\"1\"))\n\tmsg.Metadata[field] = value\n\tmetadataPull := middleware.NewMessageHasherFromMetadataField(field)\n\n\th, err := metadataPull(msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, h, value)\n\n\tdelete(msg.Metadata, field) // empty out\n\t_, err = metadataPull(msg)\n\tassert.Error(t, err)\n}\n\nfunc TestMapExpiringKeyRepositoryCleanup(t *testing.T) {\n\tt.Parallel()\n\twait := time.Millisecond * 5\n\tkr, err := middleware.NewMapExpiringKeyRepository(wait)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcount := 0\n\td := &middleware.Deduplicator{\n\t\tRepository: kr,\n\t\tKeyFactory: middleware.NewMessageHasherAdler32(1024),\n\t\tTimeout:    time.Second,\n\t}\n\th := d.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\tcount++\n\t\treturn nil, nil\n\t})\n\n\tfor i := 0; i < 6; i++ { // only one should go through\n\t\tmsg := message.NewMessage(\n\t\t\tfmt.Sprintf(\"expiring%d\", i),\n\t\t\tfmt.Appendf(nil, \"expiring%d\", i),\n\t\t)\n\t\t_, err := h(msg)\n\t\tassert.NoError(t, err)\n\t}\n\n\ttype supportsLen interface {\n\t\tLen() int\n\t}\n\tmeasurable, ok := kr.(supportsLen)\n\tif !ok {\n\t\tt.Fatal(\"repository does not allow measuring its length\")\n\t}\n\n\tif l := measurable.Len(); l != 6 {\n\t\tt.Errorf(\"expected 6 tags, but %d remain\", l)\n\t}\n\n\tassert.Eventually(\n\t\tt,\n\t\tfunc() bool {\n\t\t\treturn count == 6\n\t\t},\n\t\twait*3,\n\t\ttime.Millisecond,\n\t\t\"sent six messages, but only received %d\", count,\n\t)\n\tassert.Eventually(\n\t\tt,\n\t\tfunc() bool {\n\t\t\treturn measurable.Len() == 0\n\t\t},\n\t\twait*3,\n\t\ttime.Millisecond,\n\t\t\"tags should have been cleaned out, but %d remain\",\n\t\tmeasurable.Len(),\n\t)\n}\n"
  },
  {
    "path": "message/router/middleware/delay_on_error.go",
    "content": "package middleware\n\nimport (\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/delay\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// DelayOnError is a middleware that adds the delay metadata to the message if an error occurs.\n//\n// 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.\n// See the list of supported Pub/Subs in the documentation: https://watermill.io/advanced/delayed-messages/\ntype DelayOnError struct {\n\t// InitialInterval is the first interval between retries. Subsequent intervals will be scaled by Multiplier.\n\tInitialInterval time.Duration\n\t// MaxInterval sets the limit for the exponential backoff of retries. The interval will not be increased beyond MaxInterval.\n\tMaxInterval time.Duration\n\t// Multiplier is the factor by which the waiting interval will be multiplied between retries.\n\tMultiplier float64\n}\n\nfunc (d *DelayOnError) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\tmsgs, err := h(msg)\n\t\tif err != nil {\n\t\t\td.applyDelay(msg)\n\t\t}\n\n\t\treturn msgs, err\n\t}\n}\n\nfunc (d *DelayOnError) applyDelay(msg *message.Message) {\n\tdelayedForStr := msg.Metadata.Get(delay.DelayedForKey)\n\tdelayedFor, err := time.ParseDuration(delayedForStr)\n\tif delayedForStr != \"\" && err == nil {\n\t\tdelayedFor *= time.Duration(d.Multiplier)\n\t\tif delayedFor > d.MaxInterval {\n\t\t\tdelayedFor = d.MaxInterval\n\t\t}\n\n\t\tdelay.Message(msg, delay.For(delayedFor))\n\t} else {\n\t\tdelay.Message(msg, delay.For(d.InitialInterval))\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/delay_on_error_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/delay\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\nfunc TestDelayOnError(t *testing.T) {\n\tm := middleware.DelayOnError{\n\t\tInitialInterval: time.Second,\n\t\tMaxInterval:     time.Second * 10,\n\t\tMultiplier:      2,\n\t}\n\n\tmsg := message.NewMessage(\"1\", []byte(\"test\"))\n\n\tgetDelayFor := func(msg *message.Message) string {\n\t\treturn msg.Metadata.Get(delay.DelayedForKey)\n\t}\n\n\tokHandler := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn nil, nil\n\t}\n\n\terrHandler := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn nil, errors.New(\"error\")\n\t}\n\n\tassert.Equal(t, \"\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(okHandler)(msg)\n\tassert.Equal(t, \"\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(errHandler)(msg)\n\tassert.Equal(t, \"1s\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(errHandler)(msg)\n\tassert.Equal(t, \"2s\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(errHandler)(msg)\n\tassert.Equal(t, \"4s\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(errHandler)(msg)\n\tassert.Equal(t, \"8s\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(errHandler)(msg)\n\tassert.Equal(t, \"10s\", getDelayFor(msg))\n\n\t_, _ = m.Middleware(errHandler)(msg)\n\tassert.Equal(t, \"10s\", getDelayFor(msg))\n}\n"
  },
  {
    "path": "message/router/middleware/duplicator.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// Duplicator is processing messages twice, to ensure that the endpoint is idempotent.\nfunc Duplicator(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\tfirstProducedMessages, firstErr := h(msg)\n\t\tif firstErr != nil {\n\t\t\treturn nil, firstErr\n\t\t}\n\n\t\tsecondProducedMessages, secondErr := h(msg)\n\t\tif secondErr != nil {\n\t\t\treturn nil, secondErr\n\t\t}\n\n\t\treturn append(firstProducedMessages, secondProducedMessages...), nil\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/duplicator_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\nvar (\n\tsomeMsg = message.NewMessage(\"1\", nil)\n)\n\nfunc TestDuplicator(t *testing.T) {\n\tvar executionsCount int\n\tproducedMessages, err := middleware.Duplicator(func(msg *message.Message) ([]*message.Message, error) {\n\t\texecutionsCount++\n\t\treturn []*message.Message{msg}, nil\n\t})(someMsg)\n\n\tassert.NoError(t, err)\n\tassert.Len(t, producedMessages, 2)\n\tassert.Equal(t, \"1\", producedMessages[0].UUID)\n\tassert.Equal(t, \"1\", producedMessages[1].UUID)\n\tassert.Equal(t, 2, executionsCount)\n}\n\nfunc TestDuplicator_errors(t *testing.T) {\n\t_, err := middleware.Duplicator(func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn nil, errors.New(\"some error\")\n\t})(someMsg)\n\tassert.Error(t, err, \"some error\")\n\n\tvar wasExecuted bool\n\t_, err = middleware.Duplicator(func(msg *message.Message) ([]*message.Message, error) {\n\t\tif wasExecuted {\n\t\t\treturn nil, errors.New(\"some other error\")\n\t\t}\n\n\t\twasExecuted = true\n\t\treturn []*message.Message{msg}, nil\n\t})(someMsg)\n\tassert.Error(t, err, \"some other error\")\n}\n"
  },
  {
    "path": "message/router/middleware/ignore_errors.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// IgnoreErrors provides a middleware that makes the handler ignore some explicitly whitelisted errors.\ntype IgnoreErrors struct {\n\tignoredErrors map[string]struct{}\n}\n\n// NewIgnoreErrors creates a new IgnoreErrors middleware.\nfunc NewIgnoreErrors(errs []error) IgnoreErrors {\n\terrsMap := make(map[string]struct{}, len(errs))\n\n\tfor _, err := range errs {\n\t\terrsMap[err.Error()] = struct{}{}\n\t}\n\n\treturn IgnoreErrors{errsMap}\n}\n\n// Middleware returns the IgnoreErrors middleware.\nfunc (i IgnoreErrors) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\tevents, err := h(msg)\n\t\tif err != nil {\n\t\t\tif _, ok := i.ignoredErrors[errors.Cause(err).Error()]; ok {\n\t\t\t\treturn events, nil\n\t\t\t}\n\n\t\t\treturn events, err\n\t\t}\n\n\t\treturn events, nil\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/ignore_errors_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIgnoreErrors_Middleware(t *testing.T) {\n\ttestCases := []struct {\n\t\tName            string\n\t\tIgnoredErrors   []error\n\t\tTestError       error\n\t\tShouldBeIgnored bool\n\t}{\n\t\t{\n\t\t\tName:            \"ignored_error\",\n\t\t\tIgnoredErrors:   []error{errors.New(\"test\")},\n\t\t\tTestError:       errors.New(\"test\"),\n\t\t\tShouldBeIgnored: true,\n\t\t},\n\t\t{\n\t\t\tName:            \"not_ignored_error\",\n\t\t\tIgnoredErrors:   []error{errors.New(\"test\")},\n\t\t\tTestError:       errors.New(\"not_ignored\"),\n\t\t\tShouldBeIgnored: false,\n\t\t},\n\t\t{\n\t\t\tName:            \"wrapped_error_should_ignore\",\n\t\t\tIgnoredErrors:   []error{errors.New(\"test\")},\n\t\t\tTestError:       errors.Wrap(errors.New(\"test\"), \"wrapped\"),\n\t\t\tShouldBeIgnored: true,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tm := middleware.NewIgnoreErrors(c.IgnoredErrors)\n\n\t\t\tmessagesToProduce := []*message.Message{message.NewMessage(\"1\", nil)}\n\n\t\t\tproducedMessages, err := m.Middleware(func(msg *message.Message) ([]*message.Message, error) {\n\t\t\t\treturn messagesToProduce, c.TestError\n\t\t\t})(nil)\n\n\t\t\tif c.ShouldBeIgnored {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, c.TestError, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, messagesToProduce, producedMessages)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/instant_ack.go",
    "content": "package middleware\n\nimport \"github.com/ThreeDotsLabs/watermill/message\"\n\n// InstantAck makes the handler instantly acknowledge the incoming message, regardless of any errors.\n// It may be used to gain throughput, but at a cost:\n// If you had exactly-once delivery, you may expect at-least-once instead.\n// If you had ordered messages, the ordering might be broken.\nfunc InstantAck(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(message *message.Message) ([]*message.Message, error) {\n\t\tmessage.Ack()\n\t\treturn h(message)\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/instant_ack_test.go",
    "content": "package middleware\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestInstantAck(t *testing.T) {\n\tproducedMessages := message.Messages{message.NewMessage(\"2\", nil)}\n\tproducedErr := errors.New(\"foo\")\n\n\th := InstantAck(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn producedMessages, producedErr\n\t})\n\n\tmsg := message.NewMessage(\"1\", nil)\n\n\thandlerMessages, handlerErr := h(msg)\n\tassert.EqualValues(t, producedMessages, handlerMessages)\n\tassert.Equal(t, producedErr, handlerErr)\n\n\tselect {\n\tcase <-msg.Acked():\n\t// ok\n\tcase <-msg.Nacked():\n\t\tt.Fatal(\"expected ack, not nack\")\n\tdefault:\n\t\tt.Fatal(\"no ack received\")\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/message_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\ntype mockPublisherBehaviour int\n\nconst (\n\tBehaviourAlwaysOK mockPublisherBehaviour = iota + 1\n\tBehaviourAlwaysFail\n\tBehaviourAlwaysPanic\n)\n\nvar (\n\terrClosed   = errors.New(\"closed\")\n\terrFailed   = errors.New(\"failed\")\n\terrPanicked = errors.New(\"panicked\")\n)\n\ntype mockPublisher struct {\n\tbehaviour mockPublisherBehaviour\n\tclosed    bool\n\n\tproduced []*message.Message\n}\n\nfunc (mp *mockPublisher) Publish(topic string, messages ...*message.Message) error {\n\tif mp.closed {\n\t\treturn errClosed\n\t}\n\n\tswitch mp.behaviour {\n\tcase BehaviourAlwaysOK:\n\tcase BehaviourAlwaysFail:\n\t\treturn errFailed\n\tcase BehaviourAlwaysPanic:\n\t\tpanic(errPanicked)\n\t}\n\n\tmp.produced = append(mp.produced, messages...)\n\treturn nil\n}\n\nfunc (mp *mockPublisher) Close() error {\n\tmp.closed = true\n\treturn nil\n}\n\nfunc (mp *mockPublisher) PopMessages() []*message.Message {\n\tdefer func() { mp.produced = []*message.Message{} }()\n\treturn mp.produced\n}\n\nvar handlerFuncAlwaysOKMessages = []*message.Message{\n\tmessage.NewMessage(watermill.NewUUID(), nil),\n\tmessage.NewMessage(watermill.NewUUID(), nil),\n}\n\nfunc handlerFuncAlwaysOK(*message.Message) ([]*message.Message, error) {\n\treturn handlerFuncAlwaysOKMessages, nil\n}\n\nfunc handlerFuncAlwaysFailing(*message.Message) ([]*message.Message, error) {\n\treturn nil, errFailed\n}\n"
  },
  {
    "path": "message/router/middleware/poison.go",
    "content": "package middleware\n\nimport (\n\tstdErrors \"errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// ErrInvalidPoisonQueueTopic occurs when the topic supplied to the PoisonQueue constructor is invalid.\nvar ErrInvalidPoisonQueueTopic = errors.New(\"invalid poison queue topic\")\n\n// Metadata keys which marks the reason and context why the message was deemed poisoned.\nconst (\n\tReasonForPoisonedKey  = \"reason_poisoned\"\n\tPoisonedTopicKey      = \"topic_poisoned\"\n\tPoisonedHandlerKey    = \"handler_poisoned\"\n\tPoisonedSubscriberKey = \"subscriber_poisoned\"\n)\n\ntype poisonQueue struct {\n\ttopic string\n\tpub   message.Publisher\n\n\tshouldGoToPoisonQueue func(err error) bool\n}\n\n// PoisonQueue provides a middleware that salvages unprocessable messages and published them on a separate topic.\n// The main middleware chain then continues on, business as usual.\nfunc PoisonQueue(pub message.Publisher, topic string) (message.HandlerMiddleware, error) {\n\tif topic == \"\" {\n\t\treturn nil, ErrInvalidPoisonQueueTopic\n\t}\n\n\tpq := poisonQueue{\n\t\ttopic: topic,\n\t\tpub:   pub,\n\t\tshouldGoToPoisonQueue: func(err error) bool {\n\t\t\treturn true\n\t\t},\n\t}\n\n\treturn pq.Middleware, nil\n}\n\n// PoisonQueueWithFilter is just like PoisonQueue, but accepts a function that decides which errors qualify for the poison queue.\nfunc PoisonQueueWithFilter(pub message.Publisher, topic string, shouldGoToPoisonQueue func(err error) bool) (message.HandlerMiddleware, error) {\n\tif topic == \"\" {\n\t\treturn nil, ErrInvalidPoisonQueueTopic\n\t}\n\n\tpq := poisonQueue{\n\t\ttopic: topic,\n\t\tpub:   pub,\n\n\t\tshouldGoToPoisonQueue: shouldGoToPoisonQueue,\n\t}\n\n\treturn pq.Middleware, nil\n}\n\nfunc (pq poisonQueue) publishPoisonMessage(msg *message.Message, err error) error {\n\t// no problems encountered, carry on\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// add context why it was poisoned\n\tmsg.Metadata.Set(ReasonForPoisonedKey, err.Error())\n\tmsg.Metadata.Set(PoisonedTopicKey, message.SubscribeTopicFromCtx(msg.Context()))\n\tmsg.Metadata.Set(PoisonedHandlerKey, message.HandlerNameFromCtx(msg.Context()))\n\tmsg.Metadata.Set(PoisonedSubscriberKey, message.SubscriberNameFromCtx(msg.Context()))\n\n\t// don't intercept error from publish. Can't help you if the publisher is down as well.\n\treturn pq.pub.Publish(pq.topic, msg)\n}\n\nfunc (pq poisonQueue) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) (events []*message.Message, err error) {\n\t\tdefer func() {\n\t\t\tif err != nil {\n\t\t\t\tif !pq.shouldGoToPoisonQueue(err) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// handler didn't cope with the message; publish it on the poison topic and carry on as usual\n\t\t\t\tpublishErr := pq.publishPoisonMessage(msg, err)\n\t\t\t\tif publishErr != nil {\n\t\t\t\t\tpublishErr = errors.Wrap(publishErr, \"cannot publish message to poison queue\")\n\t\t\t\t\terr = stdErrors.Join(err, publishErr)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\terr = nil\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\n\t\t// if h fails, the deferred function will salvage all that it can\n\t\treturn h(msg)\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/poison_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"context\"\n\tstdErrors \"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nconst topic = \"testing_poison_queue_topic\"\n\n// TestPoisonQueue_handler_ok simulates the situation when the message is processed correctly\n// We expect that all messages pass through the middleware unaffected and the poison queue catches no messages.\nfunc TestPoisonQueue_handler_ok(t *testing.T) {\n\tpoisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}\n\n\tpoisonQueue, err := middleware.PoisonQueue(&poisonPublisher, topic)\n\trequire.NoError(t, err)\n\n\tpoisonQueueWithFilter, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {\n\t\treturn true\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tName       string\n\t\tMiddleware message.HandlerMiddleware\n\t}{\n\t\t{\n\t\t\tName:       \"PoisonQueue\",\n\t\t\tMiddleware: poisonQueue,\n\t\t},\n\t\t{\n\t\t\tName:       \"PoisonQueueWithFilter\",\n\t\t\tMiddleware: poisonQueueWithFilter,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tproduced, err := c.Middleware(handlerFuncAlwaysOK)(\n\t\t\t\tmessage.NewMessage(\"uuid\", nil),\n\t\t\t)\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, handlerFuncAlwaysOKMessages, produced)\n\t\t\tassert.Empty(t, poisonPublisher.PopMessages())\n\t\t})\n\t}\n}\n\nfunc TestPoisonQueue_handler_failing(t *testing.T) {\n\tpoisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}\n\n\tpoisonQueue, err := middleware.PoisonQueue(&poisonPublisher, topic)\n\trequire.NoError(t, err)\n\n\tpoisonQueueWithFilter, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {\n\t\treturn true\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tName       string\n\t\tMiddleware message.HandlerMiddleware\n\t}{\n\t\t{\n\t\t\tName:       \"PoisonQueue\",\n\t\t\tMiddleware: poisonQueue,\n\t\t},\n\t\t{\n\t\t\tName:       \"PoisonQueueWithFilter\",\n\t\t\tMiddleware: poisonQueueWithFilter,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tmsg := message.NewMessage(\"uuid\", []byte(\"payload\"))\n\t\t\tproduced, err := c.Middleware(handlerFuncAlwaysFailing)(\n\t\t\t\tmsg,\n\t\t\t)\n\n\t\t\t// the middleware itself should not fail; the publisher is working OK, so no error is passed down the chain\n\t\t\tassert.NoError(t, err)\n\n\t\t\t// but no messages should be passed\n\t\t\tassert.Empty(t, produced)\n\n\t\t\t// the original message should end up in the poison queue\n\t\t\tpoisonMsgs := poisonPublisher.PopMessages()\n\t\t\trequire.Len(t, poisonMsgs, 1)\n\n\t\t\tassert.Equal(t, msg.Payload, poisonMsgs[0].Payload)\n\n\t\t\t// there should be additional metadata telling why the message was poisoned\n\t\t\t// it should be the error that the handler failed with\n\t\t\tassert.Equal(t, errFailed.Error(), poisonMsgs[0].Metadata.Get(middleware.ReasonForPoisonedKey))\n\t\t})\n\t}\n}\n\nfunc TestPoisonQueue_context_values(t *testing.T) {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{Persistent: true},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\n\tlogger := watermill.NewStdLogger(true, true)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tpq, err := middleware.PoisonQueue(pubSub, \"poison_queue\")\n\trequire.NoError(t, err)\n\trouter.AddMiddleware(pq)\n\n\trouter.AddConsumerHandler(\"handler_name\", \"test\", pubSub, func(msg *message.Message) error {\n\t\treturn errors.New(\"error\")\n\t})\n\n\tgo func() {\n\t\trequire.NoError(t, router.Run(context.Background()))\n\t}()\n\trequire.NoError(t, err)\n\tdefer router.Close()\n\n\tselect {\n\tcase <-router.Running():\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"waiting for router timeout\")\n\t}\n\n\terr = pubSub.Publish(\"test\", message.NewMessage(\"1\", nil))\n\trequire.NoError(t, err)\n\n\tmsgs, err := pubSub.Subscribe(context.Background(), \"poison_queue\")\n\trequire.NoError(t, err)\n\n\tmessages, all := subscriber.BulkRead(msgs, 1, time.Second)\n\trequire.True(t, all, \"no messages received\")\n\n\tassert.Equal(t, \"handler_name\", messages[0].Metadata[middleware.PoisonedHandlerKey])\n\tassert.Equal(t, \"gochannel.GoChannel\", messages[0].Metadata[middleware.PoisonedSubscriberKey])\n\tassert.Equal(t, \"test\", messages[0].Metadata[middleware.PoisonedTopicKey])\n\tassert.Equal(t, \"error\", messages[0].Metadata[middleware.ReasonForPoisonedKey])\n}\n\nfunc TestPoisonQueue_handler_failing_publisher_failing(t *testing.T) {\n\tpoisonPublisher := mockPublisher{behaviour: BehaviourAlwaysFail}\n\n\tpoisonQueue, err := middleware.PoisonQueue(&poisonPublisher, topic)\n\trequire.NoError(t, err)\n\n\tpoisonQueueWithFilter, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {\n\t\treturn true\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tName       string\n\t\tMiddleware message.HandlerMiddleware\n\t}{\n\t\t{\n\t\t\tName:       \"PoisonQueue\",\n\t\t\tMiddleware: poisonQueue,\n\t\t},\n\t\t{\n\t\t\tName:       \"PoisonQueueWithFilter\",\n\t\t\tMiddleware: poisonQueueWithFilter,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tmsg := message.NewMessage(\"uuid\", nil)\n\t\t\tproduced, err := poisonQueue(handlerFuncAlwaysFailing)(\n\t\t\t\tmsg,\n\t\t\t)\n\n\t\t\t// publisher failed, can't hide the error anymore\n\t\t\t// Instead of checking the specific error, we check if the error.Is() is the same as the one we expect\n\t\t\tassert.ErrorIs(t, err, errFailed)\n\n\t\t\t// can't really expect any produced messages\n\t\t\tassert.Empty(t, produced)\n\n\t\t\t// nor poison messages\n\t\t\tassert.Empty(t, poisonPublisher.PopMessages())\n\t\t})\n\t}\n}\n\nfunc TestPoisonQueueWithFilter_poison_queue(t *testing.T) {\n\tpoisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}\n\n\tpoisonQueueErr := errors.New(\"poison queue err\")\n\tmsg := message.NewMessage(\"uuid\", []byte(\"payload\"))\n\n\tpoisonQueue, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {\n\t\treturn stdErrors.Is(err, poisonQueueErr)\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = poisonQueue(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, poisonQueueErr\n\t})(msg)\n\n\tassert.NoError(t, err)\n\trequire.Len(t, poisonPublisher.PopMessages(), 1)\n}\n\nfunc TestPoisonQueueWithFilter_non_poison_queue(t *testing.T) {\n\tpoisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}\n\n\tnonPoisonQueueErr := errors.New(\"non poison queue err\")\n\tmsg := message.NewMessage(\"uuid\", []byte(\"payload\"))\n\n\tpoisonQueue, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {\n\t\treturn !stdErrors.Is(err, nonPoisonQueueErr)\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = poisonQueue(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, nonPoisonQueueErr\n\t})(msg)\n\n\tassert.Error(t, err)\n\trequire.Len(t, poisonPublisher.PopMessages(), 0)\n}\n"
  },
  {
    "path": "message/router/middleware/randomfail.go",
    "content": "package middleware\n\nimport (\n\t\"math/rand\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc shouldFail(probability float32) bool {\n\tr := rand.Float32()\n\treturn r <= probability\n}\n\n// RandomFail makes the handler fail with an error based on random chance. Error probability should be in the range (0,1).\nfunc RandomFail(errorProbability float32) message.HandlerMiddleware {\n\treturn func(h message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(message *message.Message) ([]*message.Message, error) {\n\t\t\tif shouldFail(errorProbability) {\n\t\t\t\treturn nil, errors.New(\"random fail occurred\")\n\t\t\t}\n\n\t\t\treturn h(message)\n\t\t}\n\t}\n}\n\n// RandomPanic makes the handler panic based on random chance. Panic probability should be in the range (0,1).\nfunc RandomPanic(panicProbability float32) message.HandlerMiddleware {\n\treturn func(h message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(message *message.Message) ([]*message.Message, error) {\n\t\t\tif shouldFail(panicProbability) {\n\t\t\t\tpanic(\"random panic occurred\")\n\t\t\t}\n\n\t\t\treturn h(message)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/randomfail_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRandomFail(t *testing.T) {\n\th := middleware.RandomFail(1)(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, nil\n\t})\n\n\t_, err := h(message.NewMessage(\"1\", nil))\n\tassert.Error(t, err)\n}\n\nfunc TestRandomPanic(t *testing.T) {\n\th := middleware.RandomPanic(1)(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, nil\n\t})\n\n\tassert.Panics(t, func() {\n\t\t_, _ = h(message.NewMessage(\"1\", nil))\n\t})\n}\n"
  },
  {
    "path": "message/router/middleware/recoverer.go",
    "content": "package middleware\n\nimport (\n\t\"fmt\"\n\t\"runtime/debug\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/pkg/errors\"\n)\n\n// RecoveredPanicError holds the recovered panic's error along with the stacktrace.\ntype RecoveredPanicError struct {\n\tV          interface{}\n\tStacktrace string\n}\n\nfunc (p RecoveredPanicError) Error() string {\n\treturn fmt.Sprintf(\"panic occurred: %#v, stacktrace: \\n%s\", p.V, p.Stacktrace)\n}\n\n// Recoverer recovers from any panic in the handler and appends RecoveredPanicError with the stacktrace\n// to any error returned from the handler.\nfunc Recoverer(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(event *message.Message) (events []*message.Message, err error) {\n\t\tpanicked := true\n\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil || panicked {\n\t\t\t\terr = errors.WithStack(RecoveredPanicError{V: r, Stacktrace: string(debug.Stack())})\n\t\t\t}\n\t\t}()\n\n\t\tevents, err = h(event)\n\t\tpanicked = false\n\t\treturn events, err\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/recoverer_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRecoverer_Panic(t *testing.T) {\n\th := middleware.Recoverer(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\tpanic(\"foo\")\n\t})\n\n\t_, err := h(message.NewMessage(\"1\", nil))\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"message/router/middleware/recoverer.go\") // stacktrace part\n}\n\nfunc TestRecoverer_PanicNil(t *testing.T) {\n\th := middleware.Recoverer(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\tpanic(nil)\n\t})\n\n\t_, err := h(message.NewMessage(\"1\", nil))\n\trequire.Error(t, err)\n}\n\nfunc TestRecoverer_NoPanic(t *testing.T) {\n\th := middleware.Recoverer(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, nil\n\t})\n\n\t_, err := h(message.NewMessage(\"1\", nil))\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "message/router/middleware/retry.go",
    "content": "package middleware\n\nimport (\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// RetryParams holds the parameters for a retry attempt\ntype RetryParams struct {\n\t// Err is the error that caused the retry attempt.\n\tErr error\n\t// RetryNum is the number of the retry attempt, starting from 1.\n\tRetryNum int\n\t// Delay is the delay for the next retry attempt.\n\tDelay time.Duration\n}\n\n// Retry provides a middleware that retries the handler if errors are returned.\n// The retry behaviour is configurable, with exponential backoff and maximum elapsed time.\ntype Retry struct {\n\t// MaxRetries is maximum number of times a retry will be attempted.\n\tMaxRetries int\n\n\t// InitialInterval is the first interval between retries. Subsequent intervals will be scaled by Multiplier.\n\tInitialInterval time.Duration\n\t// MaxInterval sets the limit for the exponential backoff of retries. The interval will not be increased beyond MaxInterval.\n\tMaxInterval time.Duration\n\t// Multiplier is the factor by which the waiting interval will be multiplied between retries.\n\tMultiplier float64\n\t// MaxElapsedTime sets the time limit of how long retries will be attempted. Disabled if 0.\n\tMaxElapsedTime time.Duration\n\t// RandomizationFactor randomizes the spread of the backoff times within the interval of:\n\t// [currentInterval * (1 - randomization_factor), currentInterval * (1 + randomization_factor)].\n\tRandomizationFactor float64\n\n\t// OnRetryHook is an optional function that will be executed on each retry attempt.\n\t// The number of the current retry is passed as retryNum,\n\tOnRetryHook func(retryNum int, delay time.Duration)\n\n\t// ShouldRetry is an optional function that will be executed before each retry attempt.\n\t// If ShouldRetry returns false, the retry will not be attempted.\n\tShouldRetry func(params RetryParams) bool\n\n\t// ResetContextOnRetry indicates whether the message context should be reset on each retry attempt.\n\t// See more: https://github.com/ThreeDotsLabs/watermill/issues/467\n\t//\n\t// This is not enabled by default to keep backward compatibility\n\t// (in theory, someone may want to preserve context values between retries).\n\tResetContextOnRetry bool\n\n\tLogger watermill.LoggerAdapter\n}\n\n// Middleware returns the Retry middleware.\nfunc (r Retry) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\toriginalCtx := msg.Context()\n\t\tretryNum := 0\n\n\t\texpBackoff := backoff.NewExponentialBackOff()\n\t\texpBackoff.InitialInterval = r.InitialInterval\n\t\texpBackoff.MaxInterval = r.MaxInterval\n\t\texpBackoff.Multiplier = r.Multiplier\n\t\texpBackoff.RandomizationFactor = r.RandomizationFactor\n\n\t\t// MaxRetries + 1 because the first attempt is not a retry\n\t\tretryBackoff := backoff.WithMaxTries(uint(r.MaxRetries + 1))\n\n\t\tmaxElapsedBackoff := backoff.WithMaxElapsedTime(r.MaxElapsedTime)\n\n\t\t// notification: called on a failed retry attempt.\n\t\tnotification := func(err error, delay time.Duration) {\n\t\t\tif r.Logger != nil {\n\t\t\t\tr.Logger.Error(\"Error occurred, retrying\", err, watermill.LogFields{\n\t\t\t\t\t\"retry_no\":    retryNum,\n\t\t\t\t\t\"max_retries\": r.MaxRetries,\n\t\t\t\t\t\"wait_time\":   delay,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// operation: the function that will be retried.\n\t\toperation := func() ([]*message.Message, error) {\n\t\t\tselect {\n\t\t\tcase <-originalCtx.Done():\n\t\t\t\treturn nil, originalCtx.Err()\n\t\t\tdefault:\n\t\t\t\tif r.ResetContextOnRetry {\n\t\t\t\t\t// message is passed as a pointer, so it's context can be canceled\n\t\t\t\t\t// by the previous attempts -> it will break retries, because any\n\t\t\t\t\t// underlying logic that relies on the context will fail.\n\t\t\t\t\t// see more: https://github.com/ThreeDotsLabs/watermill/issues/467\n\t\t\t\t\t//\n\t\t\t\t\t// to avoid this, we need to reset the original context on each attempt\n\t\t\t\t\t// we may lose context value that was set by the previous attempt\n\t\t\t\t\tmsg.SetContext(originalCtx)\n\t\t\t\t}\n\n\t\t\t\tproducedMessages, err := h(msg)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn producedMessages, nil\n\t\t\t\t}\n\n\t\t\t\tif r.ShouldRetry != nil && !r.ShouldRetry(RetryParams{\n\t\t\t\t\tRetryNum: retryNum,\n\t\t\t\t\tErr:      err,\n\t\t\t\t\tDelay:    expBackoff.NextBackOff(),\n\t\t\t\t}) {\n\t\t\t\t\t// backoff.Permanent will stop the retry attempts\n\t\t\t\t\treturn producedMessages, backoff.Permanent(err)\n\t\t\t\t}\n\n\t\t\t\tif r.OnRetryHook != nil && retryNum > 0 {\n\t\t\t\t\t// call RetryHook function on each retry attempt.\n\t\t\t\t\tr.OnRetryHook(retryNum, expBackoff.NextBackOff())\n\t\t\t\t}\n\t\t\t\tretryNum++\n\t\t\t\treturn producedMessages, err\n\t\t\t}\n\t\t}\n\n\t\tproducedMessages, retryErr := backoff.Retry(\n\t\t\toriginalCtx,\n\t\t\toperation,\n\t\t\tbackoff.WithBackOff(expBackoff),\n\t\t\tretryBackoff,\n\t\t\tmaxElapsedBackoff,\n\t\t\tbackoff.WithNotify(notification),\n\t\t)\n\t\tvar backoffPermanentError *backoff.PermanentError\n\t\tif errors.As(retryErr, &backoffPermanentError) {\n\t\t\t// just in case, we don't want to expose backoff.PermanentError to the outside world\n\t\t\treturn producedMessages, backoffPermanentError.Unwrap()\n\t\t}\n\t\tif retryErr != nil {\n\t\t\treturn producedMessages, retryErr\n\t\t}\n\n\t\treturn producedMessages, nil\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/retry_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\nfunc TestRetry_retry(t *testing.T) {\n\tretry := middleware.Retry{\n\t\tMaxRetries: 1,\n\t}\n\n\trunCount := 0\n\tproducedMessages := message.Messages{message.NewMessage(\"2\", nil)}\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\trunCount++\n\t\tif runCount == 0 {\n\t\t\treturn nil, errors.New(\"foo\")\n\t\t}\n\n\t\treturn producedMessages, nil\n\t})\n\n\thandlerMessages, handlerErr := h(message.NewMessage(\"1\", nil))\n\n\tassert.Equal(t, 1, runCount)\n\tassert.EqualValues(t, producedMessages, handlerMessages)\n\tassert.NoError(t, handlerErr)\n}\n\nfunc TestRetry_max_retries(t *testing.T) {\n\tretry := middleware.Retry{\n\t\tMaxRetries: 1,\n\t\tLogger:     watermill.NewStdLogger(true, true),\n\t}\n\n\trunCount := 0\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\trunCount++\n\t\treturn nil, errors.New(\"foo\")\n\t})\n\n\t_, err := h(message.NewMessage(\"1\", nil))\n\n\tassert.Equal(t, 2, runCount)\n\tassert.EqualError(t, err, \"foo\")\n}\n\nfunc TestRetry_retry_hook(t *testing.T) {\n\tvar retriesFromHook []int\n\n\tretry := middleware.Retry{\n\t\tMaxRetries: 2,\n\t\tOnRetryHook: func(retryNum int, delay time.Duration) {\n\t\t\tretriesFromHook = append(retriesFromHook, retryNum)\n\t\t},\n\t}\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, errors.New(\"foo\")\n\t})\n\t_, _ = h(message.NewMessage(\"1\", nil))\n\n\tassert.EqualValues(t, []int{1, 2}, retriesFromHook)\n}\n\nfunc TestRetry_logger(t *testing.T) {\n\tlogger := watermill.NewCaptureLogger()\n\n\tretry := middleware.Retry{\n\t\tMaxRetries: 2,\n\t\tLogger:     logger,\n\t}\n\n\thandlerErr := errors.New(\"foo\")\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, handlerErr\n\t})\n\t_, _ = h(message.NewMessage(\"1\", nil))\n\n\tassert.True(t, logger.HasError(handlerErr))\n}\n\nfunc TestRetry_ctx_cancel(t *testing.T) {\n\tretry := middleware.Retry{\n\t\tInitialInterval: time.Minute,\n\t}\n\n\tproducedMessages := message.Messages{message.NewMessage(\"2\", nil)}\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn producedMessages, errors.New(\"err\")\n\t})\n\n\tmsg := message.NewMessage(\"1\", nil)\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tmsg.SetContext(ctx)\n\n\tdone := make(chan struct{})\n\n\ttype handlerResult struct {\n\t\tMessages message.Messages\n\t\tErr      error\n\t}\n\thandlerResultCh := make(chan handlerResult, 1)\n\n\tgo func() {\n\t\tmessages, err := h(msg)\n\t\thandlerResultCh <- handlerResult{messages, err}\n\t\tclose(done)\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\tt.Fatal(\"handler should be still during retrying\")\n\tdefault:\n\t\t// ok\n\t}\n\n\tcancel()\n\n\tselect {\n\tcase <-done:\n\t\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"ctx cancelled, retrying should be done\")\n\t}\n\n\thandlerResultReceived := <-handlerResultCh\n\n\tassert.Error(t, handlerResultReceived.Err)\n\t// produced messages are nil since ctx is canceling the operation in the middle\n\tassert.Nil(t, handlerResultReceived.Messages)\n}\n\nfunc TestRetry_max_elapsed(t *testing.T) {\n\tmaxRetries := 10\n\tsleepInHandler := time.Millisecond * 20\n\n\tretry := middleware.Retry{\n\t\tMaxElapsedTime: time.Millisecond * 10,\n\t\tMaxRetries:     maxRetries,\n\t}\n\n\trunTimeWithoutMaxElapsedTime := sleepInHandler * time.Duration(maxRetries)\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\ttime.Sleep(sleepInHandler)\n\t\treturn nil, errors.New(\"foo\")\n\t})\n\n\tstartTime := time.Now()\n\t_, _ = h(message.NewMessage(\"1\", nil))\n\ttimeElapsed := time.Since(startTime)\n\n\tassert.True(\n\t\tt,\n\t\ttimeElapsed < runTimeWithoutMaxElapsedTime,\n\t\t\"handler should run less than %s, time elapsed: %s\",\n\t\trunTimeWithoutMaxElapsedTime,\n\t\ttimeElapsed,\n\t)\n}\n\nfunc TestRetry_max_interval(t *testing.T) {\n\tt.Parallel()\n\n\tmaxRetries := 10\n\tbackoffTimes := make([]time.Duration, maxRetries)\n\tmaxInterval := time.Millisecond * 30\n\n\tretry := middleware.Retry{\n\t\tMaxRetries:          maxRetries,\n\t\tInitialInterval:     time.Millisecond * 10,\n\t\tMaxInterval:         maxInterval,\n\t\tMultiplier:          2.0,\n\t\tRandomizationFactor: 0,\n\t\tOnRetryHook: func(retryNum int, delay time.Duration) {\n\t\t\tbackoffTimes[retryNum-1] = delay\n\t\t},\n\t}\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, errors.New(\"bar\")\n\t})\n\t_, _ = h(message.NewMessage(\"2\", nil))\n\n\tfor i, delay := range backoffTimes {\n\t\tassert.True(t, delay <= maxInterval, \"wait interval %d (%s) exceeds maxInterval (%s)\", i, delay, maxInterval)\n\t}\n}\n\nfunc TestRetry_first_run_no_delay(t *testing.T) {\n\tt.Parallel()\n\n\tinitialInterval := time.Millisecond * 100\n\tretry := middleware.Retry{\n\t\tMaxElapsedTime:  initialInterval * 2,\n\t\tMaxRetries:      10,\n\t\tInitialInterval: initialInterval,\n\t}\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, nil\n\t})\n\n\tstart := time.Now()\n\t_, _ = h(message.NewMessage(\"1\", nil))\n\telapsed := time.Since(start)\n\n\tassert.True(t, elapsed < initialInterval, \"first retry should not wait, elapsed: %s\", elapsed)\n}\n\nfunc TestRetry_should_retry(t *testing.T) {\n\terrToSkip := errors.New(\"this should be skipped\")\n\n\tretry := middleware.Retry{\n\t\tMaxRetries: 5,\n\t\tShouldRetry: func(params middleware.RetryParams) bool {\n\t\t\treturn !errors.Is(params.Err, errToSkip)\n\t\t},\n\t}\n\n\trunCount := 0\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\trunCount++\n\t\treturn nil, errToSkip\n\t})\n\n\thandlerMessages, handlerErr := h(message.NewMessage(\"1\", nil))\n\n\tassert.Equal(t, 1, runCount)\n\tassert.Nil(t, handlerMessages)\n\tassert.ErrorIs(t, handlerErr, errToSkip)\n\n\t// to not create any dependency on backoff package\n\tvar backoffPermanentError *backoff.PermanentError\n\tassert.False(t, errors.As(handlerErr, &backoffPermanentError))\n}\n\n// Test that the ShouldRetry function is called on each retry attempt.\n// Under the hood, the second attempt goes over a bit different code path,\n// so we want to make sure that it works consistently.\nfunc TestRetry_should_retry_second_attempt(t *testing.T) {\n\terrToSkip := errors.New(\"this should be skipped\")\n\n\tretry := middleware.Retry{\n\t\tMaxRetries: 5,\n\t\tShouldRetry: func(params middleware.RetryParams) bool {\n\t\t\treturn !errors.Is(params.Err, errToSkip)\n\t\t},\n\t}\n\n\trunCount := 0\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\trunCount++\n\n\t\tif runCount == 1 {\n\t\t\treturn nil, errors.New(\"some other error\")\n\t\t} else {\n\t\t\treturn nil, errToSkip\n\t\t}\n\t})\n\n\thandlerMessages, handlerErr := h(message.NewMessage(\"1\", nil))\n\n\tassert.Equal(t, 2, runCount)\n\tassert.Nil(t, handlerMessages)\n\tassert.ErrorIs(t, handlerErr, errToSkip)\n\n\t// to not create any dependency on backoff package\n\tvar backoffPermanentError *backoff.PermanentError\n\tassert.False(t, errors.As(handlerErr, &backoffPermanentError))\n}\n\n// Test that if backoff.Permanent error stops the retries.\n// For sake of potential future regressions (see Hyrum's Law).\nfunc TestRetry_backoff_backoff_permanent(t *testing.T) {\n\terrToSkip := errors.New(\"this should be skipped\")\n\n\tretry := middleware.Retry{\n\t\tMaxRetries: 5,\n\t}\n\n\trunCount := 0\n\n\th := retry.Middleware(func(msg *message.Message) (messages []*message.Message, e error) {\n\t\trunCount++\n\t\treturn nil, backoff.Permanent(errToSkip)\n\t})\n\n\thandlerMessages, handlerErr := h(message.NewMessage(\"1\", nil))\n\n\tassert.Equal(t, 1, runCount)\n\tassert.Nil(t, handlerMessages)\n\tassert.ErrorIs(t, handlerErr, errToSkip)\n\n\t// to not create any dependency on backoff package\n\tvar backoffPermanentError *backoff.PermanentError\n\tassert.False(t, errors.As(handlerErr, &backoffPermanentError))\n}\n\n// TestRetry_uncancel_context checks scenario when context is canceled,\n// and we want to retry. More context: https://github.com/ThreeDotsLabs/watermill/issues/467\n//\n// Message is passed as a pointer, so underlying middlewares or handlers can cancel its context.\n// In this scenario, retrying is pointless because context is already canceled, so any operation\n// that relies on context will fail immediately.\nfunc TestRetry_uncancel_context(t *testing.T) {\n\tretry := middleware.Retry{\n\t\tMaxRetries:          5,\n\t\tResetContextOnRetry: true,\n\t}\n\n\tnum := 0\n\n\tvar ctxCancelMiddleware = func(h message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\t\tnum++\n\n\t\t\tctx, cancel := context.WithCancel(msg.Context())\n\t\t\tdefer func() {\n\t\t\t\tcancel()\n\t\t\t}()\n\n\t\t\tif num == 1 {\n\t\t\t\tt.Log(\"Run 1: canceling context\")\n\t\t\t\tcancel()\n\t\t\t} else {\n\t\t\t\tt.Logf(\"Run %d: context is not canceled\", num)\n\t\t\t}\n\n\t\t\tmsg.SetContext(ctx)\n\t\t\treturn h(msg)\n\t\t}\n\t}\n\n\th := func(msg *message.Message) (messages []*message.Message, e error) {\n\t\treturn nil, msg.Context().Err()\n\t}\n\n\th = ctxCancelMiddleware(h)\n\th = retry.Middleware(h)\n\n\t_, handlerErr := h(message.NewMessage(\"1\", nil))\n\tassert.NoError(t, handlerErr)\n}\n"
  },
  {
    "path": "message/router/middleware/throttle.go",
    "content": "package middleware\n\nimport (\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// Throttle provides a middleware that limits the amount of messages processed per unit of time.\n// This may be done e.g. to prevent excessive load caused by running a handler on a long queue of unprocessed messages.\ntype Throttle struct {\n\tticker *time.Ticker\n}\n\n// NewThrottle creates a new Throttle middleware.\n// Example duration and count: NewThrottle(10, time.Second) for 10 messages per second\nfunc NewThrottle(count int64, duration time.Duration) *Throttle {\n\treturn &Throttle{\n\t\tticker: time.NewTicker(duration / time.Duration(count)),\n\t}\n}\n\n// Middleware returns the Throttle middleware.\nfunc (t Throttle) Middleware(h message.HandlerFunc) message.HandlerFunc {\n\treturn func(message *message.Message) ([]*message.Message, error) {\n\t\t// throttle is shared by multiple handlers, which will wait for their \"tick\"\n\t\t<-t.ticker.C\n\n\t\treturn h(message)\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/throttle_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst (\n\tperSecond          = 10\n\ttestTimeout        = time.Second\n\tconcurrentHandlers = 10\n)\n\nfunc TestThrottle_Middleware(t *testing.T) {\n\tthrottle := middleware.NewThrottle(perSecond, testTimeout)\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\tproducedMessagesChannel := make(chan struct{})\n\n\tproducedMessagesCounter := 0\n\n\tfor i := 0; i < concurrentHandlers; i++ {\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\tproducedMessages := []*message.Message{message.NewMessage(\"produced\", nil)}\n\t\t\t\tproducedErr := errors.New(\"produced err\")\n\n\t\t\t\tproduced, err := throttle.Middleware(func(msg *message.Message) ([]*message.Message, error) {\n\t\t\t\t\treturn producedMessages, producedErr\n\t\t\t\t})(\n\t\t\t\t\tmessage.NewMessage(\"uuid\", nil),\n\t\t\t\t)\n\n\t\t\t\tassert.Equal(t, producedMessages, produced)\n\t\t\t\tassert.Equal(t, producedErr, err)\n\n\t\t\t\tgo func() {\n\t\t\t\t\t// non blocking counting\n\t\t\t\t\tproducedMessagesChannel <- struct{}{}\n\t\t\t\t}()\n\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\nCounterLoop:\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tbreak CounterLoop\n\t\tcase <-producedMessagesChannel:\n\t\t\tproducedMessagesCounter++\n\t\t}\n\t}\n\n\tt.Logf(\"produced %d messages in %d seconds, at rate of total %d messages per second\",\n\t\tproducedMessagesCounter,\n\t\tint(testTimeout.Seconds()),\n\t\tperSecond,\n\t)\n\n\tassert.True(t, producedMessagesCounter <= int(perSecond*testTimeout.Seconds()))\n\tassert.True(t, producedMessagesCounter > 0)\n}\n"
  },
  {
    "path": "message/router/middleware/timeout.go",
    "content": "package middleware\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// Timeout makes the handler cancel the incoming message's context after a specified time.\n// Any timeout-sensitive functionality of the handler should listen on msg.Context().Done() to know when to fail.\nfunc Timeout(timeout time.Duration) func(message.HandlerFunc) message.HandlerFunc {\n\treturn func(h message.HandlerFunc) message.HandlerFunc {\n\t\treturn func(msg *message.Message) ([]*message.Message, error) {\n\t\t\tctx, cancel := context.WithTimeout(msg.Context(), timeout)\n\t\t\tdefer func() {\n\t\t\t\tcancel()\n\t\t\t}()\n\n\t\t\tmsg.SetContext(ctx)\n\t\t\treturn h(msg)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "message/router/middleware/timeout_test.go",
    "content": "package middleware_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\nfunc TestTimeout(t *testing.T) {\n\ttimeout := middleware.Timeout(time.Millisecond * 10)\n\n\th := timeout(func(msg *message.Message) ([]*message.Message, error) {\n\t\tdelay := time.After(time.Millisecond * 100)\n\n\t\tselect {\n\t\tcase <-msg.Context().Done():\n\t\t\treturn nil, nil\n\t\tcase <-delay:\n\t\t\treturn nil, errors.New(\"timeout did not occur\")\n\t\t}\n\t})\n\n\t_, err := h(message.NewMessage(\"any-uuid\", nil))\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "message/router/plugin/signals.go",
    "content": "package plugin\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// SignalsHandler is a plugin that kills the router after SIGINT or SIGTERM is sent to the process.\nfunc SignalsHandler(r *message.Router) error {\n\tsigs := make(chan os.Signal, 1)\n\tsignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)\n\n\tgo func() {\n\t\tsig := <-sigs\n\t\tr.Logger().Info(fmt.Sprintf(\"Received %s signal, closing\\n\", sig), nil)\n\n\t\terr := r.Close()\n\t\tif err != nil {\n\t\t\tr.Logger().Error(\"Router close failed\", err, nil)\n\t\t}\n\t}()\n\treturn nil\n}\n"
  },
  {
    "path": "message/router.go",
    "content": "package message\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/internal\"\n\tsync_internal \"github.com/ThreeDotsLabs/watermill/pubsub/sync\"\n)\n\nvar (\n\t// ErrOutputInNoPublisherHandler happens when a handler func returned some messages in a no-publisher handler.\n\t// todo: maybe change the handler func signature in no-publisher handler so that there's no possibility for this\n\tErrOutputInNoPublisherHandler = errors.New(\"returned output messages in a handler without publisher\")\n)\n\n// HandlerFunc is function called when message is received.\n//\n// msg.Ack() is called automatically when HandlerFunc doesn't return error.\n// When HandlerFunc returns error, msg.Nack() is called.\n// When msg.Ack() was called in handler and HandlerFunc returns error,\n// msg.Nack() will be not sent because Ack was already sent.\n//\n// HandlerFunc's are executed parallel when multiple messages was received\n// (because msg.Ack() was sent in HandlerFunc or Subscriber supports multiple consumers).\ntype HandlerFunc func(msg *Message) ([]*Message, error)\n\n// NoPublishHandlerFunc is HandlerFunc alternative, which doesn't produce any messages.\ntype NoPublishHandlerFunc func(msg *Message) error\n\n// PassthroughHandler is a handler that passes the message unchanged from the subscriber to the publisher.\nvar PassthroughHandler HandlerFunc = func(msg *Message) ([]*Message, error) {\n\treturn []*Message{msg}, nil\n}\n\n// HandlerMiddleware allows us to write something like decorators to HandlerFunc.\n// It can execute something before handler (for example: modify consumed message)\n// or after (modify produced messages, ack/nack on consumed message, handle errors, logging, etc.).\n//\n// It can be attached to the router by using `AddMiddleware` method.\n//\n// Example:\n//\n//\tfunc ExampleMiddleware(h message.HandlerFunc) message.HandlerFunc {\n//\t\treturn func(message *message.Message) ([]*message.Message, error) {\n//\t\t\tfmt.Println(\"executed before handler\")\n//\t\t\tproducedMessages, err := h(message)\n//\t\t\tfmt.Println(\"executed after handler\")\n//\n//\t\t\treturn producedMessages, err\n//\t\t}\n//\t}\ntype HandlerMiddleware func(h HandlerFunc) HandlerFunc\n\n// RouterPlugin is function which is executed on Router start.\ntype RouterPlugin func(*Router) error\n\n// PublisherDecorator wraps the underlying Publisher, adding some functionality.\ntype PublisherDecorator func(pub Publisher) (Publisher, error)\n\n// SubscriberDecorator wraps the underlying Subscriber, adding some functionality.\ntype SubscriberDecorator func(sub Subscriber) (Subscriber, error)\n\n// RouterConfig holds the Router's configuration options.\ntype RouterConfig struct {\n\t// CloseTimeout determines how long router should work for handlers when closing.\n\tCloseTimeout time.Duration\n}\n\nfunc (c *RouterConfig) setDefaults() {\n\tif c.CloseTimeout == 0 {\n\t\tc.CloseTimeout = time.Second * 30\n\t}\n}\n\n// Validate returns Router configuration error, if any.\nfunc (c RouterConfig) Validate() error {\n\treturn nil\n}\n\n// NewRouter creates a new Router with given configuration.\nfunc NewRouter(config RouterConfig, logger watermill.LoggerAdapter) (*Router, error) {\n\tconfig.setDefaults()\n\tif err := config.Validate(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid config\")\n\t}\n\n\treturn newRouter(config, logger), nil\n}\n\n// NewDefaultRouter creates a new Router with default configuration.\nfunc NewDefaultRouter(logger watermill.LoggerAdapter) *Router {\n\tconfig := RouterConfig{}\n\tconfig.setDefaults()\n\n\treturn newRouter(config, logger)\n}\n\nfunc newRouter(config RouterConfig, logger watermill.LoggerAdapter) *Router {\n\tif logger == nil {\n\t\tlogger = watermill.NopLogger{}\n\t}\n\n\treturn &Router{\n\t\tconfig: config,\n\n\t\thandlers: map[string]*handler{},\n\n\t\thandlersWg: &sync.WaitGroup{},\n\n\t\trunningHandlersWg:     &sync.WaitGroup{},\n\t\trunningHandlersWgLock: &sync.Mutex{},\n\n\t\thandlerAdded: make(chan struct{}),\n\n\t\tmiddlewaresLock: &sync.RWMutex{},\n\t\thandlersLock:    &sync.RWMutex{},\n\n\t\tclosingInProgressCh: make(chan struct{}),\n\t\tclosedCh:            make(chan struct{}),\n\n\t\tlogger: logger,\n\n\t\trunning: make(chan struct{}),\n\t}\n}\n\ntype middleware struct {\n\tHandler       HandlerMiddleware\n\tHandlerName   string\n\tIsRouterLevel bool\n}\n\n// Router is responsible for handling messages from subscribers using provided handler functions.\n//\n// If the handler function returns a message, the message is published with the publisher.\n// You can use middlewares to wrap handlers with common logic like logging, instrumentation, etc.\ntype Router struct {\n\tconfig RouterConfig\n\n\tmiddlewares     []middleware\n\tmiddlewaresLock *sync.RWMutex\n\n\tplugins []RouterPlugin\n\n\thandlers     map[string]*handler\n\thandlersLock *sync.RWMutex\n\n\thandlersWg *sync.WaitGroup\n\n\trunningHandlersWg     *sync.WaitGroup\n\trunningHandlersWgLock *sync.Mutex\n\n\thandlerAdded chan struct{}\n\n\tclosingInProgressCh chan struct{}\n\tclosedCh            chan struct{}\n\tclosed              bool\n\tclosedLock          sync.Mutex\n\n\tlogger watermill.LoggerAdapter\n\n\tpublisherDecorators  []PublisherDecorator\n\tsubscriberDecorators []SubscriberDecorator\n\n\tisRunning bool\n\trunning   chan struct{}\n}\n\n// Logger returns the Router's logger.\nfunc (r *Router) Logger() watermill.LoggerAdapter {\n\treturn r.logger\n}\n\n// AddMiddleware adds a new middleware to the router.\n//\n// The order of middleware matters. Middleware added at the beginning is executed first.\nfunc (r *Router) AddMiddleware(m ...HandlerMiddleware) {\n\tr.logger.Debug(\"Adding middleware\", watermill.LogFields{\"count\": fmt.Sprintf(\"%d\", len(m))})\n\n\tr.addRouterLevelMiddleware(m...)\n}\n\nfunc (r *Router) addRouterLevelMiddleware(m ...HandlerMiddleware) {\n\tfor _, handlerMiddleware := range m {\n\t\tmiddleware := middleware{\n\t\t\tHandler:       handlerMiddleware,\n\t\t\tHandlerName:   \"\",\n\t\t\tIsRouterLevel: true,\n\t\t}\n\t\tr.middlewares = append(r.middlewares, middleware)\n\t}\n}\n\nfunc (r *Router) addHandlerLevelMiddleware(handlerName string, m ...HandlerMiddleware) {\n\tr.middlewaresLock.Lock()\n\tdefer r.middlewaresLock.Unlock()\n\tfor _, handlerMiddleware := range m {\n\t\tmiddleware := middleware{\n\t\t\tHandler:       handlerMiddleware,\n\t\t\tHandlerName:   handlerName,\n\t\t\tIsRouterLevel: false,\n\t\t}\n\t\tr.middlewares = append(r.middlewares, middleware)\n\t}\n}\n\n// AddPlugin adds a new plugin to the router.\n// Plugins are executed during startup of the router.\n//\n// A plugin can, for example, close the router after SIGINT or SIGTERM is sent to the process (SignalsHandler plugin).\nfunc (r *Router) AddPlugin(p ...RouterPlugin) {\n\tr.logger.Debug(\"Adding plugins\", watermill.LogFields{\"count\": fmt.Sprintf(\"%d\", len(p))})\n\n\tr.plugins = append(r.plugins, p...)\n}\n\n// AddPublisherDecorators wraps the router's Publisher.\n// The first decorator is the innermost, i.e. calls the original publisher.\nfunc (r *Router) AddPublisherDecorators(dec ...PublisherDecorator) {\n\tr.logger.Debug(\"Adding publisher decorators\", watermill.LogFields{\"count\": fmt.Sprintf(\"%d\", len(dec))})\n\n\tr.publisherDecorators = append(r.publisherDecorators, dec...)\n}\n\n// AddSubscriberDecorators wraps the router's Subscriber.\n// The first decorator is the innermost, i.e. calls the original subscriber.\nfunc (r *Router) AddSubscriberDecorators(dec ...SubscriberDecorator) {\n\tr.logger.Debug(\"Adding subscriber decorators\", watermill.LogFields{\"count\": fmt.Sprintf(\"%d\", len(dec))})\n\n\tr.subscriberDecorators = append(r.subscriberDecorators, dec...)\n}\n\n// Handlers returns all registered handlers.\nfunc (r *Router) Handlers() map[string]HandlerFunc {\n\thandlers := map[string]HandlerFunc{}\n\n\tfor handlerName, handler := range r.handlers {\n\t\thandlers[handlerName] = handler.handlerFunc\n\t}\n\n\treturn handlers\n}\n\n// DuplicateHandlerNameError is sent in a panic when you try to add a second handler with the same name.\ntype DuplicateHandlerNameError struct {\n\tHandlerName string\n}\n\nfunc (d DuplicateHandlerNameError) Error() string {\n\treturn fmt.Sprintf(\"handler with name %s already exists\", d.HandlerName)\n}\n\n// AddHandler adds a new handler.\n//\n// handlerName must be unique. For now, it is used only for debugging.\n//\n// subscribeTopic is a topic from which handler will receive messages.\n//\n// publishTopic is a topic to which router will produce messages returned by handlerFunc.\n// When handler needs to publish to multiple topics,\n// it is recommended to use AddConsumerHandler and inject a Publisher or implement middleware\n// which will catch messages and publish to topic based on metadata for example.\n//\n// If handler is added while router is already running, you need to explicitly call RunHandlers().\nfunc (r *Router) AddHandler(\n\thandlerName string,\n\tsubscribeTopic string,\n\tsubscriber Subscriber,\n\tpublishTopic string,\n\tpublisher Publisher,\n\thandlerFunc HandlerFunc,\n) *Handler {\n\tr.logger.Info(\"Adding handler\", watermill.LogFields{\n\t\t\"handler_name\": handlerName,\n\t\t\"topic\":        subscribeTopic,\n\t})\n\n\tr.handlersLock.Lock()\n\tdefer r.handlersLock.Unlock()\n\n\tif _, ok := r.handlers[handlerName]; ok {\n\t\tpanic(DuplicateHandlerNameError{handlerName})\n\t}\n\n\tpublisherName, subscriberName := internal.StructName(publisher), internal.StructName(subscriber)\n\n\tnewHandler := &handler{\n\t\tname:   handlerName,\n\t\tlogger: r.logger,\n\n\t\tsubscriber:     subscriber,\n\t\tsubscribeTopic: subscribeTopic,\n\t\tsubscriberName: subscriberName,\n\n\t\tpublisher:     publisher,\n\t\tpublishTopic:  publishTopic,\n\t\tpublisherName: publisherName,\n\n\t\thandlerFunc: handlerFunc,\n\n\t\trunningHandlersWg:     r.runningHandlersWg,\n\t\trunningHandlersWgLock: r.runningHandlersWgLock,\n\n\t\tmessagesCh:     nil,\n\t\troutersCloseCh: r.closingInProgressCh,\n\n\t\tstartedCh: make(chan struct{}),\n\t}\n\n\tr.handlersWg.Add(1)\n\tr.handlers[handlerName] = newHandler\n\n\tselect {\n\tcase r.handlerAdded <- struct{}{}:\n\tdefault:\n\t\t// watchAllHandlersStopped is not always waiting for handlerAdded\n\t}\n\n\treturn &Handler{\n\t\trouter:  r,\n\t\thandler: newHandler,\n\t}\n}\n\n// AddConsumerHandler adds a new handler that does not return any messages.\n// It can publish messages by directly using a Publisher.\n//\n// handlerName must be unique. For now, it is used only for debugging.\n//\n// subscribeTopic is a topic from which handler will receive messages.\n//\n// subscriber is Subscriber from which messages will be consumed.\n//\n// If handler is added while router is already running, you need to explicitly call RunHandlers().\nfunc (r *Router) AddConsumerHandler(\n\thandlerName string,\n\tsubscribeTopic string,\n\tsubscriber Subscriber,\n\thandlerFunc NoPublishHandlerFunc,\n) *Handler {\n\thandlerFuncAdapter := func(msg *Message) ([]*Message, error) {\n\t\treturn nil, handlerFunc(msg)\n\t}\n\n\treturn r.AddHandler(handlerName, subscribeTopic, subscriber, \"\", disabledPublisher{}, handlerFuncAdapter)\n}\n\n// AddNoPublisherHandler adds a new handler.\n// This handler cannot return messages.\n//\n// handlerName must be unique. For now, it is used only for debugging.\n//\n// subscribeTopic is a topic from which handler will receive messages.\n//\n// subscriber is Subscriber from which messages will be consumed.\n//\n// If handler is added while router is already running, you need to explicitly call RunHandlers().\n//\n// Deprecated: use AddConsumerHandler instead.\nfunc (r *Router) AddNoPublisherHandler(\n\thandlerName string,\n\tsubscribeTopic string,\n\tsubscriber Subscriber,\n\thandlerFunc NoPublishHandlerFunc,\n) *Handler {\n\treturn r.AddConsumerHandler(handlerName, subscribeTopic, subscriber, handlerFunc)\n}\n\n// Run runs all plugins and handlers and starts subscribing to provided topics.\n// This call is blocking while the router is running.\n//\n// When all handlers have stopped (for example, because subscriptions were closed), the router will also stop.\n//\n// To stop Run() you should call Close() on the router.\n//\n// ctx will be propagated to all subscribers.\n//\n// When all handlers are stopped (for example: because of closed connection), Run() will be also stopped.\nfunc (r *Router) Run(ctx context.Context) (err error) {\n\tif r.isRunning {\n\t\treturn errors.New(\"router is already running\")\n\t}\n\tr.isRunning = true\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tr.logger.Debug(\"Loading plugins\", nil)\n\tfor _, plugin := range r.plugins {\n\t\tif err := plugin(r); err != nil {\n\t\t\treturn errors.Wrapf(err, \"cannot initialize plugin %v\", plugin)\n\t\t}\n\t}\n\n\tr.watchAllHandlersStopped(ctx)\n\n\tif err := r.RunHandlers(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tclose(r.running)\n\n\t<-r.closingInProgressCh\n\tcancel()\n\n\tr.logger.Info(\"Waiting for messages\", watermill.LogFields{\n\t\t\"timeout\": r.config.CloseTimeout,\n\t})\n\n\t<-r.closedCh\n\n\tr.logger.Info(\"All messages processed\", nil)\n\n\treturn nil\n}\n\n// RunHandlers runs all handlers that were added after Run().\n// RunHandlers is idempotent, so can be called multiple times safely.\nfunc (r *Router) RunHandlers(ctx context.Context) error {\n\tif !r.isRunning {\n\t\treturn errors.New(\"you can't call RunHandlers on non-running router\")\n\t}\n\n\tr.handlersLock.Lock()\n\tdefer r.handlersLock.Unlock()\n\n\tr.logger.Info(\"Running router handlers\", watermill.LogFields{\"count\": len(r.handlers)})\n\n\tfor name, h := range r.handlers {\n\n\t\tif h.started {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := r.decorateHandlerPublisher(h); err != nil {\n\t\t\treturn errors.Wrapf(err, \"could not decorate publisher of handler %s\", name)\n\t\t}\n\t\tif err := r.decorateHandlerSubscriber(h); err != nil {\n\t\t\treturn errors.Wrapf(err, \"could not decorate subscriber of handler %s\", name)\n\t\t}\n\n\t\tlogger := r.logger.With(watermill.LogFields{\n\t\t\t\"subscriber_name\": h.name,\n\t\t\t\"topic\":           h.subscribeTopic,\n\t\t})\n\n\t\tlogger.Debug(\"Subscribing to topic\", nil)\n\n\t\tctx, cancel := context.WithCancel(ctx)\n\n\t\tmessages, err := h.subscriber.Subscribe(ctx, h.subscribeTopic)\n\t\tif err != nil {\n\t\t\tcancel()\n\t\t\treturn errors.Wrapf(err, \"cannot subscribe topic %s\", h.subscribeTopic)\n\t\t}\n\n\t\th.messagesCh = messages\n\t\th.started = true\n\t\tclose(h.startedCh)\n\n\t\th.stopFn = cancel\n\t\th.stopped = make(chan struct{})\n\n\t\tgo func() {\n\t\t\tdefer cancel()\n\n\t\t\tr.middlewaresLock.Lock()\n\t\t\tmiddlewares := append([]middleware{}, r.middlewares...)\n\t\t\tr.middlewaresLock.Unlock()\n\n\t\t\th.run(ctx, middlewares)\n\n\t\t\tr.handlersWg.Done()\n\t\t\tlogger.Info(\"Subscriber stopped\", nil)\n\n\t\t\tr.handlersLock.Lock()\n\t\t\tdelete(r.handlers, name)\n\t\t\tr.handlersLock.Unlock()\n\n\t\t\tlogger.Trace(\"Removed subscriber from r.handlers\", nil)\n\n\t\t\tclose(h.stopped)\n\t\t}()\n\t}\n\treturn nil\n}\n\n// watchAllHandlersStopped closes router when all handlers have stopped,\n// (for example, because for example all subscriptions are closed)\nfunc (r *Router) watchAllHandlersStopped(ctx context.Context) {\n\tr.handlersLock.RLock()\n\thasNoHandlersYet := len(r.handlers) == 0\n\tr.handlersLock.RUnlock()\n\n\tgo func() {\n\t\tif hasNoHandlersYet {\n\t\t\t// we can start router without any handlers,\n\t\t\t// in that situation router would be closed immediately (even if they are no routers)\n\t\t\t// let's wait for\n\t\t\tselect {\n\t\t\tcase <-r.handlerAdded:\n\t\t\t\t// it should be some handler to track\n\t\t\tcase <-r.closedCh:\n\t\t\t\t// let's avoid goroutine leak\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tr.handlersWg.Wait()\n\t\tif r.IsClosed() {\n\t\t\tr.logger.Trace(\"watchAllHandlersStopped: already closed\", nil)\n\t\t\t// already closed\n\t\t\treturn\n\t\t}\n\n\t\t// Only log an error if the context was not canceled, but handlers were stopped.\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\tdefault:\n\t\t\tr.logger.Error(\"All handlers stopped, closing router\", errors.New(\"all router handlers stopped\"), nil)\n\t\t}\n\n\t\tif err := r.Close(); err != nil {\n\t\t\tr.logger.Error(\"Cannot close router\", err, nil)\n\t\t}\n\t}()\n}\n\n// Running is closed when router is running.\n// In other words: you can wait till router is running using\n//\n//\tfmt.Println(\"Starting router\")\n//\tgo r.Run(ctx)\n//\t<- r.Running()\n//\tfmt.Println(\"Router is running\")\n//\n// Warning: for historical reasons, this channel is not aware of router closing - the channel will be closed if the router has been running and closed.\nfunc (r *Router) Running() chan struct{} {\n\treturn r.running\n}\n\n// IsRunning returns true when router is running.\n//\n// Warning: for historical reasons, this method is not aware of router closing.\n// If you want to know if the router was closed, use IsClosed.\nfunc (r *Router) IsRunning() bool {\n\tselect {\n\tcase <-r.running:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Close gracefully closes the router with a timeout provided in the configuration.\nfunc (r *Router) Close() error {\n\tr.closedLock.Lock()\n\tdefer r.closedLock.Unlock()\n\n\tr.handlersLock.Lock()\n\tdefer r.handlersLock.Unlock()\n\n\tif r.closed {\n\t\tr.logger.Debug(\"Already closed\", nil)\n\t\treturn nil\n\t}\n\n\tr.logger.Debug(\"Running Close()\", nil)\n\tr.closed = true\n\n\tr.logger.Info(\"Closing router\", nil)\n\tdefer r.logger.Info(\"Router closed\", nil)\n\n\tclose(r.closingInProgressCh)\n\tdefer close(r.closedCh)\n\n\ttimedout := r.waitForHandlers()\n\tif timedout {\n\t\treturn errors.New(\"router close timeout\")\n\t}\n\n\treturn nil\n}\n\nfunc (r *Router) waitForHandlers() bool {\n\tvar waitGroup sync.WaitGroup\n\twaitGroup.Add(1)\n\tgo func() {\n\t\tdefer waitGroup.Done()\n\t\tr.handlersWg.Wait()\n\t}()\n\twaitGroup.Add(1)\n\tgo func() {\n\t\tdefer waitGroup.Done()\n\n\t\tr.runningHandlersWgLock.Lock()\n\t\tdefer r.runningHandlersWgLock.Unlock()\n\n\t\tr.runningHandlersWg.Wait()\n\t}()\n\treturn sync_internal.WaitGroupTimeout(&waitGroup, r.config.CloseTimeout)\n}\n\nfunc (r *Router) IsClosed() bool {\n\tr.closedLock.Lock()\n\tdefer r.closedLock.Unlock()\n\n\treturn r.closed\n}\n\ntype handler struct {\n\tname   string\n\tlogger watermill.LoggerAdapter\n\n\tsubscriber     Subscriber\n\tsubscribeTopic string\n\tsubscriberName string\n\n\tpublisher     Publisher\n\tpublishTopic  string\n\tpublisherName string\n\n\thandlerFunc HandlerFunc\n\n\trunningHandlersWg     *sync.WaitGroup\n\trunningHandlersWgLock *sync.Mutex\n\n\tmessagesCh <-chan *Message\n\n\tstarted   bool\n\tstartedCh chan struct{}\n\n\tstopFn         context.CancelFunc\n\tstopped        chan struct{}\n\troutersCloseCh chan struct{}\n}\n\nfunc (h *handler) run(ctx context.Context, middlewares []middleware) {\n\th.logger.Info(\"Starting handler\", watermill.LogFields{\n\t\t\"subscriber_name\": h.name,\n\t\t\"topic\":           h.subscribeTopic,\n\t})\n\n\tmiddlewareHandler := h.handlerFunc\n\t// first added middlewares should be executed first (so should be at the top of call stack)\n\tfor i := len(middlewares) - 1; i >= 0; i-- {\n\t\tcurrentMiddleware := middlewares[i]\n\t\tisValidHandlerLevelMiddleware := currentMiddleware.HandlerName == h.name\n\t\tif currentMiddleware.IsRouterLevel || isValidHandlerLevelMiddleware {\n\t\t\tmiddlewareHandler = currentMiddleware.Handler(middlewareHandler)\n\t\t}\n\t}\n\n\tgo h.handleClose(ctx)\n\n\tfor msg := range h.messagesCh {\n\t\th.runningHandlersWgLock.Lock()\n\t\th.runningHandlersWg.Add(1)\n\t\th.runningHandlersWgLock.Unlock()\n\n\t\tgo h.handleMessage(msg, middlewareHandler)\n\t}\n\n\tif h.publisher != nil {\n\t\th.logger.Debug(\"Waiting for publisher to close\", nil)\n\t\tif err := h.publisher.Close(); err != nil {\n\t\t\th.logger.Error(\"Failed to close publisher\", err, nil)\n\t\t}\n\t\th.logger.Debug(\"Publisher closed\", nil)\n\t}\n\n\th.logger.Debug(\"Router handler stopped\", nil)\n}\n\n// Handler handles Messages.\ntype Handler struct {\n\trouter  *Router\n\thandler *handler\n}\n\n// AddMiddleware adds new middleware to the specified handler in the router.\n//\n// The order of middleware matters. Middleware added at the beginning is executed first.\nfunc (h *Handler) AddMiddleware(m ...HandlerMiddleware) {\n\thandler := h.handler\n\thandler.logger.Debug(\"Adding middleware to handler\", watermill.LogFields{\n\t\t\"count\":       fmt.Sprintf(\"%d\", len(m)),\n\t\t\"handlerName\": handler.name,\n\t})\n\n\th.router.addHandlerLevelMiddleware(handler.name, m...)\n}\n\n// Started returns channel which is stopped when handler is running.\nfunc (h *Handler) Started() chan struct{} {\n\treturn h.handler.startedCh\n}\n\n// Stop stops the handler.\n// Stop is asynchronous.\n// You can check if handler was stopped with Stopped() function.\nfunc (h *Handler) Stop() {\n\tif !h.handler.started {\n\t\tpanic(\"handler is not started\")\n\t}\n\n\th.handler.stopFn()\n}\n\n// Stopped returns channel which is stopped when handler did stop.\nfunc (h *Handler) Stopped() chan struct{} {\n\treturn h.handler.stopped\n}\n\n// decorateHandlerPublisher applies the decorator chain to handler's publisher.\n// They are applied in reverse order, so that the later decorators use the result of former ones.\nfunc (r *Router) decorateHandlerPublisher(h *handler) error {\n\tvar err error\n\tpub := h.publisher\n\tfor i := len(r.publisherDecorators) - 1; i >= 0; i-- {\n\t\tdecorator := r.publisherDecorators[i]\n\t\tpub, err = decorator(pub)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"could not apply publisher decorator\")\n\t\t}\n\t}\n\tr.handlers[h.name].publisher = pub\n\treturn nil\n}\n\n// decorateHandlerSubscriber applies the decorator chain to handler's subscriber.\n// They are applied in regular order, so that the later decorators use the result of former ones.\nfunc (r *Router) decorateHandlerSubscriber(h *handler) error {\n\tvar err error\n\tsub := h.subscriber\n\n\t// add values to message context to subscriber\n\t// it goes before other decorators, so that they may take advantage of these values\n\tmessageTransform := func(msg *Message) {\n\t\tif msg != nil {\n\t\t\th.addHandlerContext(msg)\n\t\t}\n\t}\n\tsub, err = MessageTransformSubscriberDecorator(messageTransform)(sub)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"cannot wrap subscriber with context decorator\")\n\t}\n\n\tfor _, decorator := range r.subscriberDecorators {\n\t\tsub, err = decorator(sub)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"could not apply subscriber decorator\")\n\t\t}\n\t}\n\tr.handlers[h.name].subscriber = sub\n\treturn nil\n}\n\n// addHandlerContext enriches the context with values that are relevant within this handler's context.\nfunc (h *handler) addHandlerContext(messages ...*Message) {\n\tfor i, msg := range messages {\n\t\tctx := msg.Context()\n\n\t\tif h.name != \"\" {\n\t\t\tctx = context.WithValue(ctx, handlerNameKey, h.name)\n\t\t}\n\t\tif h.publisherName != \"\" {\n\t\t\tctx = context.WithValue(ctx, publisherNameKey, h.publisherName)\n\t\t}\n\t\tif h.subscriberName != \"\" {\n\t\t\tctx = context.WithValue(ctx, subscriberNameKey, h.subscriberName)\n\t\t}\n\t\tif h.subscribeTopic != \"\" {\n\t\t\tctx = context.WithValue(ctx, subscribeTopicKey, h.subscribeTopic)\n\t\t}\n\t\tif h.publishTopic != \"\" {\n\t\t\tctx = context.WithValue(ctx, publishTopicKey, h.publishTopic)\n\t\t}\n\t\tmessages[i].SetContext(ctx)\n\t}\n}\n\nfunc (h *handler) handleClose(ctx context.Context) {\n\tselect {\n\tcase <-h.routersCloseCh:\n\t\t// for backward compatibility we are closing subscriber\n\t\th.logger.Debug(\"Waiting for subscriber to close\", nil)\n\t\tif err := h.subscriber.Close(); err != nil {\n\t\t\th.logger.Error(\"Failed to close subscriber\", err, nil)\n\t\t}\n\t\th.logger.Debug(\"Subscriber closed\", nil)\n\tcase <-ctx.Done():\n\t\t// we are closing subscriber just when entire router is closed\n\t}\n\th.stopFn()\n}\n\nfunc (h *handler) handleMessage(msg *Message, handler HandlerFunc) {\n\tdefer h.runningHandlersWg.Done()\n\tmsgFields := watermill.LogFields{\"message_uuid\": msg.UUID, \"handler_name\": h.name}\n\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\th.logger.Error(\n\t\t\t\t\"Panic recovered in handler. Stack: \"+string(debug.Stack()),\n\t\t\t\terrors.Errorf(\"%s\", recovered),\n\t\t\t\tmsgFields,\n\t\t\t)\n\t\t\tmsg.Nack()\n\t\t}\n\t}()\n\n\th.logger.Trace(\"Received message\", msgFields)\n\n\tproducedMessages, err := handler(msg)\n\tif err != nil {\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\th.logger.Error(\"Handler returned error\", err, msgFields)\n\t\t}\n\t\tmsg.Nack()\n\t\treturn\n\t}\n\n\th.addHandlerContext(producedMessages...)\n\n\tif err := h.publishProducedMessages(producedMessages, msgFields); err != nil {\n\t\th.logger.Error(\"Publishing produced messages failed\", err, nil)\n\t\tmsg.Nack()\n\t\treturn\n\t}\n\n\tmsg.Ack()\n\th.logger.Trace(\"Message acked\", msgFields)\n}\n\nfunc (h *handler) publishProducedMessages(producedMessages Messages, msgFields watermill.LogFields) error {\n\tif len(producedMessages) == 0 {\n\t\treturn nil\n\t}\n\n\tif h.publisher == nil {\n\t\treturn ErrOutputInNoPublisherHandler\n\t}\n\n\th.logger.Trace(\"Sending produced messages\", msgFields.Add(watermill.LogFields{\n\t\t\"produced_messages_count\": len(producedMessages),\n\t\t\"publish_topic\":           h.publishTopic,\n\t}))\n\n\tif err := h.publisher.Publish(h.publishTopic, producedMessages...); err != nil {\n\t\th.logger.Error(\"Cannot publish messages\", err, msgFields.Add(watermill.LogFields{\n\t\t\t\"not_sent_message\": fmt.Sprintf(\"%#v\", producedMessages),\n\t\t}))\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype disabledPublisher struct{}\n\nfunc (disabledPublisher) Publish(topic string, messages ...*Message) error {\n\treturn ErrOutputInNoPublisherHandler\n}\n\nfunc (disabledPublisher) Close() error {\n\treturn nil\n}\n"
  },
  {
    "path": "message/router_context.go",
    "content": "package message\n\nimport (\n\t\"context\"\n)\n\ntype ctxKey string\n\nconst (\n\thandlerNameKey    ctxKey = \"handler_name\"\n\tpublisherNameKey  ctxKey = \"publisher_name\"\n\tsubscriberNameKey ctxKey = \"subscriber_name\"\n\tsubscribeTopicKey ctxKey = \"subscribe_topic\"\n\tpublishTopicKey   ctxKey = \"publish_topic\"\n)\n\nfunc valFromCtx(ctx context.Context, key ctxKey) string {\n\tval, ok := ctx.Value(key).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\treturn val\n}\n\n// HandlerNameFromCtx returns the name of the message handler in the router that consumed the message.\nfunc HandlerNameFromCtx(ctx context.Context) string {\n\treturn valFromCtx(ctx, handlerNameKey)\n}\n\n// PublisherNameFromCtx returns the name of the message publisher type that published the message in the router.\n// For example, for Kafka it will be `kafka.Publisher`.\nfunc PublisherNameFromCtx(ctx context.Context) string {\n\treturn valFromCtx(ctx, publisherNameKey)\n}\n\n// SubscriberNameFromCtx returns the name of the message subscriber type that subscribed to the message in the router.\n// For example, for Kafka it will be `kafka.Subscriber`.\nfunc SubscriberNameFromCtx(ctx context.Context) string {\n\treturn valFromCtx(ctx, subscriberNameKey)\n}\n\n// SubscribeTopicFromCtx returns the topic from which message was received in the router.\nfunc SubscribeTopicFromCtx(ctx context.Context) string {\n\treturn valFromCtx(ctx, subscribeTopicKey)\n}\n\n// PublishTopicFromCtx returns the topic to which message will be published by the router.\nfunc PublishTopicFromCtx(ctx context.Context) string {\n\treturn valFromCtx(ctx, publishTopicKey)\n}\n"
  },
  {
    "path": "message/router_context_test.go",
    "content": "package message_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\ntype namedMockPublisher struct{}\n\nfunc (namedMockPublisher) Publish(topic string, messages ...*message.Message) error { return nil }\nfunc (namedMockPublisher) Close() error                                             { return nil }\nfunc (namedMockPublisher) String() string {\n\treturn \"this publisher implements Stringer\"\n}\n\ntype namedMockSubscriber struct{ ch chan *message.Message }\n\nfunc (s namedMockSubscriber) Subscribe(context.Context, string) (<-chan *message.Message, error) {\n\treturn s.ch, nil\n}\nfunc (s *namedMockSubscriber) Close() error { close(s.ch); return nil }\nfunc (namedMockSubscriber) String() string {\n\treturn \"this subscriber implements Stringer\"\n}\n\nfunc TestRouter_Context_Stringer(t *testing.T) {\n\t// If a publisher or subscriber implements Stringer, it's name is the result of String().\n\t// The messages processed by a router handler should have publisher and subscriber name in their context.\n\n\t// given\n\tcapturedMessages := make(chan *message.Message)\n\trouter, handlerFunc := setupPubsubNameTests(t, capturedMessages)\n\n\tsub := &namedMockSubscriber{make(chan *message.Message)}\n\tpub := namedMockPublisher{}\n\n\thandlerName := \"handler_name_stringer_test\"\n\trouter.AddHandler(\n\t\thandlerName,\n\t\t\"sub-topic\",\n\t\tsub,\n\t\t\"pub-topic\",\n\t\tpub,\n\t\thandlerFunc,\n\t)\n\n\tgo func() {\n\t\tif err := router.Run(context.Background()); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\tdefer func() {\n\t\tif err := router.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\t<-router.Running()\n\n\t// when\n\tsub.ch <- message.NewMessage(\"\", []byte{})\n\tcapturedMsg := <-capturedMessages\n\n\tctx := capturedMsg.Context()\n\n\t// then\n\trequire.Equal(t, handlerName, message.HandlerNameFromCtx(ctx))\n\trequire.Equal(t, sub.String(), message.SubscriberNameFromCtx(ctx))\n\trequire.Equal(t, pub.String(), message.PublisherNameFromCtx(ctx))\n\trequire.Equal(t, \"sub-topic\", message.SubscribeTopicFromCtx(ctx))\n\trequire.Equal(t, \"pub-topic\", message.PublishTopicFromCtx(ctx))\n}\n\ntype unnamedMockPublisher struct{}\n\nfunc (unnamedMockPublisher) Publish(topic string, messages ...*message.Message) error { return nil }\nfunc (unnamedMockPublisher) Close() error                                             { return nil }\n\ntype unnamedMockSubscriber struct{ ch chan *message.Message }\n\nfunc (s unnamedMockSubscriber) Subscribe(context.Context, string) (<-chan *message.Message, error) {\n\treturn s.ch, nil\n}\nfunc (s *unnamedMockSubscriber) Close() error { close(s.ch); return nil }\n\nfunc TestRouter_Context_TypeName(t *testing.T) {\n\t// If a publisher or subscriber does not implement Stringer, it's name is the type name.\n\t// The messages processed by a router handler should have publisher and subscriber name in their context.\n\n\t// given\n\tcapturedMessages := make(chan *message.Message)\n\trouter, handlerFunc := setupPubsubNameTests(t, capturedMessages)\n\n\tsub := &unnamedMockSubscriber{make(chan *message.Message)}\n\tpub := unnamedMockPublisher{}\n\n\thandlerName := \"handler_name_typename_test\"\n\trouter.AddHandler(\n\t\thandlerName,\n\t\t\"\",\n\t\tsub,\n\t\t\"\",\n\t\tpub,\n\t\thandlerFunc,\n\t)\n\n\tgo func() {\n\t\tif err := router.Run(context.Background()); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\tdefer func() {\n\t\tif err := router.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\t<-router.Running()\n\n\t// when\n\tsub.ch <- message.NewMessage(\"\", []byte{})\n\tcapturedMsg := <-capturedMessages\n\n\tctx := capturedMsg.Context()\n\n\t// then\n\trequire.Equal(t, handlerName, message.HandlerNameFromCtx(ctx))\n\trequire.Equal(t, \"message_test.unnamedMockSubscriber\", message.SubscriberNameFromCtx(ctx))\n\trequire.Equal(t, \"message_test.unnamedMockPublisher\", message.PublisherNameFromCtx(ctx))\n}\n\nfunc setupPubsubNameTests(t *testing.T, capturedMessages chan (*message.Message)) (*message.Router, message.HandlerFunc) {\n\tlogger := watermill.NewStdLogger(true, true)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\tcapturedMessages <- msg\n\t\trequire.True(t, msg.Ack())\n\t\treturn message.Messages{message.NewMessage(msg.UUID+\"_copy\", msg.Payload)}, nil\n\t}\n\n\treturn router, handlerFunc\n}\n"
  },
  {
    "path": "message/router_test.go",
    "content": "package message_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/internal\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/tests\"\n)\n\nfunc TestRouter_functional(t *testing.T) {\n\ttestID := watermill.NewUUID()\n\tsubscribeTopic := \"test_topic_\" + testID\n\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tmessagesCount := 50\n\n\tvar expectedReceivedMessages message.Messages\n\tallMessagesSent := make(chan struct{})\n\tgo func() {\n\t\texpectedReceivedMessages = publishMessagesForHandler(t, messagesCount, pub, sub, subscribeTopic)\n\t\tallMessagesSent <- struct{}{}\n\t}()\n\n\treceivedMessagesCh1 := make(chan *message.Message, messagesCount)\n\treceivedMessagesCh2 := make(chan *message.Message, messagesCount)\n\tsentByHandlerCh := make(chan *message.Message, messagesCount)\n\n\tpublishedEventsTopic := \"published_events_\" + testID\n\tpublishedByHandlerCh, err := sub.Subscribe(context.Background(), publishedEventsTopic)\n\n\tvar publishedByHandler message.Messages\n\tallPublishedByHandler := make(chan struct{})\n\n\tgo func() {\n\t\tvar all bool\n\t\tpublishedByHandler, all = subscriber.BulkRead(publishedByHandlerCh, messagesCount, time.Second*10)\n\t\tassert.True(t, all)\n\t\tallPublishedByHandler <- struct{}{}\n\t}()\n\n\trequire.NoError(t, err)\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\trequire.NoError(t, err)\n\n\tr.AddHandler(\n\t\t\"test_subscriber_1\",\n\t\tsubscribeTopic,\n\t\tsub,\n\t\tpublishedEventsTopic,\n\t\tpub,\n\t\tfunc(msg *message.Message) (producedMessages []*message.Message, err error) {\n\t\t\treceivedMessagesCh1 <- msg\n\n\t\t\ttoPublish := message.NewMessage(watermill.NewUUID(), nil)\n\t\t\tsentByHandlerCh <- toPublish\n\n\t\t\treturn []*message.Message{toPublish}, nil\n\t\t},\n\t)\n\n\tr.AddConsumerHandler(\n\t\t\"test_subscriber_2\",\n\t\tsubscribeTopic,\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treceivedMessagesCh2 <- msg\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\tassert.False(t, r.IsRunning())\n\t\tassert.NoError(t, r.Run(context.Background()))\n\t}()\n\t<-r.Running()\n\n\tdefer func() {\n\t\tassert.True(t, r.IsRunning())\n\t\tassert.NoError(t, r.Close())\n\n\t\tassert.True(t, r.IsClosed())\n\t}()\n\n\t<-allMessagesSent\n\n\texpectedSentByHandler, all := readMessages(sentByHandlerCh, len(expectedReceivedMessages), time.Second*10)\n\tassert.True(t, all)\n\n\treceivedMessages1, all := subscriber.BulkRead(receivedMessagesCh1, len(expectedReceivedMessages), time.Second*10)\n\tassert.True(t, all)\n\ttests.AssertAllMessagesReceived(t, expectedReceivedMessages, receivedMessages1)\n\n\treceivedMessages2, all := subscriber.BulkRead(receivedMessagesCh2, len(expectedReceivedMessages), time.Second*10)\n\tassert.True(t, all)\n\ttests.AssertAllMessagesReceived(t, expectedReceivedMessages, receivedMessages2)\n\n\t<-allPublishedByHandler\n\ttests.AssertAllMessagesReceived(t, expectedSentByHandler, publishedByHandler)\n}\n\nfunc TestRouter_functional_nack(t *testing.T) {\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\trequire.NoError(t, err)\n\n\tnackSend := make(chan struct{})\n\tmessageReceived := make(chan *message.Message, 2)\n\n\tr.AddConsumerHandler(\n\t\t\"test_subscriber_1\",\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\tmessageReceived <- msg\n\n\t\t\tif !internal.IsChannelClosed(nackSend) {\n\t\t\t\tmsg.Nack()\n\t\t\t\tclose(nackSend)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\trequire.NoError(t, r.Run(context.Background()))\n\t}()\n\tdefer func() {\n\t\tassert.NoError(t, r.Close())\n\t}()\n\n\t<-r.Running()\n\n\tpublishedMsg := message.NewMessage(\"1\", nil)\n\trequire.NoError(t, pub.Publish(\"subscribe_topic\", publishedMsg))\n\n\tmessages, all := subscriber.BulkRead(messageReceived, 2, time.Second)\n\tassert.True(t, all, \"not all messages received, probably not ack received, received %d\", len(messages))\n\n\ttests.AssertAllMessagesReceived(t, []*message.Message{publishedMsg, publishedMsg}, messages)\n}\n\nfunc TestRouter_ack_on_publishing_success(t *testing.T) {\n\tpublisher := &failingPublisherMock{\n\t\tshouldPanic: false,\n\t\tshouldError: false,\n\t}\n\tsubscriber := &subscriberMock{\n\t\tmessages: make(chan *message.Message),\n\t}\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{msg}, nil\n\t}\n\n\ttopic := \"ack_on_publishing_success\"\n\trouter.AddHandler(\n\t\t\"ack_on_publishing_success_handler\",\n\t\ttopic,\n\t\tsubscriber,\n\t\ttopic,\n\t\tpublisher,\n\t\thandlerFunc,\n\t)\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\n\tmsg := message.NewMessage(\"uuid\", []byte{})\n\tsubscriber.messages <- msg\n\n\tselect {\n\tcase <-msg.Acked():\n\t\t// ok\n\tcase <-msg.Nacked():\n\t\tt.Fatal(\"did not expect the message to be nacked\")\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"expected the message to be acked\")\n\t}\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n}\n\nfunc TestRouter_nack_on_publishing_failure(t *testing.T) {\n\tpublisher := &failingPublisherMock{\n\t\tshouldPanic: false,\n\t\tshouldError: true,\n\t}\n\tsubscriber := &subscriberMock{\n\t\tmessages: make(chan *message.Message),\n\t}\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{msg}, nil\n\t}\n\n\ttopic := \"nack_on_publishing_failure\"\n\trouter.AddHandler(\n\t\t\"nack_on_publishing_failure_handler\",\n\t\ttopic,\n\t\tsubscriber,\n\t\ttopic,\n\t\tpublisher,\n\t\thandlerFunc,\n\t)\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\n\tmsg := message.NewMessage(\"uuid\", []byte{})\n\tsubscriber.messages <- msg\n\n\tselect {\n\tcase <-msg.Acked():\n\t\tt.Fatal(\"did not expect the message to be acked\")\n\tcase <-msg.Nacked():\n\t\t// ok\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"expected the message to be nacked\")\n\t}\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n}\n\nfunc TestRouter_nack_on_panic(t *testing.T) {\n\tpublisher := &failingPublisherMock{\n\t\tshouldPanic: true,\n\t\tshouldError: false,\n\t}\n\tsubscriber := &subscriberMock{\n\t\tmessages: make(chan *message.Message),\n\t}\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{msg}, nil\n\t}\n\n\ttopic := \"nack_on_panic\"\n\trouter.AddHandler(\n\t\t\"nack_on_panic_handler\",\n\t\ttopic,\n\t\tsubscriber,\n\t\ttopic,\n\t\tpublisher,\n\t\thandlerFunc,\n\t)\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\n\tmsg := message.NewMessage(\"uuid\", []byte{})\n\tsubscriber.messages <- msg\n\n\tselect {\n\tcase <-msg.Acked():\n\t\tt.Fatal(\"did not expect the message to be acked\")\n\tcase <-msg.Nacked():\n\t\t// ok\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"expected the message to be nacked\")\n\t}\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n}\n\nfunc TestRouter_nack_on_handler_failure(t *testing.T) {\n\tpublisher := &failingPublisherMock{\n\t\tshouldPanic: false,\n\t\tshouldError: false,\n\t}\n\tsubscriber := &subscriberMock{\n\t\tmessages: make(chan *message.Message),\n\t}\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn nil, errors.New(\"handler error\")\n\t}\n\n\ttopic := \"nack_on_handler_failure\"\n\trouter.AddHandler(\n\t\t\"nack_on_handler_failure_handler\",\n\t\ttopic,\n\t\tsubscriber,\n\t\ttopic,\n\t\tpublisher,\n\t\thandlerFunc,\n\t)\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\n\tmsg := message.NewMessage(\"uuid\", []byte{})\n\tsubscriber.messages <- msg\n\n\tselect {\n\tcase <-msg.Acked():\n\t\tt.Fatal(\"did not expect the message to be acked\")\n\tcase <-msg.Nacked():\n\t\t// ok\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"expected the message to be nacked\")\n\t}\n}\n\nfunc TestRouter_AddMiddleware_to_router(t *testing.T) {\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\tmiddlewareCount := 3\n\tmiddlewareCh := make(chan string, middlewareCount)\n\tallMiddlewareExecuted := make(chan struct{}, 1)\n\tvar executedMiddleware []string\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{msg}, nil\n\t}\n\tfirstMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"firstMiddleware\"\n\t\treturn h\n\t}\n\n\tsecondMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"secondMiddleware\"\n\t\treturn h\n\t}\n\n\tthirdMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"thirdMiddleware\"\n\t\treturn h\n\t}\n\n\ttopic := \"some_topic\"\n\trouter.AddMiddleware(firstMiddleware)\n\trouter.AddHandler(\n\t\t\"some_topic_handler\",\n\t\ttopic,\n\t\tsub,\n\t\ttopic,\n\t\tpub,\n\t\thandlerFunc,\n\t)\n\trouter.AddMiddleware(secondMiddleware)\n\trouter.AddMiddleware(thirdMiddleware)\n\n\tgo func() {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), []byte(\"test_payload\"))\n\t\terr := pub.Publish(topic, msg)\n\t\trequire.NoError(t, err)\n\t}()\n\n\tgo func() {\n\t\tfor middlewareName := range middlewareCh {\n\t\t\texecutedMiddleware = append(executedMiddleware, middlewareName)\n\n\t\t\tif len(executedMiddleware) == 3 {\n\t\t\t\tallMiddlewareExecuted <- struct{}{}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tallMiddlewareExecuted <- struct{}{}\n\t}()\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\t<-allMiddlewareExecuted\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"firstMiddleware\", executedMiddleware[2])\n\trequire.Equal(t, \"secondMiddleware\", executedMiddleware[1])\n\trequire.Equal(t, \"thirdMiddleware\", executedMiddleware[0])\n}\n\nfunc TestRouter_AddMiddleware_to_handler(t *testing.T) {\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\tmiddlewareCount := 4\n\tmiddlewareCh := make(chan string, middlewareCount)\n\tallMiddlewareExecuted := make(chan struct{}, 1)\n\tvar executedMiddleware []string\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{msg}, nil\n\t}\n\tfirstMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"firstMiddleware\"\n\t\treturn h\n\t}\n\n\tsecondMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"secondMiddleware\"\n\t\treturn h\n\t}\n\n\tthirdMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"thirdMiddleware\"\n\t\treturn h\n\t}\n\n\tfourthMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"fourthMiddleware\"\n\t\treturn h\n\t}\n\n\ttopic := \"some_topic\"\n\trouter.AddMiddleware(firstMiddleware)\n\trouter.AddHandler(\n\t\t\"some_topic_handler\",\n\t\ttopic,\n\t\tsub,\n\t\ttopic,\n\t\tpub,\n\t\thandlerFunc,\n\t).AddMiddleware(secondMiddleware, thirdMiddleware)\n\trouter.AddMiddleware(fourthMiddleware)\n\n\tgo func() {\n\t\tfor middlewareName := range middlewareCh {\n\t\t\texecutedMiddleware = append(executedMiddleware, middlewareName)\n\n\t\t\tif len(executedMiddleware) == middlewareCount {\n\t\t\t\tallMiddlewareExecuted <- struct{}{}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tallMiddlewareExecuted <- struct{}{}\n\t}()\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\t<-allMiddlewareExecuted\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"firstMiddleware\", executedMiddleware[3])\n\trequire.Equal(t, \"secondMiddleware\", executedMiddleware[2])\n\trequire.Equal(t, \"thirdMiddleware\", executedMiddleware[1])\n\trequire.Equal(t, \"fourthMiddleware\", executedMiddleware[0])\n}\n\nfunc TestRouter_AddMiddleware_to_handler_many(t *testing.T) {\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\trouter, err := message.NewRouter(message.RouterConfig{\n\t\tCloseTimeout: time.Second,\n\t}, watermill.NewStdLogger(true, true))\n\trequire.NoError(t, err)\n\n\tmiddlewareCount := 6\n\tmiddlewareCh := make(chan string, middlewareCount)\n\tallMiddlewareExecuted := make(chan struct{}, 1)\n\tvar executedMiddleware []string\n\n\thandlerFunc := func(msg *message.Message) ([]*message.Message, error) {\n\t\treturn message.Messages{msg}, nil\n\t}\n\tfirstMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"firstMiddleware\"\n\t\treturn h\n\t}\n\n\tsecondMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"secondMiddleware\"\n\t\treturn h\n\t}\n\n\tthirdMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"thirdMiddleware\"\n\t\treturn h\n\t}\n\n\tfourthMiddleware := func(h message.HandlerFunc) message.HandlerFunc {\n\t\tmiddlewareCh <- \"fourthMiddleware\"\n\t\treturn h\n\t}\n\n\ttopic := \"some_topic\"\n\trouter.AddMiddleware(firstMiddleware)\n\trouter.AddHandler(\n\t\t\"some_topic_handler\",\n\t\ttopic,\n\t\tsub,\n\t\ttopic,\n\t\tpub,\n\t\thandlerFunc,\n\t).AddMiddleware(secondMiddleware)\n\trouter.AddHandler(\n\t\t\"some_other_topic_handler\",\n\t\t\"some_other_topic\",\n\t\tsub,\n\t\t\"some_other_topic\",\n\t\tpub,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\treturn message.Messages{msg}, nil\n\t\t},\n\t).AddMiddleware(thirdMiddleware)\n\trouter.AddMiddleware(fourthMiddleware)\n\n\tgo func() {\n\t\tfor middlewareName := range middlewareCh {\n\t\t\texecutedMiddleware = append(executedMiddleware, middlewareName)\n\n\t\t\tif len(executedMiddleware) == middlewareCount {\n\t\t\t\tallMiddlewareExecuted <- struct{}{}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tallMiddlewareExecuted <- struct{}{}\n\t}()\n\n\tgo func() {\n\t\terr := router.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\t<-router.Running()\n\t<-allMiddlewareExecuted\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 6, len(executedMiddleware))\n\tcounts := map[string]int{}\n\tfor _, m := range executedMiddleware {\n\t\tcounts[m] += 1\n\t}\n\trequire.Equal(t, 2, counts[\"firstMiddleware\"])\n\trequire.Equal(t, 1, counts[\"secondMiddleware\"])\n\trequire.Equal(t, 1, counts[\"thirdMiddleware\"])\n\trequire.Equal(t, 2, counts[\"fourthMiddleware\"])\n}\n\nfunc TestRouter_RunHandlers(t *testing.T) {\n\tctx := context.Background()\n\n\ttestID := watermill.NewUUID()\n\tsubscribeTopic := \"test_topic_\" + testID\n\n\tpubsub := gochannel.NewGoChannel(\n\t\tgochannel.Config{Persistent: true},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\tdefer func() {\n\t\tassert.NoError(t, pubsub.Close())\n\t}()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\tassert.NoError(t, r.Close())\n\t}()\n\n\tgo func() {\n\t\trequire.NoError(t, r.Run(ctx))\n\t}()\n\t<-r.Running()\n\n\tmessagesCount := 3\n\n\tvar expectedReceivedMessages message.Messages\n\n\treceivedMessagesCh := make(chan *message.Message, messagesCount)\n\n\thandler := r.AddConsumerHandler(\n\t\t\"test_subscriber_1\",\n\t\tsubscribeTopic,\n\t\tpubsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treceivedMessagesCh <- msg\n\t\t\treturn nil\n\t\t},\n\t)\n\trequire.NotNil(t, handler)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, r.RunHandlers(ctx))\n\trequire.NoError(t, r.RunHandlers(ctx)) // RunHandlers should be idempotent\n\n\tselect {\n\tcase <-handler.Started():\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timeout waiting for handler\")\n\t}\n\n\texpectedReceivedMessages = publishMessagesForHandler(t, messagesCount, pubsub, pubsub, subscribeTopic)\n\n\treceivedMessages1, all := subscriber.BulkRead(receivedMessagesCh, len(expectedReceivedMessages), time.Second*10)\n\tassert.True(t, all)\n\ttests.AssertAllMessagesReceived(t, expectedReceivedMessages, receivedMessages1)\n}\n\nfunc TestRouter_close_handler(t *testing.T) {\n\ttestID := watermill.NewUUID()\n\tsubscribeTopic1 := \"test_topic_1_\" + testID\n\tsubscribeTopic2 := \"test_topic_2_\" + testID\n\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\trequire.NoError(t, err)\n\n\tmessagesCount := 3\n\tvar expectedReceivedMessages message.Messages\n\treceivedMessagesCh1 := make(chan *message.Message, messagesCount)\n\n\thandler := r.AddConsumerHandler(\n\t\t\"test_subscriber_1\",\n\t\tsubscribeTopic1,\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treceivedMessagesCh1 <- msg\n\t\t\treturn nil\n\t\t},\n\t)\n\n\t// to keep at least one running handler to prevent router from closing\n\tr.AddConsumerHandler(\n\t\t\"noop_handler\",\n\t\twatermill.NewUUID(),\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\trequire.NoError(t, r.Run(context.Background()))\n\t}()\n\t<-r.Running()\n\n\texpectedReceivedMessages = publishMessagesForHandler(t, messagesCount, pub, sub, subscribeTopic1)\n\treceivedMessages1, all := subscriber.BulkRead(receivedMessagesCh1, len(expectedReceivedMessages), time.Second*10)\n\tassert.True(t, all)\n\ttests.AssertAllMessagesReceived(t, expectedReceivedMessages, receivedMessages1)\n\n\thandler.Stop()\n\tselect {\n\tcase <-handler.Stopped():\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timeout waiting for handler stopped\")\n\t}\n\n\t_ = publishMessagesForHandler(t, 1, pub, sub, subscribeTopic1)\n\t_, received := subscriber.BulkRead(receivedMessagesCh1, 1, time.Millisecond*1)\n\tassert.False(t, received)\n\n\treceivedMessagesCh2 := make(chan *message.Message, messagesCount)\n\n\t// we are adding the same handler again, with the same name\n\tr.AddConsumerHandler(\n\t\t\"test_subscriber_1\",\n\t\tsubscribeTopic2,\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treceivedMessagesCh2 <- msg\n\t\t\treturn nil\n\t\t},\n\t)\n\terr = r.RunHandlers(context.Background())\n\trequire.NoError(t, err)\n\n\texpectedReceivedMessages = publishMessagesForHandler(t, messagesCount, pub, sub, subscribeTopic2)\n\treceivedMessages2, all := subscriber.BulkRead(receivedMessagesCh2, len(expectedReceivedMessages), time.Second*10)\n\tassert.True(t, all)\n\ttests.AssertAllMessagesReceived(t, expectedReceivedMessages, receivedMessages2)\n}\n\ntype subscriberMock struct {\n\tmessages chan *message.Message\n}\n\nfunc (s *subscriberMock) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {\n\treturn s.messages, nil\n}\n\nfunc (s *subscriberMock) Close() error {\n\tclose(s.messages)\n\treturn nil\n}\n\ntype failingPublisherMock struct {\n\tshouldPanic bool\n\tshouldError bool\n}\n\nfunc (p *failingPublisherMock) Publish(topic string, messages ...*message.Message) error {\n\tif p.shouldPanic {\n\t\tpanic(\"publisher panicked\")\n\t}\n\tif p.shouldError {\n\t\treturn errors.New(\"publisher failed\")\n\t}\n\treturn nil\n}\n\nfunc (p *failingPublisherMock) Close() error { return nil }\n\nfunc TestRouter_stop_when_all_handlers_stopped(t *testing.T) {\n\tpub1, sub1 := createPubSub()\n\tpub2, sub2 := createPubSub()\n\n\tdefer func() {\n\t\tassert.NoError(t, pub1.Close())\n\t\tassert.NoError(t, sub1.Close())\n\t\tassert.NoError(t, pub2.Close())\n\t\tassert.NoError(t, sub2.Close())\n\t}()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\trequire.NoError(t, err)\n\n\tr.AddConsumerHandler(\n\t\t\"handler_1\",\n\t\t\"foo\",\n\t\tsub1,\n\t\tfunc(msg *message.Message) error {\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tr.AddConsumerHandler(\n\t\t\"handler_2\",\n\t\t\"foo\",\n\t\tsub2,\n\t\tfunc(msg *message.Message) error {\n\t\t\treturn nil\n\t\t},\n\t)\n\n\trouterStopped := make(chan struct{})\n\tgo func() {\n\t\tassert.NoError(t, r.Run(context.Background()))\n\t\tclose(routerStopped)\n\t}()\n\t<-r.Running()\n\n\trequire.NoError(t, pub1.Close())\n\trequire.NoError(t, sub1.Close())\n\tselect {\n\tcase <-routerStopped:\n\t\tt.Fatal(\"only one handler has stopped\")\n\tcase <-time.After(time.Millisecond * 100):\n\t\t// ok\n\t}\n\n\trequire.NoError(t, pub2.Close())\n\trequire.NoError(t, sub2.Close())\n\tselect {\n\tcase <-routerStopped:\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"router not stopped\")\n\t}\n}\n\ntype benchMockSubscriber struct {\n\tmessagesToSend []*message.Message\n}\n\nfunc (m benchMockSubscriber) Subscribe(_ context.Context, topic string) (<-chan *message.Message, error) {\n\tout := make(chan *message.Message)\n\n\tgo func() {\n\t\tfor _, msg := range m.messagesToSend {\n\t\t\tout <- msg\n\t\t\t<-msg.Acked()\n\t\t}\n\n\t\tclose(out)\n\t}()\n\n\treturn out, nil\n}\n\nfunc (benchMockSubscriber) Close() error {\n\treturn nil\n}\n\ntype nopPublisher struct{}\n\nfunc (nopPublisher) Publish(topic string, messages ...*message.Message) error { return nil }\nfunc (nopPublisher) Close() error                                             { return nil }\n\nfunc BenchmarkRouterHandler(b *testing.B) {\n\tlogger := watermill.NopLogger{}\n\n\tallProcessedWg := sync.WaitGroup{}\n\tallProcessedWg.Add(b.N)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tsub := createBenchSubscriber(b)\n\n\trouter.AddHandler(\n\t\t\"handler\",\n\t\t\"benchmark_topic\",\n\t\tsub,\n\t\t\"publish_topic\",\n\t\tnopPublisher{},\n\t\tfunc(msg *message.Message) (messages []*message.Message, e error) {\n\t\t\tallProcessedWg.Done()\n\t\t\treturn []*message.Message{msg}, nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\tallProcessedWg.Wait()\n\t\trouter.Close()\n\t}()\n\n\tb.ResetTimer()\n\tif err := router.Run(context.Background()); err != nil {\n\t\tb.Fatal(err)\n\t}\n}\n\nfunc TestRouterNoPublisherHandler(t *testing.T) {\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tlogger := watermill.NewCaptureLogger()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\twait := make(chan struct{})\n\n\tr.AddConsumerHandler(\n\t\t\"test_no_publisher_handler\",\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\tclose(wait)\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\terr = r.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\tdefer r.Close()\n\n\t<-r.Running()\n\n\tpublishedMsg := message.NewMessage(\"1\", nil)\n\terr = pub.Publish(\"subscribe_topic\", publishedMsg)\n\trequire.NoError(t, err)\n\n\tselect {\n\tcase <-wait:\n\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"no message received\")\n\t}\n\n\trequire.NoError(t, r.Close())\n}\n\nfunc BenchmarkRouterNoPublisherHandler(b *testing.B) {\n\tlogger := watermill.NopLogger{}\n\n\tallProcessedWg := sync.WaitGroup{}\n\tallProcessedWg.Add(b.N)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tsub := createBenchSubscriber(b)\n\n\trouter.AddConsumerHandler(\n\t\t\"handler\",\n\t\t\"benchmark_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) (e error) {\n\t\t\tallProcessedWg.Done()\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\tallProcessedWg.Wait()\n\t\trouter.Close()\n\t}()\n\n\tb.ResetTimer()\n\tif err := router.Run(context.Background()); err != nil {\n\t\tb.Fatal(err)\n\t}\n}\n\n// TestRouterDecoratorsOrder checks that the publisher/subscriber decorators are applied in the order they are registered.\nfunc TestRouterDecoratorsOrder(t *testing.T) {\n\tlogger := watermill.NewStdLogger(true, true)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tpub, sub := createPubSub()\n\n\tpubDecorator1 := message.MessageTransformPublisherDecorator(func(m *message.Message) {\n\t\tm.Metadata.Set(\"pub\", m.Metadata.Get(\"pub\")+\"foo\")\n\t})\n\tpubDecorator2 := message.MessageTransformPublisherDecorator(func(m *message.Message) {\n\t\tm.Metadata.Set(\"pub\", m.Metadata.Get(\"pub\")+\"bar\")\n\t})\n\n\tsubDecorator1 := message.MessageTransformSubscriberDecorator(func(m *message.Message) {\n\t\tm.Metadata.Set(\"sub\", m.Metadata.Get(\"sub\")+\"foo\")\n\t})\n\tsubDecorator2 := message.MessageTransformSubscriberDecorator(func(m *message.Message) {\n\t\tm.Metadata.Set(\"sub\", m.Metadata.Get(\"sub\")+\"bar\")\n\t})\n\n\trouter.AddPublisherDecorators(pubDecorator1, pubDecorator2)\n\trouter.AddSubscriberDecorators(subDecorator1, subDecorator2)\n\n\trouter.AddHandler(\n\t\t\"handler\",\n\t\t\"subTopic\",\n\t\tsub,\n\t\t\"pubTopic\",\n\t\tpub,\n\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\treturn message.Messages{msg}, nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\tif err := router.Run(context.Background()); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\tdefer func() {\n\t\tif err := router.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\t<-router.Running()\n\n\ttransformedMessages, err := sub.Subscribe(context.Background(), \"pubTopic\")\n\trequire.NoError(t, err)\n\n\tvar transformedMessage *message.Message\n\tmessageObtained := make(chan struct{})\n\tgo func() {\n\t\ttransformedMessage = <-transformedMessages\n\t\tclose(messageObtained)\n\t}()\n\n\trequire.NoError(t, pub.Publish(\"subTopic\", message.NewMessage(watermill.NewUUID(), []byte{})))\n\n\tselect {\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"test timed out\")\n\tcase <-messageObtained:\n\t}\n\n\tassert.Equal(t, \"foobar\", transformedMessage.Metadata.Get(\"pub\"))\n\tassert.Equal(t, \"foobar\", transformedMessage.Metadata.Get(\"sub\"))\n}\n\nfunc TestRouter_concurrent_close(t *testing.T) {\n\tlogger := watermill.NewStdLogger(true, true)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\terr := router.Close()\n\t\trequire.NoError(t, err)\n\t}()\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n}\n\nfunc TestRouter_concurrent_close_on_handlers_closed(t *testing.T) {\n\tlogger := watermill.NewStdLogger(true, true)\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\t_, sub := createPubSub()\n\n\trouter.AddConsumerHandler(\n\t\t\"handler\",\n\t\t\"subTopic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tgo func() {\n\t\tif err := router.Run(context.Background()); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\t<-router.Running()\n\n\tgo func() {\n\t\terr := sub.Close()\n\t\trequire.NoError(t, err)\n\t}()\n\n\terr = router.Close()\n\trequire.NoError(t, err)\n}\n\nfunc createBenchSubscriber(b *testing.B) benchMockSubscriber {\n\tvar messagesToSend []*message.Message\n\tfor i := 0; i < b.N; i++ {\n\t\tmessagesToSend = append(\n\t\t\tmessagesToSend,\n\t\t\tmessage.NewMessage(watermill.NewUUID(), fmt.Appendf(nil, \"%d\", i)),\n\t\t)\n\t}\n\n\treturn benchMockSubscriber{messagesToSend}\n}\n\nfunc publishMessagesForHandler(t *testing.T, messagesCount int, pub message.Publisher, sub message.Subscriber, topicName string) []*message.Message {\n\tvar messagesToPublish []*message.Message\n\n\tfor i := 0; i < messagesCount; i++ {\n\t\tmsg := message.NewMessage(watermill.NewUUID(), fmt.Appendf(nil, \"%d\", i))\n\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\t}\n\n\tfor _, msg := range messagesToPublish {\n\t\terr := pub.Publish(topicName, msg)\n\t\trequire.NoError(t, err)\n\t}\n\n\treturn messagesToPublish\n}\n\nfunc createPubSub() (message.Publisher, message.Subscriber) {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{Persistent: true},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\treturn pubSub, pubSub\n}\n\nfunc readMessages(messagesCh <-chan *message.Message, limit int, timeout time.Duration) (receivedMessages []*message.Message, all bool) {\n\tallMessagesReceived := make(chan struct{}, 1)\n\n\tgo func() {\n\t\tfor msg := range messagesCh {\n\t\t\treceivedMessages = append(receivedMessages, msg)\n\n\t\t\tif len(receivedMessages) == limit {\n\t\t\t\tallMessagesReceived <- struct{}{}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\t// messagesCh stopped\n\t\tallMessagesReceived <- struct{}{}\n\t}()\n\n\tselect {\n\tcase <-allMessagesReceived:\n\tcase <-time.After(timeout):\n\t}\n\n\treturn receivedMessages, len(receivedMessages) == limit\n}\n\nfunc TestRouter_Handlers(t *testing.T) {\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tlogger := watermill.NewCaptureLogger()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\thandlerCalled := false\n\n\thandlerName := \"test_get_handler\"\n\n\tr.AddConsumerHandler(\n\t\thandlerName,\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\thandlerCalled = true\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tactual := r.Handlers()\n\n\tassert.Len(t, actual, 1)\n\n\tactualHandler := actual[handlerName]\n\n\tassert.NotNil(t, actualHandler)\n\n\tmessages, err := actualHandler(nil)\n\n\tassert.Empty(t, messages)\n\tassert.NoError(t, err)\n\tassert.True(t, handlerCalled, \"Handler function should be the same\")\n}\n\nfunc TestRouter_wait_for_handlers_before_shutdown(t *testing.T) {\n\tt.Parallel()\n\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tlogger := watermill.NewCaptureLogger()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{},\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\thandlerStarted := make(chan struct{})\n\trouterClosed := make(chan struct{})\n\n\tr.AddConsumerHandler(\n\t\t\"foo\",\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\tclose(handlerStarted)\n\t\t\tselect {}\n\t\t},\n\t)\n\n\tgo func() {\n\t\terr := r.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\t<-r.Running()\n\n\terr = pub.Publish(\"subscribe_topic\", message.NewMessage(watermill.NewUUID(), nil))\n\trequire.NoError(t, err)\n\n\t<-handlerStarted\n\n\tgo func() {\n\t\tassert.NoError(t, r.Close())\n\t\tclose(routerClosed)\n\t}()\n\n\tselect {\n\tcase <-routerClosed:\n\t\tt.Fatal(\"Router should wait for handlers to finish\")\n\tcase <-time.After(time.Millisecond * 100):\n\t\t// ok, router is still running\n\t}\n}\n\nfunc TestRouter_wait_for_handlers_before_shutdown_timeout(t *testing.T) {\n\tt.Parallel()\n\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tlogger := watermill.NewCaptureLogger()\n\n\tr, err := message.NewRouter(\n\t\tmessage.RouterConfig{\n\t\t\tCloseTimeout: time.Millisecond * 1,\n\t\t},\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\thandlerStarted := make(chan struct{})\n\n\tr.AddConsumerHandler(\n\t\t\"foo\",\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\tclose(handlerStarted)\n\t\t\tselect {}\n\t\t},\n\t)\n\n\tgo func() {\n\t\terr := r.Run(context.Background())\n\t\tassert.NoError(t, err)\n\t}()\n\t<-r.Running()\n\n\terr = pub.Publish(\"subscribe_topic\", message.NewMessage(watermill.NewUUID(), nil))\n\trequire.NoError(t, err)\n\n\t<-handlerStarted\n\n\tassert.EqualError(t, r.Close(), \"router close timeout\")\n}\n\nfunc TestRouter_context_cancel_does_not_log_error(t *testing.T) {\n\tt.Parallel()\n\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tlogger := watermill.NewCaptureLogger()\n\n\tr, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tr.AddConsumerHandler(\n\t\t\"foo\",\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\terr := r.Run(ctx)\n\t\tassert.NoError(t, err)\n\t}()\n\t<-r.Running()\n\n\t// Cancel the context\n\tcancel()\n\n\trequire.Eventually(t, func() bool {\n\t\treturn r.IsClosed()\n\t}, 3*time.Second, 5*time.Millisecond, \"Router should be closed after all handlers are stopped\")\n\n\tassert.Empty(t, logger.Captured()[watermill.ErrorLogLevel], \"No error should be logged when context is canceled\")\n}\n\n// TestRouter_nack_on_context_canceled checks that the message is Nacked\n// when the handler returns context.Canceled.\nfunc TestRouter_nack_on_context_canceled(t *testing.T) {\n\tt.Parallel()\n\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, watermill.NopLogger{})\n\tdefer func() {\n\t\tassert.NoError(t, pubSub.Close())\n\t}()\n\n\tlogger := watermill.NewStdLogger(false, false) // Use StdLogger, logging check is in another test\n\n\tr, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\thandlerProcessed := make(chan struct{})\n\tsubscribeTopic := \"test_nack_on_context_canceled_\" + watermill.NewUUID()\n\n\tr.AddConsumerHandler(\n\t\t\"test_handler\",\n\t\tsubscribeTopic,\n\t\tpubSub,\n\t\tfunc(msg *message.Message) error {\n\t\t\tdefer func() {\n\t\t\t\thandlerProcessed <- struct{}{}\n\t\t\t}()\n\t\t\t// Simulate handler returning context.Canceled\n\t\t\treturn context.Canceled\n\t\t},\n\t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\trunErrCh := make(chan error, 1)\n\tgo func() {\n\t\trunErrCh <- r.Run(ctx)\n\t}()\n\tselect {\n\tcase <-r.Running():\n\t\t// proceed\n\tcase err := <-runErrCh:\n\t\tt.Fatalf(\"Router failed to start: %v\", err)\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Router did not start\")\n\t}\n\n\t// Publish a message to trigger the handler\n\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\terr = pubSub.Publish(subscribeTopic, msg)\n\trequire.NoError(t, err)\n\n\t// Wait for the handler to process the message\n\tselect {\n\tcase <-handlerProcessed:\n\t\t// ok\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Handler did not process message in time\")\n\t}\n\n\t// Message should be re-sent when nacked\n\tselect {\n\tcase <-handlerProcessed:\n\t\t// ok\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Handler did not process message in time\")\n\t}\n}\n\nfunc TestRouter_stopping_all_handlers_logs_error(t *testing.T) {\n\tt.Parallel()\n\n\tpub, sub := createPubSub()\n\tdefer func() {\n\t\tassert.NoError(t, pub.Close())\n\t\tassert.NoError(t, sub.Close())\n\t}()\n\n\tlogger := watermill.NewCaptureLogger()\n\n\tdefer logger.PrintCaptured(t)\n\n\tr, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\tr.AddConsumerHandler(\n\t\t\"foo\",\n\t\t\"subscribe_topic\",\n\t\tsub,\n\t\tfunc(msg *message.Message) error {\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tctx := context.Background()\n\n\tgo func() {\n\t\terr := r.Run(ctx)\n\t\tassert.NoError(t, err)\n\t}()\n\t<-r.Running()\n\n\t// Stop the subscriber - this should close the router with an error logged\n\terr = sub.Close()\n\trequire.NoError(t, err)\n\n\trequire.Eventually(\n\t\tt,\n\t\tfunc() bool {\n\t\t\treturn r.IsClosed()\n\t\t},\n\t\t1*time.Second,\n\t\t1*time.Millisecond,\n\t\t\"Router should be closed after all handlers are stopped\",\n\t)\n\n\texpectedLogMessage := watermill.CapturedMessage{\n\t\tLevel: watermill.ErrorLogLevel,\n\t\tMsg:   \"All handlers stopped, closing router\",\n\t\tErr:   errors.New(\"all router handlers stopped\"),\n\t}\n\n\t// Note: using logger.Has does not work here, since the error is not exposed (and thus not deep equal-able)\n\tfor _, capturedMessage := range logger.Captured()[watermill.ErrorLogLevel] {\n\t\tif capturedMessage.Level == expectedLogMessage.Level &&\n\t\t\tcapturedMessage.Msg == expectedLogMessage.Msg &&\n\t\t\tcapturedMessage.Err.Error() == expectedLogMessage.Err.Error() {\n\t\t\treturn\n\t\t}\n\t}\n\n\tassert.Fail(\n\t\tt,\n\t\t\"expected log message not found, logs: %#v\",\n\t\tlogger.Captured(),\n\t)\n}\n"
  },
  {
    "path": "message/subscriber/read.go",
    "content": "package subscriber\n\nimport (\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// BulkRead reads provided amount of messages from the provided channel, until a timeout occurs or the limit is reached.\nfunc BulkRead(messagesCh <-chan *message.Message, limit int, timeout time.Duration) (receivedMessages message.Messages, all bool) {\nMessagesLoop:\n\tfor len(receivedMessages) < limit {\n\t\tselect {\n\t\tcase msg, ok := <-messagesCh:\n\t\t\tif !ok {\n\t\t\t\tbreak MessagesLoop\n\t\t\t}\n\n\t\t\treceivedMessages = append(receivedMessages, msg)\n\t\t\tmsg.Ack()\n\t\tcase <-time.After(timeout):\n\t\t\tbreak MessagesLoop\n\t\t}\n\t}\n\n\treturn receivedMessages, len(receivedMessages) == limit\n}\n\n// BulkReadWithDeduplication reads provided number of messages from the provided channel, ignoring duplicates,\n// until a timeout occurs or the limit is reached.\nfunc BulkReadWithDeduplication(messagesCh <-chan *message.Message, limit int, timeout time.Duration) (receivedMessages message.Messages, all bool) {\n\treceivedIDs := map[string]struct{}{}\n\nMessagesLoop:\n\tfor len(receivedMessages) < limit {\n\t\tselect {\n\t\tcase msg, ok := <-messagesCh:\n\t\t\tif !ok {\n\t\t\t\tbreak MessagesLoop\n\t\t\t}\n\n\t\t\tif _, ok := receivedIDs[msg.UUID]; !ok {\n\t\t\t\treceivedIDs[msg.UUID] = struct{}{}\n\t\t\t\treceivedMessages = append(receivedMessages, msg)\n\t\t\t}\n\t\t\tmsg.Ack()\n\t\tcase <-time.After(timeout):\n\t\t\tbreak MessagesLoop\n\t\t}\n\t}\n\n\treturn receivedMessages, len(receivedMessages) == limit\n}\n"
  },
  {
    "path": "message/subscriber/read_test.go",
    "content": "package subscriber_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/tests\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype bulkReadFunc func(messagesCh <-chan *message.Message, limit int, timeout time.Duration) (receivedMessages message.Messages, all bool)\n\nfunc TestBulkRead(t *testing.T) {\n\ttestCases := []struct {\n\t\tName         string\n\t\tBulkReadFunc bulkReadFunc\n\t}{\n\t\t{\n\t\t\tName:         \"BulkRead\",\n\t\t\tBulkReadFunc: subscriber.BulkRead,\n\t\t},\n\t\t{\n\t\t\tName:         \"BulkReadWithDeduplication\",\n\t\t\tBulkReadFunc: subscriber.BulkReadWithDeduplication,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tmessagesCount := 100\n\n\t\t\tvar messages []*message.Message\n\t\t\tmessagesCh := make(chan *message.Message, messagesCount)\n\n\t\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\t\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\n\t\t\t\tmessages = append(messages, msg)\n\t\t\t\tmessagesCh <- msg\n\t\t\t}\n\n\t\t\treadMessages, all := subscriber.BulkRead(messagesCh, messagesCount, time.Second)\n\t\t\tassert.True(t, all)\n\n\t\t\ttests.AssertAllMessagesReceived(t, messages, readMessages)\n\t\t})\n\t}\n}\n\nfunc TestBulkRead_timeout(t *testing.T) {\n\ttestCases := []struct {\n\t\tName         string\n\t\tBulkReadFunc bulkReadFunc\n\t}{\n\t\t{\n\t\t\tName:         \"BulkRead\",\n\t\t\tBulkReadFunc: subscriber.BulkRead,\n\t\t},\n\t\t{\n\t\t\tName:         \"BulkReadWithDeduplication\",\n\t\t\tBulkReadFunc: subscriber.BulkReadWithDeduplication,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tmessagesCount := 100\n\t\t\tsendLimit := 90\n\n\t\t\tmessagesCh := make(chan *message.Message, messagesCount)\n\n\t\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\t\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\n\t\t\t\tif i < sendLimit {\n\t\t\t\t\tmessagesCh <- msg\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbulkReadStart := time.Now()\n\t\t\treadMessages, all := subscriber.BulkRead(messagesCh, messagesCount, time.Millisecond)\n\n\t\t\tassert.WithinDuration(t, bulkReadStart, time.Now(), time.Millisecond*100)\n\t\t\tassert.False(t, all)\n\t\t\tassert.Equal(t, sendLimit, len(readMessages))\n\t\t})\n\t}\n}\n\nfunc TestBulkRead_with_limit(t *testing.T) {\n\ttestCases := []struct {\n\t\tName         string\n\t\tBulkReadFunc bulkReadFunc\n\t}{\n\t\t{\n\t\t\tName:         \"BulkRead\",\n\t\t\tBulkReadFunc: subscriber.BulkRead,\n\t\t},\n\t\t{\n\t\t\tName:         \"BulkReadWithDeduplication\",\n\t\t\tBulkReadFunc: subscriber.BulkReadWithDeduplication,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tmessagesCount := 110\n\t\t\tlimit := 100\n\n\t\t\tmessagesCh := make(chan *message.Message, messagesCount)\n\n\t\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\t\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\n\t\t\t\tmessagesCh <- msg\n\t\t\t}\n\n\t\t\treadMessages, all := subscriber.BulkRead(messagesCh, limit, time.Second)\n\t\t\tassert.True(t, all)\n\t\t\tassert.Equal(t, limit, len(readMessages))\n\t\t})\n\t}\n}\n\nfunc TestBulkRead_return_on_channel_close(t *testing.T) {\n\ttestCases := []struct {\n\t\tName         string\n\t\tBulkReadFunc bulkReadFunc\n\t}{\n\t\t{\n\t\t\tName:         \"BulkRead\",\n\t\t\tBulkReadFunc: subscriber.BulkRead,\n\t\t},\n\t\t{\n\t\t\tName:         \"BulkReadWithDeduplication\",\n\t\t\tBulkReadFunc: subscriber.BulkReadWithDeduplication,\n\t\t},\n\t}\n\n\tfor _, c := range testCases {\n\t\tt.Run(c.Name, func(t *testing.T) {\n\t\t\tmessagesCount := 100\n\t\t\tsendLimit := 90\n\n\t\t\tmessagesCh := make(chan *message.Message, messagesCount)\n\t\t\tmessagesChClosed := false\n\n\t\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\t\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\n\t\t\t\tif i < sendLimit {\n\t\t\t\t\tmessagesCh <- msg\n\t\t\t\t} else if !messagesChClosed {\n\t\t\t\t\tclose(messagesCh)\n\t\t\t\t\tmessagesChClosed = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbulkReadStart := time.Now()\n\t\t\t_, all := subscriber.BulkRead(messagesCh, messagesCount, time.Second)\n\n\t\t\tassert.WithinDuration(t, bulkReadStart, time.Now(), time.Millisecond*100)\n\t\t\tassert.False(t, all)\n\t\t})\n\t}\n}\n\nfunc TestBulkReadWithDeduplication(t *testing.T) {\n\tmessagesCh := make(chan *message.Message, 3)\n\n\tmsg1 := message.NewMessage(watermill.NewUUID(), nil)\n\tmsg2 := message.NewMessage(watermill.NewUUID(), nil)\n\tmessagesCh <- msg1\n\tmessagesCh <- msg1\n\tmessagesCh <- msg2\n\n\treadMessages, all := subscriber.BulkReadWithDeduplication(messagesCh, 2, time.Second)\n\tassert.True(t, all)\n\n\tassert.Equal(t, []string{msg1.UUID, msg2.UUID}, readMessages.IDs())\n}\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build]\n  command = \"./build.sh --copy && npm run build\"\n  base = \"docs/\"\n  publish = \"docs/public/\"\n\n[build.environment]\n  NODE_VERSION = \"20.11.0\"\n  NPM_VERSION = \"10.2.4\"\n  HUGO_VERSION = \"0.127.0\"\n\n[context.deploy-preview]\n  command = \"./build.sh --copy && npm run build:branch\"\n\n[context.branch-deploy]\n  command = \"./build.sh --copy && npm run build:branch\"\n\n[[redirects]]\n  from = \"/api/event\"\n  to = \"https://academy-api.threedots.tech/api/event\"\n  force = true\n  status = 200\n\n[[redirects]]\n  from = \"/docs/fanin\"\n  to = \"/advanced/fanin/\"\n  status = 301\n\n[[redirects]]\n  from = \"/docs/forwarder\"\n  to = \"/advanced/forwarder/\"\n  status = 301\n\n[[redirects]]\n  from = \"/docs/metrics\"\n  to = \"/advanced/metrics/\"\n  status = 301\n\n[[redirects]]\n  from = \"/docs/pub-sub-implementing\"\n  to = \"/development/pub-sub-implementing/\"\n  status = 301\n\n[[redirects]]\n  from = \"/pubsubs/amazonsqs/\"\n  to = \"/pubsubs/aws/\"\n  status = 301\n\n[[redirects]]\n  from = \"/docs/getting-started\"\n  to = \"/learn/getting-started/\"\n  status = 301\n"
  },
  {
    "path": "pubsub/doc.go",
    "content": "// Infrastructure directory contains Pub/Subs implementations.\n//\n// Detailed Pub/Subs docs: https://watermill.io/pubsubs/\n// Getting started guide: https://watermill.io/learn/getting-started/\n\npackage pubsub\n"
  },
  {
    "path": "pubsub/gochannel/doc.go",
    "content": "// This is just the simplest Pub/Sub implementation\n//\n// All Pub/Sub implementations can be found at https://watermill.io/pubsubs/\n\npackage gochannel\n"
  },
  {
    "path": "pubsub/gochannel/fanout.go",
    "content": "package gochannel\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// FanOut is a component that receives messages from a topic and passes them\n// to all subscribers. In effect, messages are \"multiplied\".\n//\n// A typical use case for using FanOut is having one external subscription and multiple workers\n// inside the process.\n//\n// You need to call AddSubscription method for all topics that you want to listen to.\n// This needs to be done *before* starting the FanOut.\n//\n// FanOut exposes the standard Subscriber interface.\ntype FanOut struct {\n\tinternalPubSub *GoChannel\n\tinternalRouter *message.Router\n\n\tsubscriber message.Subscriber\n\n\tlogger watermill.LoggerAdapter\n\n\tsubscribedTopics map[string]struct{}\n\tsubscribedLock   sync.Mutex\n}\n\n// NewFanOut creates a new FanOut.\nfunc NewFanOut(\n\tsubscriber message.Subscriber,\n\tlogger watermill.LoggerAdapter,\n) (*FanOut, error) {\n\tif subscriber == nil {\n\t\treturn nil, errors.New(\"missing subscriber\")\n\t}\n\tif logger == nil {\n\t\tlogger = watermill.NopLogger{}\n\t}\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &FanOut{\n\t\tinternalPubSub: NewGoChannel(Config{}, logger),\n\t\tinternalRouter: router,\n\n\t\tsubscriber: subscriber,\n\n\t\tlogger: logger,\n\n\t\tsubscribedTopics: map[string]struct{}{},\n\t}, nil\n}\n\n// AddSubscription add an internal subscription for the given topic.\n// You need to call this method with all topics that you want to listen to, before the FanOut is started.\n// AddSubscription is idempotent.\nfunc (f *FanOut) AddSubscription(topic string) {\n\tf.subscribedLock.Lock()\n\tdefer f.subscribedLock.Unlock()\n\n\t_, ok := f.subscribedTopics[topic]\n\tif ok {\n\t\t// Subscription already exists\n\t\treturn\n\t}\n\n\tf.logger.Trace(\"Adding fan-out subscription for topic\", watermill.LogFields{\n\t\t\"topic\": topic,\n\t})\n\n\tf.internalRouter.AddHandler(\n\t\tfmt.Sprintf(\"fanout-%s\", topic),\n\t\ttopic,\n\t\tf.subscriber,\n\t\ttopic,\n\t\tf.internalPubSub,\n\t\tmessage.PassthroughHandler,\n\t)\n\n\tf.subscribedTopics[topic] = struct{}{}\n}\n\n// Run runs the FanOut.\nfunc (f *FanOut) Run(ctx context.Context) error {\n\treturn f.internalRouter.Run(ctx)\n}\n\n// Running is closed when FanOut is running.\nfunc (f *FanOut) Running() chan struct{} {\n\treturn f.internalRouter.Running()\n}\n\nfunc (f *FanOut) IsClosed() bool {\n\treturn f.internalRouter.IsClosed()\n}\n\n// Subscribe starts subscription to the FanOut's internal Pub/Sub.\nfunc (f *FanOut) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {\n\treturn f.internalPubSub.Subscribe(ctx, topic)\n}\n\n// Close closes the FanOut's internal Pub/Sub.\nfunc (f *FanOut) Close() error {\n\tvar err error\n\n\tif routerCloseErr := f.internalRouter.Close(); routerCloseErr != nil {\n\t\terr = errors.Join(err, routerCloseErr)\n\t}\n\tif internalPubSubCloseErr := f.internalPubSub.Close(); internalPubSubCloseErr != nil {\n\t\terr = errors.Join(err, internalPubSubCloseErr)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "pubsub/gochannel/fanout_test.go",
    "content": "package gochannel_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n)\n\nfunc TestFanOut(t *testing.T) {\n\tconst (\n\t\tupstreamTopic = \"upstream-topic\"\n\t)\n\n\tlogger := watermill.NopLogger{}\n\n\tupstreamPubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\n\tfanout, err := gochannel.NewFanOut(upstreamPubSub, logger)\n\trequire.NoError(t, err)\n\n\tfanout.AddSubscription(upstreamTopic)\n\n\tworkersCount := 10\n\tmessagesCount := 100\n\n\trouter, err := message.NewRouter(message.RouterConfig{}, logger)\n\trequire.NoError(t, err)\n\n\texpectedNumberOfMessages := workersCount * messagesCount\n\n\treceivedMessages := make(chan struct{}, expectedNumberOfMessages)\n\n\tfor i := 0; i < workersCount; i++ {\n\t\trouter.AddConsumerHandler(\n\t\t\tfmt.Sprintf(\"worker-%v\", i),\n\t\t\tupstreamTopic,\n\t\t\tfanout,\n\t\t\tfunc(msg *message.Message) error {\n\t\t\t\treceivedMessages <- struct{}{}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\n\tgo func() {\n\t\terr := router.Run(ctx)\n\t\trequire.NoError(t, err)\n\t}()\n\n\tgo func() {\n\t\terr := fanout.Run(ctx)\n\t\trequire.NoError(t, err)\n\t}()\n\n\t<-router.Running()\n\t<-fanout.Running()\n\n\tgo func() {\n\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\t\t\terr := upstreamPubSub.Publish(upstreamTopic, msg)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-ctx.Done()\n\n\tcounter := 0\n\nloop:\n\tfor {\n\t\tselect {\n\t\tcase <-receivedMessages:\n\t\t\tcounter += 1\n\t\tcase <-time.After(time.Second):\n\t\t\tclose(receivedMessages)\n\t\t\tbreak loop\n\t\t}\n\t}\n\n\trequire.Equal(t, expectedNumberOfMessages, counter)\n}\n\nfunc TestFanOut_RouterClosed(t *testing.T) {\n\tlogger := watermill.NopLogger{}\n\tpubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)\n\n\tfanout, err := gochannel.NewFanOut(pubSub, logger)\n\trequire.NoError(t, err)\n\n\tfanout.AddSubscription(\"some-topic\")\n\n\tgo func() {\n\t\terr := fanout.Run(context.Background())\n\t\trequire.NoError(t, err)\n\t}()\n\n\t<-fanout.Running()\n\n\terr = fanout.Close()\n\trequire.NoError(t, err)\n\n\tassert.True(t, fanout.IsClosed())\n}\n"
  },
  {
    "path": "pubsub/gochannel/pubsub.go",
    "content": "package gochannel\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/lithammer/shortuuid/v3\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\n// Config holds the GoChannel Pub/Sub's configuration options.\ntype Config struct {\n\t// Output channel buffer size.\n\tOutputChannelBuffer int64\n\n\t// If persistent is set to true, when subscriber subscribes to the topic,\n\t// it will receive all previously produced messages.\n\t//\n\t// All messages are persisted to the memory (simple slice),\n\t// so be aware that with large amount of messages you can go out of the memory.\n\tPersistent bool\n\n\t// When true, Publish will block until subscriber Ack's the message.\n\t// If there are no subscribers, Publish will not block (also when Persistent is true).\n\tBlockPublishUntilSubscriberAck bool\n\n\t// PreserveContext is a flag that determines if the context should be preserved when sending messages to subscribers.\n\t// This behavior is different from other implementations of Publishers where data travels over the network,\n\t// hence context can't be preserved in those cases\n\tPreserveContext bool\n}\n\n// GoChannel is the simplest Pub/Sub implementation.\n// It is based on Golang's channels which are sent within the process.\n//\n// GoChannel has no global state,\n// that means that you need to use the same instance for Publishing and Subscribing!\n//\n// When GoChannel is persistent, messages order is not guaranteed.\ntype GoChannel struct {\n\tconfig Config\n\tlogger watermill.LoggerAdapter\n\n\tsubscribersWg          sync.WaitGroup\n\tsubscribers            map[string][]*subscriber\n\tsubscribersLock        sync.RWMutex\n\tsubscribersByTopicLock sync.Map // map of *sync.Mutex\n\n\tclosed     bool\n\tclosedLock sync.Mutex\n\tclosing    chan struct{}\n\n\tpersistedMessages     map[string][]*message.Message\n\tpersistedMessagesLock sync.RWMutex\n}\n\n// NewGoChannel creates new GoChannel Pub/Sub.\n//\n// This GoChannel is not persistent.\n// That means if you send a message to a topic to which no subscriber is subscribed, that message will be discarded.\nfunc NewGoChannel(config Config, logger watermill.LoggerAdapter) *GoChannel {\n\tif logger == nil {\n\t\tlogger = watermill.NopLogger{}\n\t}\n\n\treturn &GoChannel{\n\t\tconfig: config,\n\n\t\tsubscribers:            make(map[string][]*subscriber),\n\t\tsubscribersByTopicLock: sync.Map{},\n\t\tlogger: logger.With(\n\t\t\twatermill.LogFields{\n\t\t\t\t\"pubsub_uuid\": shortuuid.New(),\n\t\t\t},\n\t\t),\n\n\t\tclosing: make(chan struct{}),\n\n\t\tpersistedMessages: map[string][]*message.Message{},\n\t}\n}\n\n// Publish in GoChannel is NOT blocking until all consumers consume.\n// Messages will be sent in background.\n//\n// Messages may be persisted or not, depending on persistent attribute.\nfunc (g *GoChannel) Publish(topic string, messages ...*message.Message) error {\n\tif g.isClosed() {\n\t\treturn errors.New(\"Pub/Sub closed\")\n\t}\n\n\tmessagesToPublish := make(message.Messages, len(messages))\n\tfor i, msg := range messages {\n\t\tif g.config.PreserveContext {\n\t\t\tmessagesToPublish[i] = msg.CopyWithContext()\n\t\t} else {\n\t\t\tmessagesToPublish[i] = msg.Copy()\n\t\t}\n\t}\n\n\tg.subscribersLock.RLock()\n\tdefer g.subscribersLock.RUnlock()\n\n\tsubLock, loaded := g.subscribersByTopicLock.LoadOrStore(topic, &sync.Mutex{})\n\tsubLock.(*sync.Mutex).Lock()\n\n\tif !loaded {\n\t\tdefer g.subscribersByTopicLock.Delete(topic)\n\t}\n\tdefer subLock.(*sync.Mutex).Unlock()\n\n\tif g.config.Persistent {\n\t\tg.persistedMessagesLock.Lock()\n\t\tif _, ok := g.persistedMessages[topic]; !ok {\n\t\t\tg.persistedMessages[topic] = make([]*message.Message, 0)\n\t\t}\n\t\tg.persistedMessages[topic] = append(g.persistedMessages[topic], messagesToPublish...)\n\t\tg.persistedMessagesLock.Unlock()\n\t}\n\n\tfor i := range messagesToPublish {\n\t\tmsg := messagesToPublish[i]\n\n\t\tackedBySubscribers, err := g.sendMessage(topic, msg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif g.config.BlockPublishUntilSubscriberAck {\n\t\t\tg.waitForAckFromSubscribers(msg, ackedBySubscribers)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (g *GoChannel) waitForAckFromSubscribers(msg *message.Message, ackedByConsumer <-chan struct{}) {\n\tlogFields := watermill.LogFields{\"message_uuid\": msg.UUID}\n\tg.logger.Debug(\"Waiting for subscribers ack\", logFields)\n\n\tselect {\n\tcase <-ackedByConsumer:\n\t\tg.logger.Trace(\"Message acked by subscribers\", logFields)\n\tcase <-g.closing:\n\t\tg.logger.Trace(\"Closing Pub/Sub before ack from subscribers\", logFields)\n\t}\n}\n\nfunc (g *GoChannel) sendMessage(topic string, message *message.Message) (<-chan struct{}, error) {\n\tsubscribers := g.topicSubscribers(topic)\n\tackedBySubscribers := make(chan struct{})\n\n\tlogFields := watermill.LogFields{\"message_uuid\": message.UUID, \"topic\": topic}\n\n\tif len(subscribers) == 0 {\n\t\tclose(ackedBySubscribers)\n\t\tg.logger.Info(\"No subscribers to send message\", logFields)\n\t\treturn ackedBySubscribers, nil\n\t}\n\n\tgo func(subscribers []*subscriber) {\n\t\twg := &sync.WaitGroup{}\n\n\t\tfor i := range subscribers {\n\t\t\tsubscriber := subscribers[i]\n\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tsubscriber.sendMessageToSubscriber(message, logFields)\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\t\tclose(ackedBySubscribers)\n\t}(subscribers)\n\n\treturn ackedBySubscribers, nil\n}\n\n// Subscribe returns channel to which all published messages are sent.\n// Messages are not persisted. If there are no subscribers and message is produced it will be gone.\n//\n// There are no consumer groups support etc. Every consumer will receive every produced message.\nfunc (g *GoChannel) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {\n\tg.closedLock.Lock()\n\n\tif g.closed {\n\t\tg.closedLock.Unlock()\n\t\treturn nil, errors.New(\"Pub/Sub closed\")\n\t}\n\n\tg.subscribersWg.Add(1)\n\tg.closedLock.Unlock()\n\n\tg.subscribersLock.Lock()\n\n\tsubLock, _ := g.subscribersByTopicLock.LoadOrStore(topic, &sync.Mutex{})\n\tsubLock.(*sync.Mutex).Lock()\n\n\ts := &subscriber{\n\t\tctx:             ctx,\n\t\tuuid:            watermill.NewUUID(),\n\t\toutputChannel:   make(chan *message.Message, g.config.OutputChannelBuffer),\n\t\tlogger:          g.logger,\n\t\tclosing:         make(chan struct{}),\n\t\tpreserveContext: g.config.PreserveContext,\n\t}\n\n\tgo func(s *subscriber, g *GoChannel) {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t// unblock\n\t\tcase <-g.closing:\n\t\t\t// unblock\n\t\t}\n\n\t\ts.Close()\n\n\t\tg.subscribersLock.Lock()\n\t\tdefer g.subscribersLock.Unlock()\n\n\t\tsubLock, _ := g.subscribersByTopicLock.Load(topic)\n\t\tsubLock.(*sync.Mutex).Lock()\n\t\tdefer subLock.(*sync.Mutex).Unlock()\n\n\t\tg.removeSubscriber(topic, s)\n\t\tg.subscribersWg.Done()\n\t}(s, g)\n\n\tif !g.config.Persistent {\n\t\tdefer g.subscribersLock.Unlock()\n\t\tdefer subLock.(*sync.Mutex).Unlock()\n\n\t\tg.addSubscriber(topic, s)\n\n\t\treturn s.outputChannel, nil\n\t}\n\n\tgo func(s *subscriber) {\n\t\tdefer g.subscribersLock.Unlock()\n\t\tdefer subLock.(*sync.Mutex).Unlock()\n\n\t\tg.persistedMessagesLock.RLock()\n\t\tmessages, ok := g.persistedMessages[topic]\n\t\tg.persistedMessagesLock.RUnlock()\n\n\t\tif ok {\n\t\t\tfor i := range messages {\n\t\t\t\tmsg := g.persistedMessages[topic][i]\n\t\t\t\tlogFields := watermill.LogFields{\"message_uuid\": msg.UUID, \"topic\": topic}\n\n\t\t\t\tgo s.sendMessageToSubscriber(msg, logFields)\n\t\t\t}\n\t\t}\n\n\t\tg.addSubscriber(topic, s)\n\t}(s)\n\n\treturn s.outputChannel, nil\n}\n\nfunc (g *GoChannel) addSubscriber(topic string, s *subscriber) {\n\tif _, ok := g.subscribers[topic]; !ok {\n\t\tg.subscribers[topic] = make([]*subscriber, 0)\n\t}\n\tg.subscribers[topic] = append(g.subscribers[topic], s)\n}\n\nfunc (g *GoChannel) removeSubscriber(topic string, toRemove *subscriber) {\n\tremoved := false\n\tfor i, sub := range g.subscribers[topic] {\n\t\tif sub == toRemove {\n\t\t\tg.subscribers[topic] = append(g.subscribers[topic][:i], g.subscribers[topic][i+1:]...)\n\t\t\tremoved = true\n\n\t\t\tif len(g.subscribers[topic]) == 0 && !g.config.Persistent {\n\t\t\t\t// Free up the memory taken by a topic which no longer has subscribers.\n\t\t\t\t// This operation allows publishing and subscribing to narrowly\n\t\t\t\t// focused topics that include random data like UUIDs in topic name.\n\t\t\t\t//\n\t\t\t\t// Without this operation, memory usage will grow indefinitely in a long-running service\n\t\t\t\t// as the map grows larger and larger with keys pointing to empty slices.\n\t\t\t\tdelete(g.subscribers, topic)\n\t\t\t\tg.subscribersByTopicLock.Delete(topic)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\tif !removed {\n\t\tpanic(\"cannot remove subscriber, not found \" + toRemove.uuid)\n\t}\n}\n\nfunc (g *GoChannel) topicSubscribers(topic string) []*subscriber {\n\tsubscribers, ok := g.subscribers[topic]\n\tif !ok {\n\t\treturn nil\n\t}\n\n\t// let's do a copy to avoid race conditions and deadlocks due to lock\n\tsubscribersCopy := make([]*subscriber, len(subscribers))\n\tcopy(subscribersCopy, subscribers)\n\n\treturn subscribersCopy\n}\n\nfunc (g *GoChannel) isClosed() bool {\n\tg.closedLock.Lock()\n\tdefer g.closedLock.Unlock()\n\n\treturn g.closed\n}\n\n// Close closes the GoChannel Pub/Sub.\nfunc (g *GoChannel) Close() error {\n\tg.closedLock.Lock()\n\tdefer g.closedLock.Unlock()\n\n\tif g.closed {\n\t\treturn nil\n\t}\n\n\tg.closed = true\n\tclose(g.closing)\n\n\tg.logger.Debug(\"Closing Pub/Sub, waiting for subscribers\", nil)\n\tg.subscribersWg.Wait()\n\n\tg.logger.Info(\"Pub/Sub closed\", nil)\n\tg.persistedMessages = nil\n\n\treturn nil\n}\n\ntype subscriber struct {\n\tctx context.Context\n\n\tuuid string\n\n\tsending       sync.Mutex\n\toutputChannel chan *message.Message\n\n\tlogger  watermill.LoggerAdapter\n\tclosed  bool\n\tclosing chan struct{}\n\n\tpreserveContext bool\n}\n\nfunc (s *subscriber) Close() {\n\tif s.closed {\n\t\treturn\n\t}\n\tclose(s.closing)\n\n\ts.logger.Debug(\"Closing subscriber, waiting for sending lock\", nil)\n\n\t// ensuring that we are not sending to closed channel\n\ts.sending.Lock()\n\tdefer s.sending.Unlock()\n\n\ts.logger.Debug(\"GoChannel Pub/Sub Subscriber closed\", nil)\n\ts.closed = true\n\n\tclose(s.outputChannel)\n}\n\nfunc (s *subscriber) sendMessageToSubscriber(msg *message.Message, logFields watermill.LogFields) {\n\tctx := msg.Context()\n\n\t// By default, the subscriber uses the context from the message and it's canceled right after it's processed.\n\t// If the message's context is preserved, the top-level client is responsible for canceling it.\n\tif !s.preserveContext {\n\t\tvar cancelCtx context.CancelFunc\n\t\tctx, cancelCtx = context.WithCancel(s.ctx)\n\t\tdefer cancelCtx()\n\t}\n\nSendToSubscriber:\n\tfor {\n\t\t// copy the message to prevent ack/nack propagation to other consumers\n\t\t// also allows to make retries on a fresh copy of the original message\n\t\tmsgToSend := msg.Copy()\n\t\tmsgToSend.SetContext(ctx)\n\n\t\ts.logger.Trace(\"Sending msg to subscriber\", logFields)\n\n\t\ts.sending.Lock()\n\t\tif s.closed {\n\t\t\ts.logger.Info(\"Pub/Sub closed, discarding msg\", logFields)\n\t\t\ts.sending.Unlock()\n\t\t\treturn\n\t\t}\n\n\t\tselect {\n\t\tcase s.outputChannel <- msgToSend:\n\t\t\ts.logger.Trace(\"Sent message to subscriber\", logFields)\n\t\tcase <-s.closing:\n\t\t\ts.logger.Trace(\"Closing, message discarded\", logFields)\n\t\t\ts.sending.Unlock()\n\t\t\treturn\n\t\t}\n\t\ts.sending.Unlock()\n\n\t\tselect {\n\t\tcase <-msgToSend.Acked():\n\t\t\ts.logger.Trace(\"Message acked\", logFields)\n\t\t\treturn\n\t\tcase <-msgToSend.Nacked():\n\t\t\ts.logger.Trace(\"Nack received, resending message\", logFields)\n\t\t\tcontinue SendToSubscriber\n\t\tcase <-s.closing:\n\t\t\ts.logger.Trace(\"Closing, message discarded\", logFields)\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pubsub/gochannel/pubsub_bench_test.go",
    "content": "package gochannel_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/tests\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc BenchmarkSubscriber(b *testing.B) {\n\ttests.BenchSubscriber(b, func(n int) (message.Publisher, message.Subscriber) {\n\t\tpubSub := gochannel.NewGoChannel(\n\t\t\tgochannel.Config{OutputChannelBuffer: int64(n)}, watermill.NopLogger{},\n\t\t)\n\t\treturn pubSub, pubSub\n\t})\n}\n\nfunc BenchmarkSubscriberPersistent(b *testing.B) {\n\ttests.BenchSubscriber(b, func(n int) (message.Publisher, message.Subscriber) {\n\t\tpubSub := gochannel.NewGoChannel(\n\t\t\tgochannel.Config{\n\t\t\t\tOutputChannelBuffer: int64(n),\n\t\t\t\tPersistent:          true,\n\t\t\t},\n\t\t\twatermill.NopLogger{},\n\t\t)\n\t\treturn pubSub, pubSub\n\t})\n}\n"
  },
  {
    "path": "pubsub/gochannel/pubsub_internal_test.go",
    "content": "package gochannel\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n)\n\nfunc TestSubscribe_clean_subscriber_data(t *testing.T) {\n\tsubCount := 100\n\tpubSub := NewGoChannel(\n\t\tConfig{OutputChannelBuffer: int64(subCount)},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\ttopicName := \"test_topic\"\n\n\tfor i := 0; i < subCount; i++ {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t_, err := pubSub.Subscribe(ctx, topicName+\"_index_\"+strconv.Itoa(i))\n\t\trequire.NoError(t, err)\n\t\tcancel()\n\t}\n\n\terr := pubSub.Close()\n\trequire.NoError(t, err)\n\n\tassert.Len(t, pubSub.subscribers, 0)\n\tlockCount := 0\n\tpubSub.subscribersByTopicLock.Range(func(_, _ any) bool {\n\t\tlockCount++\n\t\treturn true\n\t})\n\tassert.Equal(t, 0, lockCount)\n\n\tassert.NoError(t, pubSub.Close())\n}\n\nfunc TestPublish_clean_lock_data(t *testing.T) {\n\tmessageCount := 100\n\tpubSub := NewGoChannel(\n\t\tConfig{OutputChannelBuffer: int64(messageCount)},\n\t\twatermill.NewStdLogger(false, false),\n\t)\n\ttopicName := \"test_topic\"\n\n\t_, err := pubSub.Subscribe(context.Background(), topicName+\"_index_\"+strconv.Itoa(0))\n\trequire.NoError(t, err)\n\n\tfor i := 0; i < messageCount; i++ {\n\t\terr := pubSub.Publish(topicName+\"_index_\"+strconv.Itoa(i), message.NewMessage(watermill.NewShortUUID(), nil))\n\t\trequire.NoError(t, err)\n\t}\n\n\tlockCount := 0\n\tpubSub.subscribersByTopicLock.Range(func(_, _ any) bool {\n\t\tlockCount++\n\t\treturn true\n\t})\n\tassert.Equal(t, 1, lockCount)\n\n\tassert.NoError(t, pubSub.Close())\n}\n"
  },
  {
    "path": "pubsub/gochannel/pubsub_stress_test.go",
    "content": "//go:build stress\n// +build stress\n\npackage gochannel_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/tests\"\n)\n\nfunc TestPublishSubscribe_stress(t *testing.T) {\n\ttests.TestPubSubStressTest(\n\t\tt,\n\t\ttests.Features{\n\t\t\tConsumerGroups:        false,\n\t\t\tExactlyOnceDelivery:   true,\n\t\t\tGuaranteedOrder:       false,\n\t\t\tPersistent:            false,\n\t\t\tRequireSingleInstance: true,\n\t\t},\n\t\tcreatePersistentPubSub,\n\t\tnil,\n\t)\n}\n"
  },
  {
    "path": "pubsub/gochannel/pubsub_test.go",
    "content": "package gochannel_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/gochannel\"\n\t\"github.com/ThreeDotsLabs/watermill/pubsub/tests\"\n)\n\nfunc createPersistentPubSub(t *testing.T) (message.Publisher, message.Subscriber) {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{\n\t\t\tOutputChannelBuffer: 10000,\n\t\t\tPersistent:          true,\n\t\t},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\treturn pubSub, pubSub\n}\n\nfunc createPersistentPubSubWithContextPreserved(t *testing.T) (message.Publisher, message.Subscriber) {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{\n\t\t\tOutputChannelBuffer: 10000,\n\t\t\tPersistent:          true,\n\t\t\tPreserveContext:     true,\n\t\t},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\treturn pubSub, pubSub\n}\n\nfunc TestPublishSubscribe_persistent(t *testing.T) {\n\ttests.TestPubSub(\n\t\tt,\n\t\ttests.Features{\n\t\t\tConsumerGroups:        false,\n\t\t\tExactlyOnceDelivery:   true,\n\t\t\tGuaranteedOrder:       false,\n\t\t\tPersistent:            false,\n\t\t\tRequireSingleInstance: true,\n\t\t},\n\t\tcreatePersistentPubSub,\n\t\tnil,\n\t)\n}\n\nfunc TestPublishSubscribe_context_preserved(t *testing.T) {\n\ttests.TestPubSub(\n\t\tt,\n\t\ttests.Features{\n\t\t\tConsumerGroups:        false,\n\t\t\tExactlyOnceDelivery:   true,\n\t\t\tGuaranteedOrder:       false,\n\t\t\tPersistent:            false,\n\t\t\tRequireSingleInstance: true,\n\t\t\tContextPreserved:      true,\n\t\t},\n\t\tcreatePersistentPubSubWithContextPreserved,\n\t\tnil,\n\t)\n}\n\nfunc TestPublishSubscribe_not_persistent(t *testing.T) {\n\tmessagesCount := 100\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{OutputChannelBuffer: int64(messagesCount)},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\ttopicName := \"test_topic_\" + watermill.NewUUID()\n\n\tmsgs, err := pubSub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\tsendMessages := tests.PublishSimpleMessages(t, messagesCount, pubSub, topicName)\n\treceivedMsgs, _ := subscriber.BulkRead(msgs, messagesCount, time.Second)\n\n\ttests.AssertAllMessagesReceived(t, sendMessages, receivedMsgs)\n\n\tassert.NoError(t, pubSub.Close())\n}\n\nfunc TestPublishSubscribe_not_persistent_with_context(t *testing.T) {\n\tmessagesCount := 100\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{OutputChannelBuffer: int64(messagesCount), PreserveContext: true},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\ttopicName := \"test_topic_\" + watermill.NewUUID()\n\n\tmsgs, err := pubSub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\tconst contextKeyString = \"foo\"\n\tsendMessages := tests.PublishSimpleMessagesWithContext(t, messagesCount, contextKeyString, pubSub, topicName)\n\treceivedMsgs, _ := subscriber.BulkRead(msgs, messagesCount, time.Second)\n\n\texpectedContexts := make(map[string]context.Context)\n\tfor _, msg := range sendMessages {\n\t\texpectedContexts[msg.UUID] = msg.Context()\n\t}\n\ttests.AssertAllMessagesReceived(t, sendMessages, receivedMsgs)\n\ttests.AssertAllMessagesHaveSameContext(t, contextKeyString, expectedContexts, receivedMsgs)\n\n\tassert.NoError(t, pubSub.Close())\n}\n\nfunc TestPublishSubscribe_block_until_ack(t *testing.T) {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{BlockPublishUntilSubscriberAck: true},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\ttopicName := \"test_topic_\" + watermill.NewUUID()\n\n\tmsgs, err := pubSub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\tpublished := make(chan struct{})\n\tgo func() {\n\t\terr := pubSub.Publish(topicName, message.NewMessage(\"1\", nil))\n\t\trequire.NoError(t, err)\n\t\tclose(published)\n\t}()\n\n\tmsg1 := <-msgs\n\tselect {\n\tcase <-published:\n\t\tt.Fatal(\"publish should be blocked until ack\")\n\tdefault:\n\t\t// ok\n\t}\n\n\tmsg1.Nack()\n\tselect {\n\tcase <-published:\n\t\tt.Fatal(\"publish should be blocked after nack\")\n\tdefault:\n\t\t// ok\n\t}\n\n\tmsg2 := <-msgs\n\tmsg2.Ack()\n\n\tselect {\n\tcase <-published:\n\t\t// ok\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"publish should be not blocked after ack\")\n\t}\n}\n\nfunc TestPublishSubscribe_race_condition_on_subscribe(t *testing.T) {\n\ttestsCount := 15\n\tif testing.Short() {\n\t\ttestsCount = 3\n\t}\n\n\tfor i := 0; i < testsCount; i++ {\n\t\tt.Run(fmt.Sprintf(\"%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttestPublishSubscribeSubRace(t)\n\t\t})\n\t}\n}\n\nfunc TestSubscribe_race_condition_when_closing(t *testing.T) {\n\ttestsCount := 15\n\tif testing.Short() {\n\t\ttestsCount = 3\n\t}\n\n\tfor i := 0; i < testsCount; i++ {\n\t\tt.Run(fmt.Sprintf(\"%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tpubSub := gochannel.NewGoChannel(\n\t\t\t\tgochannel.Config{},\n\t\t\t\twatermill.NewStdLogger(true, false),\n\t\t\t)\n\t\t\tgo func() {\n\t\t\t\terr := pubSub.Close()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}()\n\t\t\t_, err := pubSub.Subscribe(context.Background(), \"topic\")\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestPublish_race_condition_when_closing(t *testing.T) {\n\ttestsCount := 15\n\tif testing.Short() {\n\t\ttestsCount = 3\n\t}\n\n\tfor i := 0; i < testsCount; i++ {\n\t\tt.Run(fmt.Sprintf(\"%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tpubSub := gochannel.NewGoChannel(\n\t\t\t\tgochannel.Config{},\n\t\t\t\twatermill.NewStdLogger(true, false),\n\t\t\t)\n\t\t\tgo func() {\n\t\t\t\t_ = pubSub.Publish(\"topic\", message.NewMessage(watermill.NewShortUUID(), nil))\n\t\t\t}()\n\n\t\t\terr := pubSub.Close()\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestPublishSubscribe_do_not_block_other_subscribers(t *testing.T) {\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{},\n\t\twatermill.NewStdLogger(true, true),\n\t)\n\ttopicName := \"test_topic_\" + watermill.NewUUID()\n\n\tmsgsFromSubscriber1, err := pubSub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\t_, err = pubSub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\tmsgsFromSubscriber3, err := pubSub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\terr = pubSub.Publish(topicName, message.NewMessage(\"1\", nil))\n\trequire.NoError(t, err)\n\n\treceived := make(chan struct{})\n\tgo func() {\n\t\tmsg := <-msgsFromSubscriber1\n\t\tmsg.Ack()\n\n\t\tmsg = <-msgsFromSubscriber3\n\t\tmsg.Ack()\n\n\t\tclose(received)\n\t}()\n\n\tselect {\n\tcase <-received:\n\t\t// ok\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"subscriber which didn't ack a message blocked other subscribers from receiving it\")\n\t}\n}\n\nfunc TestPublishSubscribe_flush_output_channel(t *testing.T) {\n\tmessagesCount := 300\n\tlogger := watermill.NewStdLogger(true, true)\n\tctx := context.Background()\n\tconfig := gochannel.Config{\n\t\tOutputChannelBuffer:            int64(messagesCount),\n\t\tPersistent:                     false,\n\t\tBlockPublishUntilSubscriberAck: false,\n\t}\n\tpubSub := gochannel.NewGoChannel(\n\t\tconfig,\n\t\tlogger,\n\t)\n\n\ttotalMessage := 0\n\tartificialWorkload := time.Millisecond * 5 //keep it small but noticeable in logs\n\ttopicName := \"test_topic\"\n\n\tmessageChannel, err := pubSub.Subscribe(ctx, topicName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// waitgroup for stopping the subscriber handler from processing/receiving messages until we are done filling the buffer of the pubSub\n\tvar wgStartSubscriber sync.WaitGroup\n\twgStartSubscriber.Add(1)\n\t// waitgroup for expected buffer to be flushed\n\tvar wgFlushBuffer sync.WaitGroup\n\twgFlushBuffer.Add(messagesCount)\n\n\t// start subscriber handler in a go routine\n\t// reads out the messages, if all is ok it should be able to (\"flush\") read all messages from a 'closed' pubsub\n\tgo func(messageChannel <-chan *message.Message) {\n\t\twgStartSubscriber.Wait()\n\t\tfor msg := range messageChannel {\n\t\t\t// artificial workload\n\t\t\ttime.Sleep(artificialWorkload)\n\t\t\tmsg.Ack()\n\t\t\tlogger.Trace(\"message acked\", nil)\n\t\t\t// would normally use atomic value here but concurrency shouldn't be an issue for this test\n\t\t\ttotalMessage++\n\t\t\twgFlushBuffer.Done()\n\t\t}\n\t\tlogger.Trace(\"channel closed\", nil)\n\t}(messageChannel)\n\n\ttests.PublishSimpleMessages(t, messagesCount, pubSub, topicName)\n\t// wait for buffer to fill then start reading in subscriber handler\n\tfor {\n\t\tif len(messageChannel) != int(config.OutputChannelBuffer) {\n\t\t\tcontinue\n\t\t} else {\n\t\t\twgStartSubscriber.Done()\n\t\t\tbreak\n\t\t}\n\t}\n\n\terr = pubSub.Close()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Publishing new message should still error as expected\n\terr = pubSub.Publish(topicName, message.NewMessage(watermill.NewUUID(), []byte(\"x\")))\n\tassert.ErrorContains(t, err, \"Pub/Sub closed\")\n\n\t// And so should subscribe\n\t_, err = pubSub.Subscribe(ctx, topicName)\n\tassert.ErrorContains(t, err, \"Pub/Sub closed\")\n\n\twgFlushBuffer.Wait()\n\t// But subscriber handler should still be able to read the remaining messages from output channel aka flushing\n\tassert.Equal(t, messagesCount, totalMessage)\n}\n\nfunc testPublishSubscribeSubRace(t *testing.T) {\n\tt.Helper()\n\n\tmessagesCount := 500\n\tsubscribersCount := 200\n\tif testing.Short() {\n\t\tmessagesCount = 200\n\t\tsubscribersCount = 20\n\t}\n\n\tpubSub := gochannel.NewGoChannel(\n\t\tgochannel.Config{\n\t\t\tOutputChannelBuffer: int64(messagesCount),\n\t\t\tPersistent:          true,\n\t\t},\n\t\twatermill.NewStdLogger(true, false),\n\t)\n\n\tallSent := sync.WaitGroup{}\n\tallSent.Add(messagesCount)\n\tallReceived := sync.WaitGroup{}\n\n\tsentMessages := message.Messages{}\n\tgo func() {\n\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\tmsg := message.NewMessage(watermill.NewUUID(), nil)\n\t\t\tsentMessages = append(sentMessages, msg)\n\n\t\t\tgo func() {\n\t\t\t\trequire.NoError(t, pubSub.Publish(\"topic\", msg))\n\t\t\t\tallSent.Done()\n\t\t\t}()\n\t\t}\n\t}()\n\n\tsubscriberReceivedCh := make(chan message.Messages, subscribersCount)\n\tfor i := 0; i < subscribersCount; i++ {\n\t\tallReceived.Add(1)\n\n\t\tgo func() {\n\t\t\tmsgs, err := pubSub.Subscribe(context.Background(), \"topic\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\treceived, _ := subscriber.BulkRead(msgs, messagesCount, time.Second*10)\n\t\t\tsubscriberReceivedCh <- received\n\n\t\t\tallReceived.Done()\n\t\t}()\n\t}\n\n\tlog.Println(\"waiting for all sent\")\n\tallSent.Wait()\n\n\tlog.Println(\"waiting for all received\")\n\tallReceived.Wait()\n\n\tclose(subscriberReceivedCh)\n\n\tlog.Println(\"asserting\")\n\n\tfor subMsgs := range subscriberReceivedCh {\n\t\ttests.AssertAllMessagesReceived(t, sentMessages, subMsgs)\n\t}\n}\n"
  },
  {
    "path": "pubsub/sync/waitgroup.go",
    "content": "package sync\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\n// WaitGroupTimeout adds timeout feature for sync.WaitGroup.Wait().\n// It returns true, when timed out.\nfunc WaitGroupTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {\n\twgClosed := make(chan struct{}, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\twgClosed <- struct{}{}\n\t}()\n\n\tselect {\n\tcase <-wgClosed:\n\t\treturn false\n\tcase <-time.After(timeout):\n\t\treturn true\n\t}\n}\n"
  },
  {
    "path": "pubsub/sync/waitgroup_test.go",
    "content": "package sync\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWaitGroupTimeout_no_timeout(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\n\ttimedout := WaitGroupTimeout(wg, time.Millisecond*100)\n\tassert.False(t, timedout)\n}\n\nfunc TestWaitGroupTimeout_timeout(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\ttimedout := WaitGroupTimeout(wg, time.Millisecond*100)\n\tassert.True(t, timedout)\n}\n"
  },
  {
    "path": "pubsub/tests/bench_pubsub.go",
    "content": "package tests\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n)\n\n// BenchmarkPubSubConstructor is a function that creates a Publisher and Subscriber to be used for benchmarks.\ntype BenchmarkPubSubConstructor func(n int) (message.Publisher, message.Subscriber)\n\n// BenchSubscriber runs benchmark on a message Subscriber.\nfunc BenchSubscriber(b *testing.B, pubSubConstructor BenchmarkPubSubConstructor) {\n\tpub, sub := pubSubConstructor(b.N)\n\ttopicName := testTopicName(TestContext{TestID: NewTestID()})\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tgo func() {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tmsg := message.NewMessage(\"1\", nil)\n\t\t\terr := pub.Publish(topicName, msg)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}()\n\n\tb.ResetTimer()\n\n\tconsumedMessages, all := subscriber.BulkRead(messages, b.N, time.Second*60)\n\tif !all {\n\t\tb.Fatalf(\"not all messages received, have %d, expected %d\", len(consumedMessages), b.N)\n\t}\n}\n"
  },
  {
    "path": "pubsub/tests/test_asserts.go",
    "content": "package tests\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc difference(a, b []string) []string {\n\tmb := map[string]bool{}\n\tfor _, x := range b {\n\t\tmb[x] = true\n\t}\n\tab := []string{}\n\tfor _, x := range a {\n\t\tif _, ok := mb[x]; !ok {\n\t\t\tab = append(ab, x)\n\t\t}\n\t}\n\treturn ab\n}\n\n// MissingMessages returns a list of missing messages UUIDs.\nfunc MissingMessages(expected message.Messages, received message.Messages) []string {\n\tsentIDs := expected.IDs()\n\treceivedIDs := received.IDs()\n\n\tsort.Strings(sentIDs)\n\tsort.Strings(receivedIDs)\n\n\treturn difference(sentIDs, receivedIDs)\n}\n\n// AssertAllMessagesReceived checks if all messages were received,\n// ignoring the order and assuming that they are already deduplicated.\nfunc AssertAllMessagesReceived(t *testing.T, sent message.Messages, received message.Messages) bool {\n\tsentIDs := sent.IDs()\n\treceivedIDs := received.IDs()\n\n\tsort.Strings(sentIDs)\n\tsort.Strings(receivedIDs)\n\n\tif len(sentIDs) != len(receivedIDs) {\n\t\tt.Errorf(\"id's count is different: received: %d, sent: %d\", len(receivedIDs), len(sentIDs))\n\t}\n\n\tmissing := MissingMessages(sent, received)\n\textra := MissingMessages(received, sent)\n\n\tif len(missing) > 0 || len(extra) > 0 {\n\t\tt.Errorf(\"received different messages ID's, missing: %s, extra %s\", missing, extra)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// AssertMessagesPayloads check if received messages have the same payload as expected in expectedPayloads.\nfunc AssertMessagesPayloads(\n\tt *testing.T,\n\texpectedPayloads map[string][]byte,\n\treceived []*message.Message,\n) bool {\n\tassert.Len(t, received, len(expectedPayloads))\n\n\treceivedMsgs := map[string]interface{}{}\n\tfor _, msg := range received {\n\t\treceivedMsgs[msg.UUID] = string(msg.Payload)\n\t}\n\n\tok := true\n\tfor msgUUID, sentMsgPayload := range expectedPayloads {\n\t\tif !assert.EqualValues(t, sentMsgPayload, receivedMsgs[msgUUID]) {\n\t\t\tok = false\n\t\t}\n\t}\n\n\treturn ok\n}\n\n// AssertMessagesMetadata checks if metadata of all received messages is the same as in expectedValues.\nfunc AssertMessagesMetadata(t *testing.T, key string, expectedValues map[string]string, received []*message.Message) bool {\n\tassert.Len(t, received, len(expectedValues))\n\n\tok := true\n\tfor _, msg := range received {\n\t\tif !assert.Equal(t, expectedValues[msg.UUID], msg.Metadata[key]) {\n\t\t\tok = false\n\t\t}\n\t}\n\n\treturn ok\n}\n\n// AssertAllMessagesHaveSameContext checks if context of all received messages is the same as in expectedValues, if PreserveContext is enabled.\nfunc AssertAllMessagesHaveSameContext(t *testing.T, contextKeyString string, expectedValues map[string]context.Context, received []*message.Message) {\n\tassert.Len(t, received, len(expectedValues))\n\tfor _, msg := range received {\n\t\texpectedValue := expectedValues[msg.UUID].Value(contextKey(contextKeyString)).(string)\n\t\tactualValue := msg.Context().Value(contextKey(contextKeyString))\n\t\tassert.Equal(t, expectedValue, actualValue)\n\t}\n}\n"
  },
  {
    "path": "pubsub/tests/test_pubsub.go",
    "content": "package tests\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"os\"\n\t\"os/exec\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill/internal\"\n\tinternalSubscriber \"github.com/ThreeDotsLabs/watermill/internal/subscriber\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/subscriber\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar defaultTimeout = time.Second * 15\n\n// TestPubSub is a universal test suite. Every Pub/Sub implementation should pass it\n// before it's considered production ready.\n//\n// Execution of the tests may be a bit different for every Pub/Sub. You can configure it by changing provided Features.\nfunc TestPubSub(\n\tt *testing.T,\n\tfeatures Features,\n\tpubSubConstructor PubSubConstructor,\n\tconsumerGroupPubSubConstructor ConsumerGroupPubSubConstructor,\n) {\n\ttestFuncs := []struct {\n\t\tFunc        func(t *testing.T, tCtx TestContext, pubSubConstructor PubSubConstructor)\n\t\tNotParallel bool\n\t}{\n\t\t{Func: TestPublishSubscribe},\n\t\t{Func: TestConcurrentSubscribe},\n\t\t{Func: TestConcurrentSubscribeMultipleTopics},\n\t\t{Func: TestResendOnError},\n\t\t{Func: TestNoAck},\n\t\t{Func: TestContinueAfterSubscribeClose},\n\t\t{Func: TestConcurrentClose},\n\t\t{Func: TestContinueAfterErrors},\n\t\t{Func: TestPublishSubscribeInOrder},\n\t\t{Func: TestPublisherClose},\n\t\t{Func: TestTopic},\n\t\t{Func: TestMessageCtx},\n\t\t{Func: TestSubscribeCtx},\n\t\t{Func: TestNewSubscriberReceivesOldMessages},\n\t\t{\n\t\t\tFunc:        TestReconnect,\n\t\t\tNotParallel: true,\n\t\t},\n\t}\n\n\tfor i := range testFuncs {\n\t\ttestFunc := testFuncs[i]\n\n\t\trunTest(\n\t\t\tt,\n\t\t\tgetTestName(testFunc.Func),\n\t\t\tfunc(t *testing.T, testCtx TestContext) {\n\t\t\t\ttestFunc.Func(t, testCtx, pubSubConstructor)\n\t\t\t},\n\t\t\tfeatures,\n\t\t\t!testFunc.NotParallel,\n\t\t)\n\t}\n\n\trunTest(\n\t\tt,\n\t\tgetTestName(TestConsumerGroups),\n\t\tfunc(t *testing.T, testCtx TestContext) {\n\t\t\tTestConsumerGroups(\n\t\t\t\tt,\n\t\t\t\ttestCtx,\n\t\t\t\tconsumerGroupPubSubConstructor,\n\t\t\t)\n\t\t},\n\t\tfeatures,\n\t\ttrue,\n\t)\n}\n\n// Features are used to configure Pub/Subs implementations behaviour.\n// Different features set decides also which, and how tests should be run.\ntype Features struct {\n\t// ConsumerGroups should be true, if consumer groups are supported.\n\tConsumerGroups bool\n\n\t// ExactlyOnceDelivery should be true, if exactly-once delivery is supported.\n\tExactlyOnceDelivery bool\n\n\t// GuaranteedOrder should be true, if order of messages is guaranteed.\n\tGuaranteedOrder bool\n\n\t// Some Pub/Subs guarantee the order only when one subscriber is subscribed at a time.\n\tGuaranteedOrderWithSingleSubscriber bool\n\n\t// Persistent should be true, if messages are persistent between multiple instances of a Pub/Sub\n\t// (in practice, only GoChannel doesn't support that).\n\tPersistent bool\n\n\t// RestartServiceCommand is a command to test reconnects. It should restart the message broker.\n\t// Example: []string{\"docker\", \"restart\", \"rabbitmq\"}\n\tRestartServiceCommand []string\n\n\t// RequireSingleInstance must be true,if a PubSub requires a single instance to work properly\n\t// (for example: GoChannel implementation).\n\tRequireSingleInstance bool\n\n\t// NewSubscriberReceivesOldMessages should be set to true if messages are persisted even\n\t// if they are already consumed (for example, like in Kafka).\n\tNewSubscriberReceivesOldMessages bool\n\n\t// GenerateTopicFunc overrides standard topic name generation.\n\tGenerateTopicFunc func(tctx TestContext) string\n\n\t// GenerateIDFunc determines which function should be used for generating test IDs, NewTestID is used by default.\n\tGenerateIDFunc func() TestID\n\n\t// ForceShort forces running tests in short mode.\n\t// It's useful for Pub/Subs that are slow or have some limitations.\n\tForceShort bool\n\n\t// ContextPreserved should be set to true if the Pub/Sub implementation preserves the context\n\t// of the message when it's published and consumed.\n\tContextPreserved bool\n}\n\n// RunOnlyFastTests returns true if -short flag was provided -race was not provided.\n// Useful for excluding some slow tests.\nfunc RunOnlyFastTests() bool {\n\treturn testing.Short() && !internal.RaceEnabled\n}\n\n// PubSubConstructor is a function that creates a Publisher and a Subscriber.\ntype PubSubConstructor func(t *testing.T) (message.Publisher, message.Subscriber)\n\n// ConsumerGroupPubSubConstructor is a function that creates a Publisher and a Subscriber that use given consumer group.\ntype ConsumerGroupPubSubConstructor func(t *testing.T, consumerGroup string) (message.Publisher, message.Subscriber)\n\n// SimpleMessage is deprecated: not used anywhere internally\ntype SimpleMessage struct {\n\tNum int `json:\"num\"`\n}\n\nfunc getTestName(testFunc interface{}) string {\n\tfullName := runtime.FuncForPC(reflect.ValueOf(testFunc).Pointer()).Name()\n\tnameSliced := strings.Split(fullName, \".\")\n\n\treturn nameSliced[len(nameSliced)-1]\n}\n\n// TestID is a unique ID of a test.\ntype TestID string\n\nfunc (t TestID) String() string {\n\treturn string(t)\n}\n\n// NewTestID returns a new unique TestID.\nfunc NewTestID() TestID {\n\treturn TestID(watermill.NewUUID())\n}\n\n// NewTestULID returns a new unique TestID using ULID.\nfunc NewTestULID() TestID {\n\treturn TestID(watermill.NewULID())\n}\n\n// TestContext is a collection of values that belong to a single test.\ntype TestContext struct {\n\t// Unique ID of the test\n\tTestID TestID\n\n\t// PubSub features\n\tFeatures Features\n}\n\nfunc runTest(\n\tt *testing.T,\n\tname string,\n\tfn func(t *testing.T, testCtx TestContext),\n\tfeatures Features,\n\tparallel bool,\n) {\n\tt.Run(name, func(t *testing.T) {\n\t\tif parallel {\n\t\t\tt.Parallel()\n\t\t}\n\t\ttestID := NewTestID()\n\n\t\tt.Run(string(testID), func(t *testing.T) {\n\t\t\ttCtx := TestContext{\n\t\t\t\tTestID:   testID,\n\t\t\t\tFeatures: features,\n\t\t\t}\n\n\t\t\tfn(t, tCtx)\n\t\t})\n\t})\n}\n\nconst defaultStressTestTestsCount = 10\n\n// TestPubSubStressTest runs stress tests on a chosen Pub/Sub.\nfunc TestPubSubStressTest(\n\tt *testing.T,\n\tfeatures Features,\n\tpubSubConstructor PubSubConstructor,\n\tconsumerGroupPubSubConstructor ConsumerGroupPubSubConstructor,\n) {\n\tstressTestsCount, _ := strconv.ParseInt(os.Getenv(\"STRESS_TEST_COUNT\"), 10, 64)\n\tif stressTestsCount == 0 {\n\t\tstressTestsCount = defaultStressTestTestsCount\n\t}\n\n\tfor i := 0; i < int(stressTestsCount); i++ {\n\t\tt.Run(fmt.Sprintf(\"%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tTestPubSub(t, features, pubSubConstructor, consumerGroupPubSubConstructor)\n\t\t})\n\t}\n}\n\n// TestPublishSubscribe runs basic publish and subscribe tests on a chosen Pub/Sub.\nfunc TestPublishSubscribe(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\n\ttopicName := testTopicName(tCtx)\n\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tvar messagesToPublish []*message.Message\n\tmessagesPayloads := map[string][]byte{}\n\tmessagesTestMetadata := map[string]string{}\n\n\tfor i := 0; i < 100; i++ {\n\t\tid := watermill.NewUUID()\n\t\ttestMetadata := watermill.NewUUID()\n\n\t\tpayload := fmt.Appendf(nil, \"%d\", i)\n\t\tmsg := message.NewMessage(id, payload)\n\n\t\tmsg.Metadata.Set(\"test\", testMetadata)\n\t\tmessagesTestMetadata[id] = testMetadata\n\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\t\tmessagesPayloads[id] = payload\n\t}\n\terr := publishWithRetry(pub, topicName, messagesToPublish...)\n\trequire.NoError(t, err, \"cannot publish message\")\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessages, all := bulkRead(tCtx, messages, len(messagesToPublish), defaultTimeout*3)\n\tassert.True(t, all)\n\n\tAssertAllMessagesReceived(t, messagesToPublish, receivedMessages)\n\tAssertMessagesPayloads(t, messagesPayloads, receivedMessages)\n\tAssertMessagesMetadata(t, \"test\", messagesTestMetadata, receivedMessages)\n\n\tclosePubSub(t, pub, sub)\n\tassertMessagesChannelClosed(t, messages)\n}\n\n// TestConcurrentSubscribe tests subscribing to messages by multiple concurrent subscribers.\nfunc TestConcurrentSubscribe(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, initSub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, initSub)\n\n\ttopicName := testTopicName(tCtx)\n\n\tmessagesCount := 5000\n\tsubscribersCount := 50\n\n\tif testing.Short() || tCtx.Features.ForceShort {\n\t\tmessagesCount = 100\n\t\tsubscribersCount = 10\n\t}\n\n\tif subscribeInitializer, ok := initSub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tpublishedMessages := AddSimpleMessagesParallel(t, messagesCount, pub, topicName, 50)\n\n\tvar sub message.Subscriber\n\tif tCtx.Features.RequireSingleInstance {\n\t\tsub = initSub\n\t} else {\n\t\tsub = createMultipliedSubscriber(t, pubSubConstructor, subscribersCount)\n\t}\n\n\tdefer closePubSub(t, pub, sub)\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessages, all := bulkRead(tCtx, messages, len(publishedMessages), defaultTimeout*3)\n\tassert.True(t, all)\n\n\tAssertAllMessagesReceived(t, publishedMessages, receivedMessages)\n}\n\n// TestConcurrentSubscribeMultipleTopics tests subscribing to messages by concurrent subscribers on multiple topics.\nfunc TestConcurrentSubscribeMultipleTopics(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\tmessagesCount := 100\n\ttopicsCount := 20\n\n\tif testing.Short() || tCtx.Features.ForceShort {\n\t\tmessagesCount = 50\n\t\ttopicsCount = 10\n\t}\n\n\tvar messagesToPublish []*message.Message\n\tfor i := 0; i < messagesCount; i++ {\n\t\tid := watermill.NewUUID()\n\n\t\tmsg := message.NewMessage(id, []byte(\"x\"))\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\t}\n\n\tsubsWg := sync.WaitGroup{}\n\tsubsWg.Add(topicsCount)\n\n\treceivedMessagesCh := make(chan message.Messages, topicsCount)\n\n\tfor i := 0; i < topicsCount; i++ {\n\t\ttopicName := testTopicName(tCtx) + fmt.Sprintf(\"-%d\", i)\n\n\t\tvar messagesToPublishForTopic []*message.Message\n\t\tfor _, msg := range messagesToPublish {\n\t\t\tnewMsg := msg.Copy()\n\t\t\tmessagesToPublishForTopic = append(messagesToPublishForTopic, newMsg)\n\t\t}\n\n\t\tgo func() {\n\t\t\tdefer subsWg.Done()\n\n\t\t\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\t\t\terr := subscribeInitializer.SubscribeInitialize(topicName)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr := publishWithRetry(pub, topicName, messagesToPublishForTopic...)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\ttopicMessages, _ := bulkRead(tCtx, messages, len(messagesToPublishForTopic), defaultTimeout*5)\n\n\t\t\treceivedMessagesCh <- topicMessages\n\t\t}()\n\t}\n\n\tsubsWg.Wait()\n\tclose(receivedMessagesCh)\n\n\ttopicsReceivedMessages := 0\n\n\tfor msgs := range receivedMessagesCh {\n\t\tAssertAllMessagesReceived(t, messagesToPublish, msgs)\n\t\ttopicsReceivedMessages++\n\t}\n\n\tassert.Equal(t, topicsCount, topicsReceivedMessages)\n}\n\n// TestPublishSubscribeInOrder tests if published messages are received in a proper order.\n// This test is skipped for Pub/Subs that don't support GuaranteedOrder feature.\nfunc TestPublishSubscribeInOrder(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tif !tCtx.Features.GuaranteedOrder {\n\t\tt.Skipf(\"order is not guaranteed\")\n\t}\n\n\tmessagesCount := 1000\n\tif testing.Short() || tCtx.Features.ForceShort {\n\t\tmessagesCount = 100\n\t}\n\n\tpub, initSub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, initSub)\n\n\ttopicName := testTopicName(tCtx)\n\n\tif subscribeInitializer, ok := initSub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tvar messagesToPublish []*message.Message\n\texpectedMessages := map[string][]string{}\n\n\tfor i := 0; i < messagesCount; i++ {\n\t\tid := watermill.NewUUID()\n\t\tmsgType := fmt.Sprintf(\"%d\", i%16)\n\n\t\tmsg := message.NewMessage(id, []byte(msgType))\n\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\n\t\tif _, ok := expectedMessages[msgType]; !ok {\n\t\t\texpectedMessages[msgType] = []string{}\n\t\t}\n\t\texpectedMessages[msgType] = append(expectedMessages[msgType], msg.UUID)\n\t}\n\n\terr := publishWithRetry(pub, topicName, messagesToPublish...)\n\trequire.NoError(t, err)\n\n\tvar sub message.Subscriber\n\tif tCtx.Features.RequireSingleInstance {\n\t\tsub = initSub\n\t} else {\n\t\tsubscribersCount := 10\n\t\tif tCtx.Features.GuaranteedOrderWithSingleSubscriber {\n\t\t\tsubscribersCount = 1\n\t\t}\n\n\t\tsub = createMultipliedSubscriber(t, pubSubConstructor, subscribersCount)\n\t\tdefer require.NoError(t, sub.Close())\n\t}\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessages, all := bulkRead(tCtx, messages, len(messagesToPublish), defaultTimeout)\n\trequire.True(t, all, \"not all messages received (%d of %d)\", len(receivedMessages), len(messagesToPublish))\n\n\treceivedMessagesByType := map[string][]string{}\n\tfor _, msg := range receivedMessages {\n\t\tif _, ok := receivedMessagesByType[string(msg.Payload)]; !ok {\n\t\t\treceivedMessagesByType[string(msg.Payload)] = []string{}\n\t\t}\n\t\treceivedMessagesByType[string(msg.Payload)] = append(receivedMessagesByType[string(msg.Payload)], msg.UUID)\n\t}\n\n\trequire.Equal(t, len(receivedMessagesByType), len(expectedMessages))\n\trequire.Equal(t, len(receivedMessages), len(messagesToPublish))\n\n\tfor key, ids := range expectedMessages {\n\t\tassert.Equal(t, ids, receivedMessagesByType[key])\n\t}\n}\n\n// TestResendOnError tests if messages are re-delivered after the subscriber fails to process them.\nfunc TestResendOnError(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopicName := testTopicName(tCtx)\n\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tmessagesToSend := 100\n\tnacksCount := 2\n\n\tvar publishedMessages message.Messages\n\tallMessagesSent := make(chan struct{})\n\n\tpublishedMessages = PublishSimpleMessages(t, messagesToSend, pub, topicName)\n\tclose(allMessagesSent)\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\nNackLoop:\n\tfor i := 0; i < nacksCount; i++ {\n\t\tselect {\n\t\tcase msg, closed := <-messages:\n\t\t\tif !closed {\n\t\t\t\tt.Fatal(\"messages channel closed before all received\")\n\t\t\t}\n\n\t\t\tlog.Println(\"sending err for \", msg.UUID)\n\t\t\tmsg.Nack()\n\t\tcase <-time.After(defaultTimeout):\n\t\t\tbreak NackLoop\n\t\t}\n\t}\n\n\treceivedMessages, _ := bulkRead(tCtx, messages, messagesToSend, defaultTimeout)\n\n\t<-allMessagesSent\n\tAssertAllMessagesReceived(t, publishedMessages, receivedMessages)\n}\n\n// TestNoAck tests if no new messages are received by the subscriber until the previous message is acknowledged.\n// This test is skipped for Pub/Subs that don't support GuaranteedOrder feature.\nfunc TestNoAck(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tif !tCtx.Features.GuaranteedOrder {\n\t\tt.Skip(\"guaranteed order is required for this test\")\n\t}\n\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopicName := testTopicName(tCtx)\n\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tfor i := 0; i < 2; i++ {\n\t\tid := watermill.NewUUID()\n\t\tlog.Printf(\"sending %s\", id)\n\n\t\tmsg := message.NewMessage(id, []byte(\"x\"))\n\n\t\terr := publishWithRetry(pub, topicName, msg)\n\t\trequire.NoError(t, err)\n\t}\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessage := make(chan struct{})\n\tunlockAck := make(chan struct{}, 1)\n\tgo func() {\n\t\tmsg := <-messages\n\t\treceivedMessage <- struct{}{}\n\t\t<-unlockAck\n\t\tmsg.Ack()\n\t}()\n\n\tselect {\n\tcase <-receivedMessage:\n\t// ok\n\tcase <-time.After(defaultTimeout):\n\t\tt.Fatal(\"timed out\")\n\t}\n\n\tselect {\n\tcase msg := <-messages:\n\t\tt.Fatalf(\"messages channel should be blocked since Ack() was not sent, received %s\", msg.UUID)\n\tcase <-time.After(time.Millisecond * 100):\n\t\t// ok\n\t}\n\n\tunlockAck <- struct{}{}\n\n\tselect {\n\tcase msg := <-messages:\n\t\tmsg.Ack()\n\tcase <-time.After(time.Second * 5):\n\t\tt.Fatal(\"messages channel should be unblocked after Ack()\")\n\t}\n\n\tif tCtx.Features.ExactlyOnceDelivery {\n\t\tselect {\n\t\tcase <-messages:\n\t\t\tt.Fatal(\"msg should be not sent again\")\n\t\tcase <-time.After(time.Millisecond * 50):\n\t\t\t// ok\n\t\t}\n\t}\n}\n\n// TestContinueAfterSubscribeClose checks, that we don't lose messages after closing subscriber.\nfunc TestContinueAfterSubscribeClose(\n\tt *testing.T,\n\ttCtx TestContext,\n\tcreatePubSub PubSubConstructor,\n) {\n\tif !tCtx.Features.Persistent {\n\t\tt.Skip(\"Non-Persistent is not supported yet\")\n\t}\n\n\tif tCtx.Features.ExactlyOnceDelivery {\n\t\tt.Skip(\"ExactlyOnceDelivery test is not supported yet\")\n\t}\n\n\ttotalMessagesCount := 5000\n\tbatches := 5\n\tif testing.Short() || tCtx.Features.ForceShort {\n\t\ttotalMessagesCount = 50\n\t\tbatches = 2\n\t}\n\tbatchSize := int(totalMessagesCount / batches)\n\treadAttempts := batches * 4\n\n\tpub, sub := createPubSub(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tpublishedMessages := AddSimpleMessagesParallel(t, totalMessagesCount, pub, topicName, 50)\n\n\treceivedMessages := map[string]*message.Message{}\n\tfor i := 0; i < readAttempts; i++ {\n\n\t\tpub, sub := createPubSub(t)\n\n\t\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\t\trequire.NoError(t, err)\n\n\t\tmessagesToRead := batchSize\n\t\tmessagesLeft := totalMessagesCount - len(receivedMessages)\n\n\t\tif messagesToRead > messagesLeft {\n\t\t\tmessagesToRead = messagesLeft\n\t\t}\n\n\t\treceivedMessagesBatch, _ := bulkRead(tCtx, messages, messagesToRead, defaultTimeout)\n\t\tclosePubSub(t, pub, sub)\n\n\t\tfor _, msg := range receivedMessagesBatch {\n\t\t\treceivedMessages[msg.UUID] = msg\n\t\t}\n\n\t\tclosePubSub(t, pub, sub)\n\n\t\tif len(receivedMessages) >= totalMessagesCount {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// to make this test more robust - let's consume all missing messages\n\t// (we care here if we didn't lose any message, not if we received duplicated)\n\tmissingMessagesCount := totalMessagesCount - len(receivedMessages)\n\tif missingMessagesCount > 0 && !tCtx.Features.ExactlyOnceDelivery {\n\t\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\t\trequire.NoError(t, err)\n\t\tdefer closePubSub(t, pub, sub)\n\n\t\ttimeout := time.After(defaultTimeout)\n\n\tMessagesLoop:\n\t\tfor len(receivedMessages) < totalMessagesCount {\n\t\t\tselect {\n\t\t\tcase msg, ok := <-messages:\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak MessagesLoop\n\t\t\t\t}\n\n\t\t\t\treceivedMessages[msg.UUID] = msg\n\t\t\t\tmsg.Ack()\n\t\t\tcase <-timeout:\n\t\t\t\tbreak MessagesLoop\n\t\t\t}\n\t\t}\n\t}\n\n\t// we need to deduplicate messages, because bulkRead will deduplicate only per one batch\n\tuniqueReceivedMessages := message.Messages{}\n\tfor _, msg := range receivedMessages {\n\t\tuniqueReceivedMessages = append(uniqueReceivedMessages, msg)\n\t}\n\n\tAssertAllMessagesReceived(t, publishedMessages, uniqueReceivedMessages)\n}\n\n// TestConcurrentClose tests if the Pub/Sub works correctly when subscribers are being closed concurrently.\nfunc TestConcurrentClose(\n\tt *testing.T,\n\ttCtx TestContext,\n\tcreatePubSub PubSubConstructor,\n) {\n\tif tCtx.Features.ExactlyOnceDelivery {\n\t\tt.Skip(\"ExactlyOnceDelivery test is not supported yet\")\n\t}\n\n\ttopicName := testTopicName(tCtx)\n\tcreatePub, createSub := createPubSub(t)\n\tif subscribeInitializer, ok := createSub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\tclosePubSub(t, createPub, createSub)\n\n\ttotalMessagesCount := 50\n\n\tcloseWg := sync.WaitGroup{}\n\tcloseWg.Add(10)\n\n\tfor i := 0; i < 10; i++ {\n\t\tgo func() {\n\t\t\tdefer closeWg.Done()\n\n\t\t\tpub, sub := createPubSub(t)\n\t\t\tdefer closePubSub(t, pub, sub)\n\n\t\t\t_, err := sub.Subscribe(context.Background(), topicName)\n\t\t\trequire.NoError(t, err)\n\t\t}()\n\t}\n\n\tcloseWg.Wait()\n\n\tpub, sub := createPubSub(t)\n\texpectedMessages := PublishSimpleMessages(t, totalMessagesCount, pub, topicName)\n\tclosePubSub(t, pub, sub)\n\n\tpub, sub = createPubSub(t)\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessages, all := bulkRead(tCtx, messages, len(expectedMessages), defaultTimeout*3)\n\tassert.True(t, all)\n\n\tAssertAllMessagesReceived(t, expectedMessages, receivedMessages)\n\tclosePubSub(t, pub, sub)\n}\n\n// TestContinueAfterErrors tests if messages are processed again after an initial failure.\nfunc TestContinueAfterErrors(\n\tt *testing.T,\n\ttCtx TestContext,\n\tcreatePubSub PubSubConstructor,\n) {\n\tpub, sub := createPubSub(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\ttotalMessagesCount := 50\n\tsubscribersToNack := 3\n\tnacksPerSubscriber := 100\n\n\tif testing.Short() || tCtx.Features.ForceShort {\n\t\tsubscribersToNack = 1\n\t\tnacksPerSubscriber = 5\n\t}\n\n\tmessagesToPublish := PublishSimpleMessages(t, totalMessagesCount, pub, topicName)\n\n\tfor i := 0; i < subscribersToNack; i++ {\n\t\tvar errorsPub message.Publisher\n\t\tvar errorsSub message.Subscriber\n\n\t\tif !tCtx.Features.Persistent {\n\t\t\terrorsPub = pub\n\t\t\terrorsSub = sub\n\t\t} else {\n\t\t\terrorsPub, errorsSub = createPubSub(t)\n\t\t}\n\n\t\tmessages, err := errorsSub.Subscribe(context.Background(), topicName)\n\t\trequire.NoError(t, err)\n\n\t\tfor j := 0; j < nacksPerSubscriber; j++ {\n\t\t\tselect {\n\t\t\tcase msg := <-messages:\n\t\t\t\tmsg.Nack()\n\t\t\tcase <-time.After(defaultTimeout):\n\t\t\t\tt.Fatal(\"no messages left, probably seek after error doesn't work\")\n\t\t\t}\n\t\t}\n\n\t\tif tCtx.Features.Persistent {\n\t\t\tclosePubSub(t, errorsPub, errorsSub)\n\t\t}\n\t}\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\t// only nacks was sent, so all messages should be consumed\n\treceivedMessages, _ := bulkRead(tCtx, messages, totalMessagesCount, defaultTimeout*6)\n\tAssertAllMessagesReceived(t, messagesToPublish, receivedMessages)\n}\n\n// TestConsumerGroups tests if the consumer groups feature behaves correctly.\n// This test is skipped for Pub/Sub that don't support ConsumerGroups feature.\nfunc TestConsumerGroups(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor ConsumerGroupPubSubConstructor,\n) {\n\tif !tCtx.Features.ConsumerGroups {\n\t\tt.Skip(\"consumer groups are not supported\")\n\t}\n\n\tpublisherPub, publisherSub := pubSubConstructor(t, \"test_\"+watermill.NewUUID())\n\tdefer closePubSub(t, publisherPub, publisherSub)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := publisherSub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\ttotalMessagesCount := 50\n\n\tgroup1 := generateConsumerGroup(t, pubSubConstructor, topicName, tCtx)\n\tgroup2 := generateConsumerGroup(t, pubSubConstructor, topicName, tCtx)\n\n\tmessagesToPublish := PublishSimpleMessages(t, totalMessagesCount, publisherPub, topicName)\n\n\tassertConsumerGroupReceivedMessages(t, tCtx, pubSubConstructor, group1, topicName, messagesToPublish)\n\tassertConsumerGroupReceivedMessages(t, tCtx, pubSubConstructor, group2, topicName, messagesToPublish)\n}\n\n// TestPublisherClose sends big amount of messages and them run close to ensure that messages are not lost during adding.\nfunc TestPublisherClose(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tmessagesCount := 10000\n\tif testing.Short() || tCtx.Features.ForceShort {\n\t\tmessagesCount = 100\n\t}\n\n\tproducedMessages := AddSimpleMessagesParallel(t, messagesCount, pub, topicName, 20)\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\treceivedMessages, _ := bulkRead(tCtx, messages, messagesCount, defaultTimeout*3)\n\n\tAssertAllMessagesReceived(t, producedMessages, receivedMessages)\n}\n\n// TestTopic tests if different topics work correctly in a Pub/Sub.\nfunc TestTopic(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopic1 := testTopicName(tCtx) + \"-1\"\n\ttopic2 := testTopicName(tCtx) + \"-2\"\n\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topic1))\n\t}\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topic2))\n\t}\n\n\ttopic1Msg := message.NewMessage(watermill.NewUUID(), []byte(\"x\"))\n\ttopic2Msg := message.NewMessage(watermill.NewUUID(), []byte(\"x\"))\n\n\trequire.NoError(t, publishWithRetry(pub, topic1, topic1Msg))\n\trequire.NoError(t, publishWithRetry(pub, topic2, topic2Msg))\n\n\tmessagesTopic1, err := sub.Subscribe(context.Background(), topic1)\n\trequire.NoError(t, err)\n\n\tmessagesTopic2, err := sub.Subscribe(context.Background(), topic2)\n\trequire.NoError(t, err)\n\n\tmessagesConsumedTopic1, received := bulkRead(tCtx, messagesTopic1, 1, defaultTimeout)\n\trequire.True(t, received, \"no messages received in topic %s\", topic1)\n\n\tmessagesConsumedTopic2, received := bulkRead(tCtx, messagesTopic2, 1, defaultTimeout)\n\trequire.True(t, received, \"no messages received in topic %s\", topic2)\n\n\tassert.Equal(t, messagesConsumedTopic1.IDs()[0], topic1Msg.UUID)\n\tassert.Equal(t, messagesConsumedTopic2.IDs()[0], topic2Msg.UUID)\n}\n\n// TestMessageCtx tests if the Message's Context works correctly.\nfunc TestMessageCtx(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\tmsg1 := message.NewMessage(watermill.NewUUID(), []byte(\"x\"))\n\tmsg2 := message.NewMessage(watermill.NewUUID(), []byte(\"x\"))\n\n\t// this might actually be an error in some pubsubs (http), because we close the subscriber without ACK.\n\t_ = pub.Publish(topicName, msg1)\n\t_ = pub.Publish(topicName, msg2)\n\n\tselect {\n\tcase msg := <-messages:\n\t\trequire.True(t, msg.Ack())\n\n\t\tif !tCtx.Features.ContextPreserved {\n\t\t\tselect {\n\t\t\tcase <-msg.Context().Done():\n\t\t\t\t// ok\n\t\t\tcase <-time.After(defaultTimeout):\n\t\t\t\tt.Fatal(\"context should be canceled after Ack\")\n\t\t\t}\n\t\t}\n\tcase <-time.After(defaultTimeout):\n\t\tt.Fatal(\"no message received\")\n\t}\n\n\tselect {\n\tcase msg := <-messages:\n\t\tgo closePubSub(t, pub, sub)\n\n\t\tif !tCtx.Features.ContextPreserved {\n\t\t\tselect {\n\t\t\tcase <-msg.Context().Done():\n\t\t\t\t// ok\n\t\t\tcase <-time.After(defaultTimeout):\n\t\t\t\tt.Fatal(\"context should be canceled after pubSub.Close()\")\n\t\t\t}\n\t\t}\n\tcase <-time.After(defaultTimeout):\n\t\tt.Fatal(\"no message received\")\n\t}\n}\n\ntype contextKey string\n\n// TestSubscribeCtx tests if the Subscriber's Context works correctly.\nfunc TestSubscribeCtx(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tpub, sub := pubSubConstructor(t)\n\tdefer closePubSub(t, pub, sub)\n\n\tconst messagesCount = 20\n\n\tctxWithCancel, cancel := context.WithCancel(context.Background())\n\tctxWithCancel = context.WithValue(ctxWithCancel, contextKey(\"foo\"), \"bar\")\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tvar publishedMessages message.Messages\n\tvar contextKeyString = \"abc\"\n\tif tCtx.Features.ContextPreserved {\n\t\tpublishedMessages = PublishSimpleMessagesWithContext(t, messagesCount, contextKeyString, pub, topicName)\n\t} else {\n\t\tpublishedMessages = PublishSimpleMessages(t, messagesCount, pub, topicName)\n\t}\n\n\tmsgsToCancel, err := sub.Subscribe(ctxWithCancel, topicName)\n\trequire.NoError(t, err)\n\tcancel()\n\n\ttimeout := time.After(defaultTimeout)\n\nClosedLoop:\n\tfor {\n\t\tselect {\n\t\tcase msg, open := <-msgsToCancel:\n\t\t\tif !open {\n\t\t\t\tbreak ClosedLoop\n\t\t\t}\n\t\t\tmsg.Nack()\n\t\tcase <-timeout:\n\t\t\tt.Fatal(\"messages channel is not closed after \", defaultTimeout)\n\t\t}\n\t\ttime.Sleep(time.Millisecond * 100)\n\t}\n\n\tctx := context.WithValue(context.Background(), contextKey(\"foo\"), \"bar\")\n\n\t// For mocking the output of pub-subs where context is preserved vs not preserved\n\texpectedContexts := make(map[string]context.Context)\n\tfor _, msg := range publishedMessages {\n\t\tif tCtx.Features.ContextPreserved {\n\t\t\texpectedContexts[msg.UUID] = msg.Context()\n\t\t} else {\n\t\t\texpectedContexts[msg.UUID] = ctx\n\t\t}\n\t}\n\n\tmsgs, err := sub.Subscribe(ctx, topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessages, _ := bulkRead(tCtx, msgs, messagesCount, defaultTimeout)\n\tAssertAllMessagesReceived(t, publishedMessages, receivedMessages)\n\tif tCtx.Features.ContextPreserved {\n\t\tAssertAllMessagesHaveSameContext(t, contextKeyString, expectedContexts, receivedMessages)\n\t}\n}\n\n// TestReconnect tests if reconnecting to a Pub/Sub works correctly.\nfunc TestReconnect(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tif len(tCtx.Features.RestartServiceCommand) == 0 {\n\t\tt.Skip(\"no RestartServiceCommand provided, cannot test reconnect\")\n\t}\n\n\tpub, sub := pubSubConstructor(t)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\n\tconst messagesCount = 10000\n\tconst publishersCount = 100\n\n\trestartAfterMessages := map[int]struct{}{\n\t\tmessagesCount / 3: {}, // restart at 1/3 of messages\n\t\tmessagesCount / 2: {}, // restart at 1/2 of messages\n\t}\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\tvar publishedMessages message.Messages\n\tmessagePublished := make(chan *message.Message, messagesCount)\n\tpublishMessage := make(chan struct{})\n\n\tgo func() {\n\t\tfor i := 0; i < messagesCount; i++ {\n\t\t\tpublishMessage <- struct{}{}\n\n\t\t\tif _, shouldRestart := restartAfterMessages[i]; shouldRestart {\n\t\t\t\tgo restartServer(t, tCtx.Features)\n\t\t\t}\n\t\t}\n\t\tclose(publishMessage)\n\t}()\n\n\tgo func() {\n\t\tfor msg := range messagePublished {\n\t\t\tpublishedMessages = append(publishedMessages, msg)\n\t\t}\n\t}()\n\n\tfor i := 0; i < publishersCount; i++ {\n\t\tgo func() {\n\t\t\tfor range publishMessage {\n\t\t\t\tid := watermill.NewUUID()\n\t\t\t\tmsg := message.NewMessage(id, []byte(\"x\"))\n\n\t\t\t\tfor {\n\t\t\t\t\tfmt.Println(\"publishing message\")\n\n\t\t\t\t\t// some randomization in sending\n\t\t\t\t\tif rand.Int31n(10) == 0 {\n\t\t\t\t\t\ttime.Sleep(time.Millisecond * 500)\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := publishWithRetry(pub, topicName, msg); err == nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tfmt.Printf(\"cannot publish message %s, trying again, err: %s\\n\", msg.UUID, err)\n\t\t\t\t\ttime.Sleep(time.Millisecond * 500)\n\t\t\t\t}\n\n\t\t\t\tmessagePublished <- msg\n\t\t\t}\n\t\t}()\n\t}\n\n\treceivedMessages, allMessages := bulkRead(tCtx, messages, messagesCount, defaultTimeout*4)\n\tassert.True(t, allMessages, \"not all messages received (has %d of %d)\", len(receivedMessages), messagesCount)\n\n\tAssertAllMessagesReceived(t, publishedMessages, receivedMessages)\n\n\tclosePubSub(t, pub, sub)\n}\n\n// TestNewSubscriberReceivesOldMessages tests if a new subscriber receives previously published messages.\nfunc TestNewSubscriberReceivesOldMessages(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor PubSubConstructor,\n) {\n\tif !tCtx.Features.NewSubscriberReceivesOldMessages {\n\t\tt.Skip(\"only subscribers with TestNewSubscriberReceivesOldMessages are supported\")\n\t}\n\n\tpublishedMessages := message.Messages{}\n\n\tpub, sub := pubSubConstructor(t)\n\n\ttopicName := testTopicName(tCtx)\n\tif subscribeInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subscribeInitializer.SubscribeInitialize(topicName))\n\t}\n\trequire.NoError(t, sub.Close())\n\n\tvar publishMessage = func() {\n\t\tpublishedMessages = append(publishedMessages, PublishSimpleMessages(t, 1, pub, topicName)...)\n\t}\n\tpublishMessage()\n\n\ttype Subscriber struct {\n\t\tMsgs             <-chan *message.Message\n\t\tSubscriber       message.Subscriber\n\t\tConsumedMessages int\n\t}\n\n\tvar subscribers []*Subscriber\n\tdefer func() {\n\t\tfor _, sub := range subscribers {\n\t\t\trequire.NoError(t, sub.Subscriber.Close())\n\t\t}\n\t}()\n\n\tvar addSubscriber = func() {\n\t\tpub, sub := pubSubConstructor(t)\n\t\trequire.NoError(t, pub.Close())\n\n\t\tmsgs, err := sub.Subscribe(context.Background(), topicName)\n\t\trequire.NoError(t, err)\n\n\t\tsubscribers = append(subscribers, &Subscriber{\n\t\t\tMsgs:             msgs,\n\t\t\tSubscriber:       sub,\n\t\t\tConsumedMessages: 0,\n\t\t})\n\t}\n\n\tvar consumeMessages = func() {\n\t\tfor i, sub := range subscribers {\n\t\t\ttoConsume := len(publishedMessages) - sub.ConsumedMessages\n\t\t\treceivedMessages, all := bulkRead(tCtx, sub.Msgs, toConsume, defaultTimeout)\n\n\t\t\trequire.True(t, all, \"subscriber %d not received all messages (%d/%d)\", i, len(receivedMessages), toConsume)\n\n\t\t\tfmt.Printf(\"subscriber no %d consumed %d messages\\n\", i, toConsume)\n\t\t\tsub.ConsumedMessages += toConsume\n\t\t}\n\t}\n\n\tpublishMessage()\n\taddSubscriber()\n\tconsumeMessages()\n\n\tpublishMessage()\n\taddSubscriber()\n\tconsumeMessages()\n\n\tpublishMessage()\n\taddSubscriber()\n\tconsumeMessages()\n}\n\nfunc restartServer(t *testing.T, features Features) {\n\tfmt.Println(\"restarting server with:\", features.RestartServiceCommand)\n\tcmd := exec.Command(features.RestartServiceCommand[0], features.RestartServiceCommand[1:]...)\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdout = os.Stdout\n\n\tif err := cmd.Run(); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tfmt.Println(\"server restarted\")\n}\n\nfunc assertConsumerGroupReceivedMessages(\n\tt *testing.T,\n\ttCtx TestContext,\n\tpubSubConstructor ConsumerGroupPubSubConstructor,\n\tconsumerGroup string,\n\ttopicName string,\n\texpectedMessages []*message.Message,\n) {\n\tpub, sub := pubSubConstructor(t, consumerGroup)\n\tdefer closePubSub(t, pub, sub)\n\n\tmessages, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\n\treceivedMessages, all := bulkRead(tCtx, messages, len(expectedMessages), defaultTimeout)\n\tassert.True(t, all)\n\n\tAssertAllMessagesReceived(t, expectedMessages, receivedMessages)\n}\n\nfunc testTopicName(tCtx TestContext) string {\n\tif tCtx.Features.GenerateTopicFunc != nil {\n\t\treturn tCtx.Features.GenerateTopicFunc(tCtx)\n\t}\n\n\treturn \"topic-\" + string(tCtx.TestID)\n}\n\nfunc newTestID(tCtx TestContext) TestID {\n\tif tCtx.Features.GenerateIDFunc != nil {\n\t\treturn tCtx.Features.GenerateIDFunc()\n\t}\n\n\treturn NewTestID()\n}\n\nfunc closePubSub(t *testing.T, pub message.Publisher, sub message.Subscriber) {\n\terr := pub.Close()\n\trequire.NoError(t, err)\n\n\terr = sub.Close()\n\trequire.NoError(t, err)\n}\n\nfunc generateConsumerGroup(t *testing.T, pubSubConstructor ConsumerGroupPubSubConstructor, topicName string, tCtx TestContext) string {\n\tgroupName := \"cg_\" + newTestID(tCtx).String()\n\n\t// create a pubsub to ensure that the consumer group exists\n\t// for those providers that require subscription before publishing messages (e.g. Google Cloud PubSub)\n\tpub, sub := pubSubConstructor(t, groupName)\n\tif subInitializer, ok := sub.(message.SubscribeInitializer); ok {\n\t\trequire.NoError(t, subInitializer.SubscribeInitialize(topicName))\n\t}\n\t_, err := sub.Subscribe(context.Background(), topicName)\n\trequire.NoError(t, err)\n\tclosePubSub(t, pub, sub)\n\n\treturn groupName\n}\n\n// PublishSimpleMessages publishes provided number of simple messages without a payload.\nfunc PublishSimpleMessages(t *testing.T, messagesCount int, publisher message.Publisher, topicName string) message.Messages {\n\tvar messagesToPublish []*message.Message\n\n\tfor i := 0; i < messagesCount; i++ {\n\t\tid := watermill.NewUUID()\n\n\t\tmsg := message.NewMessage(id, []byte(\"x\"))\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\n\t\terr := publishWithRetry(publisher, topicName, msg)\n\t\trequire.NoError(t, err, \"cannot publish messages\")\n\t}\n\n\treturn messagesToPublish\n}\n\n// PublishSimpleMessagesWithContext publishes provided number of simple messages without a payload, but custom context\nfunc PublishSimpleMessagesWithContext(t *testing.T, messagesCount int, contextKeyString string, publisher message.Publisher, topicName string) message.Messages {\n\tvar messagesToPublish []*message.Message\n\n\tfor i := 0; i < messagesCount; i++ {\n\t\tid := watermill.NewUUID()\n\n\t\tmsg := message.NewMessage(id, nil)\n\t\tmsg.SetContext(context.WithValue(context.Background(), contextKey(contextKeyString), \"bar\"+strconv.Itoa(i)))\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\n\t\terr := publishWithRetry(publisher, topicName, msg)\n\t\trequire.NoError(t, err, \"cannot publish messages\")\n\t}\n\n\treturn messagesToPublish\n}\n\n// AddSimpleMessagesParallel publishes provided number of simple messages without a payload\n// using the provided number of publishers (goroutines).\nfunc AddSimpleMessagesParallel(t *testing.T, messagesCount int, publisher message.Publisher, topicName string, publishers int) message.Messages {\n\tvar messagesToPublish []*message.Message\n\tpublishMsg := make(chan *message.Message)\n\n\twg := sync.WaitGroup{}\n\twg.Add(messagesCount)\n\n\tfor i := 0; i < publishers; i++ {\n\t\tgo func() {\n\t\t\tfor msg := range publishMsg {\n\t\t\t\terr := publishWithRetry(publisher, topicName, msg.Copy())\n\t\t\t\trequire.NoError(t, err, \"cannot publish messages\")\n\t\t\t\twg.Done()\n\t\t\t}\n\t\t}()\n\t}\n\n\tfor i := 0; i < messagesCount; i++ {\n\t\tid := watermill.NewUUID()\n\n\t\tmsg := message.NewMessage(id, []byte(\"x\"))\n\t\tmessagesToPublish = append(messagesToPublish, msg)\n\n\t\tpublishMsg <- msg\n\t}\n\tclose(publishMsg)\n\n\twg.Wait()\n\n\treturn messagesToPublish\n}\n\nfunc assertMessagesChannelClosed(t *testing.T, messages <-chan *message.Message) bool {\n\tselect {\n\tcase _, open := <-messages:\n\t\treturn assert.False(t, open)\n\tdefault:\n\t\tt.Error(\"messages channel is not closed (blocked)\")\n\t\treturn false\n\t}\n}\n\nfunc publishWithRetry(publisher message.Publisher, topic string, messages ...*message.Message) error {\n\tretries := 5\n\n\tfor {\n\t\terr := publisher.Publish(topic, messages...)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tretries--\n\n\t\tfmt.Printf(\"error on publish: %s, %d retries left\\n\", err, retries)\n\n\t\tif retries == 0 {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc bulkRead(testCtx TestContext, messagesCh <-chan *message.Message, limit int, timeout time.Duration) (receivedMessages message.Messages, all bool) {\n\tstart := time.Now()\n\n\tdefer func() {\n\t\tduration := time.Since(start)\n\n\t\tlogMsg := \"all messages (%d/%d) received in bulk read after %s of %s (test ID: %s)\\n\"\n\t\tif !all {\n\t\t\tlogMsg = \"not \" + logMsg\n\t\t}\n\n\t\tlog.Printf(logMsg, len(receivedMessages), limit, duration, timeout, testCtx.TestID)\n\t}()\n\n\tif !testCtx.Features.ExactlyOnceDelivery {\n\t\treturn subscriber.BulkReadWithDeduplication(messagesCh, limit, timeout)\n\t}\n\n\treturn subscriber.BulkRead(messagesCh, limit, timeout)\n}\n\nfunc createMultipliedSubscriber(t *testing.T, pubSubConstructor PubSubConstructor, subscribersCount int) message.Subscriber {\n\treturn internalSubscriber.NewMultiplier(\n\t\tfunc() (message.Subscriber, error) {\n\t\t\tpub, sub := pubSubConstructor(t)\n\t\t\trequire.NoError(t, pub.Close()) // pub is not needed\n\n\t\t\treturn sub, nil\n\t\t},\n\t\tsubscribersCount,\n\t)\n}\n"
  },
  {
    "path": "pubsub/tests/test_pubsub_stress.go",
    "content": "//go:build stress\n// +build stress\n\npackage tests\n\nimport (\n\t\"runtime\"\n)\n\nfunc init() {\n\t// stress tests may work a bit slower\n\tdefaultTimeout *= 5\n\n\t// Set GOMAXPROCS to double the number of CPUs\n\truntime.GOMAXPROCS(runtime.GOMAXPROCS(0) * 2)\n}\n"
  },
  {
    "path": "slog.go",
    "content": "package watermill\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\n// LevelTrace must be added, because [slog] package does not have one by default. Generate it by subtracting 4 levels from [slog.Debug] following the example of [slog.LevelWarn] and [slog.LevelError] which are set to 4 and 8.\nconst LevelTrace = slog.LevelDebug - 4\n\nfunc slogAttrsFromFields(fields LogFields) []any {\n\tresult := make([]any, 0, len(fields)*2)\n\n\tfor key, value := range fields {\n\t\tresult = append(result, key, value)\n\t}\n\n\treturn result\n}\n\n// SlogLoggerAdapter wraps [slog.Logger].\ntype SlogLoggerAdapter struct {\n\tslog *slog.Logger\n\n\twatermillLevelToSlog map[slog.Level]slog.Level\n}\n\n// Error logs a message to [slog.LevelError].\nfunc (s *SlogLoggerAdapter) Error(msg string, err error, fields LogFields) {\n\ts.log(slog.LevelError, msg, append(slogAttrsFromFields(fields), \"error\", err)...)\n}\n\n// Info logs a message to [slog.LevelInfo].\nfunc (s *SlogLoggerAdapter) Info(msg string, fields LogFields) {\n\ts.log(slog.LevelInfo, msg, slogAttrsFromFields(fields)...)\n}\n\n// Debug logs a message to [slog.LevelDebug].\nfunc (s *SlogLoggerAdapter) Debug(msg string, fields LogFields) {\n\ts.log(slog.LevelDebug, msg, slogAttrsFromFields(fields)...)\n}\n\n// Trace logs a message to [LevelTrace].\nfunc (s *SlogLoggerAdapter) Trace(msg string, fields LogFields) {\n\ts.log(\n\t\tLevelTrace,\n\t\tmsg,\n\t\tslogAttrsFromFields(fields)...,\n\t)\n}\n\nfunc (s *SlogLoggerAdapter) log(level slog.Level, msg string, args ...any) {\n\tmappedLevel, ok := s.watermillLevelToSlog[level]\n\tif ok {\n\t\tlevel = mappedLevel\n\t}\n\n\ts.slog.Log(\n\t\t// Void context, following the slog example\n\t\t// as it treats context slightly differently from\n\t\t// normal usage, minding contextual\n\t\t// values, but ignoring contextual deadline.\n\t\t// See the [slog] package documentation\n\t\t// for more details.\n\t\tcontext.Background(),\n\t\tlevel,\n\t\tmsg,\n\t\targs...,\n\t)\n}\n\n// With return a [SlogLoggerAdapter] with a set of fields injected into all consequent logging messages.\nfunc (s *SlogLoggerAdapter) With(fields LogFields) LoggerAdapter {\n\treturn &SlogLoggerAdapter{slog: s.slog.With(slogAttrsFromFields(fields)...), watermillLevelToSlog: s.watermillLevelToSlog}\n}\n\n// NewSlogLogger creates an adapter to the standard library's structured logging package. A `nil` logger is substituted for the result of [slog.Default].\nfunc NewSlogLogger(logger *slog.Logger) LoggerAdapter {\n\tif logger == nil {\n\t\tlogger = slog.Default()\n\t}\n\treturn &SlogLoggerAdapter{\n\t\tslog: logger,\n\t}\n}\n\n// NewSlogLoggerWithLevelMapping creates an adapter to the standard library's structured logging package. A `nil` logger is substituted for the result of [slog.Default].\n// The `watermillLevelToSlog` parameter is a map that maps Watermill's log levels to the levels of the structured logger.\n// It's helpful, when want to for example log Watermill's info logs as debug in slog.\nfunc NewSlogLoggerWithLevelMapping(logger *slog.Logger, watermillLevelToSlog map[slog.Level]slog.Level) LoggerAdapter {\n\tif logger == nil {\n\t\tlogger = slog.Default()\n\t}\n\treturn &SlogLoggerAdapter{\n\t\tslog:                 logger,\n\t\twatermillLevelToSlog: watermillLevelToSlog,\n\t}\n}\n"
  },
  {
    "path": "slog_test.go",
    "content": "package watermill\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"log/slog\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSlogLoggerAdapter(t *testing.T) {\n\tb := &bytes.Buffer{}\n\n\tlogger := NewSlogLogger(slog.New(slog.NewTextHandler(\n\t\tb, // output\n\t\t&slog.HandlerOptions{\n\t\t\tLevel: LevelTrace,\n\t\t\tReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {\n\t\t\t\tif a.Key == \"time\" && len(groups) == 0 {\n\t\t\t\t\t// omit time stamp to make the test idempotent\n\t\t\t\t\ta.Value = slog.StringValue(\"[omit]\")\n\t\t\t\t}\n\t\t\t\treturn a\n\t\t\t},\n\t\t},\n\t)))\n\n\tlogger = logger.With(LogFields{\n\t\t\"common1\": \"commonvalue\",\n\t})\n\tlogger.Trace(\"test trace\", LogFields{\n\t\t\"field1\": \"value1\",\n\t})\n\tlogger.Error(\"test error\", errors.New(\"error message\"), LogFields{\n\t\t\"field2\": \"value2\",\n\t})\n\tlogger.Info(\"test info\", LogFields{\n\t\t\"field3\": \"value3\",\n\t})\n\n\tassert.Equal(t,\n\t\tstrings.TrimSpace(`\ntime=[omit] level=DEBUG-4 msg=\"test trace\" common1=commonvalue field1=value1\ntime=[omit] level=ERROR msg=\"test error\" common1=commonvalue field2=value2 error=\"error message\"\ntime=[omit] level=INFO msg=\"test info\" common1=commonvalue field3=value3\n      `),\n\t\tstrings.TrimSpace(b.String()),\n\t\t\"Logging output does not match saved template.\",\n\t)\n}\n\nfunc TestSlogLoggerAdapter_level_mapping(t *testing.T) {\n\tb := &bytes.Buffer{}\n\n\tlogger := NewSlogLoggerWithLevelMapping(\n\t\tslog.New(slog.NewTextHandler(\n\t\t\tb, // output\n\t\t\t&slog.HandlerOptions{\n\t\t\t\tLevel: LevelTrace,\n\t\t\t\tReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {\n\t\t\t\t\tif a.Key == \"time\" && len(groups) == 0 {\n\t\t\t\t\t\t// omit time stamp to make the test idempotent\n\t\t\t\t\t\ta.Value = slog.StringValue(\"[omit]\")\n\t\t\t\t\t}\n\t\t\t\t\treturn a\n\t\t\t\t},\n\t\t\t},\n\t\t)),\n\t\tmap[slog.Level]slog.Level{\n\t\t\tslog.LevelInfo: slog.LevelDebug,\n\t\t},\n\t)\n\n\tlogger = logger.With(LogFields{\n\t\t\"common1\": \"commonvalue\",\n\t})\n\tlogger.Trace(\"test trace\", LogFields{\n\t\t\"field1\": \"value1\",\n\t})\n\tlogger.Error(\"test error\", errors.New(\"error message\"), LogFields{\n\t\t\"field2\": \"value2\",\n\t})\n\tlogger.Info(\"test info mapped to debug\", LogFields{\n\t\t\"field3\": \"value3\",\n\t})\n\n\tassert.Equal(t,\n\t\tstrings.TrimSpace(`\ntime=[omit] level=DEBUG-4 msg=\"test trace\" common1=commonvalue field1=value1\ntime=[omit] level=ERROR msg=\"test error\" common1=commonvalue field2=value2 error=\"error message\"\ntime=[omit] level=DEBUG msg=\"test info mapped to debug\" common1=commonvalue field3=value3\n      `),\n\t\tstrings.TrimSpace(b.String()),\n\t\t\"Logging output does not match saved template.\",\n\t)\n}\n"
  },
  {
    "path": "tools/mill/.default-config.yml",
    "content": "# These are the current default settings for the Watermill CLI tool.\n# Use them as a template for your own config\nlog: false\ntrace: false\ndebug: false\n\namqp:\n  uri: \"\"\n  durable: true\n  consume:\n    exchange: \"\"\n    queue: \"\"\n  produce:\n    exchange: \"\"\n    exchangetype: fanout\n    routingkey: \"\"\n\ngooglecloud:\n  projectid: \"\"\n  topic: \"\"\n  consume:\n    subscription: \"\"\n  produce:\n  subscription:\n    add:\n      ackdeadline: 10s\n      labels: \"\"\n      retainacked: false\n      retentionduration: 168h0m0s\n    rm:\n      # no flags for rm yet\n\nkafka:\n  brokers: []\n  topic: \"\"\n  consume:\n    consumergroup: \"\"\n    frombeginning: false\n  produce:\n    # no flags for produce yet\n"
  },
  {
    "path": "tools/mill/Makefile",
    "content": "mill:\n\tgo build -o mill main.go\n"
  },
  {
    "path": "tools/mill/README.md",
    "content": "# mill - a simple CLI tool for Watermill\n\n`mill` is a CLI tool for the [Watermill](https://watermill.io) library.\n\nIt has two basic functionalities, namely producing and consuming messages on the following\nPub/Subs:\n\n1. Kafka\n2. Google Cloud Pub/Sub\n3. RabbitMQ\n\nSee Watermill's [Supported Pub/Subs](https://watermill.io/pubsubs) for more details on how this works.\n\n## Installation\n\nTo install this tool, just execute:\n\n```bash\ngo install github.com/ThreeDotsLabs/watermill/tools/mill@latest\n```\n\nThis will install a `mill` binary in your system.\n\n## Consume mode\n\nIn consume mode, the tool subscribes to a topic/queue/subscription (nomenclature depending on the particular Pub/Sub provider)\nand prints the messages' payload to the standard output.\n\nOther outputs, for example ones that add a timestamp or preserve UUIDs or metadata, are easily attainable by modification\nof the marshaling function of the `io.Publisher` of the `consumeCmd`.\n\n## Produce mode\n\nIn produce mode, subsequent lines of data from the standard input are transformed into messages outgoing to the requested\nprovider's topic/exchange. \n\nThe message's payload is set to the line from stdin, the UUID is auto-generated and the metadata is empty.\n\nSimilarly, the contents of the message could be parsed differently from stdin, by modification\nof the unmarshaling function of the `io.Subscriber` of the `produceCmd`.\n\n## Usage\n\nThe basic syntax of the tool is:\n\n```bash\nmill <provider> <command>\n```\n\nwith the appropriate flags regulating the specific behaviour of each command.\n\n`command` is usually one of `produce` or `consume`, but some providers may handle additional commands\nthat are specific for them.\n\nThe flags are context-specific, so the best way to find out about them is to use the `-h` flag and study \nwhich flags are available/required for the specific context and act accordingly.\n\n### Advanced usage\n\nA neat feature of producers and consumers is that you can use the power of stdin/stdout piping for stuff like:\n\n```bash\nmyservice | tee myservice.log | mill kafka produce -t myservice-logs --brokers kafka-host:8082 \n```\n\nAnd on another host:\n\n```bash\nmill kafka consume -t myservice-logs --brokers kafka-host:8082 >> myservice.log\n```\n\nIn the above example, the host on which `myservice` runs has its own copy of `myservice.log` and any host that consumes\nfrom the kafka topic will replicate the log entries in their local copy.\n\n\n## Additional functionalities\n\n### Google Cloud Pub/Sub\n\n#### Adding/Removing subscriptions\n\nYou can use `mill` to create/remove subscriptions for Google Cloud Pub/Sub:\n\n```bash\nmill googlecloud subscription add -t <topic> <subscription_id>\n\nmill googlecloud subscription rm <subscription_id>\n```\n\nAdditional flags are available for `subscription add` to regulate the newly created subscription's settings.\n\n#### Listing subscriptions\n\nYou can use `mill` to list existing subscriptions:\n\n```bash\nmill googlecloud subscription ls [-t topic]\n```\n\nThe topic is optional. If omitted, all topics will be listed with their subscriptions.\n"
  },
  {
    "path": "tools/mill/cmd/amqp.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp\"\n)\n\nvar amqpCmd = &cobra.Command{\n\tUse:   \"amqp\",\n\tShort: \"Commands for the AMQP Pub/Sub provider\",\n\tLong: `Consume or produce messages from the AMQP Pub/Sub provider.\n\nFor the configuration of consuming/producing of the messages, check the help of the relevant command.`,\n\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\terr := rootCmd.PersistentPreRunE(cmd, args)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlogger.Debug(\"Using AMQP Pub/Sub\", nil)\n\n\t\tif cmd.Use == \"consume\" {\n\t\t\tconsumer, err = amqp.NewSubscriber(amqpConsumerConfig(), logger)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif cmd.Use == \"produce\" {\n\t\t\tproducer, err = amqp.NewPublisher(amqpProducerConfig(), logger)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\nfunc amqpConsumerConfig() amqp.Config {\n\turi := viper.GetString(\"amqp.uri\")\n\tqueue := viper.GetString(\"amqp.consume.queue\")\n\texchangeName := viper.GetString(\"amqp.consume.exchange\")\n\texchangeType := viper.GetString(\"amqp.produce.exchangeType\")\n\tdurable := viper.GetBool(\"amqp.durable\")\n\n\treturn amqp.Config{\n\t\tConnection: amqp.ConnectionConfig{\n\t\t\tAmqpURI: uri,\n\t\t},\n\t\tMarshaler: amqp.DefaultMarshaler{},\n\t\tQueue: amqp.QueueConfig{\n\t\t\tGenerateName: func(topic string) string {\n\t\t\t\treturn queue\n\t\t\t},\n\t\t\tDurable: durable,\n\t\t},\n\t\tConsume: amqp.ConsumeConfig{\n\t\t\tQos: amqp.QosConfig{\n\t\t\t\tPrefetchCount: 1,\n\t\t\t},\n\t\t},\n\n\t\tExchange: amqp.ExchangeConfig{\n\t\t\tGenerateName: func(topic string) string {\n\t\t\t\treturn exchangeName\n\t\t\t},\n\t\t\tType:    exchangeType,\n\t\t\tDurable: durable,\n\t\t},\n\t}\n}\n\nfunc amqpProducerConfig() amqp.Config {\n\turi := viper.GetString(\"amqp.uri\")\n\texchangeName := viper.GetString(\"amqp.produce.exchange\")\n\texchangeType := viper.GetString(\"amqp.produce.exchangeType\")\n\troutingKey := viper.GetString(\"amqp.produce.routingKey\")\n\tdurable := viper.GetBool(\"amqp.durable\")\n\n\treturn amqp.Config{\n\t\tConnection: amqp.ConnectionConfig{\n\t\t\tAmqpURI: uri,\n\t\t},\n\t\tMarshaler: amqp.DefaultMarshaler{},\n\t\tExchange: amqp.ExchangeConfig{\n\t\t\tGenerateName: func(topic string) string {\n\t\t\t\treturn exchangeName\n\t\t\t},\n\t\t\tType:    exchangeType,\n\t\t\tDurable: durable,\n\t\t},\n\t\tPublish: amqp.PublishConfig{\n\t\t\tGenerateRoutingKey: func(topic string) string {\n\t\t\t\treturn routingKey\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc init() {\n\trootCmd.AddCommand(amqpCmd)\n\tconfigureAmqpCmd()\n\tconsumeCmd := addConsumeCmd(amqpCmd, \"amqp.consume.queue\")\n\tconfigureConsumeCmd(consumeCmd)\n\tproduceCmd := addProduceCmd(amqpCmd, \"amqp.produce.exchange\")\n\tconfigureProduceCmd(produceCmd)\n}\n\nfunc configureAmqpCmd() {\n\tamqpCmd.PersistentFlags().StringP(\n\t\t\"uri\",\n\t\t\"u\",\n\t\t\"\",\n\t\t\"The URI to the AMQP instance (required)\",\n\t)\n\tensure(amqpCmd.MarkPersistentFlagRequired(\"uri\"))\n\tensure(viper.BindPFlag(\"amqp.uri\", amqpCmd.PersistentFlags().Lookup(\"uri\")))\n\n\tamqpCmd.PersistentFlags().Bool(\n\t\t\"durable\",\n\t\ttrue,\n\t\t\"If true, the queues and exchanges created automatically (if any) will be durable\",\n\t)\n\tensure(viper.BindPFlag(\"amqp.durable\", amqpCmd.PersistentFlags().Lookup(\"durable\")))\n\n\tamqpCmd.PersistentFlags().String(\n\t\t\"exchange-type\",\n\t\t\"fanout\",\n\t\t\"If exchange needs to be created, it will be created with this type. The common types are 'direct', 'fanout', 'topic' and 'headers'.\",\n\t)\n\tensure(viper.BindPFlag(\"amqp.produce.exchangeType\", amqpCmd.PersistentFlags().Lookup(\"exchange-type\")))\n}\n\nfunc configureConsumeCmd(consumeCmd *cobra.Command) {\n\tconsumeCmd.PersistentFlags().StringP(\n\t\t\"queue\",\n\t\t\"q\",\n\t\t\"\",\n\t\t\"The name of the AMQP queue to consume messages from (required)\",\n\t)\n\tensure(consumeCmd.MarkPersistentFlagRequired(\"queue\"))\n\tensure(viper.BindPFlag(\"amqp.consume.queue\", consumeCmd.PersistentFlags().Lookup(\"queue\")))\n\n\tconsumeCmd.PersistentFlags().StringP(\n\t\t\"exchange\",\n\t\t\"x\",\n\t\t\"\",\n\t\t\"If non-empty, an exchange with this name is created if it didn't exist. Then, the queue is bound to this exchange.\",\n\t)\n\tensure(viper.BindPFlag(\"amqp.consume.exchange\", consumeCmd.PersistentFlags().Lookup(\"exchange\")))\n}\n\nfunc configureProduceCmd(produceCmd *cobra.Command) {\n\tproduceCmd.PersistentFlags().StringP(\n\t\t\"exchange\",\n\t\t\"x\",\n\t\t\"\",\n\t\t\"The name of the AMQP exchange to produce messages to (required)\",\n\t)\n\tensure(produceCmd.MarkPersistentFlagRequired(\"exchange\"))\n\tensure(viper.BindPFlag(\"amqp.produce.exchange\", produceCmd.PersistentFlags().Lookup(\"exchange\")))\n\n\tproduceCmd.PersistentFlags().StringP(\n\t\t\"routing-key\",\n\t\t\"r\",\n\t\t\"\",\n\t\t\"The routing key to use when publishing the message.\",\n\t)\n\tensure(produceCmd.MarkPersistentFlagRequired(\"routing-key\"))\n\tensure(viper.BindPFlag(\"amqp.produce.routingKey\", produceCmd.PersistentFlags().Lookup(\"routing-key\")))\n}\n"
  },
  {
    "path": "tools/mill/cmd/consume.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/ThreeDotsLabs/watermill-io/pkg/io\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\n// consumer is initialized by the parent command to the pub/sub provider of choice.\nvar consumer message.Subscriber\n\nfunc addConsumeCmd(parent *cobra.Command, topicKey string) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"consume\",\n\t\tShort: \"Consume messages from a pub/sub and print them to stdout\",\n\t\tLong: `Consume messages from the pub/sub of your choice and print them on the standard output.\n\nFor the configuration of particular pub/sub providers, see the help for the provider commands.`,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\ttopic := viper.GetString(topicKey)\n\t\t\trouter, err := message.NewRouter(\n\t\t\t\tmessage.RouterConfig{\n\t\t\t\t\tCloseTimeout: 5 * time.Second,\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not create router\")\n\t\t\t}\n\n\t\t\trouter.AddPlugin(plugin.SignalsHandler)\n\n\t\t\tout, err := io.NewPublisher(\n\t\t\t\tos.Stdout,\n\t\t\t\tio.PublisherConfig{\n\t\t\t\t\tMarshalFunc: io.PayloadMarshalFunc,\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not create console producer\")\n\t\t\t}\n\n\t\t\trouter.AddHandler(\n\t\t\t\t\"dump_to_stdout\",\n\t\t\t\ttopic,\n\t\t\t\tconsumer,\n\t\t\t\t\"\",\n\t\t\t\tout,\n\t\t\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\t\t\t// just forward the message to stdout\n\t\t\t\t\treturn message.Messages{msg}, nil\n\t\t\t\t},\n\t\t\t)\n\n\t\t\treturn router.Run(context.Background())\n\t\t},\n\t}\n\n\tparent.AddCommand(cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "tools/mill/cmd/googlecloud.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"cloud.google.com/go/pubsub\"\n\t\"github.com/ThreeDotsLabs/watermill\"\n\t\"github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud\"\n\t\"github.com/ThreeDotsLabs/watermill/tools/mill/cmd/internal\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/api/iterator\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nvar googleCloudTempSubscriptionID string\n\nvar googleCloudCmd = &cobra.Command{\n\tUse:   \"googlecloud\",\n\tShort: \"Commands for the Google Cloud Pub/Sub provider\",\n\tLong: `Consume or produce messages from the Google Cloud Pub/Sub provider. Manage subscriptions.\n\nFor the configuration of consuming/producing of the messages, check the help of the relevant command.`,\n\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\terr := rootCmd.PersistentPreRunE(cmd, args)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlogger.Debug(\"Using Google Cloud Pub/Sub\", nil)\n\t\tif cmd.Use == \"consume\" {\n\t\t\tsubName := viper.GetString(\"googlecloud.consume.subscription\")\n\t\t\tif subName == \"\" {\n\t\t\t\tsubName, err = generateTempSubscription()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconsumer, err = googlecloud.NewSubscriber(\n\t\t\t\tgooglecloud.SubscriberConfig{\n\t\t\t\t\tGenerateSubscriptionName: func(topic string) string {\n\t\t\t\t\t\treturn subName\n\t\t\t\t\t},\n\t\t\t\t\tProjectID: projectID(),\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif cmd.Use == \"produce\" {\n\t\t\tproducer, err = googlecloud.NewPublisher(\n\t\t\t\tgooglecloud.PublisherConfig{\n\t\t\t\t\tProjectID: projectID(),\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t},\n\tPersistentPostRunE: func(cmd *cobra.Command, args []string) error {\n\t\tlogger.Debug(\"Google Cloud Pub/Sub cleanup\", nil)\n\t\tif googleCloudTempSubscriptionID != \"\" {\n\t\t\tif err := removeTempSubscription(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n}\n\nvar googleCloudSubscriptionCmd = &cobra.Command{\n\tUse:   \"subscription\",\n\tShort: \"Manage Google Cloud Pub/Sub subscriptions\",\n\tLong:  `Add or remove subscriptions for the Google Cloud Pub/Sub provider.`,\n}\n\nvar googleCloudSubscriptionAddCmd = &cobra.Command{\n\tUse:       \"add <subscription_id>\",\n\tShort:     \"Add a new subscription in Google Cloud Pub/Sub\",\n\tArgs:      cobra.ExactArgs(1),\n\tValidArgs: []string{\"subscriptionID\"},\n\tRunE: func(cmd *cobra.Command, args []string) (err error) {\n\t\tsubID := args[0]\n\n\t\ttopic := viper.GetString(\"googlecloud.subscription.add.topic\")\n\t\tackDeadline := viper.GetDuration(\"googlecloud.subscription.add.ackDeadline\")\n\t\tretainAcked := viper.GetBool(\"googlecloud.subscription.add.retainAcked\")\n\t\tretentionDuration := viper.GetDuration(\"googlecloud.subscription.add.retentionDuration\")\n\n\t\t// StringToString doesn't work with viper, so let's parse this manually\n\t\tlabels := strings.Split(viper.GetString(\"googlecloud.subscription.add.labels\"), \",\")\n\t\tlabelsMap := make(map[string]string, len(labels))\n\t\tfor _, l := range labels {\n\t\t\tfields := strings.Split(l, \"=\")\n\t\t\tif len(fields) < 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlabelsMap[fields[0]] = fields[1]\n\t\t}\n\n\t\tlogger := logger.With(watermill.LogFields{\n\t\t\t\"subscription_id\":   subID,\n\t\t\t\"topic\":             topic,\n\t\t\t\"ackDeadline\":       ackDeadline,\n\t\t\t\"retainAcked\":       retainAcked,\n\t\t\t\"retentionDuration\": retentionDuration,\n\t\t\t\"labels\":            labelsMap,\n\t\t})\n\t\tlogger.Info(\"Creating new subscription\", nil)\n\n\t\tdefer func() {\n\t\t\tif err == nil {\n\t\t\t\tlogger.Info(\"Subscription created\", nil)\n\t\t\t}\n\t\t}()\n\n\t\treturn addSubscription(\n\t\t\tsubID,\n\t\t\ttopic,\n\t\t\tackDeadline,\n\t\t\tretainAcked,\n\t\t\tretentionDuration,\n\t\t\tlabelsMap,\n\t\t)\n\t},\n}\n\nvar googleCloudSubscriptionRmCmd = &cobra.Command{\n\tUse:       \"rm <subscription_id>\",\n\tShort:     \"Remove a subscription in Google Cloud Pub/Sub\",\n\tArgs:      cobra.ExactArgs(1),\n\tValidArgs: []string{\"subscriptionID\"},\n\tRunE: func(cmd *cobra.Command, args []string) (err error) {\n\t\tsubID := args[0]\n\n\t\tlogger := logger.With(watermill.LogFields{\n\t\t\t\"subscription_id\": subID,\n\t\t})\n\t\tlogger.Info(\"Removing a subscription\", nil)\n\n\t\tdefer func() {\n\t\t\tif err == nil {\n\t\t\t\tlogger.Info(\"Subscription removed\", nil)\n\t\t\t}\n\t\t}()\n\n\t\treturn removeSubscription(subID)\n\t},\n}\n\nvar googleCloudSubscriptionLsCmd = &cobra.Command{\n\tUse:   \"ls\",\n\tShort: \"List subscriptions in Google Cloud Pub/Sub. Topic may be provided optionally to filter subscriptions by topic.\",\n\tArgs:  cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) (err error) {\n\t\ttopic := viper.GetString(\"googlecloud.subscription.ls.topic\")\n\t\tverbose := viper.GetBool(\"googlecloud.subscription.ls.verbose\")\n\n\t\tlogger := logger\n\t\tif topic != \"\" {\n\t\t\tlogger = logger.With(watermill.LogFields{\n\t\t\t\t\"topic\": topic,\n\t\t\t})\n\t\t}\n\t\tlogger.Info(\"Listing all subscriptions\", nil)\n\n\t\treturn listSubscriptions(topic, logger, verbose)\n\t},\n}\n\nfunc generateTempSubscription() (id string, err error) {\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlogger.Debug(\"Temp subscription created\", watermill.LogFields{\n\t\t\t\t\"subscription_name\": id,\n\t\t\t})\n\t\t\tgoogleCloudTempSubscriptionID = id\n\t\t}\n\t}()\n\n\trandomID := \"watermill_console_consumer_\" + watermill.NewShortUUID()\n\treturn randomID, addSubscription(\n\t\trandomID,\n\t\tviper.GetString(\"googlecloud.topic\"),\n\t\t10*time.Second,\n\t\tfalse,\n\t\t10*time.Minute,\n\t\tnil,\n\t)\n}\n\nfunc addSubscription(\n\tid string,\n\ttopic string,\n\tackDeadline time.Duration,\n\tretainAckedMessages bool,\n\tretentionDuration time.Duration,\n\tlabels map[string]string,\n) error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cancel()\n\n\tclient, err := pubsub.NewClient(ctx, projectID())\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not create pubsub client\")\n\t}\n\n\tt := client.Topic(topic)\n\texists, err := t.Exists(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not check if topic exists\")\n\t}\n\tif !exists {\n\t\tt, err = client.CreateTopic(ctx, t.ID())\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"could not create topic\")\n\t\t}\n\t}\n\n\t_, err = client.CreateSubscription(ctx, id, pubsub.SubscriptionConfig{\n\t\tTopic:               t,\n\t\tAckDeadline:         ackDeadline,\n\t\tRetainAckedMessages: retainAckedMessages,\n\t\tRetentionDuration:   retentionDuration,\n\t\tLabels:              labels,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not create subscription\")\n\t}\n\n\treturn nil\n}\n\nfunc removeTempSubscription() (err error) {\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlogger.Debug(\"Temporary subscription removed\", watermill.LogFields{\n\t\t\t\t\"subscription_name\": googleCloudTempSubscriptionID,\n\t\t\t})\n\t\t}\n\t}()\n\treturn removeSubscription(googleCloudTempSubscriptionID)\n}\n\nfunc removeSubscription(id string) error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cancel()\n\n\tclient, err := pubsub.NewClient(ctx, projectID())\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not create pubsub client\")\n\t}\n\n\tsub := client.Subscription(id)\n\texists, err := sub.Exists(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not check if sub exists\")\n\t}\n\n\tif !exists {\n\t\treturn nil\n\t}\n\n\treturn sub.Delete(ctx)\n}\n\nfunc listSubscriptions(topic string, adapter watermill.LoggerAdapter, verbose bool) error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cancel()\n\n\tclient, err := pubsub.NewClient(ctx, projectID())\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not create pubsub client\")\n\t}\n\n\tif topic != \"\" {\n\t\ttopic := client.Topic(topic)\n\t\treturn listSubscriptionsForTopic(ctx, client, topic, logger, verbose)\n\t}\n\n\tit := client.Topics(ctx)\n\tnoTopics := true\n\tfor {\n\t\ttopic, err := it.Next()\n\t\tif err == iterator.Done {\n\t\t\tif noTopics {\n\t\t\t\tlogger.Info(\"No topics in project\", nil)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"could not retrieve next subscription\")\n\t\t}\n\n\t\tnoTopics = false\n\t\terr = listSubscriptionsForTopic(ctx, client, topic, logger, verbose)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"error listing subscriptions for topic\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc listSubscriptionsForTopic(ctx context.Context, client *pubsub.Client, topic *pubsub.Topic, logger watermill.LoggerAdapter, verbose bool) error {\n\tnoSubs := true\n\texists, err := topic.Exists(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not check if topic exists\")\n\t}\n\tif !exists {\n\t\tlogger.Info(\"Topic does not exist\", watermill.LogFields{\"topic\": topic.String()})\n\t\treturn nil\n\t}\n\n\tit := topic.Subscriptions(ctx)\n\tfor {\n\t\tsub, err := it.Next()\n\t\tif err == iterator.Done {\n\t\t\tif noSubs {\n\t\t\t\tlogger.Info(\"No subscriptions for the topic\", watermill.LogFields{\"topic\": topic.String()})\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"could not retrieve next subscription\")\n\t\t}\n\n\t\tif noSubs {\n\t\t\tnoSubs = false\n\t\t\tfmt.Printf(\"Topic %s:\\n\", topic.String())\n\t\t}\n\t\tname := sub.String()\n\t\tconfig, err := sub.Config(ctx)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"could not retrieve subscription config for subscription '%s'\", name)\n\t\t}\n\n\t\terr = printSubscriptionInfo(name, config)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error printing subscription '%s'\", name)\n\t\t}\n\t}\n}\n\nfunc printSubscriptionInfo(name string, config pubsub.SubscriptionConfig) error {\n\tb, err := yaml.Marshal(subscriptionConfig{\n\t\tName: name,\n\t\tPushConfig: subscriptionConfigPushConfig{\n\t\t\tEndpoint:   config.PushConfig.Endpoint,\n\t\t\tAttributes: config.PushConfig.Attributes,\n\t\t},\n\t\tAckDeadline:         config.AckDeadline,\n\t\tRetainAckedMessages: config.RetainAckedMessages,\n\t\tRetentionDuration:   config.RetentionDuration,\n\t\tLabels:              config.Labels,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(internal.Indent(string(b), \"  \"))\n\treturn nil\n}\n\n// subscriptionConfig provides a marshallable form to pubsub.SubscriptionConfig\ntype subscriptionConfig struct {\n\tName                string\n\tPushConfig          subscriptionConfigPushConfig `yaml:\"push_config\"`\n\tAckDeadline         time.Duration                `yaml:\"ack_deadline\"`\n\tRetainAckedMessages bool                         `yaml:\"retain_acked_messages\"`\n\tRetentionDuration   time.Duration                `yaml:\"retention_duration\"`\n\tLabels              map[string]string            `yaml:\"labels\"`\n}\n\ntype subscriptionConfigPushConfig struct {\n\tEndpoint   string\n\tAttributes map[string]string\n}\n\nfunc projectID() string {\n\tprojectID := viper.GetString(\"googlecloud.projectID\")\n\tif projectID == \"\" {\n\t\tprojectID = os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\t}\n\n\treturn projectID\n}\n\nfunc init() {\n\trootCmd.AddCommand(googleCloudCmd)\n\n\tgoogleCloudCmd.PersistentFlags().StringP(\n\t\t\"topic\",\n\t\t\"t\",\n\t\t\"\",\n\t\t\"The topic to produce messages to (produce), consume message from (consume), list from (ls) or the topic for the newly created subscription (subscription.add)\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.topic\", googleCloudCmd.PersistentFlags().Lookup(\"topic\")))\n\tensure(googleCloudCmd.MarkPersistentFlagRequired(\"topic\"))\n\n\tconsumeCmd := addConsumeCmd(googleCloudCmd, \"googlecloud.topic\")\n\taddProduceCmd(googleCloudCmd, \"googlecloud.topic\")\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\tgoogleCloudCmd.PersistentFlags().String(\"project\", \"\", \"The projectID for Google Cloud Pub/Sub. Defaults to the GOOGLE_CLOUD_PROJECT environment variable.\")\n\tensure(viper.BindPFlag(\"googlecloud.projectID\", googleCloudCmd.PersistentFlags().Lookup(\"project\")))\n\n\tconsumeCmd.PersistentFlags().StringP(\n\t\t\"subscription\",\n\t\t\"s\",\n\t\t\"\",\n\t\t\"The subscription for Google Cloud Pub/Sub. If left empty, a temporary subscription is created and removed when the consumer is closed\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.consume.subscription\", consumeCmd.PersistentFlags().Lookup(\"subscription\")))\n\n\tgoogleCloudCmd.AddCommand(googleCloudSubscriptionCmd)\n\tgoogleCloudSubscriptionCmd.AddCommand(googleCloudSubscriptionAddCmd)\n\n\tgoogleCloudSubscriptionAddCmd.Flags().StringP(\"topic\", \"t\", \"\", \"The topic for the new subscription (required)\")\n\tensure(googleCloudSubscriptionAddCmd.MarkFlagRequired(\"topic\"))\n\tensure(viper.BindPFlag(\"googlecloud.subscription.add.topic\", googleCloudSubscriptionAddCmd.Flags().Lookup(\"topic\")))\n\n\tgoogleCloudSubscriptionAddCmd.Flags().DurationP(\n\t\t\"ack-deadline\",\n\t\t\"a\",\n\t\t10*time.Second,\n\t\t\"How long Pub/Sub waits for the subscriber to acknowledge receipt before resending the message. Deadline time is from 10 seconds to 600 seconds\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.subscription.add.ackDeadline\", googleCloudSubscriptionAddCmd.Flags().Lookup(\"ack-deadline\")))\n\n\tgoogleCloudSubscriptionAddCmd.Flags().Bool(\n\t\t\"retain-acked\",\n\t\tfalse,\n\t\t\"Acknowledged messages will be kept 7 days from publication unless set otherwise in \\\"message retention duration\\\".\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.subscription.add.retainAcked\", googleCloudSubscriptionAddCmd.Flags().Lookup(\"retain-acked\")))\n\n\tgoogleCloudSubscriptionAddCmd.Flags().Duration(\n\t\t\"retention-duration\",\n\t\t7*24*time.Hour,\n\t\t\"How long the retained messages will be kept. The allowed duration is from 10 minutes to 7 days, which is the default.\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.subscription.add.retentionDuration\", googleCloudSubscriptionAddCmd.Flags().Lookup(\"retention-duration\")))\n\n\t// StringToString doesn't work correctly with viper\n\tgoogleCloudSubscriptionAddCmd.Flags().String(\n\t\t\"labels\",\n\t\t\"\",\n\t\t\"The set of labels for the subscription. Format: '--labels key1=value1,key2=value2,...'\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.subscription.add.labels\", googleCloudSubscriptionAddCmd.Flags().Lookup(\"labels\")))\n\n\tgoogleCloudSubscriptionCmd.AddCommand(googleCloudSubscriptionRmCmd)\n\n\tgoogleCloudSubscriptionCmd.AddCommand(googleCloudSubscriptionLsCmd)\n\tgoogleCloudSubscriptionLsCmd.Flags().StringP(\n\t\t\"topic\",\n\t\t\"t\",\n\t\t\"\",\n\t\t\"The topic for the new subscription (optional, will list subscriptions for all topics if omitted)\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.subscription.ls.topic\", googleCloudSubscriptionLsCmd.Flags().Lookup(\"topic\")))\n\n\tgoogleCloudSubscriptionLsCmd.Flags().BoolP(\n\t\t\"verbose\",\n\t\t\"v\",\n\t\tfalse,\n\t\t\"will print more information, including the subscription config\",\n\t)\n\tensure(viper.BindPFlag(\"googlecloud.subscription.ls.verbose\", googleCloudSubscriptionLsCmd.Flags().Lookup(\"verbose\")))\n}\n"
  },
  {
    "path": "tools/mill/cmd/internal/indent.go",
    "content": "package internal\n\nimport \"strings\"\n\n// Indent indents all lines in the given string with a given prefix.\nfunc Indent(s, prefix string) string {\n\tendsWithNewline := strings.HasSuffix(s, \"\\n\")\n\tsplit := strings.Split(s, \"\\n\")\n\n\tfor i, ss := range split {\n\t\tsplit[i] = prefix + ss\n\t}\n\tjoined := strings.Join(split, \"\\n\")\n\tif endsWithNewline {\n\t\tjoined += \"\\n\"\n\t}\n\n\treturn joined\n}\n"
  },
  {
    "path": "tools/mill/cmd/kafka.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/IBM/sarama\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka\"\n)\n\nvar kafkaCmd = &cobra.Command{\n\tUse:   \"kafka\",\n\tShort: \"Commands for the kafka Pub/Sub provider\",\n\tLong: `Consume or produce messages from the kafka Pub/Sub provider.\n\nFor the configuration of consuming/producing of the messages, check the help of the relevant command.`,\n\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\terr := rootCmd.PersistentPreRunE(cmd, args)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogger.Debug(\"Using kafka pub/sub\", nil)\n\n\t\tbrokers := viper.GetStringSlice(\"kafka.brokers\")\n\n\t\tif cmd.Use == \"consume\" {\n\t\t\tsaramaSubscriberConfig := kafka.DefaultSaramaSubscriberConfig()\n\n\t\t\tif viper.GetBool(\"kafka.consume.fromBeginning\") {\n\t\t\t\tlogger.Trace(\"Configured sarama to consume messages from beginning\", nil)\n\t\t\t\t// equivalent of auto.offset.reset: earliest\n\t\t\t\tsaramaSubscriberConfig.Consumer.Offsets.Initial = sarama.OffsetOldest\n\t\t\t}\n\n\t\t\tconsumer, err = kafka.NewSubscriber(\n\t\t\t\tkafka.SubscriberConfig{\n\t\t\t\t\tBrokers:               brokers,\n\t\t\t\t\tUnmarshaler:           kafka.DefaultMarshaler{},\n\t\t\t\t\tOverwriteSaramaConfig: saramaSubscriberConfig,\n\t\t\t\t\tConsumerGroup:         viper.GetString(\"kafka.consume.consumerGroup\"),\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif cmd.Use == \"produce\" {\n\t\t\tproducer, err = kafka.NewPublisher(\n\t\t\t\tkafka.PublisherConfig{\n\t\t\t\t\tBrokers:   brokers,\n\t\t\t\t\tMarshaler: kafka.DefaultMarshaler{},\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(kafkaCmd)\n\n\tkafkaCmd.PersistentFlags().StringP(\n\t\t\"topic\",\n\t\t\"t\",\n\t\t\"\",\n\t\t\"The topic to produce messages to (produce) or consume message from (consume)\",\n\t)\n\tensure(kafkaCmd.MarkPersistentFlagRequired(\"topic\"))\n\tensure(viper.BindPFlag(\"kafka.topic\", kafkaCmd.PersistentFlags().Lookup(\"topic\")))\n\n\tconsumeCmd := addConsumeCmd(kafkaCmd, \"kafka.topic\")\n\t_ = addProduceCmd(kafkaCmd, \"kafka.topic\")\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\tkafkaCmd.PersistentFlags().StringSliceP(\"brokers\", \"b\", nil, \"A list of kafka brokers\")\n\tensure(kafkaCmd.MarkPersistentFlagRequired(\"brokers\"))\n\tensure(viper.BindPFlag(\"kafka.brokers\", kafkaCmd.PersistentFlags().Lookup(\"brokers\")))\n\n\tconsumeCmd.PersistentFlags().Bool(\"from-beginning\", false, \"Equivalent to auto.offset.reset: earliest\")\n\tensure(viper.BindPFlag(\"kafka.consume.fromBeginning\", consumeCmd.PersistentFlags().Lookup(\"from-beginning\")))\n\n\tconsumeCmd.PersistentFlags().StringP(\"consumer-group\", \"c\", \"\", \"The kafka consumer group. Defaults to empty.\")\n\tensure(viper.BindPFlag(\"kafka.consume.consumerGroup\", consumeCmd.PersistentFlags().Lookup(\"consumer-group\")))\n}\n"
  },
  {
    "path": "tools/mill/cmd/produce.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/ThreeDotsLabs/watermill-io/pkg/io\"\n\t\"github.com/ThreeDotsLabs/watermill/message\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/plugin\"\n)\n\n// producer is initialized by parent command to the pub/sub provider of choice.\nvar producer message.Publisher\n\nfunc addProduceCmd(parent *cobra.Command, topicKey string) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"produce\",\n\t\tShort: \"Produce messages to a pub/sub from the stdin\",\n\t\tLong: `Produce messages to the pub/sub of your choice from the standard input.\n\nFor the configuration of particular pub/sub providers, see the help for the provider commands.`,\n\t\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn nil\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\ttopic := viper.GetString(topicKey)\n\t\t\trouter, err := message.NewRouter(\n\t\t\t\tmessage.RouterConfig{\n\t\t\t\t\tCloseTimeout: 10 * time.Second,\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not create router\")\n\t\t\t}\n\n\t\t\trouter.AddPlugin(plugin.SignalsHandler)\n\n\t\t\tin, err := io.NewSubscriber(\n\t\t\t\tos.Stdin,\n\t\t\t\tio.SubscriberConfig{\n\t\t\t\t\tPollInterval:  time.Second,\n\t\t\t\t\tUnmarshalFunc: io.PayloadUnmarshalFunc,\n\t\t\t\t},\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not create console subscriber\")\n\t\t\t}\n\n\t\t\trouter.AddHandler(\n\t\t\t\t\"produce_from_stdin\",\n\t\t\t\t\"stdin\",\n\t\t\t\tin,\n\t\t\t\ttopic,\n\t\t\t\tproducer,\n\t\t\t\tfunc(msg *message.Message) ([]*message.Message, error) {\n\t\t\t\t\tif string(msg.Payload) == \"\\n\" {\n\t\t\t\t\t\tlogger.Trace(\"Message is empty, don't publish\", nil)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t}\n\t\t\t\t\t// just pass the message along\n\t\t\t\t\treturn message.Messages{msg}, nil\n\t\t\t\t},\n\t\t\t)\n\n\t\t\treturn router.Run(context.Background())\n\t\t},\n\t}\n\n\tparent.AddCommand(cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "tools/mill/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\thomedir \"github.com/mitchellh/go-homedir\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/spf13/viper\"\n\tyaml \"gopkg.in/yaml.v2\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n)\n\nvar cfgFile string\nvar logger watermill.LoggerAdapter\n\nvar rootCmd = &cobra.Command{\n\tUse:   \"mill\",\n\tShort: \"A CLI for Watermill.\",\n\tLong: `A CLI for Watermill.\n\nUse console-based producer or consumer for various pub/sub providers.`,\n\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\tlog := viper.GetBool(\"log\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\ttrace := viper.GetBool(\"trace\")\n\t\tif log || debug || trace {\n\t\t\tlogger = watermill.NewStdLogger(debug, trace)\n\t\t} else {\n\t\t\tlogger = watermill.NopLogger{}\n\t\t}\n\n\t\tif err := checkRequiredFlags(cmd.Flags()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\twriteConfig := viper.GetString(\"writeConfig\")\n\t\tif writeConfig != \"\" {\n\t\t\tsettings := viper.AllSettings()\n\t\t\tdelete(settings, \"writeconfig\")\n\t\t\tb, err := yaml.Marshal(settings)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not marshal config to yaml\")\n\t\t\t}\n\n\t\t\tf, err := os.Create(writeConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not create file for write\")\n\t\t\t}\n\t\t\t_, err = fmt.Fprintf(f, \"%s\", b)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"could not write to file\")\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\tcobra.OnInitialize(initConfig)\n\trootCmd.PersistentFlags().SortFlags = false\n\n\trootCmd.PersistentFlags().StringVar(&cfgFile, \"config\", \"\", \"config file (default is $HOME/.mill.yaml)\")\n\n\toutputFlags := pflag.NewFlagSet(\"output\", pflag.ExitOnError)\n\toutputFlags.BoolP(\"log\", \"l\", false, \"If true, the logger output is sent to stderr. No logger output otherwise.\")\n\tensure(viper.BindPFlag(\"log\", outputFlags.Lookup(\"log\")))\n\n\toutputFlags.BoolP(\"debug\", \"d\", false, \"If true, debug output is enabled from the logger\")\n\tensure(viper.BindPFlag(\"debug\", outputFlags.Lookup(\"debug\")))\n\n\toutputFlags.Bool(\"trace\", false, \"If true, trace output is enabled from the logger\")\n\tensure(viper.BindPFlag(\"trace\", outputFlags.Lookup(\"trace\")))\n\n\toutputFlags.String(\"write-config\", \"\", \"Write the config of the current command as yaml to the specified path\")\n\tensure(viper.BindPFlag(\"writeConfig\", outputFlags.Lookup(\"write-config\")))\n\n\trootCmd.PersistentFlags().AddFlagSet(outputFlags)\n}\n\n// initConfig reads in config file and ENV variables if set.\nfunc initConfig() {\n\tif cfgFile != \"\" {\n\t\t// Use config file from the flag.\n\t\tviper.SetConfigFile(cfgFile)\n\t} else {\n\t\t// Find home directory.\n\t\thome, err := homedir.Dir()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tviper.AddConfigPath(home)\n\t\tviper.SetConfigName(\".mill\")\n\t}\n\n\t// read in environment variables that match\n\tviper.AutomaticEnv()\n\n\t// If a config file is found, read it in.\n\tif err := viper.ReadInConfig(); err == nil {\n\t\tfmt.Println(\"Using config file:\", viper.ConfigFileUsed())\n\t}\n}\n\nfunc ensure(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc checkRequiredFlags(flags *pflag.FlagSet) error {\n\trequiredError := false\n\tflagName := \"\"\n\n\tflags.VisitAll(func(flag *pflag.Flag) {\n\t\trequiredAnnotation := flag.Annotations[cobra.BashCompOneRequiredFlag]\n\t\tif len(requiredAnnotation) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tflagRequired := requiredAnnotation[0] == \"true\"\n\n\t\tif flagRequired && !flag.Changed {\n\t\t\trequiredError = true\n\t\t\tflagName = flag.Name\n\t\t}\n\t})\n\n\tif requiredError {\n\t\treturn errors.New(\"Required flag `\" + flagName + \"` has not been set\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "tools/mill/go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill/tools/mill\n\ngo 1.25\n\nrequire (\n\tcloud.google.com/go/pubsub v1.50.0\n\tgithub.com/IBM/sarama v1.46.0\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2\n\tgithub.com/ThreeDotsLabs/watermill-googlecloud v1.2.6\n\tgithub.com/ThreeDotsLabs/watermill-io v1.1.2\n\tgithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/spf13/cobra v1.10.1\n\tgithub.com/spf13/pflag v1.0.9\n\tgithub.com/spf13/viper v1.20.1\n\tgoogle.golang.org/api v0.248.0\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tcloud.google.com/go v0.121.6 // indirect\n\tcloud.google.com/go/auth v0.16.5 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.8.0 // indirect\n\tcloud.google.com/go/iam v1.5.2 // indirect\n\tcloud.google.com/go/pubsub/v2 v2.0.0 // indirect\n\tgithub.com/cenkalti/backoff/v3 v3.2.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/rabbitmq/amqp091-go v1.10.0 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/sagikazarmark/locafero v0.10.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.14.0 // indirect\n\tgithub.com/spf13/cast v1.9.2 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgolang.org/x/crypto v0.41.0 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect\n\tgoogle.golang.org/grpc v1.75.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.8 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "tools/mill/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=\ncloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=\ncloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=\ncloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=\ncloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=\ncloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=\ncloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=\ncloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=\ncloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=\ncloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=\ncloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=\ncloud.google.com/go/pubsub v1.50.0 h1:hnYpOIxVlgVD1Z8LN7est4DQZK3K6tvZNurZjIVjUe0=\ncloud.google.com/go/pubsub v1.50.0/go.mod h1:Di2Y+nqXBpIS+dXUEJPQzLh8PbIQZMLE9IVUFhf2zmM=\ncloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=\ncloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=\ngithub.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2 h1:aeyFSR4SUsbszmocuFiYY13nsHorc6CXIS2Hy7+xgFU=\ngithub.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2/go.mod h1:+8tCh6VCuBcQWhfETCwzRINKQ1uyeg9moH3h7jMKxQk=\ngithub.com/ThreeDotsLabs/watermill-googlecloud v1.2.6 h1:Ll1NzWoiEXr3mtQ6APuVfMBoRNeGeLLvPcclYrpPTCA=\ngithub.com/ThreeDotsLabs/watermill-googlecloud v1.2.6/go.mod h1:74wkEkvh9NawpHArWQ7OhHnuldkFj6+J//ZMi0Fgw58=\ngithub.com/ThreeDotsLabs/watermill-io v1.1.2 h1:t3wismmE++6HbB+fDnSdvLtSKs0yYaSXRtLmyUcRyTk=\ngithub.com/ThreeDotsLabs/watermill-io v1.1.2/go.mod h1:DF6rhoPWBOeWRW/1wWjNfLkke8rZsB5BUzBox/L6fRI=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0 h1:DfhVM1ieq+rb+bboB7aoymUgfKsEM3UbH3noQZ6+RJ4=\ngithub.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.0/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=\ngithub.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=\ngithub.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=\ngithub.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=\ngithub.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=\ngithub.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=\ngithub.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=\ngithub.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=\ngithub.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=\ngithub.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=\ngithub.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=\ngithub.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=\ngo.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=\ngoogle.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=\ngoogle.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=\ngoogle.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=\ngoogle.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "tools/mill/main.go",
    "content": "package main\n\nimport \"github.com/ThreeDotsLabs/watermill/tools/mill/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n\n// TODO: alternative input/output modes: json, gob, protobuf... (?)\n"
  },
  {
    "path": "tools/pq/README.md",
    "content": "# pq\n\npq is a CLI tool for working with delayed messages on poison queues.\n\nFor now, it supports the PostgreSQL Pub/Sub implementation.\n\n## Install\n\n```bash\ngo install github.com/ThreeDotsLabs/watermill/tools/pq@latest\n```\n\n## Usage\n\nSet the `DATABASE_URL` environment variable to your PostgreSQL connection string.\n\nFor example, to connect to the database used for the [delayed requeue example](../../_examples/real-world-examples/delayed-requeue):\n\n```bash\nexport DATABASE_URL=\"postgres://watermill:password@postgres:5432/watermill?sslmode=disable\"\n```\n\n```bash\npq -backend postgres -topic requeue\n```\n\nThis will use the default `watermill_` prefix, so will use the `watermill_requeue` table.\n\nIf you use a custom prefix, use the `-raw-topic` flag instead:\n\n```bash\npq -backend postgres -raw-topic my_prefix_requeue\n```\n\n## Commands\n\n- Requeue — Updates the `_watermill_delayed_until` metadata to the current time, so the message will be instantly requeued.\n- Ack — Removes the message from the queue (be careful — you will lose the message forever).\n"
  },
  {
    "path": "tools/pq/backend/postgres.go",
    "content": "package backend\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/lib/pq\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/delay\"\n\t\"github.com/ThreeDotsLabs/watermill/tools/pq/cli\"\n)\n\ntype PostgresMessage struct {\n\tOffset   int    `db:\"offset\"`\n\tUUID     string `db:\"uuid\"`\n\tPayload  string `db:\"payload\"`\n\tMetadata string `db:\"metadata\"`\n}\n\ntype PostgresBackend struct {\n\tdb     *sqlx.DB\n\tconfig cli.BackendConfig\n}\n\nfunc NewPostgresBackend(ctx context.Context, config cli.BackendConfig) (*PostgresBackend, error) {\n\tdbURL := os.Getenv(\"DATABASE_URL\")\n\tif dbURL == \"\" {\n\t\treturn nil, fmt.Errorf(\"missing DATABASE_URL\")\n\t}\n\n\tdb, err := sqlx.Connect(\"postgres\", dbURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &PostgresBackend{\n\t\tdb:     db,\n\t\tconfig: config,\n\t}, nil\n}\n\nfunc (r *PostgresBackend) AllMessages(ctx context.Context) ([]cli.Message, error) {\n\tvar dbMessages []PostgresMessage\n\terr := r.db.SelectContext(ctx, &dbMessages, fmt.Sprintf(`SELECT \"offset\", uuid, payload, metadata FROM %v WHERE acked = false ORDER BY \"offset\"`, r.topic()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar messages []cli.Message\n\n\tfor _, dbMsg := range dbMessages {\n\t\tvar metadata map[string]string\n\t\terr := json.Unmarshal([]byte(dbMsg.Metadata), &metadata)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmsg, err := cli.NewMessage(fmt.Sprint(dbMsg.Offset), dbMsg.UUID, dbMsg.Payload, metadata)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmessages = append(messages, msg)\n\t}\n\n\treturn messages, nil\n}\n\nfunc (r *PostgresBackend) Requeue(ctx context.Context, msg cli.Message) error {\n\t_, 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()),\n\t\tdelay.DelayedUntilKey, time.Now().UTC().Format(time.RFC3339), msg.ID,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (r *PostgresBackend) Ack(ctx context.Context, msg cli.Message) error {\n\t_, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET acked = true WHERE \"offset\" = %v`, r.topic(), msg.ID))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (r *PostgresBackend) topic() string {\n\tif r.config.Topic != \"\" {\n\t\treturn fmt.Sprintf(`\"watermill_%v\"`, r.config.Topic)\n\t}\n\n\treturn fmt.Sprintf(`\"%v\"`, r.config.RawTopic)\n}\n"
  },
  {
    "path": "tools/pq/cli/backend.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\n\t\"github.com/pkg/errors\"\n)\n\ntype BackendConfig struct {\n\tTopic    string\n\tRawTopic string\n}\n\nfunc (c BackendConfig) Validate() error {\n\tif c.Topic == \"\" && c.RawTopic == \"\" {\n\t\treturn errors.New(\"topic or raw topic must be provided\")\n\t}\n\n\tif c.Topic != \"\" && c.RawTopic != \"\" {\n\t\treturn errors.New(\"only one of topic or raw topic must be provided\")\n\t}\n\n\treturn nil\n}\n\ntype BackendConstructor func(ctx context.Context, cfg BackendConfig) (Backend, error)\n\ntype Backend interface {\n\tAllMessages(ctx context.Context) ([]Message, error)\n\tRequeue(ctx context.Context, msg Message) error\n\tAck(ctx context.Context, msg Message) error\n}\n"
  },
  {
    "path": "tools/pq/cli/message.go",
    "content": "package cli\n\nimport (\n\t\"time\"\n\n\t\"github.com/ThreeDotsLabs/watermill/components/delay\"\n\t\"github.com/ThreeDotsLabs/watermill/message/router/middleware\"\n)\n\ntype Message struct {\n\t// ID is a unique message ID across the Pub/Sub's topic.\n\tID       string\n\tUUID     string\n\tPayload  string\n\tMetadata map[string]string\n\n\tOriginalTopic string\n\tDelayedUntil  string\n\tDelayedFor    string\n\tRequeueIn     time.Duration\n}\n\nfunc NewMessage(id string, uuid string, payload string, metadata map[string]string) (Message, error) {\n\toriginalTopic := metadata[middleware.PoisonedTopicKey]\n\n\t// Calculate the time until the message should be requeued\n\tdelayedUntil, err := time.Parse(time.RFC3339, metadata[delay.DelayedUntilKey])\n\tif err != nil {\n\t\treturn Message{}, err\n\t}\n\n\tdelayedFor := metadata[delay.DelayedForKey]\n\trequeueIn := delayedUntil.Sub(time.Now().UTC()).Round(time.Second)\n\n\treturn Message{\n\t\tID:            id,\n\t\tUUID:          uuid,\n\t\tPayload:       payload,\n\t\tMetadata:      metadata,\n\t\tOriginalTopic: originalTopic,\n\t\tDelayedUntil:  delayedUntil.String(),\n\t\tDelayedFor:    delayedFor,\n\t\tRequeueIn:     requeueIn,\n\t}, nil\n}\n"
  },
  {
    "path": "tools/pq/cli/model.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/table\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"golang.org/x/exp/maps\"\n)\n\nvar warningStyle = lipgloss.NewStyle().\n\tBackground(lipgloss.Color(\"196\")).\n\tAlign(lipgloss.Center).\n\tPadding(1, 10)\n\nvar dialogStyle = lipgloss.NewStyle().\n\tBorder(lipgloss.RoundedBorder()).\n\tBorderForeground(lipgloss.Color(\"241\")).\n\tPadding(1, 4)\n\nvar buttonStyle = lipgloss.NewStyle()\n\nvar buttonSelectedStyle = lipgloss.NewStyle().\n\tBackground(lipgloss.Color(\"57\"))\n\nvar readOnlyMessageActions = []string{\"<- Back\", \"Show payload\"}\nvar writeMessageActions = []string{\"Requeue\", \"Ack (drop)\"}\n\nvar dialogActions = []string{\"Cancel\", \"Confirm\"}\n\ntype MessagesUpdated struct {\n\tMessages []Message\n}\n\ntype DialogResult struct {\n\tErr error\n}\n\nfunc (m Model) FetchMessages() tea.Cmd {\n\treturn func() tea.Msg {\n\t\tfor {\n\t\t\tmsgs, err := m.backend.AllMessages(context.Background())\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tm.sub <- MessagesUpdated{\n\t\t\t\tMessages: msgs,\n\t\t\t}\n\n\t\t\ttime.Sleep(time.Second)\n\t\t}\n\t}\n}\n\nfunc (m Model) WaitForMessages() tea.Cmd {\n\treturn func() tea.Msg {\n\t\treturn <-m.sub\n\t}\n}\n\nvar baseStyle = lipgloss.NewStyle().\n\tBorderStyle(lipgloss.NormalBorder()).\n\tBorderForeground(lipgloss.Color(\"240\"))\n\ntype Model struct {\n\tbackend Backend\n\tsub     chan MessagesUpdated\n\n\tchosenMessage     *Message\n\tchosenMessageGone bool\n\n\ttable    table.Model\n\tmessages []Message\n\n\tchosenAction  int\n\tcurrentDialog *Dialog\n\n\tshowingPayload  bool\n\tpayloadViewport viewport.Model\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn tea.Batch(\n\t\tm.FetchMessages(),\n\t\tm.WaitForMessages(),\n\t)\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase MessagesUpdated:\n\t\trows := make([]table.Row, len(msg.Messages))\n\t\tfor i, message := range msg.Messages {\n\t\t\trows[i] = table.Row{\n\t\t\t\tmessage.ID,\n\t\t\t\tmessage.UUID,\n\t\t\t\tmessage.OriginalTopic,\n\t\t\t\tmessage.DelayedFor,\n\t\t\t\tmessage.RequeueIn.String(),\n\t\t\t}\n\t\t}\n\t\tm.table.SetRows(rows)\n\t\tm.messages = msg.Messages\n\n\t\t// If the chosen message is no longer in the list, go back to the table.\n\t\t// This is to avoid accidentally making an action on a message that has been requeued or deleted.\n\t\tif m.chosenMessage != nil {\n\t\t\tfound := false\n\t\t\tfor _, message := range m.messages {\n\t\t\t\tif message.ID == m.chosenMessage.ID {\n\t\t\t\t\tfoundMessage := message\n\t\t\t\t\tm.chosenMessage = &foundMessage\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif found {\n\t\t\t\tm.chosenMessageGone = false\n\t\t\t} else {\n\t\t\t\tif !m.chosenMessageGone {\n\t\t\t\t\tm.chosenAction = 0\n\t\t\t\t}\n\n\t\t\t\tm.chosenMessageGone = true\n\t\t\t}\n\t\t}\n\n\t\treturn m, m.WaitForMessages()\n\t}\n\n\tif m.chosenMessage == nil {\n\t\tswitch msg := msg.(type) {\n\t\tcase tea.KeyMsg:\n\t\t\tswitch msg.String() {\n\t\t\tcase \"ctrl+c\", \"q\":\n\t\t\t\treturn m, tea.Quit\n\t\t\tcase \" \", \"enter\":\n\t\t\t\tc := m.table.Cursor()\n\t\t\t\tm.chosenAction = 0\n\t\t\t\tchosenMessage := m.messages[c]\n\t\t\t\tm.chosenMessage = &chosenMessage\n\t\t\t\tm.chosenMessageGone = false\n\t\t\t}\n\t\t}\n\n\t\tvar cmd tea.Cmd\n\t\tm.table, cmd = m.table.Update(msg)\n\t\treturn m, cmd\n\t} else if m.showingPayload {\n\t\tswitch msg := msg.(type) {\n\t\tcase tea.KeyMsg:\n\t\t\tswitch msg.String() {\n\t\t\tcase \"ctrl+c\", \"q\":\n\t\t\t\treturn m, tea.Quit\n\t\t\tcase \"esc\", \"backspace\":\n\t\t\t\tm.showingPayload = false\n\t\t\t}\n\t\t}\n\n\t\tvar cmd tea.Cmd\n\t\tm.payloadViewport, cmd = m.payloadViewport.Update(msg)\n\t\treturn m, cmd\n\t} else if m.currentDialog != nil {\n\t\tswitch msg := msg.(type) {\n\t\tcase tea.KeyMsg:\n\t\t\tswitch msg.String() {\n\t\t\tcase \"ctrl+c\", \"q\":\n\t\t\t\treturn m, tea.Quit\n\t\t\tcase \"esc\", \"backspace\":\n\t\t\t\tm.currentDialog = nil\n\t\t\tcase \"h\", \"left\":\n\t\t\t\tm.currentDialog.Choice--\n\t\t\t\tif m.currentDialog.Choice < 0 {\n\t\t\t\t\tm.currentDialog.Choice = 0\n\t\t\t\t}\n\t\t\tcase \"l\", \"right\":\n\t\t\t\tm.currentDialog.Choice++\n\t\t\t\tif m.currentDialog.Choice >= len(dialogActions) {\n\t\t\t\t\tm.currentDialog.Choice = len(dialogActions) - 1\n\t\t\t\t}\n\t\t\tcase \" \", \"enter\":\n\t\t\t\tswitch m.currentDialog.Choice {\n\t\t\t\tcase 0:\n\t\t\t\t\tm.currentDialog = nil\n\t\t\t\tcase 1:\n\t\t\t\t\tm.currentDialog.Running = true\n\t\t\t\t\treturn m, m.currentDialog.Action\n\t\t\t\t}\n\t\t\t}\n\t\tcase DialogResult:\n\t\t\tif msg.Err != nil {\n\t\t\t\t// TODO Could be handled better\n\t\t\t\tpanic(msg.Err)\n\t\t\t}\n\n\t\t\tm.currentDialog = nil\n\t\t}\n\n\t\treturn m, nil\n\t} else {\n\t\tmessageActions := len(readOnlyMessageActions)\n\t\tif !m.chosenMessageGone {\n\t\t\tmessageActions += len(writeMessageActions)\n\t\t}\n\n\t\tswitch msg := msg.(type) {\n\t\tcase tea.KeyMsg:\n\t\t\tswitch msg.String() {\n\t\t\tcase \"ctrl+c\", \"q\":\n\t\t\t\treturn m, tea.Quit\n\t\t\tcase \"esc\", \"backspace\":\n\t\t\t\tm.chosenMessage = nil\n\t\t\t\tm.chosenMessageGone = false\n\t\t\tcase \"j\", \"down\":\n\t\t\t\tm.chosenAction++\n\t\t\t\tif m.chosenAction >= messageActions {\n\t\t\t\t\tm.chosenAction = messageActions - 1\n\t\t\t\t}\n\t\t\tcase \"k\", \"up\":\n\t\t\t\tm.chosenAction--\n\t\t\t\tif m.chosenAction < 0 {\n\t\t\t\t\tm.chosenAction = 0\n\t\t\t\t}\n\t\t\tcase \" \", \"enter\":\n\t\t\t\tswitch m.chosenAction {\n\t\t\t\tcase 0:\n\t\t\t\t\tm.chosenMessage = nil\n\t\t\t\t\tm.chosenMessageGone = false\n\t\t\t\tcase 1:\n\t\t\t\t\t// Show payload\n\t\t\t\t\tm.showingPayload = true\n\t\t\t\t\tm.payloadViewport = viewport.New(80, 20)\n\t\t\t\t\tb := lipgloss.RoundedBorder()\n\t\t\t\t\tm.payloadViewport.Style = lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)\n\n\t\t\t\t\tpayload := m.chosenMessage.Payload\n\n\t\t\t\t\tvar jsonPayload any\n\t\t\t\t\terr := json.Unmarshal([]byte(payload), &jsonPayload)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tpretty, err := json.MarshalIndent(jsonPayload, \"\", \"    \")\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tpayload = string(pretty)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tm.payloadViewport.SetContent(payload)\n\t\t\t\tcase 2:\n\t\t\t\t\tchosenMessage := *m.chosenMessage\n\t\t\t\t\tm.currentDialog = &Dialog{\n\t\t\t\t\t\tPrompt: \"Requeue message? It will go back to the original topic.\",\n\t\t\t\t\t\tAction: func() tea.Msg {\n\t\t\t\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\t\t\t\t\t\tdefer cancel()\n\t\t\t\t\t\t\treturn DialogResult{\n\t\t\t\t\t\t\t\tErr: m.backend.Requeue(ctx, chosenMessage),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\tcase 3:\n\t\t\t\t\tchosenMessage := *m.chosenMessage\n\t\t\t\t\tm.currentDialog = &Dialog{\n\t\t\t\t\t\tPrompt: \"Acknowledge message? It will be dropped from the topic.\",\n\t\t\t\t\t\tAction: func() tea.Msg {\n\t\t\t\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\t\t\t\t\t\tdefer cancel()\n\t\t\t\t\t\t\treturn DialogResult{\n\t\t\t\t\t\t\t\tErr: m.backend.Ack(ctx, chosenMessage),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn m, nil\n\t}\n}\n\nfunc (m Model) View() string {\n\tif m.chosenMessage == nil {\n\t\treturn baseStyle.Render(m.table.View()) + \"\\n  \" + m.table.HelpView() + \"\\n\"\n\t}\n\n\tmsg := m.chosenMessage\n\n\tvar out string\n\n\tif m.chosenMessageGone {\n\t\tout += warningStyle.Render(\"Read only — the message is gone.\")\n\t\tout += \"\\n\"\n\t}\n\n\tout += fmt.Sprintf(\n\t\t\"ID: %v\\nUUID: %v\\nOriginal Topic: %v\\nDelayed For: %v\\nDelayed Until: %v\\nRequeue In: %v\\n\\n\",\n\t\tmsg.ID,\n\t\tmsg.UUID,\n\t\tmsg.OriginalTopic,\n\t\tmsg.DelayedFor,\n\t\tmsg.DelayedUntil,\n\t\tmsg.RequeueIn,\n\t)\n\n\tif m.showingPayload {\n\t\tout += m.payloadViewport.View()\n\t\treturn out\n\t}\n\n\tout += \"Metadata:\\n\"\n\n\tkeys := maps.Keys(msg.Metadata)\n\tslices.Sort(keys)\n\tfor _, k := range keys {\n\t\tv := msg.Metadata[k]\n\t\tout += fmt.Sprintf(\"  %v: %v\\n\", k, v)\n\t}\n\n\tif m.currentDialog != nil {\n\t\tprompt := m.currentDialog.Prompt + \"\\n\\n\"\n\n\t\tif m.currentDialog.Running {\n\t\t\tprompt += \"Running...\"\n\t\t} else {\n\t\t\tfor i, action := range dialogActions {\n\t\t\t\tstyle := buttonStyle\n\t\t\t\tif i == m.currentDialog.Choice {\n\t\t\t\t\tstyle = buttonSelectedStyle\n\t\t\t\t}\n\n\t\t\t\tprompt += fmt.Sprintf(\"%v\", style.MarginLeft(13).Render(action))\n\t\t\t}\n\t\t}\n\n\t\tout += dialogStyle.Render(prompt)\n\t} else {\n\t\tout += \"\\nActions:\\n\"\n\n\t\tmessageActions := readOnlyMessageActions\n\t\tif !m.chosenMessageGone {\n\t\t\tmessageActions = append(messageActions, writeMessageActions...)\n\t\t}\n\n\t\tfor i, action := range messageActions {\n\t\t\tstyle := buttonStyle\n\t\t\tif i == m.chosenAction {\n\t\t\t\tstyle = buttonSelectedStyle\n\t\t\t}\n\n\t\t\tout += fmt.Sprintf(\"%v\\n\", style.MarginLeft(2).Render(action))\n\t\t}\n\t}\n\n\treturn out\n}\n\nfunc NewModel(backend Backend) Model {\n\tcolumns := []table.Column{\n\t\t{Title: \"ID\", Width: 8},\n\t\t{Title: \"UUID\", Width: 40},\n\t\t{Title: \"Original Topic\", Width: 20},\n\t\t{Title: \"Delayed For\", Width: 14},\n\t\t{Title: \"Requeue In\", Width: 14},\n\t}\n\n\tt := table.New(\n\t\ttable.WithColumns(columns),\n\t\ttable.WithFocused(true),\n\t\ttable.WithHeight(20),\n\t)\n\n\ts := table.DefaultStyles()\n\ts.Header = s.Header.\n\t\tBorderStyle(lipgloss.NormalBorder()).\n\t\tBorderForeground(lipgloss.Color(\"240\")).\n\t\tBorderBottom(true).\n\t\tBold(false)\n\ts.Selected = s.Selected.\n\t\tForeground(lipgloss.Color(\"229\")).\n\t\tBackground(lipgloss.Color(\"57\")).\n\t\tBold(false)\n\tt.SetStyles(s)\n\n\treturn Model{\n\t\tbackend: backend,\n\t\tsub:     make(chan MessagesUpdated),\n\t\ttable:   t,\n\t}\n}\n\ntype Dialog struct {\n\tPrompt  string\n\tAction  func() tea.Msg\n\tChoice  int\n\tRunning bool\n}\n"
  },
  {
    "path": "tools/pq/go.mod",
    "content": "module github.com/ThreeDotsLabs/watermill/tools/pq\n\ngo 1.25\n\nrequire (\n\tgithub.com/ThreeDotsLabs/watermill v1.5.1\n\tgithub.com/charmbracelet/bubbles v0.21.0\n\tgithub.com/charmbracelet/bubbletea v1.3.6\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/jmoiron/sqlx v1.4.0\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/pkg/errors v0.9.1\n\tgolang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b\n)\n\nrequire (\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.3.2 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.10.1 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/lithammer/shortuuid/v3 v3.0.7 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n)\n"
  },
  {
    "path": "tools/pq/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=\ngithub.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=\ngithub.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=\ngithub.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=\ngithub.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=\ngithub.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=\ngithub.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=\ngithub.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=\ngithub.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=\ngithub.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=\ngithub.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=\ngithub.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngolang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=\ngolang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "tools/pq/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"log\"\n\n\t\"github.com/ThreeDotsLabs/watermill/tools/pq/backend\"\n\t\"github.com/ThreeDotsLabs/watermill/tools/pq/cli\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n)\n\nvar (\n\tbackendFlag  = flag.String(\"backend\", \"\", \"backend to use\")\n\ttopicFlag    = flag.String(\"topic\", \"\", \"topic to use\")\n\trawTopicFlag = flag.String(\"raw-topic\", \"\", \"raw topic to use\")\n)\n\nfunc main() {\n\tflag.Parse()\n\n\tconfig := cli.BackendConfig{\n\t\tTopic:    *topicFlag,\n\t\tRawTopic: *rawTopicFlag,\n\t}\n\n\terr := config.Validate()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tvar b cli.Backend\n\tswitch *backendFlag {\n\tcase \"postgres\":\n\t\tb, err = backend.NewPostgresBackend(context.Background(), config)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\tdefault:\n\t\tlog.Fatalf(\"unknown backend: %s\", *backendFlag)\n\t}\n\n\tm := cli.NewModel(b)\n\n\tp := tea.NewProgram(m, tea.WithAltScreen())\n\t_, err = p.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "uuid.go",
    "content": "package watermill\n\nimport (\n\t\"crypto/rand\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/lithammer/shortuuid/v3\"\n\t\"github.com/oklog/ulid\"\n)\n\n// NewUUID returns a new UUID Version 4.\nfunc NewUUID() string {\n\treturn uuid.New().String()\n}\n\n// NewShortUUID returns a new short UUID.\nfunc NewShortUUID() string {\n\treturn shortuuid.New()\n}\n\n// NewULID returns a new ULID.\nfunc NewULID() string {\n\treturn ulid.MustNew(ulid.Now(), rand.Reader).String()\n}\n"
  },
  {
    "path": "uuid_test.go",
    "content": "package watermill_test\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/ThreeDotsLabs/watermill\"\n)\n\nfunc testUniqueness(t *testing.T, genFunc func() string) {\n\tproducers := 100\n\tuuidsPerProducer := 10000\n\n\tif testing.Short() {\n\t\tproducers = 10\n\t\tuuidsPerProducer = 1000\n\t}\n\n\tuuidsCount := producers * uuidsPerProducer\n\n\tuuids := make(chan string, uuidsCount)\n\tallGenerated := sync.WaitGroup{}\n\tallGenerated.Add(producers)\n\n\tfor i := 0; i < producers; i++ {\n\t\tgo func() {\n\t\t\tfor j := 0; j < uuidsPerProducer; j++ {\n\t\t\t\tuuids <- genFunc()\n\t\t\t}\n\t\t\tallGenerated.Done()\n\t\t}()\n\t}\n\n\tuniqueUUIDs := make(map[string]struct{}, uuidsCount)\n\n\tallGenerated.Wait()\n\tclose(uuids)\n\n\tfor uuid := range uuids {\n\t\tif _, ok := uniqueUUIDs[uuid]; ok {\n\t\t\tt.Error(uuid, \" has duplicate\")\n\t\t}\n\t\tuniqueUUIDs[uuid] = struct{}{}\n\t}\n}\n\nfunc TestUUID(t *testing.T) {\n\ttestUniqueness(t, watermill.NewUUID)\n}\n\nfunc TestShortUUID(t *testing.T) {\n\ttestUniqueness(t, watermill.NewShortUUID)\n}\n\nfunc TestULID(t *testing.T) {\n\ttestUniqueness(t, watermill.NewULID)\n}\n"
  }
]