[
  {
    "path": ".dialyzer_ignore.exs",
    "content": "[\n]\n"
  },
  {
    "path": ".formatter.exs",
    "content": "[\n  inputs: [\"{.formatter,mix}.exs\", \"{lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": ".github/workflows/CI.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  schedule:\n    - cron: \"0 0 1 */1 *\"\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    name: Elixir ${{ matrix.pair.elixir }} / OTP ${{ matrix.pair.otp }} / NATS ${{ matrix.pair.nats }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        pair:\n          - otp: \"25\"\n            elixir: \"1.14\"\n            nats: \"2.10.0\"\n\n          - otp: \"26\"\n            elixir: \"1.16\"\n            nats: \"2.10.24\"\n\n          - otp: \"27\"\n            elixir: \"1.17\"\n            nats: \"2.10.24\"\n\n          - otp: \"27\"\n            elixir: \"1.18\"\n            nats: \"2.10.24\"\n\n          - otp: \"28\"\n            elixir: \"1.18\"\n            nats: \"2.11.11\"\n\n          - otp: \"28\"\n            elixir: \"1.18\"\n            nats: \"2.10.24\"\n\n          # the main pair handles things like format checking and running dialyzer\n          - main: true\n            otp: \"28\"\n            elixir: \"1.19\"\n            nats: \"2.10.24\"\n\n    env:\n      MIX_ENV: test\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ matrix.pair.elixir }}\n          otp-version: ${{ matrix.pair.otp }}\n\n      - name: Start NATS Jetstream\n        run: docker run --rm -d --network host nats:${{ matrix.pair.nats }} -js\n\n      - name: Restore deps cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: deps-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-${{ hashFiles('mix.lock') }}\n\n      - name: Install package dependencies\n        run: mix deps.get\n\n      - name: Check for valid formatting\n        if: matrix.pair.main\n        run: mix format --check-formatted\n\n      - name: Run unit tests\n        run: mix test --color\n\n      - name: Run NATS 2.11 specific tests\n        if: ${{ matrix.pair.nats == '2.11.11' }}\n        run: mix test --only message_ttl\n\n      - name: Cache Dialyzer PLTs\n        if: matrix.pair.main\n        uses: actions/cache@v4\n        with:\n          path: |\n            priv/plts/*.plt\n            priv/plts/*.plt.hash\n          key: dialyzer-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-${{ hashFiles('mix.lock') }}\n          restore-keys: |\n            dialyzer-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-\n\n      - name: Run Dialyzer\n        if: matrix.pair.main\n        env:\n          MIX_ENV: dev\n        run: |\n          mix dialyzer\n"
  },
  {
    "path": ".gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover\n\n# The directory Mix downloads your dependencies sources to.\n/deps\n\n/tmp\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc\n\n# Dialyzer PLT files\npriv/plts/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Do not commit the counterexamples stored during property testing\n/test/counter_examples\n\n# ASDF file to for specifying which erlang/elixir version to use for local development\n.tool-versions\n\n# Ignore Cluster details\nscripts/cluster/data\nscripts/cluster/logs\nscripts/cluster/pids"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 1.14\n\n* Add `PullConsumer.handle_connected/2` optional callback to get consumer info\n* Add `PullConsumer.handle_status/2` optional callback to observe status messages\n* Added support for `batch_size` option in PullConsumer options to pull messages\n  and acknowledge them in batches.\n* Add `Gnat.Jetstream.API.KV.Entry` with `from_message/2` for parsing a raw\n  NATS message from a KV bucket's underlying stream into a structured entry\n  (operation, key, value, revision, created, delta). Intended to be shared\n  between the built-in `KV.Watcher` and user-supplied `PullConsumer`\n  implementations (e.g. caches that need to detect when they are caught up\n  with the stream). Returns `:ignore` for messages that are not KV records.\n* `KV.Watcher` now uses `KV.Entry` internally; its public callback API is\n  unchanged. The push consumer it creates now enables server-driven flow\n  control and a 5s idle heartbeat (matching nats.go's ordered-consumer\n  defaults), so slow handlers apply backpressure instead of being dropped\n  as slow consumers.\n* **Behavior change (bugfix):** `PullConsumer` no longer forwards JetStream\n  informational status messages (e.g. `100` idle heartbeat, `409` leadership\n  change) to `c:handle_message/2`. These are not stream records and cannot\n  be acked. In single-message mode the consumer now drops them and re-issues\n  a pull request.\n* Add an optional `c:handle_status/2` callback to `Gnat.Jetstream.PullConsumer`\n  for users who want to observe status messages (e.g. log on `409`).\n\n## 1.11\n\n* Allow clients to force authentication without server auth_required by @mmmries in #205\n* Support ephemeral consumers and auto-cleanup consumers in the PullConsumer module by @mmmries in #202\n* Implement regularly scheduled PING/PONG health check by @mmmries in #200\n* Add Erlang 28 with Elixir 1.18 to the build matrix by @davydog187 in #201\n* Use Pager module for KV.contents by @mmmries in #198\n* Hint `:timeout` option in KV's typespec by @rixmann in #193\n\n## 1.10\n\n* Clarify authentication setup during test by @davydog187 in #187\n* Test on Elixir 1.18 and NATS 2.10.24 by @davydog187 in #188\n* Gnat.Jetstream.API.KV.info/3 by @davydog187 in #189\n* Remove function_exported? check for Keyword.validate!/2 by @davydog187 in #186\n* Tiny optimization to KV.list_buckets/1 by @davydog187 in #185\n* make KV-watcher emit :key_added events when the message has a header by @rixmann in #191\n* add :compression to stream attributes by @rixmann in #192\n* fix: unknown field domain in Stream.create (#194) by @c0deaddict\n* feat: add jetstream message metadata helper (#197) by @c0deaddict\n* fix: deliver policy (#196) by @c0deaddict\n\n## 1.9\n\n* Housecleaning by @mmmries in #176\n  * switch to charlist sigils\n  * update to newest nkeys\n  * require elixir 1.14 and erlang 25+\n* Fix incorrect useage of charlist by @davydog187 in #179\n* Soft deprecate is_kv_bucket_stream?/1 in favor of kv_bucket_stream?/1 by @davydog187 in #183\n* Clean up examples in KV by @davydog187 in #181\n* Document options for Gnat.Jetstream.API.KV by @davydog187 in #180\n\n## 1.8\n\n* Integrated the jetstream functionality into this client directly https://github.com/nats-io/nats.ex/pull/146\n* Add ability to list KV buckets https://github.com/nats-io/nats.ex/pull/152\n* Improve CI Reliability https://github.com/nats-io/nats.ex/pull/154\n* Bugfix to treat no streams as an empty list rather than a null https://github.com/nats-io/nats.ex/pull/155\n* Added supported for `allow_direct` and `mirror_direct` attributes of streams https://github.com/nats-io/nats.ex/pull/161\n* Added support for `discard_new_per_subject` attribute of streams https://github.com/nats-io/nats.ex/pull/163\n* Added support for `Object.list_buckets` https://github.com/nats-io/nats.ex/pull/169\n\n## 1.7\n\n * Added support for the NATS [services API](https://github.com/nats-io/nats.go/blob/main/micro/README.md), letting developers participate in service discovery and stats https://github.com/nats-io/nats.ex/pull/141\n * A bugfix to remove the queue_group from a service config and some optimization for the services API https://github.com/nats-io/nats.ex/pull/145\n\n## 1.6\n\n* added the `no_responders` behavior https://github.com/nats-io/nats.ex/pull/137\n\n## 1.5\n\n* add the `inbox_prefix` option https://github.com/nats-io/nats.ex/pull/121\n* add the `Gnat.server_info/1` function https://github.com/nats-io/nats.ex/pull/124\n* fix header parsing issue https://github.com/nats-io/nats.ex/pull/125\n\n## 1.4\n\n* add the `Gnat.request_multi/4` function https://github.com/nats-io/nats.ex/pull/120\n* add elixir 1.13 to the test matrix\n\n## 1.3\n\n* adding support for sending and receiving headers https://github.com/nats-io/nats.ex/pull/116\n\n## 1.2\n\n* `Gnat.Server` behaviour with support in the `ConsumerSupervisor` https://github.com/nats-io/nats.ex/compare/1b1adc85e4b28231218ef87c7fc3445fce854377...b24a7e14325b51fbb93fde7e3d891d18b4fa8afb\n* avoid logging sensitive credentials https://github.com/nats-io/nats.ex/pull/105\n* deprecate Gnat.ping, improved typespecs https://github.com/nats-io/nats.ex/pull/103 \n* relax the version constraint on nimble_parsec https://github.com/nats-io/nats.ex/issues/112\n\n## 1.1\n\n* add support for nkeys and NGS https://github.com/nats-io/nats.ex/pull/101\n* Fix supervisor ConsumerSuperivsor crash https://github.com/nats-io/nats.ex/pull/96\n\n## 1.0\n\n* Make supervisors officially supported https://github.com/nats-io/nats.ex/pull/96\n\n## 0.7.0\n\n* update to telemetry 0.4 https://github.com/nats-io/nats.ex/pull/86 and https://github.com/nats-io/nats.ex/pull/87\n* support for token authentication https://github.com/nats-io/nats.ex/pull/92\n* support elixir 1.9 https://github.com/nats-io/nats.ex/pull/93\n\n## 0.6.0\n\n* Dropped support for Erlang < 19 and Elixir <= 1.5\n* Added Telemetry to the project (thanks @rubysolo)\n* Switched to nimble_parsec for parsing\n  * Updated benchmarking/performance information. We can now do 170k requests per second on a 16-core server.\n* Fixed a bug around re-subscribing for the `ConsumerSupervisor`\n* Pass `sid` when delivering message (thanks @entone)\n* Documentation fixes from @deini and @johannestroeger\n\n## 0.5.0\n\n* Dropped support for Elixir 1.4 and OTP 18 releases. You will need to use Elixir 1.5+ and OTP 19+.\n* Switched to running our tests against gnatsd `1.3.0`\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright 2017 Michael Ries\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "# Maintainers\n\nMaintainership is on a per project basis.\n\n### Maintainers\n  - Colin Sullivan <colin@nats.io> [@ColinSullivan1](https://github.com/ColinSullivan1)\n  - Michael Ries <riesmmm@gmail.com> [@mmmries](https://github.com/mmmries)\n  - Kevin Hoffman <alothien@gmail.com> [@autodidaddict](https//github.com/autodidaddict)\n"
  },
  {
    "path": "README.md",
    "content": "[![hex.pm](https://img.shields.io/hexpm/v/gnat.svg)](https://hex.pm/packages/gnat)\n[![hex.pm](https://img.shields.io/hexpm/dt/gnat.svg)](https://hex.pm/packages/gnat)\n[![hex.pm](https://img.shields.io/hexpm/l/gnat.svg)](https://hex.pm/packages/gnat)\n[![github.com](https://img.shields.io/github/last-commit/nats-io/nats.ex.svg)](https://github.com/nats-io/nats.ex)\n\n![NATS](https://nats.io/img/logos/nats-horizontal-color.png)\n\n# Gnat\n\nA [nats.io](https://nats.io/) client for Elixir.\nThe goals of the project are resiliency, performance, and ease of use.\n\n> Hex documentation available here: https://hex.pm/packages/gnat\n\n## Usage\n\n``` elixir\n{:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222})\n# Or if the server requires TLS you can start a connection with:\n# {:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, tls: true})\n\n{:ok, subscription} = Gnat.sub(gnat, self(), \"pawnee.*\")\n:ok = Gnat.pub(gnat, \"pawnee.news\", \"Leslie Knope recalled from city council (Jammed)\")\nreceive do\n  {:msg, %{body: body, topic: \"pawnee.news\", reply_to: nil}} ->\n    IO.puts(body)\nend\n```\n\n## Authentication\n\n``` elixir\n# with user and password\n{:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, username: \"joe\", password: \"123\", auth_required: true})\n\n# with token\n{:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, token: \"secret\", auth_required: true})\n\n# with an nkey seed\n{:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, nkey_seed: \"SUAM...\", auth_required: true})\n\n# with decentralized user credentials (JWT)\n{:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, nkey_seed: \"SUAM...\", jwt: \"eyJ0eX...\", auth_required: true})\n\n# connect to NGS with JWT\n{:ok, gnat} = Gnat.start_link(%{host: \"connect.ngs.global\", tls: true, jwt: \"ey...\", nkey_seed: \"SUAM...\"})\n```\n\n## TLS Connections\n\n[NATS Server](https://github.com/nats-io/nats-server) is often configured to accept or require TLS connections.\nIn order to connect to these clusters you'll want to pass some extra TLS settings to your `Gnat` connection.\n\n``` elixir\n# using a basic TLS connection\n{:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, tls: true})\n\n# Passing a Client Certificate for verification\n{:ok, gnat} = Gnat.start_link(%{tls: true, ssl_opts: [certfile: \"client-cert.pem\", keyfile: \"client-key.pem\"]})\n```\n\n## Resiliency\n\nIf you would like to stay connected to a cluster of nats servers, you should consider using `Gnat.ConnectionSupervisor` .\nThis can be added to your supervision tree in your project and will handle automatically re-connecting to the cluster.\n\nFor long-lived subscriptions consider using `Gnat.ConsumerSupervisor` .\nThis can also be added to your supervision tree and use a supervised connection to re-establish a subscription.\nIt also handles details like handling each message in a supervised process so you isolate failures and get OTP logs when an unexpected error occurs.\n\n## Services\nIf you supply a module that implements the `Gnat.Services.Server` behavior and the `service_definition` \nconfiguration field to a `Gnat.ConsumerSupervisor`, then this client will automatically take care\nof exposing the service to discovery, responding to pings, and maintaining and exposing statistics like request and error counts, and processing times.\n\n## Instrumentation\n\nGnat uses [telemetry](https://hex.pm/packages/telemetry) to make instrumentation data available to clients.\nIf you want to record metrics around the number of messages or latency of message publishes, subscribes, requests, etc you can do the following in your project:\n\n``` elixir\niex(1)> metrics_function = fn(event_name, measurements, event_meta, config) ->\n  IO.inspect([event_name, measurements, event_meta, config])\n  :ok\nend\n#Function<4.128620087/4 in :erl_eval.expr/5>\niex(2)> names = [[:gnat, :pub], [:gnat, :sub], [:gnat, :message_received], [:gnat, :request], [:gnat, :unsub]]\n[\n  [:gnat, :pub],\n  [:gnat, :sub],\n  [:gnat, :message_received],\n  [:gnat, :request],\n  [:gnat, :unsub],\n  [:gnat, :service_request],\n  [:gnat, :service_error]\n]\niex(3)> :telemetry.attach_many(\"my listener\", names, metrics_function, %{my_config: true})\n:ok\niex(4)> {:ok, gnat} = Gnat.start_link()\n{:ok, #PID<0.203.0>}\niex(5)> Gnat.sub(gnat, self(), \"topic\")\n[[:gnat, :sub], %{latency: 128000}, %{topic: \"topic\"}, %{my_config: true}]\n{:ok, 1}\niex(6)> Gnat.pub(gnat, \"topic\", \"ohai\")\n[[:gnat, :pub], %{latency: 117000}, %{topic: \"topic\"}, %{my_config: true}]\n[[:gnat, :message_received], %{count: 1}, %{topic: \"topic\"}, %{my_config: true}]\n:ok\n```\n\nThe `pub` , `sub` , `request` , and `unsub` events all report the latency of those respective calls.\nThe `message_received` event reports a number of messages like `%{count: 1}` because there isn't a good latency metric to report. Any microservices managed by a consumer supervisor will also report `service_request` and `service_error`. In addition to the `:topic` metadata, microservices will also include `:endpoint` and `:group` (which can be `nil`) in their telemetry reports.\n\nAll of the events (except `unsub` ) include metadata with a `:topic` key so you can split your metrics by topic.\n\n## Benchmarks\n\nPart of the motivation for building this library is to get better performance.\nTo this end, there is a `bench` branch on this project which includes a `server.exs` and `client.exs` that can be used for benchmarking various scenarios.\n\nAs of this commit, the [latest benchmark on a 16-core server](https://gist.github.com/mmmries/08fe44fdd47a6f8838936f41170f270a) shows that you can make 170k+ req/sec or up to 192MB/sec.\n\nThe `bench/*.exs` files also contain some straight-line single-CPU performance tests.\nAs of this commit my 2018 MacBook pro shows.\n\n|               | ips      | average   | deviation | median |\n| ------------- | -------- | --------- | --------- | ------ |\n| parse-128     | 487.67 K | 2.19 μs   | ±1701.54% | 2 μs   |\n| pub - 128     | 96.37 K  | 10.38 μs  | ±102.94%  | 10 μs  |\n| req-reply-128 | 8.32 K   | 120.16 μs | ±23.68%   | 114 μs |\n\n## Development\n\nBefore running the tests make sure you have a locally running copy of `nats-server` ([installation instructions](https://docs.nats.io/running-a-nats-service/introduction/installation)). By default, tests are run with no authentication. Make sure your NATS configuration contains no users, or has an account with [no_auth_user](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/accounts#no-auth-user) explicitly enabled.\n\nWe currently use version `2.10.24` in CI, but anything higher than `2.2.0` should be fine.\nVersions from `0.9.6` up to `2.2.0` should work fine for everything except header support.\nMake sure to enable jetstream with the `nats-server -js` argument and you might also want to enable debug and verbose logging if you're trying to understand the messages being sent to/from nats (ie `nats-server -js -D -V`).\nThe typical `mix test` will run all the basic unit tests.\n\nYou can also run the `multi_server` set of tests that test connectivity to different\n`nats-server` configurations. You can run these with `mix test --only multi_server` .\nThe tests will tell you how to start the different configurations.\n\nThere are also some property-based tests that generate a lot of test cases.\nYou can tune how many test cases by setting the environment variable `N=200 mix test --only property` (default it 100).\n\nFor more details you can look at how Github runs these things in the CI flow.\n"
  },
  {
    "path": "bench/client.exs",
    "content": "Application.put_env(:client, :num_connections, 4)\nnum_requesters = 16\nrequests_per_requester = 500\n\ndefmodule Client do\n  require Logger\n\n  def setup(id) do\n    num_connections = Application.get_env(:client, :num_connections)\n    partition = rem(id, num_connections)\n    String.to_atom(\"gnat#{partition}\")\n  end\n\n  def send_request(gnat, request) do\n    {:ok, _} = Gnat.request(gnat, \"echo\", request)# |> IO.inspect\n  end\n\n  def send_requests(gnat, how_many, request) do\n    :lists.seq(1, how_many)\n    |> Enum.each(fn(_) ->\n      {micro_seconds, _result} = :timer.tc(fn() -> send_request(gnat, request) end)\n      Benchmark.record_rpc_time(micro_seconds)\n    end)\n  end\nend\n\ndefmodule Benchmark do\n  def benchmark(num_actors, requests_per_actor, request) do\n    {:ok, _pid} = Agent.start_link(fn -> [] end, name: __MODULE__)\n    {total_micros, _result} = time_benchmark(num_actors, requests_per_actor, request)\n    total_requests = num_actors * requests_per_actor\n    total_bytes = total_requests * byte_size(request) * 2\n    print_statistics(total_requests, total_bytes, total_micros)\n    Agent.stop(__MODULE__, :normal)\n  end\n\n  def record_rpc_time(micro_seconds) do\n    Agent.update(__MODULE__, fn(list) -> [micro_seconds | list] end)\n  end\n\n  def print_statistics(total_requests, total_bytes, total_micros) do\n    total_seconds = total_micros / 1_000_000.0\n    req_throughput = total_requests / total_seconds\n    kilobyte_throughput = total_bytes / 1024 / total_seconds\n    IO.puts \"It took #{total_seconds}sec\"\n    IO.puts \"\\t#{req_throughput} req/sec\"\n    IO.puts \"\\t#{kilobyte_throughput} kb/sec\"\n    Agent.get(__MODULE__, fn(list_of_rpc_times) ->\n      tc_l = list_of_rpc_times\n      tc_n = Enum.count(list_of_rpc_times)\n      tc_min = :lists.min(tc_l)\n      tc_max = :lists.max(tc_l)\n      sorted = :lists.sort(tc_l)\n      tc_med = :lists.nth(round(tc_n * 0.5), sorted)\n      tc_90th = :lists.nth(round(tc_n * 0.9), sorted)\n      tc_avg = round(Enum.sum(tc_l) / tc_n)\n      IO.puts \"\\tmin: #{tc_min}µs\"\n      IO.puts \"\\tmax: #{tc_max}µs\"\n      IO.puts \"\\tmedian: #{tc_med}µs\"\n      IO.puts \"\\t90th percentile: #{tc_90th}µs\"\n      IO.puts \"\\taverage: #{tc_avg}µs\"\n      IO.puts \"\\t#{tc_min},#{tc_max},#{tc_med},#{tc_90th},#{tc_avg},#{req_throughput},#{kilobyte_throughput}\"\n    end)\n  end\n\n  def time_benchmark(num_actors, requests_per_actor, request) do\n    :timer.tc(fn() ->\n      (1..num_actors) |> Enum.map(fn(i) ->\n        parent = self()\n        spawn(fn() ->\n          gnat = Client.setup(i)\n          #IO.puts \"starting requests #{i}\"\n          Client.send_requests(gnat, requests_per_actor, request)\n          #IO.puts \"done with requests #{i}\"\n          send parent, :ack\n        end)\n      end)\n      wait_for_times(num_actors)\n    end)\n  end\n\n  def wait_for_times(0), do: :done\n  def wait_for_times(n) do\n    receive do\n      :ack ->\n        wait_for_times(n-1)\n    end\n  end\nend\n\nnum_connections = Application.get_env(:client, :num_connections)\nEnum.each(0..(num_connections - 1), fn(i) ->\n  name = :\"gnat#{i}\"\n  {:ok, _pid} = Gnat.start_link(%{}, name: name)\nend)\n:timer.sleep(500) # let the connections get started\n\n#request = \"ping\"\nrequest = :crypto.strong_rand_bytes(16)\nBenchmark.benchmark(num_requesters, requests_per_requester, request)\n"
  },
  {
    "path": "bench/kv_consume.exs",
    "content": "# bench/kv_consume.exs\n#\n# Compares three approaches for consuming all messages from a KV bucket into ETS:\n#\n#   1. Pager (batch 500, ack_policy: :all) — fetch a page, process, ack last, repeat\n#   2. Pull + ack_next pipeline (initial batch 500, ack_policy: :explicit) — prime the\n#      pipeline with a batch request, then ack_next each message to keep flow continuous\n#   3. PullConsumer with batch_size (ack_policy: :all) — the new batch mode using the\n#      actual PullConsumer behaviour, batches messages and acks only the last per batch\n#\n# Prerequisites:\n#   - NATS server with JetStream enabled: nats-server -js\n#   - Run with: mix run bench/kv_consume.exs\n#\n# Optional env vars:\n#   - BENCH_COUNT: number of messages (default 100000)\n#   - BENCH_BATCH: batch size (default 500)\n#   - BENCH_TIME: seconds per scenario (default 60)\n\nLogger.configure(level: :warning)\n\nrequire Logger\nLogger.configure(level: :warning)\n\nalias Gnat.Jetstream.API.{Consumer, KV}\nalias Gnat.Jetstream.API.Util\n\ndefmodule BenchBatchPullConsumer do\n  use Gnat.Jetstream.PullConsumer\n\n  def start(args) do\n    Gnat.Jetstream.PullConsumer.start(__MODULE__, args)\n  end\n\n  @impl true\n  def init(%{tab: tab, notify: pid, expected: expected, batch_size: batch_size}) do\n    consumer = %Consumer{\n      stream_name: \"KV_BENCH_KV\",\n      ack_policy: :all,\n      ack_wait: 30_000_000_000,\n      deliver_policy: :all,\n      replay_policy: :instant\n    }\n\n    {:ok, %{tab: tab, notify: pid, expected: expected, received: 0},\n     connection_name: :gnat_bench, consumer: consumer, batch_size: batch_size}\n  end\n\n  @impl true\n  def handle_message(message, state) do\n    :ets.insert(state.tab, {message.topic, message.body})\n    received = state.received + 1\n\n    if received >= state.expected do\n      send(state.notify, {:done, received})\n    end\n\n    {:ack, %{state | received: received}}\n  end\nend\n\ndefmodule KVConsumeBench do\n  @bucket \"BENCH_KV\"\n  @stream \"KV_BENCH_KV\"\n  @value_size 64\n\n  def setup(conn, count) do\n    # Clean up previous state\n    KV.delete_bucket(conn, @bucket)\n    :timer.sleep(500)\n\n    {:ok, _} = KV.create_bucket(conn, @bucket, history: 1)\n\n    IO.puts(\"Populating #{count} messages (#{@value_size} byte values)...\")\n    start = System.monotonic_time(:millisecond)\n\n    Enum.each(1..count, fn i ->\n      key = \"key.#{String.pad_leading(Integer.to_string(i), 7, \"0\")}\"\n      value = :crypto.strong_rand_bytes(@value_size) |> Base.encode64()\n      :ok = KV.put_value(conn, @bucket, key, value)\n\n      if rem(i, 10_000) == 0 do\n        elapsed = System.monotonic_time(:millisecond) - start\n        rate = round(i / elapsed * 1000)\n        IO.puts(\"  #{i}/#{count} (#{rate} msg/s)\")\n      end\n    end)\n\n    elapsed = System.monotonic_time(:millisecond) - start\n    IO.puts(\"Setup complete: #{count} messages in #{div(elapsed, 1000)}s\\n\")\n  end\n\n  # ---------------------------------------------------------------------------\n  # Strategy 1: Pager (ack_policy: :all, batch fetch, ack last per page)\n  # ---------------------------------------------------------------------------\n  def pager_consume(conn, batch_size) do\n    tab = :ets.new(:pager_cache, [:set])\n\n    {:ok, _} =\n      Gnat.Jetstream.Pager.reduce(conn, @stream, [batch: batch_size], nil, fn msg, acc ->\n        :ets.insert(tab, {msg.topic, msg.body})\n        acc\n      end)\n\n    count = :ets.info(tab, :size)\n    :ets.delete(tab)\n    count\n  end\n\n  # ---------------------------------------------------------------------------\n  # Strategy 2: Pull + ack_next pipeline (ack_policy: :explicit, continuous)\n  # ---------------------------------------------------------------------------\n  def pull_ack_next_consume(conn, batch_size) do\n    {:ok, consumer_info} =\n      Consumer.create(conn, %Consumer{\n        stream_name: @stream,\n        ack_policy: :explicit,\n        deliver_policy: :all,\n        replay_policy: :instant,\n        inactive_threshold: 30_000_000_000\n      })\n\n    total = consumer_info.num_pending\n    inbox = Util.reply_inbox()\n    {:ok, sub} = Gnat.sub(conn, self(), inbox)\n\n    :ok =\n      Consumer.request_next_message(\n        conn,\n        @stream,\n        consumer_info.name,\n        inbox,\n        nil,\n        batch: batch_size,\n        no_wait: true\n      )\n\n    tab = :ets.new(:pull_cache, [:set])\n    receive_with_ack_next(sub, inbox, tab, 0, total)\n\n    count = :ets.info(tab, :size)\n    :ets.delete(tab)\n\n    Gnat.unsub(conn, sub)\n    Consumer.delete(conn, @stream, consumer_info.name)\n\n    count\n  end\n\n  @terminals [\"404\", \"408\"]\n\n  defp receive_with_ack_next(_sub, _inbox, _tab, total, total), do: :ok\n\n  defp receive_with_ack_next(sub, inbox, tab, count, total) do\n    receive do\n      {:msg, %{sid: ^sub, status: status}} when status in @terminals ->\n        receive_with_ack_next(sub, inbox, tab, count, total)\n\n      {:msg, %{sid: ^sub, reply_to: nil}} ->\n        receive_with_ack_next(sub, inbox, tab, count, total)\n\n      {:msg, %{sid: ^sub} = message} ->\n        :ets.insert(tab, {message.topic, message.body})\n\n        if count + 1 < total do\n          Gnat.Jetstream.ack_next(message, inbox)\n        else\n          Gnat.Jetstream.ack(message)\n        end\n\n        receive_with_ack_next(sub, inbox, tab, count + 1, total)\n    after\n      30_000 ->\n        IO.puts(\"WARNING: timeout after receiving #{count}/#{total} messages\")\n        :timeout\n    end\n  end\n\n  # ---------------------------------------------------------------------------\n  # Strategy 3: PullConsumer with batch_size (ack_policy: :all, batch mode)\n  #\n  # Uses the actual PullConsumer behaviour with the new batch_size option.\n  # This is the real-world usage pattern we want to validate.\n  # ---------------------------------------------------------------------------\n  def batch_pull_consumer_consume(expected, batch_size) do\n    tab = :ets.new(:batch_pc_cache, [:set, :public])\n\n    {:ok, pid} =\n      BenchBatchPullConsumer.start(%{\n        tab: tab,\n        notify: self(),\n        expected: expected,\n        batch_size: batch_size\n      })\n\n    receive do\n      {:done, _received} -> :ok\n    after\n      60_000 ->\n        IO.puts(\"WARNING: PullConsumer timeout\")\n    end\n\n    count = :ets.info(tab, :size)\n    Gnat.Jetstream.PullConsumer.close(pid)\n    :ets.delete(tab)\n    count\n  end\nend\n\n# -- Configuration -----------------------------------------------------------\n\ncount = String.to_integer(System.get_env(\"BENCH_COUNT\", \"100000\"))\nbatch = String.to_integer(System.get_env(\"BENCH_BATCH\", \"500\"))\ntime = String.to_integer(System.get_env(\"BENCH_TIME\", \"60\"))\n\nIO.puts(\"\"\"\nKV Consume Benchmark\n====================\nMessages:   #{count}\nBatch size: #{batch}\nTime/scenario: #{time}s\n\"\"\")\n\n# -- Setup --------------------------------------------------------------------\n\n# Named connection for the PullConsumer\nconn_settings = %{\n  name: :gnat_bench,\n  backoff_period: 1_000,\n  connection_settings: [%{host: '127.0.0.1', port: 4222}]\n}\n\n{:ok, _} = Gnat.ConnectionSupervisor.start_link(conn_settings)\n:timer.sleep(500)\n\n# Direct connection for Pager and manual pull\n{:ok, conn} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\n\nKVConsumeBench.setup(conn, count)\n\n# -- Verify all approaches produce correct results ----------------------------\n\nIO.puts(\"Verifying approaches...\")\n\ncount1 = KVConsumeBench.pager_consume(conn, batch)\nIO.puts(\"  Pager:              #{count1} entries\")\n\ncount2 = KVConsumeBench.pull_ack_next_consume(conn, batch)\nIO.puts(\"  Pull+ack_next:      #{count2} entries\")\n\ncount3 = KVConsumeBench.batch_pull_consumer_consume(count, batch)\nIO.puts(\"  Batch PullConsumer: #{count3} entries\")\n\nexpected_counts = [count1, count2, count3]\n\nif Enum.any?(expected_counts, &(&1 != count)) do\n  IO.puts(\"\\nERROR: expected #{count} entries from each approach\")\n  Gnat.stop(conn)\n  System.halt(1)\nend\n\nIO.puts(\"\\nAll approaches verified. Starting benchmark...\\n\")\n\n# -- Benchmark ---------------------------------------------------------------\n\nBenchee.run(\n  %{\n    \"pager (batch #{batch})\" => fn ->\n      KVConsumeBench.pager_consume(conn, batch)\n    end,\n    \"pull+ack_next (initial batch #{batch})\" => fn ->\n      KVConsumeBench.pull_ack_next_consume(conn, batch)\n    end,\n    \"batch_pull_consumer (batch #{batch})\" => fn ->\n      KVConsumeBench.batch_pull_consumer_consume(count, batch)\n    end\n  },\n  time: time,\n  warmup: 0,\n  memory_time: 0,\n  formatters: [{Benchee.Formatters.Console, comparisons: true}]\n)\n\nGnat.stop(conn)\n"
  },
  {
    "path": "bench/parse.exs",
    "content": "msg1024 = :crypto.strong_rand_bytes(1024)\nmsg128  = :crypto.strong_rand_bytes(128)\nmsg16   = :crypto.strong_rand_bytes(16)\n\ninputs = %{\n  \"16 byte\" => \"MSG topic 1 16\\r\\n#{msg16}\\r\\n\",\n  \"128 byte\" => \"MSG topic 1 128\\r\\n#{msg128}\\r\\n\",\n  \"1024 byte\" => \"MSG topic 1 1024\\r\\n#{msg1024}\\r\\n\",\n  \"7 byte with headers\" => \"HMSG SUBJECT 1 REPLY 48 55\\r\\nNATS/1.0\\r\\nHeader1: X\\r\\nHeader1: Y\\r\\nHeader2: Z\\r\\n\\r\\nPAYLOAD\\r\\n\"\n}\n\nparsec = Gnat.Parsec.new()\nBenchee.run(%{\n  \"parsec\" => fn(tcp_packet) -> {_parse, [_msg]} = Gnat.Parsec.parse(parsec, tcp_packet) end,\n}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])\n"
  },
  {
    "path": "bench/publish.exs",
    "content": "inputs = %{\n  \"16 byte\" => :crypto.strong_rand_bytes(16),\n  \"128 byte\" => :crypto.strong_rand_bytes(128),\n  \"1024 byte\" => :crypto.strong_rand_bytes(1024),\n}\n\n{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\n\nBenchee.run(%{\n  \"pub\" => fn(msg) -> :ok = Gnat.pub(client_pid, \"echo\", msg) end,\n}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])\n"
  },
  {
    "path": "bench/request.exs",
    "content": "defmodule EchoServer do\n  def run(gnat) do\n    spawn(fn -> init(gnat) end)\n  end\n\n  def init(gnat) do\n    Gnat.sub(gnat, self(), \"echo\")\n    loop(gnat)\n  end\n\n  def loop(gnat) do\n    receive do\n      {:msg, %{topic: \"echo\", reply_to: reply_to, body: msg}} ->\n        Gnat.pub(gnat, reply_to, msg)\n      other ->\n        IO.puts \"server received: #{inspect other}\"\n    end\n\n    loop(gnat)\n  end\nend\n\n{:ok, server_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\nEchoServer.run(server_pid)\n\ninputs = %{\n  \"16 byte\" => :crypto.strong_rand_bytes(16),\n  \"128 byte\" => :crypto.strong_rand_bytes(128),\n  \"1024 byte\" => :crypto.strong_rand_bytes(1024),\n}\n\n{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\n\nBenchee.run(%{\n  \"request\" => fn(msg) -> {:ok, %{body: _}} = Gnat.request(client_pid, \"echo\", msg) end,\n}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])\n"
  },
  {
    "path": "bench/request_multi.exs",
    "content": "defmodule EchoServer do\n  def run(gnat) do\n    spawn(fn -> init(gnat) end)\n  end\n\n  def init(gnat) do\n    Gnat.sub(gnat, self(), \"echo\")\n    loop(gnat)\n  end\n\n  def loop(gnat) do\n    receive do\n      {:msg, %{topic: \"echo\", reply_to: reply_to, body: msg}} ->\n        Gnat.pub(gnat, reply_to, msg)\n      other ->\n        IO.puts \"server received: #{inspect other}\"\n    end\n\n    loop(gnat)\n  end\nend\n\n{:ok, server_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\n# run 3 servers to get 3 responses\nEchoServer.run(server_pid)\nEchoServer.run(server_pid)\nEchoServer.run(server_pid)\n\ninputs = %{\n  \"16 byte\" => :crypto.strong_rand_bytes(16),\n  \"128 byte\" => :crypto.strong_rand_bytes(128),\n  \"1024 byte\" => :crypto.strong_rand_bytes(1024),\n}\n\n{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\n\nBenchee.run(%{\n  \"request_multi\" => fn(msg) -> {:ok, [%{body: _}, %{}, %{}]} = Gnat.request_multi(client_pid, \"echo\", msg, max_messages: 3) end,\n}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])\n"
  },
  {
    "path": "bench/server.exs",
    "content": "num_connections = 4\nnum_subscribers = 4\n\nEnum.each(0..(num_connections - 1), fn(i) ->\n  name = :\"gnat#{i}\"\n  {:ok, _pid} = Gnat.start_link(%{}, name: name)\nend)\n\nEnum.each(0..(num_subscribers - 1), fn(i) ->\n  name = :\"consumer#{i}\"\n  conn_name = :\"gnat#{rem(i, num_connections)}\"\n  IO.puts \"#{name} will use #{conn_name}\"\n  {:ok, _pid} = Gnat.ConsumerSupervisor.start_link(%{connection_name: conn_name, consuming_function: {EchoServer, :handle}, subscription_topics: [%{topic: \"echo\", queue_group: \"echo\"}]})\nend)\n\ndefmodule EchoServer do\n  def handle(%{body: body, reply_to: reply_to, gnat: gnat_pid}) do\n    Gnat.pub(gnat_pid, reply_to, body)\n  end\n\n  def wait_loop do\n    :timer.sleep(1_000)\n    wait_loop()\n  end\nend\n\nEchoServer.wait_loop()\n"
  },
  {
    "path": "bench/service_bench.exs",
    "content": "defmodule EchoService do\n  use Gnat.Services.Server\n\n  def request(%{body: body}, \"echo\", _group) do\n    {:reply, body}\n  end\n\n  def definition do\n    %{\n      name: \"echo\",\n      description: \"This is an example service\",\n      version: \"0.0.1\",\n      endpoints: [\n        %{\n          name: \"echo\",\n          group_name: \"mygroup\",\n        }\n      ]\n    }\n  end\nend\n\nconn_supervisor_settings = %{\n  name: :gnat, # (required) the registered named you want to give the Gnat connection\n  backoff_period: 1_000, # number of milliseconds to wait between consecutive reconnect attempts (default: 2_000)\n  connection_settings: [\n    %{host: '127.0.0.1', port: 4222},\n  ]\n}\n{:ok, _pid} = Gnat.ConnectionSupervisor.start_link(conn_supervisor_settings)\n\n# let the connection get established\n:timer.sleep(100)\n\nconsumer_supervisor_settings = %{\n  connection_name: :gnat,\n  module: EchoService, # a module that implements the Gnat.Services.Server behaviour\n  service_definition: EchoService.definition()\n}\n\n{:ok, _pid} = Gnat.ConsumerSupervisor.start_link(consumer_supervisor_settings)\n\n# wait for the connection and consumer to be ready\n:timer.sleep(2000)\n\n{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})\n\ninputs = %{\n  \"16 byte\" => :crypto.strong_rand_bytes(16),\n  \"256 byte\" => :crypto.strong_rand_bytes(256),\n  \"1024 byte\" => :crypto.strong_rand_bytes(1024),\n}\n\nBenchee.run(%{\n  \"service\" => fn(msg) -> {:ok, %{body: ^msg}} = Gnat.request(client_pid, \"mygroup.echo\", msg) end,\n}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])\n"
  },
  {
    "path": "dependencies.md",
    "content": "# Project Dependencies\n\nThis is a list of dependencies that will be pulled into your project when you use this library. Unless otherwise specified all dependencies are Hex packages obtained via mix.\nThis list of dependencies was produced on October 13 2023.\n\n| Dependency | License |\n|-|-|\n| ed25519 1.4.0 | MIT |\n| cowlib 2.11.0 | ISC |\n| deep_merge 1.0.0 | MIT |\n| jason 1.2.2  | Apache 2.0 |\n| nimble_parsec 1.2.0  | Apache 2.0 |\n| nkeys 0.2.1  | MIT |\n| telemetry 1.0.0 (rebar3) | Apache 2.0 |\n\n# Development Dependencies\n\nThis is a list of dependencies used to build and test this library.\n\n| Dependency | License |\n|-|-|\n| propcheck 1.4.1 | GPL 3.0 |\n| proper 1.4.0 | GPL 3.0 |\n| erlex 0.2.6  | Apache 2.0 |\n| makeup 1.0.5  | BSD |\n| makeup_elixir 0.15.2 | BSD |\n| makeup_erlang 0.1.1 | BSD |\n| deep_merge 0.2.0  | MIT |\n| benchee 1.0.1  | MIT |\n| dialyxir 1.1.0 | Apache 2.0 |\n| earmark 1.3.1  | Apache 2.0 |\n| makeup_elixir 0.13.0  | BSD |\n| earmark_parser 1.4.18 | Apache-2.0 |\n| ex_doc 0.26.0  | Apache 2.0  |\n"
  },
  {
    "path": "docs/js/guides/broadway.md",
    "content": "# Using Broadway with Jetstream\n\nBroadway is a library which allows building concurrent and multi-stage data ingestion and data\nprocessing pipelines with Elixir easily. You can learn about it more in\n[Broadway documentation](https://hexdocs.pm/broadway/introduction.html).\n\nJetstream library comes with tools necessary to use NATS Jetstream with Broadway.\n\n## Getting started\n\nIn order to use Broadway with NATS Jetstream you need to:\n\n1. Setup a NATS Server with JetStream turned on\n2. Create stream and consumer on NATS server\n3. Configure Gnat connection in your Elixir project\n4. Configure your project to use Broadway\n\nIn this guide, we are going to focus on the fourth point. To learn how to start Jetstream locally\nwith Docker Compose and then add Gnat and Jetstream to your application, see the Starting Jetstream\nsection in [Getting Started guide](../introduction/getting_started.md).\n\n### Adding Broadway to your application\n\nOnce we have NATS with JetStream running and the stream and consumer we are going to use are\ncreated, we can proceed to adding Broadway to our project. First, put `:broadway` to the list of\ndependencies in `mix.exs`.\n\n```elixir\ndefp deps do\n  [\n    ...\n    {:broadway, ...version...},\n    ...\n  ]\nend\n```\n\nVisit [Broadway page on Hex.pm](https://hex.pm/packages/broadway) to check for current version\nto put in `deps`.\n\nTo install the dependencies, run:\n\n```shell\nmix deps.get\n```\n\n### Defining the pipeline configuration\n\nThe next step is to define your Broadway module. We need to implement three functions in order\nto define a Broadway pipeline: `start_link/1`, `handle_message/3` and `handle_batch/4`.\nLet's create `start_link/1` first:\n\n```elixir\ndefmodule MyBroadway do\n  use Broadway\n\n  alias Broadway.Message\n\n  def start_link(_opts) do\n    Broadway.start_link(\n      __MODULE__,\n      name: MyBroadwayExample,\n      producer: [\n        module: {\n          OffBroadway.Jetstream.Producer,\n          connection_name: :gnat,\n          stream_name: \"TEST_STREAM\",\n          consumer_name: \"TEST_CONSUMER\"\n        },\n        concurrency: 10\n      ],\n      processors: [\n        default: [concurrency: 10]\n      ],\n      batchers: [\n        default: [\n          concurrency: 5,\n          batch_size: 10,\n          batch_timeout: 2_000\n        ]\n      ]\n      ...\n    )\n  end\n\n  ...callbacks..\nend\n```\n\nAll `start_link/1` does is just delegating to `Broadway.start_link/2`.\n\nTo understand what all these options mean and to learn about other possible settings, visit\n[Broadway documentation](https://hexdocs.pm/broadway/Broadway.html).\n\nThe part that interests us the most in this guide is the `producer.module`. Here we're choosing\n`OffBroadway.Jetstream.Producer` as the producer module and passing the connection options,\nsuch as Gnat process name and stream name. For full list of available options, visit\n[Producer](`OffBroadway.Jetstream.Producer`) documentation.\n\n### Implementing Broadway callbacks\n\nBroadway requires some callbacks to be implemented in order to process messages. For full list\nof available callbacks visit\n[Broadway documentation](https://hexdocs.pm/broadway/Broadway.html#callbacks).\n\nA simple example:\n\n```elixir\ndefmodule MyBroadway do\n  use Broadway\n\n  alias Broadway.Message\n\n  ...start_link...\n\n  def handle_message(_processor_name, message, _context) do\n    message\n    |> Message.update_data(&process_data/1)\n    |> case do\n      \"FOO\" -> Message.configure_ack(on_success: :term)\n      \"BAR\" -> Message.configure_ack(on_success: :nack)\n      message -> message\n    end\n  end\n\n  defp process_data(data) do\n    String.upcase(data)\n  end\n\n  def handle_batch(_, messages, _, _) do\n    list = messages |> Enum.map(fn e -> e.data end)\n    IO.puts(\"Got a batch: #{inspect(list)}. Sending acknowledgements...\")\n    messages\n  end\n```\n\nFirst, in `handle_message/3` we update our messages' data individually by converting them to\nuppercase. Then, in the same callback, we're changing the success ack option of the message\nto `:term` if its content is `\"FOO\"` or to `:nack` if the message is `\"BAR\"`. In the end we\nprint each batch in `handle_batch/4`. It's not quite useful but should be enough for this\nguide.\n\n## Running the Broadway pipeline\n\nOnce we have our pipeline fully defined, we need to add it as a child in the supervision tree.\nMost applications have a supervision tree defined at `lib/my_app/application.ex`.\n\n```elixir\nchildren = [\n  {MyBroadway, []}\n]\n\nSupervisor.start_link(children, strategy: :one_for_one)\n```\n\nYou can now test the pipeline. Let's start the application:\n\n```shell\niex -S mix\n```\n\nUse Gnat API to send messages to your stream:\n\n```elixir\nGnat.pub(:gnat, \"test_subject\", \"foo\")\nGnat.pub(:gnat, \"test_subject\", \"bar\")\nGnat.pub(:gnat, \"test_subject\", \"baz\")\n```\n\nBatcher should then print:\n\n```\nGot a batch: [\"FOO\", \"BAR\", \"BAZ\"]. Sending acknowledgements...\n```"
  },
  {
    "path": "docs/js/guides/managing.md",
    "content": "# Managing Streams and Consumers\n\nJetstream provides a JSON API for managing streams and consumers.\nThis library exposes this API via interactions with the `Jetstream.Api.Stream` and `Jetstream.Api.Consumer` modules.\n\nThese modules act as native wrappers for the API and do not attempt to simplify any of the common use-cases.\nAs this library matures we may introduce a separate layer of functions to handle these scenarios, but for now our aim is to provide full access to the Jetstream API."
  },
  {
    "path": "docs/js/guides/push_based_consumer.md",
    "content": "# Push based consumer\n\n```elixir\n# Start a nats server with jetstream enabled and default configs\n# Now run the following snippets in an IEx terminal\nalias Jetstream.API.{Consumer,Stream}\n\n# Setup a connection to the nats server and create the stream/consumer\n# This is the equivalent of these two nats cli commands\n#   nats stream add TEST --subjects=\"greetings\" --max-msgs=-1 --max-msg-size=-1 --max-bytes=-1 --max-age=-1 --storage=file --retention=limits --discard=old\n#   nats consumer add TEST TEST --target consumer.greetings --replay instant --deliver=all --ack all --wait=5s --filter=\"\" --max-deliver=10\n{:ok, connection} = Gnat.start_link()\nstream = %Stream{name: \"TEST\", subjects: [\"greetings\"]}\n{:ok, _response} = Stream.create(connection, stream)\nconsumer = %Consumer{stream_name: \"TEST\", name: \"TEST\", deliver_subject: \"consumer.greetings\", ack_wait: 5_000_000_000, max_deliver: 10}\n{:ok, _response} = Consumer.create(connection, consumer)\n\n# Setup Consuming Function\ndefmodule Subscriber do\n  def handle(msg) do\n    IO.inspect(msg)\n    case msg.body do\n      \"hola\" -> Jetstream.ack(msg)\n      \"bom dia\" -> Jetstream.nack(msg)\n      _ -> nil\n    end\n  end\nend\n\n# normally you would add the `ConnectionSupervisor` and `ConsumerSupervisor` to your supervisrion tree\n# here we start them up manually in an IEx session\n{:ok, _pid} = Gnat.ConnectionSupervisor.start_link(%{\n  name: :gnat,\n  backoff_period: 4_000,\n  connection_settings: [\n    %{}\n  ]\n})\n{:ok, _pid} = Gnat.ConsumerSupervisor.start_link(%{\n  connection_name: :gnat,\n  consuming_function: {Subscriber, :handle},\n  subscription_topics: [\n    %{topic: \"consumer.greetings\"}\n  ]\n})\n\n# now publish some messages into the stream\nGnat.pub(:gnat, \"greetings\", \"hello\") # no ack will be sent back, so you'll see this message received 10 times with a 5sec pause between each one\nGnat.pub(:gnat, \"greetings\", \"hola\") # an ack is sent back so this will only be received once\nGnat.pub(:gnat, \"greetings\", \"bom dia\") # a -NAK is sent back so you'll see this received 10 times very quickly\n```"
  },
  {
    "path": "docs/js/introduction/getting_started.md",
    "content": "# Getting Started\n\nIn this guide, we're going to learn how to install Jetstream in your project and start consuming\nmessages from your streams.\n\n## Starting Jetstream\n\nThe following Docker Compose file will do the job:\n\n```yaml\nversion: \"3\"\nservices:\n  nats:\n    image: nats:latest\n    command:\n      - -js\n    ports:\n      - 4222:4222\n```\n\nSave this snippet as `docker-compose.yml` and run the following command:\n\n```shell\ndocker compose up -d\n```\n\nLet's also create Jetstream stream where we will publish our hello world messages:\n\n```shell\nnats stream add HELLO --subjects=\"greetings\"\n```\n\n> #### Tip {: .tip}\n>\n> You can also manage Jetstream streams and consumers via Elixir. You can see more details in\n> [this guide](../guides/managing.md).\n\n## Adding Jetstream and Gnat to an application\n\nTo start off with, we'll generate a new Elixir application by running this command:\n\n```\nmix new hello_jetstream --sup\n```\n\nWe need to have [a supervision tree](http://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html)\nup and running in your app, and the `--sup` option ensures that.\n\nTo add Jetstream to this application, you need to add [Jetstream](https://hex.pm/packages/jetstream)\nand [Gnat](https://hex.pm/packages/gnat) libraries to your `deps` definition in our `mix.exs` file.\n**Fill exact version requirements from each package Hex.pm pages.**\n\n```elixir\ndefp deps do\n  [\n    {:gnat, ...},\n    {:jetstream, ...}\n  ]\nend\n```\n\nTo install these dependencies, we will run this command:\n\n```shell\nmix deps.get\n```\n\nNow let's connect to our NATS server. To do this, you need to start `Gnat.ConnectionSupervisor`\nunder our application's supervision tree. Add following to `lib/hello_jetstream/application.ex`:\n\n```elixir\ndef start(_type, _args) do\n  children = [\n    ...\n\n    # Create NATS connection\n    {Gnat.ConnectionSupervisor,\n      %{\n        name: :gnat,\n        connection_settings: [\n          %{host: \"localhost\", port: 4222}\n        ]\n      }},\n  ]\n\n  ...\n```\n\nThis piece of configuration will start Gnat processes that connect to the NATS server and allow\npublishing and subscribing to any subjects. Jetstream operates using plain NATS subjects which\nfollow specific naming and message format conventions.\n\nLet's now create a _pull consumer_ which will subscribe a specific Jetstream stream and print\nincoming messages to standard output.\n\n## Creating a pull consumer\n\nJetstream requires us to allocate a view/cursor of the stream that our consumer will operate on.\nIn Jetstream terminology, this view is called a _consumer_ (Funnily enough we've just implemented\na consumer in our code, coincidence?). [Jetstream](https://docs.nats.io/nats-concepts/jetstream/consumers)\n[documentation](https://docs.nats.io/nats-concepts/jetstream/consumers/example_configuration)\noffers great insights on benefits of having this separate concept so we won't duplicate work here.\n\nJetstream offers two stream consuming modes: _push_ and _pull_.\n\nIn _push_ mode, Jetstream will simply send messages to selected consumers immediately when they are\nreceived. This approach does offer congestion control, so it is not recommended for high-volume\nand/or reliability sensitive streams. You do not really need this library to implement push\nconsumer because all building blocks are in `Gnat` library. You can read more about push consumers\nin [this guide](../guides/push_based_consumer.md).\n\nOn the other hand, in _pull_ mode consumers ask Jetstream for more messages when they are ready\nto process them. This is the recommended approach for most use cases and we will proceed with it\nin this guide.\n\n> #### This is just a brief outline {: .tip}\n>\n> For more details about differences between consumer modes, consult\n> [Jetstream documentation](https://docs.nats.io/nats-concepts/jetstream/consumers).\n\nLet's create a pull consumer module within our application at\n`lib/hello_jetstream/logger_pull_consumer.ex`:\n\n```elixir\ndefmodule HelloJetstream.LoggerPullConsumer do\n  use Jetstream.PullConsumer\n\n  def start_link([]) do\n    Jetstream.PullConsumer.start_link(__MODULE__, [])\n  end\n\n  @impl true\n  def init([]) do\n    {:ok, nil, connection_name: :gnat, stream_name: \"HELLO\", consumer_name: \"LOGGER\"}\n  end\n\n  @impl true\n  def handle_message(message, state) do\n    IO.inspect(message)\n    {:ack, state}\n  end\nend\n```\n\nPull Consumer is a regular `GenServer` and it takes a reference to `Gnat.ConnectionSupervisor`\nalong with names of Jetstream stream and consumer as options passed to\n`Jetstream.PullConsumer.start*` functions. These options are passed as keyword list in third element\nof tuple returned from the `c:Jetstream.PullConsumer.init/1` callback.\n\nThe only required callbacks are well known gen server's `c:Jetstream.PullConsumer.init/1` and\n`c:Jetstream.PullConsumer.handle_message/2`, which takes new message as its first argument and\nis expected to return an _ACK action_ instructing underlying process loop what to do with this\nmessage. Here we are asking it to automatically send for us an ACK message back to Jetstream.\n\nLet's now create a consumer in our NATS server. We will call it `LOGGER` as we plan to let it simply\nlog everything published to the stream.\n\n```shell\nnats consumer add --pull --deliver=all HELLO LOGGER\n```\n\nNow, let's start our pull consumer under application's supervision tree.\n\n```elixir\ndef start(_type, _args) do\n  children = [\n    ...\n\n    # Jetstream Pull Consumer\n    HelloJetstream.LoggerPullConsumer,\n  ]\n\n  ...\n```\n\nLet's now publish some messages to our `HELLO` stream, so something will be waiting for our\napplication to be read when it starts.\n\n## Publishing messages to streams\n\nJetstream listens on regular NATS subjects, so publishing messages is dead simple with `Gnat.pub/3`:\n\n```elixir\nGnat.pub(:gnat, \"greetings\", \"Hello World\")\n```\n\nOr via NATS CLI:\n\n```shell\nnats pub greetings \"Hello World\"\n```\n\nThat's it! When you run your app, you should see your messages being read by your application."
  },
  {
    "path": "docs/js/introduction/overview.md",
    "content": "# Overview\n\n[Jetstream](https://docs.nats.io/nats-concepts/jetstream) is a distributed persistence system\nbuilt-in to [NATS](https://nats.io/). It provides a streaming system that lets you capture streams\nof events from various sources and persist these into persistent stores, which you can immediately\nor later replay for processing.\n\nThis library exposes interfaces for publishing, consuming and managing Jetstream services. It builds\non top of [Gnat](https://hex.pm/packages/gnat), the officially supported Elixir client for NATS.\n\n* [Let's get Jetstream up and running](./getting_started.md)\n* [Using Broadway with Jetstream](../guides/broadway.md)\n* [Pull Consumer API](`Gnat.Jetstream.PullConsumer`)\n* [Create, update and delete Jetstream streams and consumers via Elixir](../guides/managing.md)"
  },
  {
    "path": "lib/gnat/command.ex",
    "content": "defmodule Gnat.Command do\n  @moduledoc false\n\n  @newline \"\\r\\n\"\n  @hpub \"HPUB\"\n  @pub \"PUB\"\n  @sub \"SUB\"\n  @unsub \"UNSUB\"\n\n  def build(:pub, topic, payload, []),\n    do: [@pub, \" \", topic, \" #{IO.iodata_length(payload)}\", @newline, payload, @newline]\n\n  def build(:pub, topic, payload, reply_to: reply),\n    do: [\n      @pub,\n      \" \",\n      topic,\n      \" \",\n      reply,\n      \" #{IO.iodata_length(payload)}\",\n      @newline,\n      payload,\n      @newline\n    ]\n\n  def build(:pub, topic, payload, headers: headers) do\n    # it takes 10 bytes to add the nats header version line\n    # and 2 more for the newline between headers and payload\n    header_len = IO.iodata_length(headers) + 12\n    total_len = IO.iodata_length(payload) + header_len\n\n    [\n      @hpub,\n      \" \",\n      topic,\n      \" \",\n      Integer.to_string(header_len),\n      \" \",\n      Integer.to_string(total_len),\n      \"\\r\\nNATS/1.0\\r\\n\",\n      headers,\n      @newline,\n      payload,\n      @newline\n    ]\n  end\n\n  def build(:pub, topic, payload, headers: headers, reply_to: reply) do\n    # it takes 10 bytes to add the nats header version line\n    # and 2 more for the newline between headers and payload\n    header_len = IO.iodata_length(headers) + 12\n    total_len = IO.iodata_length(payload) + header_len\n\n    [\n      @hpub,\n      \" \",\n      topic,\n      \" \",\n      reply,\n      \" \",\n      Integer.to_string(header_len),\n      \" \",\n      Integer.to_string(total_len),\n      \"\\r\\nNATS/1.0\\r\\n\",\n      headers,\n      @newline,\n      payload,\n      @newline\n    ]\n  end\n\n  def build(:sub, topic, sid, []), do: [@sub, \" \", topic, \" \", Integer.to_string(sid), @newline]\n\n  def build(:sub, topic, sid, queue_group: qg),\n    do: [@sub, \" \", topic, \" \", qg, \" \", Integer.to_string(sid), @newline]\n\n  def build(:unsub, sid, []), do: [@unsub, \" #{sid}\", @newline]\n  def build(:unsub, sid, max_messages: max), do: [@unsub, \" #{sid}\", \" #{max}\", @newline]\nend\n"
  },
  {
    "path": "lib/gnat/connection_supervisor.ex",
    "content": "defmodule Gnat.ConnectionSupervisor do\n  use GenServer\n  require Logger\n\n  @moduledoc \"\"\"\n  A process that can supervise a named connection for you\n\n  If you would like to supervise a Gnat connection and have it automatically re-connect in case of failure you can use this module in your supervision tree.\n  It takes a map with the following data:\n\n  ```\n  gnat_supervisor_settings = %{\n    name: :gnat, # (required) the registered named you want to give the Gnat connection\n    backoff_period: 4_000, # number of milliseconds to wait between consecutive reconnect attempts (default: 2_000)\n    connection_settings: [\n      %{host: '10.0.0.100', port: 4222},\n      %{host: '10.0.0.101', port: 4222},\n    ]\n  }\n  ```\n\n  The connection settings can specify all of the same values that you pass to `Gnat.start_link/1`. Each time a connection is attempted we will use one of the provided connection settings to open the connection. This is a simplistic way of load balancing your connections across a cluster of nats nodes and allowing failover to other nodes in the cluster if one goes down.\n\n  To use this in your supervision tree add an entry like this:\n\n  ```\n  import Supervisor.Spec\n  worker(Gnat.ConnectionSupervisor, [gnat_supervisor_settings, [name: :my_connection_supervisor]])\n  ```\n\n  The second argument is used as GenServer options so you can give the supervisor a registered name as well if you like. Now in the rest of your code you can call things like:\n\n  ```\n  :ok = Gnat.pub(:gnat, \"subject\", \"message\")\n  ```\n\n  And it will use your supervised connection. If the connection is down when you call that function (or dies during that function) it will raise an error.\n  \"\"\"\n  @spec start_link(map(), keyword()) :: GenServer.on_start()\n  def start_link(settings, options \\\\ []) do\n    GenServer.start_link(__MODULE__, settings, options)\n  end\n\n  @impl GenServer\n  def init(options) do\n    state = %{\n      backoff_period: Map.get(options, :backoff_period, 2000),\n      connection_settings: Map.fetch!(options, :connection_settings),\n      name: Map.fetch!(options, :name),\n      gnat: nil\n    }\n\n    Process.flag(:trap_exit, true)\n    send(self(), :attempt_connection)\n    {:ok, state}\n  end\n\n  @impl GenServer\n  def handle_info(:attempt_connection, state) do\n    connection_config = random_connection_config(state)\n    Logger.debug(\"connecting to #{inspect(connection_config)}\")\n\n    case Gnat.start_link(connection_config, name: state.name) do\n      {:ok, gnat} ->\n        {:noreply, %{state | gnat: gnat}}\n\n      {:error, err} ->\n        Logger.error(\"failed to connect #{inspect(err)}\")\n        Process.send_after(self(), :attempt_connection, state.backoff_period)\n        {:noreply, %{state | gnat: nil}}\n    end\n  end\n\n  # in OTP 25 and below, we will get back an EXIT message in addition to receiving the {:error, reason}\n  # tuple on from the start_link call above. So if we get an exit message when there is no connection tracked\n  # it means will have already scheduled a new attempt_connection\n  def handle_info({:EXIT, _pid, _reason}, %{gnat: nil} = state) do\n    {:noreply, state}\n  end\n\n  def handle_info({:EXIT, _pid, reason}, state) do\n    Logger.error(\"connection failed #{inspect(reason)}\")\n    send(self(), :attempt_connection)\n    {:noreply, state}\n  end\n\n  def handle_info(msg, state) do\n    Logger.error(\"#{__MODULE__} received unexpected message #{inspect(msg)}\")\n    {:noreply, state}\n  end\n\n  defp random_connection_config(%{connection_settings: connection_settings}) do\n    connection_settings |> Enum.random()\n  end\nend\n"
  },
  {
    "path": "lib/gnat/consumer_supervisor.ex",
    "content": "defmodule Gnat.ConsumerSupervisor do\n  use GenServer\n  require Logger\n  alias Gnat.Services.Service\n\n  @moduledoc \"\"\"\n  A process that can supervise consumers for you\n\n  If you want to subscribe to a few topics and have that subscription last across restarts for you, then this worker can be of help.\n  It also spawns a supervised `Task` for each message it receives.\n  This way errors in message processing don't crash the consumers, but you will still get SASL reports that you can send to services like honeybadger.\n\n  To use this just add an entry to your supervision tree like this:\n\n  ```\n  consumer_supervisor_settings = %{\n    connection_name: :name_of_supervised_connection,\n    module: MyApp.Server, # a module that implements the Gnat.Server behaviour\n    subscription_topics: [\n      %{topic: \"rpc.MyApp.search\", queue_group: \"rpc.MyApp.search\"},\n      %{topic: \"rpc.MyApp.create\", queue_group: \"rpc.MyApp.create\"},\n    ],\n  }\n  worker(Gnat.ConsumerSupervisor, [consumer_supervisor_settings, [name: :rpc_consumer]], shutdown: 30_000)\n  ```\n\n  The second argument is a keyword list that gets used as the GenServer options so you can pass a name that you want to register for the consumer process if you like. The `:consuming_function` specifies which module and function to call when messages arrive. The function will be called with a single argument which is a `t:Gnat.message/0` just like you get when you call `Gnat.sub/4` directly.\n\n  You can have a single consumer that subscribes to multiple topics or multiple consumers that subscribe to different topics and call different consuming functions. It is recommended that your `ConsumerSupervisor`s are present later in your supervision tree than your `ConnectionSupervisor`. That way during a shutdown the `ConsumerSupervisor` can attempt a graceful shutdown of the consumer before shutting down the connection.\n\n  If you want this consumer supervisor to host a NATS service, then you can specify a module that\n  implements the `Gnat.Services.Server` behavior. You'll need to specify the `service_definition` field in the consumer\n  supervisor settings and conforms to the `Gnat.Services.Server.service_configuration` type. Here is an example of configuring\n  the consumer supervisor to manage a service:\n\n  ```\n  consumer_supervisor_settings = %{\n    connection_name: :name_of_supervised_connection,\n    module: MyApp.Service, # a module that implements the Gnat.Services.Server behaviour\n    service_definition: %{\n      name: \"exampleservice\",\n      description: \"This is an example service\",\n      version: \"0.1.0\",\n      endpoints: [\n        %{\n          name: \"add\",\n          group_name: \"calc\",\n        },\n        %{\n          name: \"sub\",\n          group_name: \"calc\"\n        }\n      ]\n    }\n  }\n  worker(Gnat.ConsumerSupervisor, [consumer_supervisor_settings, [name: :myservice_consumer]], shutdown: 30_000)\n  ```\n\n  It's also possible to pass a `%{consuming_function: {YourModule, :your_function}}` rather than a `:module` in your settings.\n  In that case no error handling or replying is taking care of for you, microservices cannot be used, and it will be up to your function to take whatever action you want with each message.\n  \"\"\"\n  @spec start_link(map(), keyword()) :: GenServer.on_start()\n  def start_link(settings, options \\\\ []) do\n    GenServer.start_link(__MODULE__, settings, options)\n  end\n\n  @impl GenServer\n  def init(settings) do\n    Process.flag(:trap_exit, true)\n    {:ok, task_supervisor_pid} = Task.Supervisor.start_link()\n    connection_name = Map.get(settings, :connection_name)\n    subscription_topics = Map.get(settings, :subscription_topics)\n\n    state = %{\n      connection_name: connection_name,\n      connection_pid: nil,\n      svc_responder_pid: nil,\n      status: :disconnected,\n      subscription_topics: subscription_topics,\n      subscriptions: [],\n      task_supervisor_pid: task_supervisor_pid\n    }\n\n    with {:ok, state} <- maybe_append_service(state, settings),\n         {:ok, state} <- maybe_append_module(state, settings),\n         {:ok, state} <- maybe_append_consuming_function(state, settings),\n         :ok <- validate_state(state) do\n      send(self(), :connect)\n      {:ok, state}\n    end\n  end\n\n  @impl GenServer\n  def handle_info(:connect, %{connection_name: name} = state) do\n    case Process.whereis(name) do\n      nil ->\n        Process.send_after(self(), :connect, 2_000)\n        {:noreply, state}\n\n      connection_pid ->\n        _ref = Process.monitor(connection_pid)\n        subscriptions = subscribe_to_topics(state, connection_pid)\n\n        {:noreply,\n         %{\n           state\n           | status: :connected,\n             connection_pid: connection_pid,\n             subscriptions: subscriptions\n         }}\n    end\n  end\n\n  def handle_info(\n        {:DOWN, _ref, :process, connection_pid, _reason},\n        %{connection_pid: connection_pid} = state\n      ) do\n    Process.send_after(self(), :connect, 2_000)\n    {:noreply, %{state | status: :disconnected, connection_pid: nil, subscriptions: []}}\n  end\n\n  # Ignore DOWN and task result messages from the spawned tasks\n  def handle_info({:DOWN, _ref, :process, _task_pid, _reason}, state), do: {:noreply, state}\n  def handle_info({ref, _result}, state) when is_reference(ref), do: {:noreply, state}\n\n  def handle_info(\n        {:EXIT, supervisor_pid, _reason},\n        %{task_supervisor_pid: supervisor_pid} = state\n      ) do\n    {:ok, task_supervisor_pid} = Task.Supervisor.start_link()\n    {:noreply, Map.put(state, :task_supervisor_pid, task_supervisor_pid)}\n  end\n\n  def handle_info({:msg, gnat_message}, %{service: service, module: module} = state) do\n    Task.Supervisor.async_nolink(state.task_supervisor_pid, Gnat.Services.Server, :execute, [\n      module,\n      gnat_message,\n      service\n    ])\n\n    {:noreply, state}\n  end\n\n  def handle_info({:msg, gnat_message}, %{module: module} = state) do\n    Task.Supervisor.async_nolink(state.task_supervisor_pid, Gnat.Server, :execute, [\n      module,\n      gnat_message\n    ])\n\n    {:noreply, state}\n  end\n\n  def handle_info({:msg, gnat_message}, %{consuming_function: {mod, fun}} = state) do\n    Task.Supervisor.async_nolink(state.task_supervisor_pid, mod, fun, [gnat_message])\n    {:noreply, state}\n  end\n\n  def handle_info(other, state) do\n    Logger.error(\"#{__MODULE__} received unexpected message #{inspect(other)}\")\n    {:noreply, state}\n  end\n\n  @impl GenServer\n  def terminate(:shutdown, state) do\n    Logger.info(\"#{__MODULE__} starting graceful shutdown\")\n\n    Enum.each(state.subscriptions, fn subscription ->\n      :ok = Gnat.unsub(state.connection_pid, subscription)\n    end)\n\n    # wait for final messages from broker\n    Process.sleep(500)\n    receive_final_broker_messages(state)\n    wait_for_empty_task_supervisor(state)\n    Logger.info(\"#{__MODULE__} finished graceful shutdown\")\n  end\n\n  def terminate(reason, _state) do\n    Logger.error(\"#{__MODULE__} unexpected shutdown #{inspect(reason)}\")\n  end\n\n  defp receive_final_broker_messages(state) do\n    receive do\n      info ->\n        handle_info(info, state)\n        receive_final_broker_messages(state)\n    after\n      0 ->\n        :done\n    end\n  end\n\n  defp wait_for_empty_task_supervisor(%{task_supervisor_pid: pid} = state) do\n    case Task.Supervisor.children(pid) do\n      [] ->\n        :ok\n\n      children ->\n        Logger.info(\"#{__MODULE__}\\t\\t#{Enum.count(children)} tasks remaining\")\n        Process.sleep(1_000)\n        wait_for_empty_task_supervisor(state)\n    end\n  end\n\n  defp subscribe_to_topics(%{service: service}, connection_pid) do\n    Service.subscription_topics_with_queue_group(service)\n    |> Enum.map(fn\n      {topic, nil} ->\n        {:ok, subscription} = Gnat.sub(connection_pid, self(), topic)\n        subscription\n\n      {topic, queue_group} ->\n        {:ok, subscription} = Gnat.sub(connection_pid, self(), topic, queue_group: queue_group)\n        subscription\n    end)\n  end\n\n  defp subscribe_to_topics(state, connection_pid) do\n    Enum.map(state.subscription_topics, fn topic_and_queue_group ->\n      topic = Map.fetch!(topic_and_queue_group, :topic)\n\n      {:ok, subscription} =\n        case Map.get(topic_and_queue_group, :queue_group) do\n          nil -> Gnat.sub(connection_pid, self(), topic)\n          queue_group -> Gnat.sub(connection_pid, self(), topic, queue_group: queue_group)\n        end\n\n      subscription\n    end)\n  end\n\n  defp maybe_append_service(state, %{service_definition: config}) do\n    case Service.init(config) do\n      {:ok, service} ->\n        {:ok, Map.put(state, :service, service)}\n\n      {:error, errors} ->\n        {:stop, \"Invalid service configuration: #{Enum.join(errors, \",\")}\"}\n    end\n  end\n\n  defp maybe_append_service(state, _), do: {:ok, state}\n\n  defp maybe_append_module(state, %{module: module}) do\n    {:ok, Map.put(state, :module, module)}\n  end\n\n  defp maybe_append_module(state, _), do: {:ok, state}\n\n  defp maybe_append_consuming_function(state, %{consuming_function: consuming_function}) do\n    {:ok, Map.put(state, :consuming_function, consuming_function)}\n  end\n\n  defp maybe_append_consuming_function(state, _), do: {:ok, state}\n\n  defp validate_state(state) do\n    partial = Map.take(state, [:module, :consuming_function])\n\n    case Enum.count(partial) do\n      0 ->\n        {:stop, \"You must provide a module or consuming function for the consumer supervisor\"}\n\n      1 ->\n        :ok\n\n      _ ->\n        {:stop,\n         \"You cannot provide both a module and consuming function. Please specify one or the other.\"}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/handshake.ex",
    "content": "defmodule Gnat.Handshake do\n  @moduledoc false\n  alias Gnat.Parsec\n\n  @doc \"\"\"\n  This function handles all of the variations of establishing a connection to\n  a nats server and just returns {:ok, socket} or {:error, reason}\n  \"\"\"\n  def connect(settings) do\n    host = settings.host |> to_charlist\n\n    case :gen_tcp.connect(host, settings.port, settings.tcp_opts, settings.connection_timeout) do\n      {:ok, tcp} -> perform_handshake(tcp, settings)\n      result -> result\n    end\n  end\n\n  def negotiate_settings(server_settings, user_settings) do\n    auth_required = server_settings[:auth_required] || user_settings[:auth_required] || false\n\n    %{verbose: false}\n    |> negotiate_auth(server_settings, user_settings, auth_required)\n    |> negotiate_headers(server_settings, user_settings)\n    |> negotiate_no_responders(server_settings, user_settings)\n  end\n\n  defp perform_handshake(tcp, user_settings) do\n    receive do\n      {:tcp, ^tcp, operation} ->\n        {_, [{:info, server_settings}]} = Parsec.parse(Parsec.new(), operation)\n        {:ok, socket} = upgrade_connection(tcp, user_settings)\n        settings = negotiate_settings(server_settings, user_settings)\n        :ok = send_connect(user_settings, settings, socket)\n        {:ok, socket, server_settings}\n    after\n      1000 ->\n        {:error, \"timed out waiting for info\"}\n    end\n  end\n\n  defp send_connect(%{tls: true}, settings, socket) do\n    :ssl.send(socket, \"CONNECT \" <> Jason.encode!(settings, maps: :strict) <> \"\\r\\n\")\n  end\n\n  defp send_connect(_, settings, socket) do\n    :gen_tcp.send(socket, \"CONNECT \" <> Jason.encode!(settings, maps: :strict) <> \"\\r\\n\")\n  end\n\n  defp negotiate_auth(\n         settings,\n         _server,\n         %{username: username, password: password} = _user,\n         true = _auth_required\n       ) do\n    Map.merge(settings, %{user: username, pass: password})\n  end\n\n  defp negotiate_auth(settings, _server, %{token: token} = _user, true = _auth_required) do\n    Map.merge(settings, %{auth_token: token})\n  end\n\n  defp negotiate_auth(\n         settings,\n         %{nonce: nonce} = _server,\n         %{nkey_seed: seed, jwt: jwt} = _user,\n         true = _auth_required\n       ) do\n    {:ok, nkey} = NKEYS.from_seed(seed)\n    signature = NKEYS.sign(nkey, nonce) |> Base.url_encode64() |> String.replace(\"=\", \"\")\n\n    Map.merge(settings, %{sig: signature, protocol: 1, jwt: jwt})\n  end\n\n  defp negotiate_auth(\n         settings,\n         %{nonce: nonce} = _server,\n         %{nkey_seed: seed} = _user,\n         true = _auth_required\n       ) do\n    {:ok, nkey} = NKEYS.from_seed(seed)\n    signature = NKEYS.sign(nkey, nonce) |> Base.url_encode64() |> String.replace(\"=\", \"\")\n    public = NKEYS.public_nkey(nkey)\n\n    Map.merge(settings, %{sig: signature, protocol: 1, nkey: public})\n  end\n\n  defp negotiate_auth(settings, _server, _user, _auth_required) do\n    settings\n  end\n\n  defp negotiate_headers(settings, %{headers: true} = _server, user_settings) do\n    if Map.get(user_settings, :headers, true) do\n      Map.put(settings, :headers, true)\n    else\n      Map.put(settings, :headers, false)\n    end\n  end\n\n  defp negotiate_headers(_settings, _server, %{headers: true} = _user) do\n    raise \"NATS Server does not support headers, but your connection settings specify header support\"\n  end\n\n  defp negotiate_headers(settings, _server, _user) do\n    settings\n  end\n\n  defp negotiate_no_responders(%{headers: true} = settings, _server_settings, %{\n         no_responders: true\n       }) do\n    Map.put(settings, :no_responders, true)\n  end\n\n  defp negotiate_no_responders(settings, _server_settings, _user_settings) do\n    settings\n  end\n\n  defp upgrade_connection(tcp, %{tls: true, ssl_opts: opts}) do\n    :ok = :inet.setopts(tcp, active: true)\n    :ssl.connect(tcp, opts, 1_000)\n  end\n\n  defp upgrade_connection(tcp, _settings), do: {:ok, tcp}\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/consumer.ex",
    "content": "defmodule Gnat.Jetstream.API.Consumer do\n  @moduledoc \"\"\"\n  A module representing a NATS JetStream Consumer.\n\n  Learn more about consumers: https://docs.nats.io/nats-concepts/jetstream/consumers\n\n  ## The Jetstream.API.Consumer struct\n\n  The struct's only mandatory field to set is the `:stream_name`. The rest will have\n  the NATS default values set.\n\n  Note that consumers are ephemeral by default. Set the `:durable_name` to make it durable.\n\n  Consumer struct fields explanation:\n\n  * `:stream_name` - name of a stream the consumer is pointing at.\n  * `:domain` - JetStream domain the stream is on.\n  * `:ack_policy` - how the messages should be acknowledged. It has the following options:\n      - `:explicit` - the default policy. It means that each individual message must be acknowledged.\n        It is the only allowed option for pull consumers.\n      - `:none` - no need to ack messages, the server will assume ack on delivery.\n      - `:all` - only the last received message needs to be acked, all the previous messages received\n        are automatically acknowledged.\n  * `:ack_wait` - time in nanoseconds that server will wait for an ack for any individual. If an ack\n     is not received in time, the message will be redelivered.\n  * `:backoff` - list of durations that represents a retry timescale for NAK'd messages or those being\n     normally retried.\n  * `:deliver_group` - when set, will only deliver messages to subscriptions matching that group.\n  * `:deliver_policy` - specifies where in the stream it wants to start receiving messages. It has the\n     following options:\n       - `:all` - the default policy. The consumer will start receiving from the earliest available\n         message.\n       - `:last` - the consumer will start receiving messages with the last message added to the stream.\n       - `:new` - the consumer will only start receiving messages that were created after the customer\n         was created.\n       - `:by_start_sequence` - the consumer is required to specify `:opt_start_seq`, the sequence number\n         to start on. It will receive the closest available message moving forward in the sequence\n         should the message specified have been removed based on the stream limit policy.\n       - `:by_start_time` - the consumer will start with messages on or after this time. The consumer is\n         required to specify `:opt_start_time`, the time in the stream to start at.\n       - `:last_per_subject` - the consumer will start with the latest one for each filtered subject\n         currently  in the stream.\n  * `:deliver_subject` - the subject to deliver observed messages. Not allowed for pull subscriptions.\n    A delivery subject is required for queue subscribing as it configures a subject that all the queue\n    consumers should listen on.\n  * `:description` - a short description of the purpose of this customer.\n  * `:durable_name` - the name of the consumer, which the server will track, allowing resuming consumption\n    where left off. By default, a consumer is ephemeral. To make the consumer durable, set the name.\n    See [naming](https://docs.nats.io/running-a-nats-service/nats_admin/jetstream_admin/naming).\n  * `:filter_subject` - when consuming from a stream with a wildcard subject, this allows you to select\n    a subset of the full wildcard subject to receive messages from.\n  * `:flow_control` - when set to true, an empty message with Status header 100 and a reply subject will\n    be sent. Consumers must reply to these messages to control the rate of message delivery.\n  * `:headers_only` - delivers only the headers of messages in the stream and not the bodies. Additionally\n    adds the Nats-Msg-Size header to indicate the size of the removed payload.\n  * `:idle_heartbeat` - if set, the server will regularly send a status message to the client while there\n    are  no new messages to send. This lets the client know that the JetStream service is still up and\n    running, even when there is no activity on the stream. The message status header will have a code of 100.\n    Unlike `:flow_control`, it will have no reply to address. It may have a description like\n    \"Idle Heartbeat\".\n  * `:inactive_threshold` - duration that instructs the server to clean up ephemeral consumers that are\n    inactive for that long.\n  * `:max_ack_pending` - it sets the maximum number of messages without an acknowledgement that can be\n    outstanding, once this limit is reached, message delivery will be suspended. It cannot be used with\n    `:ack_none` ack policy. This maximum number of pending acks applies for all the consumer's\n    subscriber processes. A value of -1 means there can be any number of pending acks (i.e. no flow\n    control).\n  * `:max_batch` - the largest batch property that may be specified when doing a pull on a Pull consumer.\n  * `:max_deliver` - the maximum number of times a specific message will be delivered. Applies to any\n    message that is re-sent due to ack policy.\n  * `:max_expires` - the maximum expires value that may be set when doing a pull on a Pull consumer.\n  * `:max_waiting` - the number of pulls that can be outstanding on a pull consumer, pulls received after\n    this is reached are ignored.\n  * `:opt_start_seq` - use with `:deliver_policy` set to `:by_start_sequence`. It represents the sequence\n    number to start consuming on.\n  * `:opt_start_time` - use with `:deliver_policy` set to `:by_start_time`. It represents the time to start\n    consuming at.\n  * `:rate_limit_bps` - used to throttle the delivery of messages to the consumer, in bits per second.\n  * `:replay_policy` - it applies when the `:deliver_policy` is set to `:all`, `:by_start_sequence` or\n    `:by_start_time`. It has the following options:\n       - `:instant` - the default policy. The messages will be pushed to the client as fast as possible.\n       - `:original` - the messages in the stream will be pushed to the client at the same rate that they\n         were originally received.\n  * `:sample_freq` - Sets the percentage of acknowledgements that should be sampled for observability, 0-100.\n    This value is a binary and for example allows both `30` and `30%` as valid values.\n  \"\"\"\n\n  import Gnat.Jetstream.API.Util\n\n  @enforce_keys [:stream_name]\n  defstruct [\n    :backoff,\n    :deliver_group,\n    :deliver_subject,\n    :description,\n    :domain,\n    :durable_name,\n    :filter_subject,\n    :flow_control,\n    :headers_only,\n    :idle_heartbeat,\n    :inactive_threshold,\n    :max_batch,\n    :max_expires,\n    :max_waiting,\n    :opt_start_seq,\n    :opt_start_time,\n    :rate_limit_bps,\n    :sample_freq,\n    :stream_name,\n    ack_policy: :explicit,\n    ack_wait: 30_000_000_000,\n    deliver_policy: :all,\n    max_ack_pending: 20_000,\n    max_deliver: -1,\n    replay_policy: :instant\n  ]\n\n  @type t :: %__MODULE__{\n          stream_name: binary(),\n          domain: nil | binary(),\n          ack_policy: :none | :all | :explicit,\n          ack_wait: nil | non_neg_integer(),\n          backoff: nil | [non_neg_integer()],\n          deliver_group: nil | binary(),\n          deliver_policy:\n            :all | :last | :new | :by_start_sequence | :by_start_time | :last_per_subject,\n          deliver_subject: nil | binary(),\n          description: nil | binary(),\n          durable_name: nil | binary(),\n          filter_subject: nil | binary(),\n          flow_control: nil | boolean(),\n          headers_only: nil | boolean(),\n          idle_heartbeat: nil | non_neg_integer(),\n          inactive_threshold: nil | non_neg_integer(),\n          max_ack_pending: nil | integer(),\n          max_batch: nil | integer(),\n          max_deliver: nil | integer(),\n          max_expires: nil | non_neg_integer(),\n          max_waiting: nil | integer(),\n          opt_start_seq: nil | non_neg_integer(),\n          opt_start_time: nil | DateTime.t(),\n          rate_limit_bps: nil | non_neg_integer(),\n          replay_policy: :instant | :original,\n          sample_freq: nil | binary()\n        }\n\n  @type info :: %{\n          ack_floor: %{\n            consumer_seq: non_neg_integer(),\n            stream_seq: non_neg_integer()\n          },\n          cluster:\n            nil\n            | %{\n                optional(:name) => binary(),\n                optional(:leader) => binary(),\n                optional(:replicas) => [\n                  %{\n                    :active => non_neg_integer(),\n                    :current => boolean(),\n                    :name => binary(),\n                    optional(:lag) => non_neg_integer(),\n                    optional(:offline) => boolean()\n                  }\n                ]\n              },\n          config: config(),\n          created: DateTime.t(),\n          delivered: %{\n            consumer_seq: non_neg_integer(),\n            stream_seq: non_neg_integer()\n          },\n          name: binary(),\n          num_ack_pending: non_neg_integer(),\n          num_pending: non_neg_integer(),\n          num_redelivered: non_neg_integer(),\n          num_waiting: non_neg_integer(),\n          push_bound: nil | boolean(),\n          stream_name: binary()\n        }\n\n  @type config :: %{\n          ack_policy: :none | :all | :explicit,\n          ack_wait: nil | non_neg_integer(),\n          backoff: nil | [non_neg_integer()],\n          deliver_group: nil | binary(),\n          deliver_policy:\n            :all | :last | :new | :by_start_sequence | :by_start_time | :last_per_subject,\n          deliver_subject: nil | binary(),\n          description: nil | binary(),\n          durable_name: nil | binary(),\n          filter_subject: nil | binary(),\n          flow_control: nil | boolean(),\n          headers_only: nil | boolean(),\n          idle_heartbeat: nil | non_neg_integer(),\n          inactive_threshold: nil | non_neg_integer(),\n          max_ack_pending: nil | integer(),\n          max_batch: nil | integer(),\n          max_deliver: nil | integer(),\n          max_expires: nil | non_neg_integer(),\n          max_waiting: nil | integer(),\n          opt_start_seq: nil | non_neg_integer(),\n          opt_start_time: nil | DateTime.t(),\n          rate_limit_bps: nil | non_neg_integer(),\n          replay_policy: :instant | :original,\n          sample_freq: nil | binary()\n        }\n\n  @type consumers :: %{\n          consumers: list(binary()),\n          limit: non_neg_integer(),\n          offset: non_neg_integer(),\n          total: non_neg_integer()\n        }\n\n  @doc \"\"\"\n  Creates a consumer. When consumer's `:durable_name` field is not set, the function\n  creates an ephemeral consumer. Otherwise, it creates a durable consumer.\n\n  ## Examples\n\n      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"astream\", subjects: [\"subject\"]})\n      iex> {:ok, %{name: \"consumer\", stream_name: \"astream\"}} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: \"consumer\", stream_name: \"astream\"})\n\n      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"astream\", subjects: [\"subject\"]})\n      iex> {:error, %{\"description\" => \"consumer delivery policy is deliver by start sequence, but optional start sequence is not set\"}} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: \"consumer\", stream_name: \"astream\", deliver_policy: :by_start_sequence})\n\n  \"\"\"\n  @spec create(conn :: Gnat.t(), consumer :: t()) :: {:ok, info()} | {:error, term()}\n  def create(conn, %__MODULE__{durable_name: name} = consumer) when not is_nil(name) do\n    create_topic =\n      \"#{js_api(consumer.domain)}.CONSUMER.DURABLE.CREATE.#{consumer.stream_name}.#{name}\"\n\n    with :ok <- validate_durable(consumer),\n         {:ok, raw_response} <- request(conn, create_topic, create_payload(consumer)) do\n      {:ok, to_info(raw_response)}\n    end\n  end\n\n  def create(conn, %__MODULE__{} = consumer) do\n    create_topic = \"#{js_api(consumer.domain)}.CONSUMER.CREATE.#{consumer.stream_name}\"\n\n    with :ok <- validate(consumer),\n         {:ok, raw_response} <- request(conn, create_topic, create_payload(consumer)) do\n      {:ok, to_info(raw_response)}\n    end\n  end\n\n  @doc \"\"\"\n  Deletes a consumer.\n\n  ## Examples\n\n      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"astream\", subjects: [\"subject\"]})\n      iex> {:ok, _response} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: \"consumer\", stream_name: \"astream\"})\n      iex> Gnat.Jetstream.API.Consumer.delete(:gnat, \"astream\", \"consumer\")\n      :ok\n\n      iex> {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} = Gnat.Jetstream.API.Consumer.delete(:gnat, \"wrong_stream\", \"consumer\")\n\n  \"\"\"\n  @spec delete(\n          conn :: Gnat.t(),\n          stream_name :: binary(),\n          consumer_name :: binary(),\n          domain :: nil | binary()\n        ) ::\n          :ok | {:error, any()}\n  def delete(conn, stream_name, consumer_name, domain \\\\ nil) do\n    topic = \"#{js_api(domain)}.CONSUMER.DELETE.#{stream_name}.#{consumer_name}\"\n\n    with {:ok, _response} <- request(conn, topic, \"\") do\n      :ok\n    end\n  end\n\n  @doc \"\"\"\n  Information about the consumer.\n\n  ## Examples\n\n      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"astream\", subjects: [\"subject\"]})\n      iex> {:ok, _response} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: \"consumer\", stream_name: \"astream\"})\n      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Consumer.info(:gnat, \"astream\", \"consumer\")\n\n      iex>  {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} = Gnat.Jetstream.API.Consumer.info(:gnat, \"wrong_stream\", \"consumer\")\n\n  \"\"\"\n  @spec info(\n          conn :: Gnat.t(),\n          stream_name :: binary(),\n          consumer_name :: binary(),\n          domain :: nil | binary()\n        ) ::\n          {:ok, info()} | {:error, any()}\n  def info(conn, stream_name, consumer_name, domain \\\\ nil) do\n    topic = \"#{js_api(domain)}.CONSUMER.INFO.#{stream_name}.#{consumer_name}\"\n\n    with {:ok, raw} <- request(conn, topic, \"\") do\n      {:ok, to_info(raw)}\n    end\n  end\n\n  @doc \"\"\"\n  Paged list of known consumers, including their current info.\n\n  ## Examples\n\n      iex> {:ok, _response} =  Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"astream\", subjects: [\"subject\"]})\n      iex> {:ok, %{consumers: _, limit: 1024, offset: 0, total: _}} = Gnat.Jetstream.API.Consumer.list(:gnat, \"astream\")\n\n      iex> {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} = Gnat.Jetstream.API.Consumer.list(:gnat, \"wrong_stream\")\n\n  \"\"\"\n  @spec list(\n          conn :: Gnat.t(),\n          stream_name :: binary(),\n          params :: [offset: non_neg_integer(), domain: nil | binary()]\n        ) ::\n          {:ok, consumers()} | {:error, term()}\n  def list(conn, stream_name, params \\\\ []) do\n    domain = Keyword.get(params, :domain)\n\n    payload =\n      Jason.encode!(%{\n        offset: Keyword.get(params, :offset, 0)\n      })\n\n    with {:ok, raw} <- request(conn, \"#{js_api(domain)}.CONSUMER.NAMES.#{stream_name}\", payload) do\n      response = %{\n        consumers: Map.get(raw, \"consumers\"),\n        limit: Map.get(raw, \"limit\"),\n        offset: Map.get(raw, \"offset\"),\n        total: Map.get(raw, \"total\")\n      }\n\n      {:ok, response}\n    end\n  end\n\n  @doc \"\"\"\n  Requests a next message from a stream to be consumed. The response (consumed message)will be sent\n  on the subject given as the `reply_to` parameter.\n\n  ## Options\n\n  * `batch` - How many messages to receive. Messages will be sent to the `reply_to` subject\n    separately. Defaults to 1.\n\n  * `expires` - Time in nanoseconds the request will be kept in the server. Once this time passes\n    a message with empty body and topic set to `reply_to` subject is sent. Useful when polling\n    the server frequently and not wanting the pull requests to accumulate. By default, the pull\n    request stays in the server until a message comes.\n\n  * `no_wait` - Boolean value which indicates whether the pull request should be accumulated on\n    the server. When set to true and no message is present to be consumed, a message with empty\n    body and topic value set to `reply_to` is sent. Defaults to false.\n\n  ## Example\n\n      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"astream\", subjects: [\"subject\"]})\n      iex> {:ok, _response} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: \"consumer\", stream_name: \"astream\"})\n      iex> {:ok, _sid} = Gnat.sub(:gnat, self(), \"reply_subject\")\n      iex> :ok = Gnat.Jetstream.API.Consumer.request_next_message(:gnat, \"astream\", \"consumer\", \"reply_subject\")\n      iex> :ok = Gnat.pub(:gnat, \"subject\", \"message1\")\n      iex> assert_receive {:msg, %{body: \"message1\", topic: \"subject\"}}\n  \"\"\"\n  @spec request_next_message(\n          conn :: Gnat.t(),\n          stream_name :: binary(),\n          consumer_name :: binary(),\n          reply_to :: String.t(),\n          domain :: nil | binary(),\n          opts :: keyword()\n        ) :: :ok\n  def request_next_message(\n        conn,\n        stream_name,\n        consumer_name,\n        reply_to,\n        domain \\\\ nil,\n        opts \\\\ []\n      ) do\n    default_payload = %{batch: 1}\n\n    put_option_if_not_nil = fn payload, option_key ->\n      if option_value = opts[option_key] do\n        Map.put(payload, option_key, option_value)\n      else\n        payload\n      end\n    end\n\n    payload =\n      default_payload\n      |> put_option_if_not_nil.(:batch)\n      |> put_option_if_not_nil.(:no_wait)\n      |> put_option_if_not_nil.(:expires)\n      |> Jason.encode!()\n\n    Gnat.pub(\n      conn,\n      \"#{js_api(domain)}.CONSUMER.MSG.NEXT.#{stream_name}.#{consumer_name}\",\n      payload,\n      reply_to: reply_to\n    )\n  end\n\n  # https://docs.nats.io/running-a-nats-service/configuration/leafnodes/jetstream_leafnodes\n  defp js_api(nil), do: \"$JS.API\"\n  defp js_api(\"\"), do: \"$JS.API\"\n  defp js_api(domain), do: \"$JS.#{domain}.API\"\n\n  defp create_payload(%__MODULE__{} = cons) do\n    %{\n      config: %{\n        ack_policy: cons.ack_policy,\n        ack_wait: cons.ack_wait,\n        backoff: cons.backoff,\n        deliver_group: cons.deliver_group,\n        deliver_policy: cons.deliver_policy,\n        deliver_subject: cons.deliver_subject,\n        description: cons.description,\n        durable_name: cons.durable_name,\n        filter_subject: cons.filter_subject,\n        flow_control: cons.flow_control,\n        headers_only: cons.headers_only,\n        idle_heartbeat: cons.idle_heartbeat,\n        inactive_threshold: cons.inactive_threshold,\n        max_ack_pending: cons.max_ack_pending,\n        max_batch: cons.max_batch,\n        max_deliver: cons.max_deliver,\n        max_expires: cons.max_expires,\n        max_waiting: cons.max_waiting,\n        opt_start_seq: cons.opt_start_seq,\n        opt_start_time: cons.opt_start_time,\n        rate_limit_bps: cons.rate_limit_bps,\n        replay_policy: cons.replay_policy,\n        sample_freq: cons.sample_freq\n      },\n      stream_name: cons.stream_name\n    }\n    |> Jason.encode!()\n  end\n\n  defp to_config(raw) do\n    %{\n      ack_policy: raw |> Map.get(\"ack_policy\") |> to_sym(),\n      ack_wait: raw |> Map.get(\"ack_wait\"),\n      backoff: Map.get(raw, \"backoff\"),\n      deliver_group: Map.get(raw, \"deliver_group\"),\n      deliver_policy: raw |> Map.get(\"deliver_policy\") |> to_sym(),\n      deliver_subject: raw |> Map.get(\"deliver_subject\"),\n      description: Map.get(raw, \"description\"),\n      durable_name: Map.get(raw, \"durable_name\"),\n      filter_subject: raw |> Map.get(\"filter_subject\"),\n      flow_control: Map.get(raw, \"flow_control\"),\n      headers_only: Map.get(raw, \"headers_only\"),\n      idle_heartbeat: Map.get(raw, \"idle_heartbeat\"),\n      inactive_threshold: Map.get(raw, \"inactive_threshold\"),\n      max_ack_pending: Map.get(raw, \"max_ack_pending\"),\n      max_batch: Map.get(raw, \"max_batch\"),\n      max_deliver: Map.get(raw, \"max_deliver\"),\n      max_expires: Map.get(raw, \"max_expires\"),\n      max_waiting: Map.get(raw, \"max_waiting\"),\n      opt_start_seq: raw |> Map.get(\"opt_start_seq\"),\n      opt_start_time: raw |> Map.get(\"opt_start_time\") |> to_datetime(),\n      rate_limit_bps: Map.get(raw, \"rate_limit_bps\"),\n      replay_policy: raw |> Map.get(\"replay_policy\") |> to_sym(),\n      sample_freq: Map.get(raw, \"sample_freq\")\n    }\n  end\n\n  defp to_info(raw) do\n    %{\n      ack_floor: %{\n        consumer_seq: get_in(raw, [\"ack_floor\", \"consumer_seq\"]),\n        stream_seq: get_in(raw, [\"ack_floor\", \"stream_seq\"])\n      },\n      cluster: Map.get(raw, \"cluster\"),\n      config: to_config(Map.get(raw, \"config\")),\n      created: raw |> Map.get(\"created\") |> to_datetime(),\n      delivered: %{\n        consumer_seq: get_in(raw, [\"delivered\", \"consumer_seq\"]),\n        stream_seq: get_in(raw, [\"delivered\", \"stream_seq\"])\n      },\n      name: Map.get(raw, \"name\"),\n      num_ack_pending: Map.get(raw, \"num_ack_pending\"),\n      num_pending: Map.get(raw, \"num_pending\"),\n      num_redelivered: Map.get(raw, \"num_redelivered\"),\n      num_waiting: Map.get(raw, \"num_waiting\"),\n      push_bound: Map.get(raw, \"push_bound\"),\n      stream_name: Map.get(raw, \"stream_name\")\n    }\n  end\n\n  defp validate(consumer) do\n    cond do\n      consumer.stream_name == nil ->\n        {:error, \"must have a :stream_name set\"}\n\n      is_binary(consumer.stream_name) == false ->\n        {:error, \"stream_name must be a string\"}\n\n      valid_name?(consumer.stream_name) == false ->\n        {:error, \"invalid stream_name: \" <> invalid_name_message()}\n\n      consumer.deliver_policy not in [\n        :all,\n        :last,\n        :new,\n        :by_start_sequence,\n        :by_start_time,\n        :last_per_subject\n      ] ->\n        {:error, \"invalid deliver policy: #{consumer.deliver_policy}\"}\n\n      consumer.replay_policy not in [:instant, :original] ->\n        {:error, \"invalid replay policy: #{consumer.replay_policy}\"}\n\n      true ->\n        :ok\n    end\n  end\n\n  defp validate_durable(consumer) do\n    with :ok <- validate(consumer) do\n      cond do\n        is_binary(consumer.durable_name) == false ->\n          {:error, \"durable_name must be a string\"}\n\n        valid_name?(consumer.durable_name) == false ->\n          {:error, \"invalid durable_name: \" <> invalid_name_message()}\n\n        true ->\n          :ok\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/kv/entry.ex",
    "content": "defmodule Gnat.Jetstream.API.KV.Entry do\n  @moduledoc \"\"\"\n  A parsed view of a single message from a Key/Value bucket's underlying stream.\n\n  Messages delivered from a KV bucket's stream encode three different operations\n  (put, delete, purge) using a combination of the `kv-operation` header, the\n  `nats-marker-reason` header, and the absence of any headers. Recovering the\n  original key also requires stripping the `$KV.<bucket>.` subject prefix.\n\n  This module captures that convention in one place so both the built-in\n  `Gnat.Jetstream.API.KV.Watcher` (push consumer) and user-supplied\n  `Gnat.Jetstream.PullConsumer` implementations can share it.\n\n  ## Using with a custom PullConsumer\n\n  A common use case is hydrating a local cache from a KV bucket by driving a\n  `Gnat.Jetstream.PullConsumer`. Inside `c:handle_message/2`, convert the raw\n  message into an `Entry` and branch on the operation:\n\n      defmodule MyApp.KVCache do\n        use Gnat.Jetstream.PullConsumer\n\n        alias Gnat.Jetstream.API.KV\n\n        @bucket \"my_bucket\"\n\n        @impl true\n        def handle_message(message, state) do\n          case KV.Entry.from_message(message, @bucket) do\n            {:ok, %KV.Entry{operation: :put, key: key, value: value}} ->\n              {:ack, put_in(state.cache[key], value)}\n\n            {:ok, %KV.Entry{operation: op, key: key}} when op in [:delete, :purge] ->\n              {:ack, update_in(state.cache, &Map.delete(&1, key))}\n\n            :ignore ->\n              {:ack, state}\n          end\n        end\n      end\n\n  The returned struct also carries the JetStream `revision` (stream sequence),\n  `created` timestamp, and `delta` (`num_pending`) when the message includes\n  JetStream metadata, which is useful for detecting when the consumer has\n  caught up with the tail of the stream (`delta == 0`).\n\n  ## Messages that are not KV records\n\n  `from_message/2` returns `:ignore` when the input is not a KV record — for\n  example a JetStream status message (`100` heartbeat, `404`/`408` pull\n  terminator, `409` leadership change) or a message whose subject does not\n  belong to the given bucket. In normal operation the `Watcher` and\n  `PullConsumer` layers filter status messages out before they reach user\n  code, so this is a defensive fallback rather than something consumers are\n  expected to rely on.\n  \"\"\"\n\n  alias Gnat.Jetstream.API.Message\n\n  @operation_header \"kv-operation\"\n  @operation_del \"DEL\"\n  @operation_purge \"PURGE\"\n  @nats_marker_reason_header \"nats-marker-reason\"\n  @subject_prefix \"$KV.\"\n\n  @type operation :: :put | :delete | :purge\n\n  @type t :: %__MODULE__{\n          bucket: String.t(),\n          key: String.t(),\n          value: binary(),\n          operation: operation(),\n          revision: non_neg_integer() | nil,\n          created: DateTime.t() | nil,\n          delta: non_neg_integer() | nil\n        }\n\n  defstruct [:bucket, :key, :value, :operation, :revision, :created, :delta]\n\n  @doc \"\"\"\n  Parse a NATS message delivered from a KV bucket's underlying stream into an\n  `Entry`.\n\n  `bucket_name` must match the bucket the message was published to; it is used\n  to strip the `$KV.<bucket>.` subject prefix and recover the key.\n\n  Returns `:ignore` if the message is not a KV record for the given bucket\n  (JetStream status message, wrong subject, etc.).\n\n  The `:revision`, `:created`, and `:delta` fields are populated when the\n  message carries a JetStream `$JS.ACK...` reply subject. For messages without\n  one (e.g. direct get responses), those fields are `nil`.\n  \"\"\"\n  @spec from_message(Gnat.message(), bucket_name :: String.t()) :: {:ok, t()} | :ignore\n  def from_message(message, bucket_name) do\n    with false <- status_message?(message),\n         {:ok, key} <- extract_key(message, bucket_name) do\n      entry = %__MODULE__{\n        bucket: bucket_name,\n        key: key,\n        value: Map.get(message, :body, \"\"),\n        operation: operation(message)\n      }\n\n      {:ok, apply_metadata(entry, message)}\n    else\n      _ -> :ignore\n    end\n  end\n\n  defp status_message?(%{status: status}) when is_binary(status) and status != \"\", do: true\n  defp status_message?(_), do: false\n\n  defp extract_key(%{topic: topic}, bucket_name) do\n    prefix = @subject_prefix <> bucket_name <> \".\"\n\n    if String.starts_with?(topic, prefix) do\n      {:ok, binary_part(topic, byte_size(prefix), byte_size(topic) - byte_size(prefix))}\n    else\n      :error\n    end\n  end\n\n  defp operation(%{headers: headers}) when is_list(headers) do\n    Enum.find_value(headers, :put, fn\n      {@operation_header, @operation_del} -> :delete\n      {@operation_header, @operation_purge} -> :purge\n      {@nats_marker_reason_header, _} -> :delete\n      _ -> false\n    end)\n  end\n\n  defp operation(_message), do: :put\n\n  defp apply_metadata(entry, message) do\n    case Message.metadata(message) do\n      {:ok, metadata} ->\n        %__MODULE__{\n          entry\n          | revision: metadata.stream_seq,\n            created: metadata.timestamp,\n            delta: metadata.num_pending\n        }\n\n      {:error, _} ->\n        entry\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/kv/watcher.ex",
    "content": "defmodule Gnat.Jetstream.API.KV.Watcher do\n  @moduledoc \"\"\"\n  The watcher server establishes a subscription to the changes that occur to a given key-value bucket. The\n  consumer-supplied handler function will be sent an indicator as to whether the change is a delete or an add,\n  as well as the key being changed and the value (if it was added).\n\n  Ensure that you call `stop` with a watcher pid when you no longer need to be notified about key changes\n  \"\"\"\n  use GenServer\n\n  alias Gnat.Jetstream.API.{Consumer, KV, Util}\n  alias Gnat.Jetstream.API.KV.Entry\n\n  # Matches the ordered-consumer defaults used by the nats.go KV watcher:\n  # a 5s idle heartbeat and server-driven flow control. The flow-control\n  # messages arrive as 100-status messages with a reply subject — the client\n  # is expected to publish an empty reply so the server releases backpressure.\n  @flow_control_heartbeat_ns 5_000_000_000\n\n  @type keywatch_handler ::\n          (action :: :key_deleted | :key_added, key :: String.t(), value :: any() -> nil)\n\n  @type watcher_options ::\n          {:conn, Gnat.t()}\n          | {:bucket_name, String.t()}\n          | {:handler, keywatch_handler()}\n\n  @spec start_link(opts :: [watcher_options()]) :: GenServer.on_start()\n  def start_link(opts) do\n    GenServer.start_link(__MODULE__, opts)\n  end\n\n  def stop(pid) do\n    GenServer.stop(pid)\n  end\n\n  def init(opts) do\n    {:ok, {sub, consumer_name}} = subscribe(opts[:conn], opts[:bucket_name])\n\n    {:ok,\n     %{\n       handler: opts[:handler],\n       conn: opts[:conn],\n       bucket_name: opts[:bucket_name],\n       sub: sub,\n       consumer_name: consumer_name,\n       domain: Keyword.get(opts, :domain)\n     }}\n  end\n\n  def terminate(_reason, state) do\n    stream = KV.stream_name(state.bucket_name)\n    :ok = Gnat.unsub(state.conn, state.sub)\n    :ok = Consumer.delete(state.conn, stream, state.consumer_name, state.domain)\n  end\n\n  # Flow-control request: the server is asking us to acknowledge that we're\n  # keeping up. Responding releases backpressure so the server continues\n  # delivering messages to slow handlers rather than dropping us as a slow\n  # consumer.\n  def handle_info({:msg, %{status: \"100\", reply_to: reply_to}}, state)\n      when is_binary(reply_to) and reply_to != \"\" do\n    :ok = Gnat.pub(state.conn, reply_to, \"\")\n    {:noreply, state}\n  end\n\n  # Idle heartbeat (status 100 with no reply_to) and any other informational\n  # status message (404, 408, 409, etc.) — not a stream record, drop it.\n  def handle_info({:msg, %{status: status}}, state)\n      when is_binary(status) and status != \"\" do\n    {:noreply, state}\n  end\n\n  def handle_info({:msg, message}, state) do\n    case Entry.from_message(message, state.bucket_name) do\n      {:ok, entry} ->\n        state.handler.(action(entry.operation), entry.key, entry.value)\n\n      :ignore ->\n        :ok\n    end\n\n    {:noreply, state}\n  end\n\n  defp action(:put), do: :key_added\n  defp action(:delete), do: :key_deleted\n  defp action(:purge), do: :key_purged\n\n  defp subscribe(conn, bucket_name) do\n    stream = KV.stream_name(bucket_name)\n    inbox = Util.reply_inbox()\n    consumer_name = \"all_key_values_watcher_#{Util.nuid()}\"\n\n    with {:ok, sub} <- Gnat.sub(conn, self(), inbox),\n         {:ok, _consumer} <-\n           Consumer.create(conn, %Consumer{\n             durable_name: consumer_name,\n             deliver_subject: inbox,\n             stream_name: stream,\n             ack_policy: :none,\n             max_ack_pending: -1,\n             max_deliver: 1,\n             flow_control: true,\n             idle_heartbeat: @flow_control_heartbeat_ns\n           }) do\n      {:ok, {sub, consumer_name}}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/kv.ex",
    "content": "defmodule Gnat.Jetstream.API.KV do\n  @moduledoc \"\"\"\n  API for interacting with the Key/Value store functionality in Nats Jetstream.\n\n  Learn about the Key/Value store: https://docs.nats.io/nats-concepts/jetstream/key-value-store\n\n  ## Consuming KV changes from a custom PullConsumer\n\n  `watch/3` covers the common \"push consumer\" use case for reacting to bucket\n  changes. If you need a pull-based consumer instead (e.g. to hydrate a local\n  cache and detect when it is caught up with the stream), see\n  `Gnat.Jetstream.API.KV.Entry` for a shared helper that parses a raw NATS\n  message into a KV operation/key/value triple with optional revision\n  metadata.\n  \"\"\"\n  alias Gnat.Jetstream.API.{Stream}\n\n  @stream_prefix \"KV_\"\n  @subject_prefix \"$KV.\"\n  @two_minutes_in_nanoseconds 120_000_000_000\n\n  @type bucket_options ::\n          {:history, non_neg_integer()}\n          | {:ttl, non_neg_integer()}\n          | {:limit_marker_ttl, non_neg_integer()}\n          | {:max_bucket_size, non_neg_integer()}\n          | {:max_value_size, non_neg_integer()}\n          | {:description, binary()}\n          | {:replicas, non_neg_integer()}\n          | {:storage, :file | :memory}\n          | {:placement, Stream.placement()}\n\n  @doc \"\"\"\n  Create a new Key/Value bucket. Can include the following options\n\n  * `:history` - How many historic values to keep per key (defaults to 1, max of 64)\n  * `:ttl` - How long to keep values for (in nanoseconds)\n  * `:limit_marker_ttl` - How long the bucket keeps markers when keys are removed by the TTL setting.\n  * `:max_bucket_size` - The max number of bytes the bucket can hold\n  * `:max_value_size` - The max number of bytes a value may be\n  * `:description` - A description for the bucket\n  * `:replicas` - How many replicas of the data to store\n  * `:storage` - Storage backend to use (:file, :memory)\n  * `:placement` - A map with :cluster (required) and :tags (optional)\n\n  ## Examples\n\n     iex> {:ok, info} = Jetstream.API.KV.create_bucket(:gnat, \"my_bucket\")\n  \"\"\"\n  @spec create_bucket(conn :: Gnat.t(), bucket_name :: binary(), params :: [bucket_options()]) ::\n          {:ok, Stream.info()} | {:error, any()}\n  def create_bucket(conn, bucket_name, params \\\\ []) do\n    # The primary NATS docs don't provide information about how to interact\n    # with Key-Value functionality over the wire. Turns out the KV store is\n    # just a Stream under-the-hood\n    # Discovered these settings from looking at the `nats-server -js -DV` logs\n    # as well as the GoLang implementation https://github.com/nats-io/nats.go/blob/dd91b86bc4f7fa0f061fefe11506aaee413bfafd/kv.go#L339\n    # If the settings aren't correct, NATS will not consider it a valid KV store\n    stream = %Stream{\n      name: stream_name(bucket_name),\n      subjects: stream_subjects(bucket_name),\n      description: Keyword.get(params, :description),\n      max_msgs_per_subject: Keyword.get(params, :history, 1),\n      discard: :new,\n      deny_delete: true,\n      allow_rollup_hdrs: true,\n      max_age: Keyword.get(params, :ttl, 0),\n      max_bytes: Keyword.get(params, :max_bucket_size, -1),\n      max_msg_size: Keyword.get(params, :max_value_size, -1),\n      num_replicas: Keyword.get(params, :replicas, 1),\n      storage: Keyword.get(params, :storage, :file),\n      placement: Keyword.get(params, :placement),\n      duplicate_window: adjust_duplicate_window(Keyword.get(params, :ttl, 0)),\n      subject_delete_marker_ttl: Keyword.get(params, :limit_marker_ttl, 0)\n    }\n\n    Stream.create(conn, stream)\n  end\n\n  # The `duplicate_window` can't be greater than the `max_age`. The default `duplicate_window`\n  # is 2 minutes. We'll keep the 2 minute window UNLESS the ttl is less than 2 minutes\n  defp adjust_duplicate_window(ttl) when ttl > 0 and ttl < @two_minutes_in_nanoseconds, do: ttl\n  defp adjust_duplicate_window(_ttl), do: @two_minutes_in_nanoseconds\n\n  @doc \"\"\"\n  Delete a Key/Value bucket\n\n  ## Examples\n\n     iex> :ok = Jetstream.API.KV.delete_bucket(:gnat, \"my_bucket\")\n  \"\"\"\n  @spec delete_bucket(conn :: Gnat.t(), bucket_name :: binary()) :: :ok | {:error, any()}\n  def delete_bucket(conn, bucket_name) do\n    Stream.delete(conn, stream_name(bucket_name))\n  end\n\n  @doc \"\"\"\n  Create a Key in a Key/Value Bucket\n\n  ## Options\n\n  * `:timeout` - receive timeout for the request\n\n  ## Examples\n\n      iex> :ok = Jetstream.API.KV.create_key(:gnat, \"my_bucket\", \"my_key\", \"my_value\")\n  \"\"\"\n  @spec create_key(\n          conn :: Gnat.t(),\n          bucket_name :: binary(),\n          key :: binary(),\n          value :: binary(),\n          opts :: keyword()\n        ) ::\n          :ok | {:error, any()}\n  def create_key(conn, bucket_name, key, value, opts \\\\ []) do\n    timeout = Keyword.get(opts, :timeout, 5_000)\n\n    reply = Gnat.request(conn, key_name(bucket_name, key), value, receive_timeout: timeout)\n\n    case reply do\n      {:ok, _} -> :ok\n      error -> error\n    end\n  end\n\n  @doc \"\"\"\n  Delete a Key from a K/V Bucket\n\n  ## Examples\n\n      iex> :ok = Jetstream.API.KV.delete_key(:gnat, \"my_bucket\", \"my_key\")\n  \"\"\"\n  @spec delete_key(\n          conn :: Gnat.t(),\n          bucket_name :: binary(),\n          key :: binary(),\n          opts :: keyword()\n        ) ::\n          :ok | {:error, any()}\n  def delete_key(conn, bucket_name, key, opts \\\\ []) do\n    timeout = Keyword.get(opts, :timeout, 5_000)\n\n    reply =\n      Gnat.request(conn, key_name(bucket_name, key), \"\",\n        headers: [{\"KV-Operation\", \"DEL\"}],\n        receive_timeout: timeout\n      )\n\n    case reply do\n      {:ok, _} -> :ok\n      error -> error\n    end\n  end\n\n  @doc \"\"\"\n  Purge a Key from a K/V bucket. This will remove any revision history the key had\n\n  ## Examples\n\n      iex> :ok = Jetstream.API.KV.purge_key(:gnat, \"my_bucket\", \"my_key\")\n  \"\"\"\n  @spec purge_key(\n          conn :: Gnat.t(),\n          bucket_name :: binary(),\n          key :: binary(),\n          opts :: keyword()\n        ) ::\n          :ok | {:error, any()}\n  def purge_key(conn, bucket_name, key, opts \\\\ []) do\n    timeout = Keyword.get(opts, :timeout, 5_000)\n\n    reply =\n      Gnat.request(conn, key_name(bucket_name, key), \"\",\n        headers: [{\"KV-Operation\", \"PURGE\"}, {\"Nats-Rollup\", \"sub\"}],\n        receive_timeout: timeout\n      )\n\n    case reply do\n      {:ok, _} -> :ok\n      error -> error\n    end\n  end\n\n  @doc \"\"\"\n  Put a value into a Key in a K/V Bucket\n\n  ## Examples\n\n      iex> :ok = Jetstream.API.KV.put_value(:gnat, \"my_bucket\", \"my_key\", \"my_value\")\n  \"\"\"\n  @spec put_value(\n          conn :: Gnat.t(),\n          bucket_name :: binary(),\n          key :: binary(),\n          value :: binary(),\n          opts :: keyword()\n        ) ::\n          :ok | {:error, any()}\n  def put_value(conn, bucket_name, key, value, opts \\\\ []) do\n    timeout = Keyword.get(opts, :timeout, 5_000)\n\n    reply = Gnat.request(conn, key_name(bucket_name, key), value, receive_timeout: timeout)\n\n    case reply do\n      {:ok, _} -> :ok\n      error -> error\n    end\n  end\n\n  @doc \"\"\"\n  Get the value for a key in a particular K/V bucket\n\n  ## Examples\n\n      iex> \"my_value\" = Jetstream.API.KV.get_value(:gnat, \"my_bucket\", \"my_key\")\n  \"\"\"\n  @spec get_value(conn :: Gnat.t(), bucket_name :: binary(), key :: binary()) ::\n          binary() | {:error, any()} | nil\n  def get_value(conn, bucket_name, key) do\n    case Stream.get_message(conn, stream_name(bucket_name), %{\n           last_by_subj: key_name(bucket_name, key)\n         }) do\n      {:ok, message} -> message.data\n      error -> error\n    end\n  end\n\n  @doc \"\"\"\n  Get all the non-deleted key-value pairs for a Bucket\n\n  ## Options\n\n  * `:batch` - Number of messages to fetch per request (default: 250)\n  * `:domain` - JetStream domain (default: nil)\n\n  ## Examples\n\n      iex> {:ok, %{\"key1\" => \"value1\"}} = Jetstream.API.KV.contents(:gnat, \"my_bucket\")\n      iex> {:ok, contents} = Jetstream.API.KV.contents(:gnat, \"my_bucket\", batch: 500)\n  \"\"\"\n  @spec contents(conn :: Gnat.t(), bucket_name :: binary(), opts :: keyword()) ::\n          {:ok, map()} | {:error, binary()}\n  def contents(conn, bucket_name, opts \\\\ []) do\n    alias Gnat.Jetstream.Pager\n    stream = stream_name(bucket_name)\n    domain = Keyword.get(opts, :domain)\n    batch_size = Keyword.get(opts, :batch, 250)\n\n    pager_opts = [domain: domain, batch: batch_size]\n\n    Pager.reduce(conn, stream, pager_opts, %{}, fn msg, acc ->\n      case msg do\n        %{topic: key, body: body, headers: headers} ->\n          if {\"kv-operation\", \"DEL\"} in headers do\n            acc\n          else\n            Map.put(acc, subject_to_key(key, bucket_name), body)\n          end\n\n        %{topic: key, body: body} ->\n          Map.put(acc, subject_to_key(key, bucket_name), body)\n      end\n    end)\n  end\n\n  @doc \"\"\"\n  Get all the non-deleted keys for a Bucket\n\n  ## Options\n\n  * `:batch` - Number of messages to fetch per request (default: 250)\n  * `:domain` - JetStream domain (default: nil)\n\n  ## Examples\n\n      iex> {:ok, [\"key1\", \"key2\"]} = Jetstream.API.KV.keys(:gnat, \"my_bucket\")\n      iex> {:ok, keys} = Jetstream.API.KV.keys(:gnat, \"my_bucket\", batch: 500)\n  \"\"\"\n  @spec keys(conn :: Gnat.t(), bucket_name :: binary(), opts :: keyword()) ::\n          {:ok, list(binary())} | {:error, binary()}\n  def keys(conn, bucket_name, opts \\\\ []) do\n    alias Gnat.Jetstream.Pager\n    stream = stream_name(bucket_name)\n    domain = Keyword.get(opts, :domain)\n    batch_size = Keyword.get(opts, :batch, 250)\n\n    pager_opts = [domain: domain, headers_only: true, batch: batch_size]\n\n    result =\n      Pager.reduce(conn, stream, pager_opts, MapSet.new(), fn msg, acc ->\n        case msg do\n          %{topic: key, headers: headers} ->\n            cond do\n              {\"kv-operation\", \"DEL\"} in headers ->\n                MapSet.delete(acc, subject_to_key(key, bucket_name))\n\n              {\"kv-operation\", \"PURGE\"} in headers ->\n                MapSet.delete(acc, subject_to_key(key, bucket_name))\n\n              true ->\n                MapSet.put(acc, subject_to_key(key, bucket_name))\n            end\n\n          %{topic: key} ->\n            MapSet.put(acc, subject_to_key(key, bucket_name))\n        end\n      end)\n\n    case result do\n      {:ok, key_set} -> {:ok, Enum.sort(MapSet.to_list(key_set))}\n      error -> error\n    end\n  end\n\n  @doc \"\"\"\n  Information about the state of the bucket's Stream.\n\n  ## Opts\n  * `:domain` - (default `nil`) the domain of the bucket\n  \"\"\"\n  @spec info(conn :: Gnat.t(), bucket_name :: binary(), keyword()) ::\n          {:ok, Stream.info()} | {:error, any()}\n  def info(conn, bucket_name, opts \\\\ []) do\n    Stream.info(conn, @stream_prefix <> bucket_name, Keyword.get(opts, :domain))\n  end\n\n  @doc ~S\"\"\"\n  Starts a monitor for key changes in a given bucket. Supply a handler that will receive\n  key change notifications.\n\n  ## Examples\n\n      iex> {:ok, _pid} = Jetstream.API.KV.watch(:gnat, \"my_bucket\", fn action, key, value ->\n      ...>  IO.puts(\"#{action} taken on #{key}\")\n      ...> end)\n  \"\"\"\n  def watch(conn, bucket_name, handler) do\n    Gnat.Jetstream.API.KV.Watcher.start_link(\n      conn: conn,\n      bucket_name: bucket_name,\n      handler: handler\n    )\n  end\n\n  @doc ~S\"\"\"\n  Stops a previously running monitor. This will unsubscribe from the key changes and remove the\n  ephemeral consumer\n\n  ## Examples\n\n      iex> :ok = Jetstream.API.KV.unwatch(pid)\n  \"\"\"\n  def unwatch(pid) do\n    Gnat.Jetstream.API.KV.Watcher.stop(pid)\n  end\n\n  @spec is_kv_bucket_stream?(stream_name :: binary()) :: boolean()\n  @deprecated \"Use Gnat.Jetstream.API.KV.kv_bucket_stream?/1 instead\"\n  def is_kv_bucket_stream?(stream_name) do\n    kv_bucket_stream?(stream_name)\n  end\n\n  @doc \"\"\"\n  Returns true if the provided stream is a KV bucket, false otherwise\n\n  ## Parameters\n  * `stream_name` - the stream name to test\n  \"\"\"\n  @spec kv_bucket_stream?(stream_name :: binary()) :: boolean()\n  def kv_bucket_stream?(stream_name) do\n    String.starts_with?(stream_name, \"KV_\")\n  end\n\n  @doc \"\"\"\n  Returns a list of all the buckets in the KV\n  \"\"\"\n  @spec list_buckets(conn :: Gnat.t()) :: {:error, term()} | {:ok, list(String.t())}\n  def list_buckets(conn) do\n    with {:ok, %{streams: streams}} <- Stream.list(conn) do\n      stream_names =\n        for bucket <- streams, kv_bucket_stream?(bucket) do\n          String.trim_leading(bucket, @stream_prefix)\n        end\n\n      {:ok, stream_names}\n    else\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  @doc false\n  def stream_name(bucket_name) do\n    \"#{@stream_prefix}#{bucket_name}\"\n  end\n\n  defp stream_subjects(bucket_name) do\n    [\"#{@subject_prefix}#{bucket_name}.>\"]\n  end\n\n  defp key_name(bucket_name, key) do\n    \"#{@subject_prefix}#{bucket_name}.#{key}\"\n  end\n\n  @doc false\n  def subject_to_key(subject, bucket_name) do\n    String.replace(subject, \"#{@subject_prefix}#{bucket_name}.\", \"\")\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/message.ex",
    "content": "defmodule Gnat.Jetstream.API.Message do\n  @moduledoc \"\"\"\n  This module provides a way to parse the `reply_to` received by a PullConsumer\n  and get some useful information about the state of the consumer.\n  \"\"\"\n\n  # Based on:\n  # https://github.com/nats-io/nats.py/blob/d9f24b4beae541b7723873ba0a786ea7c0ecb3d5/nats/aio/msg.py#L182\n\n  defmodule Metadata do\n    @type t :: %__MODULE__{\n            :stream_seq => integer(),\n            :consumer_seq => integer(),\n            :num_pending => integer(),\n            :num_delivered => integer(),\n            :timestamp => DateTime.t(),\n            :stream => String.t(),\n            :consumer => String.t(),\n            :domain => String.t() | nil\n          }\n\n    defstruct [\n      :stream_seq,\n      :consumer_seq,\n      :num_pending,\n      :num_delivered,\n      :timestamp,\n      :stream,\n      :consumer,\n      :domain\n    ]\n  end\n\n  @spec metadata(message :: Gnat.message()) :: {:ok, Metadata.t()} | {:error, term()}\n  def metadata(%{reply_to: \"$JS.ACK.\" <> ack_topic}),\n    do: decode_reply_to(String.split(ack_topic, \".\"))\n\n  def metadata(_), do: {:error, :no_jetstream_message}\n\n  # # Subject without domain:\n  # $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>\n  defp decode_reply_to([\n         stream,\n         consumer,\n         num_delivered,\n         stream_seq,\n         consumer_seq,\n         ts,\n         num_pending\n       ]) do\n    with {ts, \"\"} <- Integer.parse(ts),\n         {:ok, ts} <- DateTime.from_unix(ts, :nanosecond),\n         {stream_seq, \"\"} <- Integer.parse(stream_seq),\n         {consumer_seq, \"\"} <- Integer.parse(consumer_seq),\n         {num_delivered, \"\"} <- Integer.parse(num_delivered),\n         {num_pending, \"\"} <- Integer.parse(num_pending) do\n      {:ok,\n       %Metadata{\n         stream_seq: stream_seq,\n         consumer_seq: consumer_seq,\n         num_delivered: num_delivered,\n         num_pending: num_pending,\n         timestamp: ts,\n         stream: stream,\n         consumer: consumer\n       }}\n    else\n      _ ->\n        {:error, :invalid_ack_reply_to}\n    end\n  end\n\n  # Subject with domain:\n  # $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.\n  #   <cseq>.<tm>.<pending>.<a token with a random value>\n  defp decode_reply_to([\n         domain,\n         _account_hash,\n         stream,\n         consumer,\n         num_delivered,\n         stream_seq,\n         consumer_seq,\n         ts,\n         num_pending,\n         _random_value\n       ]) do\n    case decode_reply_to([\n           stream,\n           consumer,\n           num_delivered,\n           stream_seq,\n           consumer_seq,\n           ts,\n           num_pending\n         ]) do\n      {:ok, metadata} ->\n        domain = if domain == \"_\", do: nil, else: domain\n        {:ok, %{metadata | domain: domain}}\n\n      err = {:error, _} ->\n        err\n    end\n  end\n\n  defp decode_reply_to(_), do: {:error, :invalid_ack_reply_to}\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/object/meta.ex",
    "content": "defmodule Gnat.Jetstream.API.Object.Meta do\n  @enforce_keys [:bucket, :chunks, :digest, :name, :nuid, :size]\n  defstruct bucket: nil,\n            chunks: nil,\n            deleted: false,\n            digest: nil,\n            name: nil,\n            nuid: nil,\n            size: nil\n\n  @type t :: %__MODULE__{\n          bucket: String.t(),\n          chunks: non_neg_integer(),\n          deleted: boolean(),\n          digest: String.t(),\n          name: String.t(),\n          nuid: String.t(),\n          size: non_neg_integer()\n        }\nend\n\ndefimpl Jason.Encoder, for: Gnat.Jetstream.API.Object.Meta do\n  alias Gnat.Jetstream.API.Object.Meta\n\n  def encode(%Meta{deleted: true} = meta, opts) do\n    Map.take(meta, [:bucket, :chunks, :deleted, :digest, :name, :nuid, :size])\n    |> Jason.Encode.map(opts)\n  end\n\n  def encode(meta, opts) do\n    Map.take(meta, [:bucket, :chunks, :digest, :name, :nuid, :size])\n    |> Jason.Encode.map(opts)\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/object.ex",
    "content": "defmodule Gnat.Jetstream.API.Object do\n  @moduledoc \"\"\"\n  API for interacting with the JetStream Object Store\n\n  Learn more about Object Store: https://docs.nats.io/nats-concepts/jetstream/obj_store\n  \"\"\"\n  alias Gnat.Jetstream.API.{Consumer, Stream, Util}\n  alias Gnat.Jetstream.API.Object.Meta\n\n  @stream_prefix \"OBJ_\"\n  @subject_prefix \"$O.\"\n\n  @type bucket_opt ::\n          {:description, String.t()}\n          | {:max_bucket_size, integer()}\n          | {:max_chunk_size, integer()}\n          | {:placement, Stream.placement()}\n          | {:replicas, non_neg_integer()}\n          | {:storage, :file | :memory}\n          | {:ttl, non_neg_integer()}\n  @spec create_bucket(Gnat.t(), String.t(), list(bucket_opt)) ::\n          {:ok, Stream.info()} | {:error, any()}\n  def create_bucket(conn, bucket_name, params \\\\ []) do\n    with :ok <- validate_bucket_name(bucket_name) do\n      stream = %Stream{\n        name: stream_name(bucket_name),\n        subjects: stream_subjects(bucket_name),\n        description: Keyword.get(params, :description),\n        discard: :new,\n        allow_rollup_hdrs: true,\n        max_age: Keyword.get(params, :ttl, 0),\n        max_bytes: Keyword.get(params, :max_bucket_size, -1),\n        max_msg_size: Keyword.get(params, :max_chunk_size, -1),\n        num_replicas: Keyword.get(params, :replicas, 1),\n        storage: Keyword.get(params, :storage, :file),\n        placement: Keyword.get(params, :placement),\n        duplicate_window: adjust_duplicate_window(Keyword.get(params, :ttl, 0))\n      }\n\n      Stream.create(conn, stream)\n    end\n  end\n\n  @spec delete_bucket(Gnat.t(), String.t()) :: :ok | {:error, any}\n  def delete_bucket(conn, bucket_name) do\n    Stream.delete(conn, stream_name(bucket_name))\n  end\n\n  @spec delete(Gnat.t(), String.t(), String.t()) :: :ok | {:error, any}\n  def delete(conn, bucket_name, object_name) do\n    with {:ok, meta = %Meta{}} <- info(conn, bucket_name, object_name),\n         meta <- %Meta{meta | deleted: true},\n         topic <- meta_stream_topic(bucket_name, object_name),\n         {:ok, body} <- Jason.encode(meta),\n         {:ok, _msg} <- Gnat.request(conn, topic, body, headers: [{\"Nats-Rollup\", \"sub\"}]) do\n      filter = chunk_stream_topic(meta)\n      Stream.purge(conn, stream_name(bucket_name), nil, %{filter: filter})\n    end\n  end\n\n  @spec get(Gnat.t(), String.t(), String.t(), (binary -> any())) :: :ok | {:error, any}\n  def get(conn, bucket_name, object_name, chunk_fun) do\n    with {:ok, %{config: _stream}} <- Stream.info(conn, stream_name(bucket_name)),\n         {:ok, meta} <- info(conn, bucket_name, object_name) do\n      receive_chunks(conn, meta, chunk_fun)\n    end\n  end\n\n  @spec info(Gnat.t(), String.t(), String.t()) :: {:ok, Meta.t()} | {:error, any}\n  def info(conn, bucket_name, object_name) do\n    with {:ok, _stream_info} <- Stream.info(conn, stream_name(bucket_name)) do\n      Stream.get_message(conn, stream_name(bucket_name), %{\n        last_by_subj: meta_stream_topic(bucket_name, object_name)\n      })\n      |> case do\n        {:ok, message} ->\n          meta = json_to_meta(message.data)\n          {:ok, meta}\n\n        error ->\n          error\n      end\n    end\n  end\n\n  @type list_option :: {:show_deleted, boolean()}\n  @spec list(Gnat.t(), String.t(), list(list_option())) :: {:error, any} | {:ok, list(Meta.t())}\n  def list(conn, bucket_name, options \\\\ []) do\n    with {:ok, %{config: stream}} <- Stream.info(conn, stream_name(bucket_name)),\n         topic <- Util.reply_inbox(),\n         {:ok, sub} <- Gnat.sub(conn, self(), topic),\n         {:ok, consumer} <-\n           Consumer.create(conn, %Consumer{\n             stream_name: stream.name,\n             deliver_subject: topic,\n             deliver_policy: :last_per_subject,\n             filter_subject: meta_stream_subject(bucket_name),\n             ack_policy: :none,\n             max_ack_pending: nil,\n             replay_policy: :instant,\n             max_deliver: 1\n           }),\n         {:ok, messages} <- receive_all_metas(sub, consumer.num_pending) do\n      :ok = Gnat.unsub(conn, sub)\n      :ok = Consumer.delete(conn, stream.name, consumer.name)\n\n      show_deleted = Keyword.get(options, :show_deleted, false)\n\n      if show_deleted do\n        {:ok, messages}\n      else\n        {:ok, Enum.reject(messages, &(&1.deleted == true))}\n      end\n    end\n  end\n\n  @spec put(Gnat.t(), String.t(), String.t(), File.io_device()) ::\n          {:ok, Meta.t()} | {:error, any()}\n  def put(conn, bucket_name, object_name, io) do\n    nuid = Util.nuid()\n    chunk_topic = chunk_stream_topic(bucket_name, nuid)\n\n    with {:ok, %{config: _}} <- Stream.info(conn, stream_name(bucket_name)),\n         :ok <- purge_prior_chunks(conn, bucket_name, object_name),\n         {:ok, chunks, size, digest} <- send_chunks(conn, io, chunk_topic) do\n      object_meta = %Meta{\n        name: object_name,\n        bucket: bucket_name,\n        nuid: nuid,\n        size: size,\n        chunks: chunks,\n        digest: \"SHA-256=#{Base.url_encode64(digest)}\"\n      }\n\n      topic = meta_stream_topic(bucket_name, object_name)\n      body = Jason.encode!(object_meta)\n\n      case Gnat.request(conn, topic, body, headers: [{\"Nats-Rollup\", \"sub\"}]) do\n        {:ok, _} ->\n          {:ok, object_meta}\n\n        error ->\n          error\n      end\n    end\n  end\n\n  @doc \"\"\"\n  Returns true if the provided stream is an Object bucket, false otherwise\n  ## Parameters\n  * `stream_name` - the stream name to test\n  \"\"\"\n  @spec is_object_bucket_stream?(stream_name :: binary()) :: boolean()\n  def is_object_bucket_stream?(stream_name) do\n    String.starts_with?(stream_name, \"OBJ_\")\n  end\n\n  @doc \"\"\"\n  Returns a list of all Object buckets\n  \"\"\"\n  @spec list_buckets(conn :: Gnat.t()) :: {:error, term()} | {:ok, list(String.t())}\n  def list_buckets(conn) do\n    with {:ok, %{streams: streams}} <- Stream.list(conn) do\n      stream_names =\n        streams\n        |> Enum.flat_map(fn bucket ->\n          if is_object_bucket_stream?(bucket) do\n            [bucket |> String.trim_leading(@stream_prefix)]\n          else\n            []\n          end\n        end)\n\n      {:ok, stream_names}\n    else\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp stream_name(bucket_name) do\n    \"#{@stream_prefix}#{bucket_name}\"\n  end\n\n  defp stream_subjects(bucket_name) do\n    [\n      chunk_stream_subject(bucket_name),\n      meta_stream_subject(bucket_name)\n    ]\n  end\n\n  defp chunk_stream_subject(bucket_name) do\n    \"#{@subject_prefix}#{bucket_name}.C.>\"\n  end\n\n  defp chunk_stream_topic(bucket_name, nuid) do\n    \"#{@subject_prefix}#{bucket_name}.C.#{nuid}\"\n  end\n\n  defp chunk_stream_topic(%Meta{bucket: bucket, nuid: nuid}) do\n    \"#{@subject_prefix}#{bucket}.C.#{nuid}\"\n  end\n\n  defp meta_stream_subject(bucket_name) do\n    \"#{@subject_prefix}#{bucket_name}.M.>\"\n  end\n\n  defp meta_stream_topic(bucket_name, object_name) do\n    key = Base.url_encode64(object_name)\n    \"#{@subject_prefix}#{bucket_name}.M.#{key}\"\n  end\n\n  @two_minutes_in_nanoseconds 120_000_000_000\n  # The `duplicate_window` can't be greater than the `max_age`. The default `duplicate_window`\n  # is 2 minutes. We'll keep the 2 minute window UNLESS the ttl is less than 2 minutes\n  defp adjust_duplicate_window(ttl) when ttl > 0 and ttl < @two_minutes_in_nanoseconds, do: ttl\n  defp adjust_duplicate_window(_ttl), do: @two_minutes_in_nanoseconds\n\n  defp json_to_meta(json) do\n    raw = Jason.decode!(json)\n\n    %{\n      \"bucket\" => bucket,\n      \"chunks\" => chunks,\n      \"digest\" => digest,\n      \"name\" => name,\n      \"nuid\" => nuid,\n      \"size\" => size\n    } = raw\n\n    %Meta{\n      bucket: bucket,\n      chunks: chunks,\n      digest: digest,\n      deleted: Map.get(raw, \"deleted\", false),\n      name: name,\n      nuid: nuid,\n      size: size\n    }\n  end\n\n  defp purge_prior_chunks(conn, bucket, name) do\n    case info(conn, bucket, name) do\n      {:ok, meta} ->\n        Stream.purge(conn, stream_name(bucket), nil, %{filter: chunk_stream_topic(meta)})\n\n      {:error, %{\"code\" => 404}} ->\n        :ok\n\n      {:error, other} ->\n        {:error, other}\n    end\n  end\n\n  defp receive_all_metas(sid, num_pending, messages \\\\ [])\n\n  defp receive_all_metas(_sid, 0, messages) do\n    {:ok, messages}\n  end\n\n  defp receive_all_metas(sid, remaining, messages) do\n    receive do\n      {:msg, %{sid: ^sid, body: body}} ->\n        meta = json_to_meta(body)\n        receive_all_metas(sid, remaining - 1, [meta | messages])\n    after\n      10_000 ->\n        {:error, :timeout_waiting_for_messages}\n    end\n  end\n\n  defp receive_chunks(conn, %Meta{} = meta, chunk_fun) do\n    topic = chunk_stream_topic(meta)\n    stream = stream_name(meta.bucket)\n    inbox = Util.reply_inbox()\n    {:ok, sub} = Gnat.sub(conn, self(), inbox)\n\n    {:ok, consumer} =\n      Consumer.create(conn, %Consumer{\n        stream_name: stream,\n        deliver_subject: inbox,\n        deliver_policy: :all,\n        filter_subject: topic,\n        ack_policy: :none,\n        max_ack_pending: nil,\n        replay_policy: :instant,\n        max_deliver: 1,\n        flow_control: true,\n        idle_heartbeat: 5_000_000_000\n      })\n\n    :ok = receive_chunks(conn, sub, meta.chunks, chunk_fun)\n\n    :ok = Gnat.unsub(conn, sub)\n    :ok = Consumer.delete(conn, stream, consumer.name)\n  end\n\n  defp receive_chunks(_conn, _sub, 0, _chunk_fun) do\n    :ok\n  end\n\n  defp receive_chunks(conn, sub, remaining, chunk_fun) do\n    receive do\n      # Flow control message with reply - respond to it\n      {:msg, %{sid: ^sub, status: \"100\", description: \"FlowControl Request\", reply_to: reply}}\n      when not is_nil(reply) ->\n        Gnat.pub(conn, reply, \"\")\n        receive_chunks(conn, sub, remaining, chunk_fun)\n\n      # Flow control or heartbeat message without reply - get next message\n      {:msg, %{sid: ^sub, body: \"\", status: \"100\"}} ->\n        receive_chunks(conn, sub, remaining, chunk_fun)\n\n      # Regular data message\n      {:msg, %{sid: ^sub, body: body}} ->\n        chunk_fun.(body)\n        receive_chunks(conn, sub, remaining - 1, chunk_fun)\n    after\n      10_000 ->\n        {:error, :timeout_waiting_for_messages}\n    end\n  end\n\n  @chunk_size 128 * 1024\n  defp send_chunks(conn, io, topic) do\n    sha = :crypto.hash_init(:sha256)\n    size = 0\n    chunks = 0\n    send_chunks(conn, io, topic, sha, size, chunks)\n  end\n\n  defp send_chunks(conn, io, topic, sha, size, chunks) do\n    case IO.binread(io, @chunk_size) do\n      :eof ->\n        sha = :crypto.hash_final(sha)\n        {:ok, chunks, size, sha}\n\n      {:error, err} ->\n        {:error, err}\n\n      bytes ->\n        sha = :crypto.hash_update(sha, bytes)\n        size = size + byte_size(bytes)\n        chunks = chunks + 1\n\n        case Gnat.request(conn, topic, bytes) do\n          {:ok, _} ->\n            send_chunks(conn, io, topic, sha, size, chunks)\n\n          error ->\n            error\n        end\n    end\n  end\n\n  defp validate_bucket_name(name) do\n    case Regex.match?(~r/^[a-zA-Z0-9_-]+$/, name) do\n      true -> :ok\n      false -> {:error, \"invalid bucket name\"}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/stream.ex",
    "content": "defmodule Gnat.Jetstream.API.Stream do\n  @moduledoc \"\"\"\n  A module representing a NATS JetStream Stream.\n\n  Learn more about Streams: https://docs.nats.io/nats-concepts/jetstream/streams\n\n  ## The Jetstream.API.Stream struct\n\n  The struct's mandatory fields are `:name` and `:subjects`. The rest will have the NATS\n  default values set.\n\n  Stream struct fields explanation:\n\n  * `:allow_direct` - Allow higher performance, direct access to get individual messages. E.g. KeyValue\n  * `:allow_msg_ttl` - Allow header initiated per-message TTLs.\n  * `:allow_rollup_hdrs` - allows the use of the Nats-Rollup header to replace all contents of a stream,\n    or subject in a stream, with a single new message.\n  * `:deny_delete` - restricts the ability to delete messages from a stream via the API. Cannot be changed\n    once set to true.\n  * `:deny_purge` - restricts the ability to purge messages from a stream via the API. Cannot be change\n    once set to true.\n  * `:description` - a short description of the purpose of this stream.\n  * `:discard` - determines what happens when a Stream reaches its limits. It has the following options:\n     - `:old` - the default option. Old messages are deleted.\n     - `:new` - refuses new messages.\n  * `:discard_new_per_subject` - - allows to enable discarding new messages per subject when limits are reached.\n    Requires `discard: :new` and the `:max_msgs_per_subject` to be configured.\n  * `:domain` - JetStream domain, mainly used for leaf nodes.\n     See [JetStream on Leaf Nodes](https://docs.nats.io/running-a-nats-service/configuration/leafnodes/jetstream_leafnodes).\n  * `:duplicate_window` - the window within which to track duplicate messages, expressed in nanoseconds.\n  * `:max_age` - maximum age of any message in the Stream, expressed in nanoseconds.\n  * `:max_bytes` - how many bytes the Stream may contain. Adheres to `:discard`, removing oldest or\n    refusing new messages if the Stream exceeds this size.\n  * `:max_consumers` - how many Consumers can be defined for a given Stream, -1 for unlimited.\n  * `:max_msg_size` - the largest message that will be accepted by the Stream.\n  * `:max_msgs_per_subject` - For wildcard streams ensure that for every unique subject this many messages are kept - a per subject retention limit.\n    Only available on nats-server versions greater than 2.3.0\n  * `:max_msgs` - how many messages may be in a Stream. Adheres to `:discard`, removing oldest or refusing\n    new messages if the Stream exceeds this number of messages\n  * `:mirror` - maintains a 1:1 mirror of another stream with name matching this property.  When a mirror\n    is configured subjects and sources must be empty.\n  * `:mirror_direct` - Allow higher performance and unified direct access for mirrors as well.\n  * `:name` - a name for the Stream.\n    See [naming](https://docs.nats.io/running-a-nats-service/nats_admin/jetstream_admin/naming).\n  * `:no_ack` - disables acknowledging messages that are received by the Stream.\n  * `:num_replicas` - how many replicas to keep for each message.\n  * `:placement` - placement directives to consider when placing replicas of this stream, random placement\n    when unset. It has the following properties:\n     - `:cluster` - the desired cluster name to place the stream.\n     - `:tags` - tags required on servers hosting this stream.\n  * `:retention` - how messages are retained in the Stream. Once this is exceeded, old messages are removed.\n    It has the following options:\n     - `:limits` - the default policy.\n     - `:interest`\n     - `:workqueue`\n  * `:sealed` - sealed streams do not allow messages to be deleted via limits or API, sealed streams can not\n    be unsealed via configuration update. Can only be set on already created streams via the Update API.\n  * `:sources` - list of stream names to replicate into this stream.\n  * `:storage` - the type of storage backend. Available options:\n     - `:file`\n     - `:memory`\n  * `:compression` - If file-based and a compression algorithm is specified, the stream data will be compressed on disk.\n    Valid options are \"none\" for no compression or \"s2\" for Snappy compression.\n  * `:subjects` - a list of subjects to consume, supports wildcards.\n  * `:subject_delete_marker_ttl` - Enables and sets a duration expressed in nanoseconds. for adding server markers for\n    delete, purge and max age limits.\n  * `:template_owner` - when the Stream is managed by a Stream Template this identifies the template that\n    manages the Stream.\n  \"\"\"\n\n  import Gnat.Jetstream.API.Util\n\n  @enforce_keys [:name, :subjects]\n  @derive {Jason.Encoder, except: [:domain]}\n  defstruct [\n    :description,\n    :mirror,\n    :name,\n    :domain,\n    :no_ack,\n    :placement,\n    :sources,\n    :subjects,\n    :template_owner,\n    allow_direct: false,\n    allow_msg_ttl: false,\n    allow_rollup_hdrs: false,\n    deny_delete: false,\n    deny_purge: false,\n    discard: :old,\n    duplicate_window: 120_000_000_000,\n    max_age: 0,\n    max_bytes: -1,\n    max_consumers: -1,\n    max_msg_size: -1,\n    max_msgs_per_subject: -1,\n    max_msgs: -1,\n    mirror_direct: false,\n    num_replicas: 1,\n    retention: :limits,\n    sealed: false,\n    storage: :file,\n    subject_delete_marker_ttl: 0,\n    discard_new_per_subject: false,\n    compression: \"none\"\n  ]\n\n  @type nanoseconds :: non_neg_integer()\n\n  @type placement :: %{\n          :cluster => binary(),\n          optional(:tags) => list(binary())\n        }\n\n  @type t :: %__MODULE__{\n          allow_direct: boolean(),\n          allow_msg_ttl: boolean(),\n          allow_rollup_hdrs: boolean(),\n          deny_delete: boolean(),\n          deny_purge: boolean(),\n          description: nil | binary(),\n          discard: :old | :new,\n          domain: nil | binary(),\n          duplicate_window: nil | nanoseconds(),\n          max_age: nanoseconds(),\n          max_bytes: integer(),\n          max_consumers: integer(),\n          max_msg_size: nil | integer(),\n          max_msgs: integer(),\n          max_msgs_per_subject: integer(),\n          mirror: nil | source(),\n          mirror_direct: boolean(),\n          name: binary(),\n          no_ack: nil | boolean(),\n          num_replicas: pos_integer(),\n          placement: nil | placement(),\n          retention: :limits | :workqueue | :interest,\n          sealed: boolean(),\n          sources: nil | list(source()),\n          storage: :file | :memory,\n          subjects: nil | list(binary()),\n          subject_delete_marker_ttl: nanoseconds(),\n          template_owner: nil | binary(),\n          discard_new_per_subject: boolean(),\n          compression: binary()\n        }\n\n  @typedoc \"\"\"\n  Stream source fields explained:\n\n  * `:name` - stream name.\n  * `:opt_start_seq` - sequence to start replicating from.\n  * `:opt_start_time` - timestamp to start replicating from.\n  * `:filter_subject` - replicate only a subset of messages based on filter.\n  * `:external` - configuration referencing a stream source in another account or JetStream domain.\n    It has the following parameters:\n     - `:api` - the subject prefix that imports other account/domain `$JS.API.CONSUMER.>` subjects\n     - `:deliver` - the delivery subject to use for push consumer\n  \"\"\"\n  @type source :: %{\n          :name => binary(),\n          optional(:opt_start_seq) => integer(),\n          optional(:opt_start_time) => DateTime.t(),\n          optional(:filter_subject) => binary(),\n          optional(:external) => %{\n            api: binary(),\n            deliver: binary()\n          }\n        }\n\n  @type info :: %{\n          cluster:\n            nil\n            | %{\n                optional(:name) => binary(),\n                optional(:leader) => binary(),\n                optional(:replicas) =>\n                  list(%{\n                    :active => nanoseconds(),\n                    :name => binary(),\n                    :current => boolean(),\n                    optional(:offline) => boolean(),\n                    optional(:lag) => non_neg_integer()\n                  })\n              },\n          config: t(),\n          created: DateTime.t(),\n          mirror: nil | source_info(),\n          sources: nil | list(source_info()),\n          state: state()\n        }\n\n  @type state :: %{\n          bytes: non_neg_integer(),\n          consumer_count: non_neg_integer(),\n          deleted: nil | [non_neg_integer()],\n          first_seq: non_neg_integer(),\n          first_ts: DateTime.t(),\n          last_seq: non_neg_integer(),\n          last_ts: DateTime.t(),\n          lost: nil | list(%{msgs: [non_neg_integer()], bytes: non_neg_integer()}),\n          messages: non_neg_integer(),\n          num_deleted: nil | integer(),\n          num_subjects: nil | integer(),\n          subjects:\n            nil\n            | %{}\n            | %{\n                binary() => non_neg_integer()\n              }\n        }\n\n  @typedoc \"\"\"\n  * `code` - HTTP like error code in the 300 to 500 range\n  * `description` - A human friendly description of the error\n  * `err_code` - The NATS error code unique to each kind of error\n  \"\"\"\n  @type response_error :: %{\n          :code => non_neg_integer(),\n          optional(:description) => binary(),\n          optional(:err_code) => non_neg_integer()\n        }\n\n  @type source_info :: %{\n          :active => nanoseconds(),\n          :lag => non_neg_integer(),\n          :name => binary(),\n          optional(:external) => %{\n            api: binary(),\n            deliver: binary()\n          },\n          optional(:error) => response_error()\n        }\n\n  @type streams :: %{\n          limit: non_neg_integer(),\n          offset: non_neg_integer(),\n          streams: list(binary()),\n          total: non_neg_integer()\n        }\n\n  @typedoc \"\"\"\n  * `seq` - Stream sequence number of the message to retrieve, cannot be combined with `last_by_subj`\n  * `last_by_subj` - Retrieves the last message for a given subject, cannot be combined with `seq`\n  \"\"\"\n  @type message_access_method :: %{\n          optional(:seq) => non_neg_integer(),\n          optional(:last_by_subj) => binary()\n        }\n\n  @typedoc \"\"\"\n  * `data` - The decoded message payload\n  * `subject` - The subject the message was originally received on\n  * `time` - The time the message was received\n  * `seq` - The sequence number of the message in the Stream\n  * `hdrs` - The decoded headers for the message\n  \"\"\"\n  @type message_response :: %{\n          :data => any(),\n          :seq => non_neg_integer(),\n          :subject => binary(),\n          :time => DateTime.t(),\n          :hdrs => nil | binary()\n        }\n\n  # @doc \"\"\"\n  # Initialize a Stream struct\n\n  # ## Examples\n\n  #     iex> %Stream{} = Gnat.Jetstream.API.Stream.new(:gnat, name: \"NEW_STREAM\", subjects: [\"NEW_STREAM.subjects\"])\n  # \"\"\"\n  # @spec new(conn :: Gnat.t(), fields :: keyword()) :: t()\n  # def new(conn, fields \\\\ []) do\n  #   %__MODULE__{}\n  # end\n\n  @doc \"\"\"\n  Creates a new Stream.\n\n  ## Examples\n\n      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"anewstream\", subjects: [\"anewsubject\"]})\n\n  \"\"\"\n  @spec create(conn :: Gnat.t(), stream :: t()) :: {:ok, info()} | {:error, any()}\n  def create(conn, %__MODULE__{} = stream) do\n    with :ok <- validate(stream),\n         {:ok, stream} <-\n           request(\n             conn,\n             \"#{js_api(stream.domain)}.STREAM.CREATE.#{stream.name}\",\n             Jason.encode!(stream)\n           ) do\n      {:ok, to_info(stream)}\n    end\n  end\n\n  @doc \"\"\"\n  Updates a Stream.\n\n  ## Examples\n\n      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"update_test_stream\", subjects: [\"update_subject\"]})\n      iex> {:ok, _} = Gnat.Jetstream.API.Stream.update(:gnat, %Gnat.Jetstream.API.Stream{name: \"update_test_stream\", subjects: [\"update_subject\", \"new.update_subject\"]})\n\n  \"\"\"\n  @spec update(conn :: Gnat.t(), stream :: t()) :: {:ok, info()} | {:error, any()}\n  def update(conn, %__MODULE__{} = stream) do\n    with :ok <- validate(stream),\n         {:ok, stream} <-\n           request(\n             conn,\n             \"#{js_api(stream.domain)}.STREAM.UPDATE.#{stream.name}\",\n             Jason.encode!(stream)\n           ) do\n      {:ok, to_info(stream)}\n    end\n  end\n\n  @doc \"\"\"\n  Deletes a Stream and all its data.\n\n  ## Examples\n\n      iex> Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"delstream\", subjects: [\"delsubject\"]})\n      iex> Gnat.Jetstream.API.Stream.delete(:gnat, \"delstream\")\n      :ok\n\n      iex> {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} = Gnat.Jetstream.API.Stream.delete(:gnat, \"wrong_stream\")\n\n  \"\"\"\n  @spec delete(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary()) ::\n          :ok | {:error, any()}\n  def delete(conn, stream_name, domain \\\\ nil) when is_binary(stream_name) do\n    with {:ok, _response} <- request(conn, \"#{js_api(domain)}.STREAM.DELETE.#{stream_name}\", \"\") do\n      :ok\n    end\n  end\n\n  @doc \"\"\"\n  Purges all of data in the stream but doesn't delete the stream.\n\n  ## Examples\n\n      iex> Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"purgestream\", subjects: [\"purgesubject\"]})\n      iex> Gnat.Jetstream.API.Stream.purge(:gnat, \"purgestream\")\n      :ok\n\n      iex> {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} = Gnat.Jetstream.API.Stream.purge(:gnat, \"wrong_stream\")\n\n  \"\"\"\n  @spec purge(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary()) ::\n          :ok | {:error, any()}\n  def purge(conn, stream_name, domain \\\\ nil) when is_binary(stream_name) do\n    with {:ok, _response} <- request(conn, \"#{js_api(domain)}.STREAM.PURGE.#{stream_name}\", \"\") do\n      :ok\n    end\n  end\n\n  @doc \"\"\"\n  Purges some of the messages in a stream according to the supplied filter\n\n  ## Examples\n\n      iex> Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"pstream\", subjects: [\"psub1\", \"psub2\"]})\n      iex> Gnat.Jetstream.API.Stream.purge(:gnat, \"pstream\", nil, %{filter: \"psub1\"})\n      :ok\n\n  \"\"\"\n  @type method :: %{filter: String.t()}\n  @spec purge(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary(), method) ::\n          :ok | {:error, any()}\n  def purge(conn, stream_name, domain, method) when is_binary(stream_name) do\n    with :ok <- validate_purge_method(method),\n         body <- Jason.encode!(method),\n         {:ok, _response} <- request(conn, \"#{js_api(domain)}.STREAM.PURGE.#{stream_name}\", body) do\n      :ok\n    end\n  end\n\n  @doc \"\"\"\n  Information about config and state of a Stream.\n\n  ## Examples\n\n      iex> {:ok, _} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: \"infostream\", subjects: [\"infosubject\"]})\n      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Stream.info(:gnat, \"infostream\")\n\n      iex> {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} = Gnat.Jetstream.API.Stream.info(:gnat, \"wrong_stream\")\n\n  \"\"\"\n  @spec info(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary()) ::\n          {:ok, info()} | {:error, any()}\n  def info(conn, stream_name, domain \\\\ nil) when is_binary(stream_name) do\n    with {:ok, decoded} <- request(conn, \"#{js_api(domain)}.STREAM.INFO.#{stream_name}\", \"\") do\n      {:ok, to_info(decoded)}\n    end\n  end\n\n  @doc \"\"\"\n  Paged list of known Streams including all their current information.\n\n  ## Options\n\n  * `:offset` - Number of records to skip\n  * `:subject` - A subject the `Stream` must collect to appear in the list.\n\n  ## Examples\n\n      iex> {:ok, %{total: _, offset: 0, limit: 1024, streams: _}} = Gnat.Jetstream.API.Stream.list(:gnat)\n\n  \"\"\"\n  @spec list(\n          conn :: Gnat.t(),\n          params :: [offset: non_neg_integer(), subject: binary(), domain: nil | binary()]\n        ) ::\n          {:ok, streams()} | {:error, term()}\n  def list(conn, params \\\\ []) do\n    domain = Keyword.get(params, :domain)\n\n    payload =\n      Jason.encode!(%{\n        offset: Keyword.get(params, :offset, 0),\n        subject: Keyword.get(params, :subject)\n      })\n\n    with {:ok, decoded} <- request(conn, \"#{js_api(domain)}.STREAM.NAMES\", payload) do\n      # Recent versions of NATS sometimes return `\"streams\": null` in their JSON payload to indicate\n      # that no streams are defined. But, that would mean callers have to handle both `nil` and a list, so\n      # we coerce that to an empty list to represent no streams being defined.\n      streams =\n        case Map.get(decoded, \"streams\") do\n          nil -> []\n          names when is_list(names) -> names\n        end\n\n      result = %{\n        limit: Map.get(decoded, \"limit\"),\n        offset: Map.get(decoded, \"offset\"),\n        streams: streams,\n        total: Map.get(decoded, \"total\")\n      }\n\n      {:ok, result}\n    end\n  end\n\n  @doc \"\"\"\n  Get a message from the stream either by \"stream sequence number\" or the \"last message for a given subject\"\n  \"\"\"\n  @spec get_message(\n          conn :: Gnat.t(),\n          stream_name :: binary(),\n          method :: message_access_method(),\n          domain :: nil | binary()\n        ) ::\n          {:ok, message_response()} | {:error, response_error()}\n  def get_message(conn, stream_name, method, domain \\\\ nil) when is_map(method) do\n    with :ok <- validate_message_access_method(method),\n         {:ok, %{\"message\" => message}} <-\n           request(conn, \"#{js_api(domain)}.STREAM.MSG.GET.#{stream_name}\", Jason.encode!(method)) do\n      {:ok,\n       %{\n         data: decode_base64(message[\"data\"]),\n         seq: message[\"seq\"],\n         subject: message[\"subject\"],\n         time: to_datetime(message[\"time\"]),\n         hdrs: decode_base64(message[\"hdrs\"])\n       }}\n    end\n  end\n\n  # https://docs.nats.io/running-a-nats-service/configuration/leafnodes/jetstream_leafnodes\n  defp js_api(nil), do: \"$JS.API\"\n  defp js_api(\"\"), do: \"$JS.API\"\n  defp js_api(domain), do: \"$JS.#{domain}.API\"\n\n  defp to_state(state) do\n    %{\n      bytes: Map.fetch!(state, \"bytes\"),\n      consumer_count: Map.fetch!(state, \"consumer_count\"),\n      deleted: Map.get(state, \"deleted\"),\n      first_seq: Map.fetch!(state, \"first_seq\"),\n      first_ts: Map.fetch!(state, \"first_ts\") |> to_datetime(),\n      last_seq: Map.fetch!(state, \"last_seq\"),\n      last_ts: Map.fetch!(state, \"last_ts\") |> to_datetime(),\n      lost: Map.get(state, \"lost\"),\n      messages: Map.fetch!(state, \"messages\"),\n      num_deleted: Map.get(state, \"num_deleted\"),\n      num_subjects: Map.get(state, \"num_subjects\"),\n      subjects: Map.get(state, \"subjects\")\n    }\n  end\n\n  defp to_stream(stream) do\n    %__MODULE__{\n      description: Map.get(stream, \"description\"),\n      discard: Map.fetch!(stream, \"discard\") |> to_sym(),\n      duplicate_window: Map.get(stream, \"duplicate_window\"),\n      max_age: Map.fetch!(stream, \"max_age\"),\n      max_bytes: Map.fetch!(stream, \"max_bytes\"),\n      max_consumers: Map.fetch!(stream, \"max_consumers\"),\n      max_msg_size: Map.get(stream, \"max_msg_size\"),\n      max_msgs_per_subject: Map.get(stream, \"max_msgs_per_subject\", -1),\n      max_msgs: Map.fetch!(stream, \"max_msgs\"),\n      mirror: Map.get(stream, \"mirror\"),\n      name: Map.fetch!(stream, \"name\"),\n      no_ack: Map.get(stream, \"no_ack\"),\n      num_replicas: Map.fetch!(stream, \"num_replicas\"),\n      placement: Map.get(stream, \"placement\"),\n      retention: Map.fetch!(stream, \"retention\") |> to_sym(),\n      sources: Map.get(stream, \"sources\"),\n      storage: Map.fetch!(stream, \"storage\") |> to_sym(),\n      subjects: Map.get(stream, \"subjects\"),\n      template_owner: Map.get(stream, \"template_owner\")\n    }\n    # Check for fields added in NATS versions higher than 2.2.0\n    |> put_if_exist(:allow_direct, stream, \"allow_direct\")\n    |> put_if_exist(:allow_msg_ttl, stream, \"allow_msg_ttl\")\n    |> put_if_exist(:allow_rollup_hdrs, stream, \"allow_rollup_hdrs\")\n    |> put_if_exist(:deny_delete, stream, \"deny_delete\")\n    |> put_if_exist(:deny_purge, stream, \"deny_purge\")\n    |> put_if_exist(:discard_new_per_subject, stream, \"discard_new_per_subject\")\n    |> put_if_exist(:mirror_direct, stream, \"mirror_direct\")\n    |> put_if_exist(:sealed, stream, \"sealed\")\n    |> put_if_exist(:subject_delete_marker_ttl, stream, \"subject_delete_marker_ttl\")\n    |> put_if_exist(:compression, stream, \"compression\")\n  end\n\n  defp to_info(%{\"config\" => config, \"state\" => state, \"created\" => created} = response) do\n    with {:ok, created, _} <- DateTime.from_iso8601(created) do\n      %{\n        cluster: Map.get(response, \"cluster\"),\n        config: to_stream(config),\n        created: created,\n        mirror: Map.get(response, \"mirror\"),\n        sources: Map.get(response, \"sources\"),\n        state: to_state(state)\n      }\n    end\n  end\n\n  defp validate(stream_settings) do\n    cond do\n      Map.has_key?(stream_settings, :name) == false ->\n        {:error, \"Must have a :name set\"}\n\n      is_binary(Map.get(stream_settings, :name)) == false ->\n        {:error, \"name must be a string\"}\n\n      valid_name?(stream_settings.name) == false ->\n        {:error, \"invalid name: \" <> invalid_name_message()}\n\n      Map.has_key?(stream_settings, :subjects) == false ->\n        {:error, \"You must specify a :subjects key\"}\n\n      is_list(Map.get(stream_settings, :subjects)) == false ->\n        {:error, \":subjects must be a list of strings\"}\n\n      Enum.all?(Map.get(stream_settings, :subjects), &is_binary/1) == false ->\n        {:error, \":subjects must be a list of strings\"}\n\n      true ->\n        :ok\n    end\n  end\n\n  defp validate_message_access_method(method) do\n    if map_size(method) == 1 do\n      :ok\n    else\n      {:error, \"To get a message you must use only one of `seq` or `last_by_subj`\"}\n    end\n  end\n\n  defp validate_purge_method(%{filter: subject}) when is_binary(subject) do\n    :ok\n  end\n\n  defp validate_purge_method(_) do\n    {:error, \"When purging, you must pass a %{filter: subject}\"}\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/api/util.ex",
    "content": "defmodule Gnat.Jetstream.API.Util do\n  @moduledoc false\n\n  @default_inbox_prefix \"_INBOX.\"\n\n  def request(conn, topic, payload) do\n    with {:ok, %{body: body}} <- Gnat.request(conn, topic, payload),\n         {:ok, decoded} <- Jason.decode(body) do\n      case decoded do\n        %{\"error\" => err} ->\n          {:error, err}\n\n        other ->\n          {:ok, other}\n      end\n    end\n  end\n\n  def to_datetime(nil), do: nil\n\n  def to_datetime(str) do\n    {:ok, datetime, _} = DateTime.from_iso8601(str)\n    datetime\n  end\n\n  def to_sym(nil), do: nil\n\n  def to_sym(str) when is_binary(str) do\n    String.to_existing_atom(str)\n  end\n\n  def put_if_exist(target_map, target_key, source_map, source_key) do\n    case Map.fetch(source_map, source_key) do\n      {:ok, value} -> Map.put(target_map, target_key, value)\n      _ -> target_map\n    end\n  end\n\n  def valid_name?(name) do\n    !String.contains?(name, [\".\", \"*\", \">\", \" \", \"\\t\"])\n  end\n\n  def invalid_name_message do\n    \"cannot contain '.', '>', '*', spaces or tabs\"\n  end\n\n  def decode_base64(nil), do: nil\n  def decode_base64(data), do: Base.decode64!(data)\n\n  def reply_inbox(prefix \\\\ @default_inbox_prefix)\n  def reply_inbox(nil), do: reply_inbox()\n  def reply_inbox(prefix), do: prefix <> nuid()\n\n  def nuid() do\n    :crypto.strong_rand_bytes(12) |> Base.url_encode64()\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/jetstream.ex",
    "content": "defmodule Gnat.Jetstream do\n  @moduledoc \"\"\"\n  Provides functions for interacting with a [NATS Jetstream](https://github.com/nats-io/jetstream)\n  server.\n  \"\"\"\n\n  @doc \"\"\"\n  Sends `AckAck` acknowledgement to the server.\n\n  Acknowledges a message was completely handled.\n  \"\"\"\n  @spec ack(message :: Gnat.message()) :: :ok\n  def ack(%{reply_to: nil}) do\n    {:error, \"Cannot ack message with no reply-to\"}\n  end\n\n  def ack(%{gnat: gnat, reply_to: reply_to}) do\n    Gnat.pub(gnat, reply_to, \"\")\n  end\n\n  @doc \"\"\"\n  Sends `AckNext` acknowledgement to the server.\n\n  Acknowledges the message was handled and requests delivery of the next message to the reply\n  subject. Only applies to Pull-mode.\n  \"\"\"\n  @spec ack_next(message :: Gnat.message(), consumer_subject :: binary()) :: :ok\n  def ack_next(%{reply_to: nil}, _consumer_subject) do\n    {:error, \"Cannot ack message with no reply-to\"}\n  end\n\n  def ack_next(%{gnat: gnat, reply_to: reply_to}, consumer_subject) do\n    Gnat.pub(gnat, reply_to, \"+NXT\", reply_to: consumer_subject)\n  end\n\n  @doc \"\"\"\n  Sends `AckNak` acknowledgement to the server.\n\n  Signals that the message will not be processed now and processing can move onto the next message.\n  NAK'd message will be retried.\n  \"\"\"\n  @spec nack(message :: Gnat.message()) :: :ok\n  def nack(%{reply_to: nil}) do\n    {:error, \"Cannot ack message with no reply-to\"}\n  end\n\n  def nack(%{gnat: gnat, reply_to: reply_to}) do\n    Gnat.pub(gnat, reply_to, \"-NAK\")\n  end\n\n  @doc \"\"\"\n  Sends `AckProgress` acknowledgement to the server.\n\n  When sent before the `AckWait` period indicates that work is ongoing and the period should be\n  extended by another equal to `AckWait`.\n  \"\"\"\n  @spec ack_progress(message :: Gnat.message()) :: :ok\n  def ack_progress(%{reply_to: nil}) do\n    {:error, \"Cannot ack message with no reply-to\"}\n  end\n\n  def ack_progress(%{gnat: gnat, reply_to: reply_to}) do\n    Gnat.pub(gnat, reply_to, \"+WPI\")\n  end\n\n  @doc \"\"\"\n  Sends `AckTerm` acknowledgement to the server.\n\n  Instructs the server to stop redelivery of a message without acknowledging it as successfully\n  processed.\n  \"\"\"\n  @spec ack_term(message :: Gnat.message()) :: :ok\n  def ack_term(%{reply_to: nil}) do\n    {:error, \"Cannot ack message with no reply-to\"}\n  end\n\n  def ack_term(%{gnat: gnat, reply_to: reply_to}) do\n    Gnat.pub(gnat, reply_to, \"+TERM\")\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/pager.ex",
    "content": "defmodule Gnat.Jetstream.Pager do\n  @moduledoc \"\"\"\n  Page through all the messages in a stream\n\n  This module provides a synchronous API to inspect the messages in a stream.\n  You can use the reduce module to write a simple function that works like `Enum.reduce` across each message individually.\n  If you want to handle messages in batches, you can use the `init` + `page` functions to accomplish that.\n  \"\"\"\n\n  alias Gnat.Jetstream\n  alias Gnat.Jetstream.API.{Consumer, Util}\n\n  @opaque pager :: map()\n  @type message :: Gnat.message()\n  @type opts :: list(opt())\n\n  @typedoc \"\"\"\n  Options you can pass to the pager\n\n  * `batch` controls the maximum number of messages we'll pull in each page/batch (default 10)\n  * `domain` You can specify a jetstream domain if needed\n  * `from_datetime` Only page through messages recorded on or after this datetime\n  * `from_seq` Only page through messages with a sequence number equal or above this option\n  * `headers_only` You can pass `true` to this if you only want to see the headers from each message. Can be useful to get metadata without having to receieve large body payloads.\n\n  \"\"\"\n  @type opt ::\n          {:batch, non_neg_integer()}\n          | {:domain, String.t()}\n          | {:from_datetime, DateTime.t()}\n          | {:from_seq, non_neg_integer}\n          | {:headers_only, boolean()}\n\n  @spec init(Gnat.t(), String.t(), opts()) :: {:ok, pager()} | {:error, term()}\n  def init(conn, stream_name, opts) do\n    domain = Keyword.get(opts, :domain)\n\n    consumer = %Consumer{\n      stream_name: stream_name,\n      domain: domain,\n      ack_policy: :all,\n      ack_wait: 30_000_000_000,\n      deliver_policy: :all,\n      description: \"ephemeral consumer\",\n      replay_policy: :instant,\n      inactive_threshold: 30_000_000_000,\n      headers_only: Keyword.get(opts, :headers_only)\n    }\n\n    consumer = apply_opts_to_consumer(consumer, opts)\n\n    inbox = Util.reply_inbox()\n\n    with {:ok, consumer_info} <- Consumer.create(conn, consumer),\n         {:ok, sub} <- Gnat.sub(conn, self(), inbox) do\n      state = %{\n        conn: conn,\n        stream_name: stream_name,\n        consumer_name: consumer_info.name,\n        domain: domain,\n        inbox: inbox,\n        batch: Keyword.get(opts, :batch, 10),\n        sub: sub\n      }\n\n      {:ok, state}\n    end\n  end\n\n  @spec page(pager()) :: {:page, list(message())} | {:done, list(message())} | {:error, term()}\n  def page(state) do\n    with :ok <- request_next_message(state) do\n      receive_messages(state, [])\n    end\n  end\n\n  def cleanup(%{conn: conn} = state) do\n    :ok = Gnat.unsub(conn, state.sub)\n    Consumer.delete(conn, state.stream_name, state.consumer_name, state.domain)\n  end\n\n  @doc \"\"\"\n  Similar to Enum.reduce but you can iterate through all messages in a stream\n\n  ```\n  # Assume we have a stream with messages like \"1\", \"2\", ... \"10\"\n  Gnat.Jetstream.Pager.reduce(:gnat, \"NUMBERS_STREAM\", [batch_size: 5], 0, fn(message, total) ->\n    num = String.to_integer(message.body)\n    total + num\n  end)\n\n  # => {:ok, 55}\n  ```\n  \"\"\"\n  @spec reduce(\n          Gnat.t(),\n          String.t(),\n          opts(),\n          Enum.acc(),\n          (Gnat.message(), Enum.acc() -> Enum.acc())\n        ) :: {:ok, Enum.acc()} | {:error, term()}\n  def reduce(conn, stream_name, opts, initial_state, fun) do\n    with {:ok, pager} <- init(conn, stream_name, opts) do\n      page_through(pager, initial_state, fun)\n    end\n  end\n\n  defp page_through(pager, state, fun) do\n    case page(pager) do\n      {:page, messages} ->\n        new_state = Enum.reduce(messages, state, fun)\n        page_through(pager, new_state, fun)\n\n      {:done, messages} ->\n        new_state = Enum.reduce(messages, state, fun)\n        cleanup(pager)\n        {:ok, new_state}\n    end\n  end\n\n  defp request_next_message(state) do\n    opts = [batch: state.batch, no_wait: true]\n\n    Consumer.request_next_message(\n      state.conn,\n      state.stream_name,\n      state.consumer_name,\n      state.inbox,\n      state.domain,\n      opts\n    )\n  end\n\n  defp receive_messages(%{batch: batch}, messages) when length(messages) == batch do\n    last = hd(messages)\n    :ok = Jetstream.ack(last)\n    {:page, Enum.reverse(messages)}\n  end\n\n  @terminals [\"404\", \"408\"]\n  defp receive_messages(%{sub: sid} = state, messages) do\n    receive do\n      {:msg, %{sid: ^sid, status: status}} when status in @terminals ->\n        {:done, Enum.reverse(messages)}\n\n      {:msg, %{sid: ^sid, reply_to: nil}} ->\n        {:done, Enum.reverse(messages)}\n\n      {:msg, %{sid: ^sid} = message} ->\n        receive_messages(state, [message | messages])\n    end\n  end\n\n  ## Helpers for accepting user options\n  defp apply_opts_to_consumer(consumer = %Consumer{}, opts) do\n    from = {Keyword.get(opts, :from_seq), Keyword.get(opts, :from_datetime)}\n\n    case from do\n      {nil, nil} ->\n        consumer\n\n      {seq, _} when is_integer(seq) ->\n        %Consumer{consumer | deliver_policy: :by_start_sequence, opt_start_seq: seq}\n\n      {_, %DateTime{} = dt} ->\n        %Consumer{consumer | deliver_policy: :by_start_time, opt_start_time: dt}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/pull_consumer/connection_options.ex",
    "content": "defmodule Gnat.Jetstream.PullConsumer.ConnectionOptions do\n  @moduledoc false\n\n  @default_retry_timeout 1000\n  @default_retries 10\n\n  @enforce_keys [\n    :connection_name,\n    :connection_retry_timeout,\n    :connection_retries,\n    :inbox_prefix,\n    :domain\n  ]\n\n  # 5 Seconds in nanoseconds\n  @default_request_expires 5_000_000_000\n\n  defstruct @enforce_keys ++\n              [\n                :stream_name,\n                :consumer_name,\n                :consumer,\n                batch_size: 1,\n                request_expires: @default_request_expires\n              ]\n\n  def validate!(connection_options) do\n    validated_opts =\n      Keyword.validate!(connection_options, [\n        :connection_name,\n        :stream_name,\n        :consumer_name,\n        :consumer,\n        connection_retry_timeout: @default_retry_timeout,\n        connection_retries: @default_retries,\n        inbox_prefix: nil,\n        domain: nil,\n        batch_size: 1,\n        request_expires: @default_request_expires\n      ])\n\n    stream_name = validated_opts[:stream_name]\n    consumer_name = validated_opts[:consumer_name]\n    consumer = validated_opts[:consumer]\n\n    cond do\n      consumer && (stream_name || consumer_name) ->\n        raise ArgumentError,\n              \"cannot specify :consumer with :stream_name or :consumer_name - use consumer struct's stream_name instead\"\n\n      consumer && !is_struct(consumer, Gnat.Jetstream.API.Consumer) ->\n        raise ArgumentError, \":consumer must be a Consumer struct\"\n\n      consumer && consumer.durable_name != nil && consumer.inactive_threshold == nil ->\n        raise ArgumentError,\n              \"durable consumers specified via :consumer must have inactive_threshold set for auto-cleanup\"\n\n      consumer && validated_opts[:batch_size] > 1 && consumer.ack_policy != :all ->\n        raise ArgumentError,\n              \"batch_size > 1 requires ack_policy: :all on the consumer, \" <>\n                \"got: #{inspect(consumer.ack_policy)}. With ack_policy: :explicit, \" <>\n                \"only the last message in each batch would be acknowledged and the \" <>\n                \"server would redeliver the rest\"\n\n      consumer ->\n        # For ephemeral/auto-cleanup consumer case, extract stream_name from consumer struct\n        validated_opts = Keyword.put(validated_opts, :stream_name, consumer.stream_name)\n        struct!(__MODULE__, validated_opts)\n\n      stream_name && consumer_name ->\n        # For traditional durable consumer case\n        struct!(__MODULE__, validated_opts)\n\n      true ->\n        raise ArgumentError,\n              \"must specify either :consumer (ephemeral/auto-cleanup) or both :stream_name and :consumer_name (durable)\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/pull_consumer/server.ex",
    "content": "defmodule Gnat.Jetstream.PullConsumer.Server do\n  @moduledoc false\n\n  require Logger\n\n  use Connection\n\n  alias Gnat.Jetstream.PullConsumer.ConnectionOptions\n  alias Gnat.Jetstream.API.Util\n\n  defstruct [\n    :connection_options,\n    :state,\n    :listening_topic,\n    :module,\n    :subscription_id,\n    :connection_monitor_ref,\n    :consumer_name,\n    current_retry: 0,\n    buffer: []\n  ]\n\n  def init(%{module: module, init_arg: init_arg}) do\n    _ = Process.put(:\"$initial_call\", {module, :init, 1})\n\n    case module.init(init_arg) do\n      {:ok, state, connection_options} when is_list(connection_options) ->\n        Process.flag(:trap_exit, true)\n\n        connection_options = ConnectionOptions.validate!(connection_options)\n\n        gen_state = %__MODULE__{\n          connection_options: connection_options,\n          state: state,\n          listening_topic: Util.reply_inbox(connection_options.inbox_prefix),\n          module: module,\n          consumer_name: connection_options.consumer_name\n        }\n\n        {:connect, :init, gen_state}\n\n      :ignore ->\n        :ignore\n\n      {:stop, _} = stop ->\n        stop\n    end\n  end\n\n  def connect(\n        _,\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            stream_name: stream_name,\n            consumer_name: consumer_name,\n            consumer: consumer,\n            connection_name: connection_name,\n            connection_retry_timeout: connection_retry_timeout,\n            connection_retries: connection_retries,\n            domain: domain\n          },\n          listening_topic: listening_topic,\n          module: module\n        } = gen_state\n      ) do\n    Logger.debug(\n      \"#{__MODULE__} for #{stream_name}.#{gen_state.consumer_name} is connecting to Gnat.\",\n      module: module,\n      listening_topic: listening_topic,\n      connection_name: connection_name\n    )\n\n    with {:ok, conn} <- connection_pid(connection_name),\n         monitor_ref = Process.monitor(conn),\n         {:ok, consumer_info} <-\n           ensure_consumer_exists(\n             conn,\n             stream_name,\n             consumer_name,\n             consumer,\n             domain\n           ),\n         :ok <- validate_batch_ack_policy(gen_state.connection_options, consumer_info),\n         final_consumer_name = consumer_info.name,\n         state = maybe_handle_connected(module, consumer_info, gen_state.state),\n         {:ok, sid} <- Gnat.sub(conn, self(), listening_topic),\n         gen_state = %{\n           gen_state\n           | subscription_id: sid,\n             connection_monitor_ref: monitor_ref,\n             consumer_name: final_consumer_name,\n             state: state\n         },\n         :ok <-\n           initial_fetch(\n             gen_state,\n             conn,\n             stream_name,\n             final_consumer_name,\n             domain,\n             listening_topic\n           ),\n         gen_state = %{gen_state | current_retry: 0} do\n      {:ok, gen_state}\n    else\n      {:error, reason} ->\n        if gen_state.current_retry >= connection_retries do\n          Logger.error(\n            \"\"\"\n            #{__MODULE__} for #{stream_name}.#{gen_state.consumer_name} failed to connect to NATS and \\\n            retries limit has been exhausted. Stopping.\n            \"\"\",\n            module: module,\n            listening_topic: listening_topic,\n            connection_name: connection_name\n          )\n\n          {:stop, :timeout, %{gen_state | current_retry: 0}}\n        else\n          Logger.debug(\n            \"\"\"\n            #{__MODULE__} for #{stream_name}.#{gen_state.consumer_name} failed to connect to Gnat \\\n            and will retry. Reason: #{inspect(reason)}\n            \"\"\",\n            module: module,\n            listening_topic: listening_topic,\n            connection_name: connection_name\n          )\n\n          gen_state = Map.update!(gen_state, :current_retry, &(&1 + 1))\n          {:backoff, connection_retry_timeout, gen_state}\n        end\n    end\n  end\n\n  def disconnect(\n        {:close, from},\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            stream_name: stream_name,\n            connection_name: connection_name\n          },\n          listening_topic: listening_topic,\n          subscription_id: subscription_id,\n          module: module,\n          consumer_name: consumer_name\n        } = gen_state\n      ) do\n    Logger.debug(\n      \"#{__MODULE__} for #{stream_name}.#{consumer_name} is disconnecting from Gnat.\",\n      module: module,\n      listening_topic: listening_topic,\n      subscription_id: subscription_id,\n      connection_name: connection_name\n    )\n\n    with {:ok, conn} <- connection_pid(connection_name),\n         true <- Process.demonitor(gen_state.connection_monitor_ref, [:flush]),\n         :ok <- Gnat.unsub(conn, subscription_id) do\n      Logger.debug(\n        \"#{__MODULE__} for #{stream_name}.#{consumer_name} is shutting down.\",\n        module: module,\n        listening_topic: listening_topic,\n        subscription_id: subscription_id,\n        connection_name: connection_name\n      )\n\n      Connection.reply(from, :ok)\n      {:stop, :shutdown, gen_state}\n    end\n  end\n\n  defp ensure_consumer_exists(gnat, stream_name, consumer_name, nil, domain) do\n    # Durable consumer case - just check it exists\n    try do\n      case Gnat.Jetstream.API.Consumer.info(gnat, stream_name, consumer_name, domain) do\n        {:ok, consumer_info} -> {:ok, consumer_info}\n        {:error, reason} -> {:error, reason}\n      end\n    catch\n      :exit, reason -> {:error, {:process_exit, reason}}\n      kind, reason -> {:error, {kind, reason}}\n    end\n  end\n\n  defp ensure_consumer_exists(gnat, _stream_name, nil, consumer_struct, _domain) do\n    # Ephemeral or auto-cleanup durable consumer case - create it\n    try do\n      with {:ok, consumer_definition} <- validate_consumer_for_creation(consumer_struct),\n           {:ok, consumer_info} <- Gnat.Jetstream.API.Consumer.create(gnat, consumer_definition) do\n        {:ok, consumer_info}\n      end\n    catch\n      :exit, reason -> {:error, {:process_exit, reason}}\n      kind, reason -> {:error, {kind, reason}}\n    end\n  end\n\n  defp validate_consumer_for_creation(consumer_definition) do\n    cond do\n      consumer_definition.durable_name == nil && consumer_definition.inactive_threshold != nil ->\n        {:error, \"ephemeral consumers (durable_name: nil) cannot have inactive_threshold set\"}\n\n      consumer_definition.durable_name != nil && consumer_definition.inactive_threshold == nil ->\n        {:error,\n         \"durable consumers specified via :consumer must have inactive_threshold set for auto-cleanup\"}\n\n      true ->\n        {:ok, consumer_definition}\n    end\n  end\n\n  defp validate_batch_ack_policy(%ConnectionOptions{batch_size: batch_size}, consumer_info)\n       when batch_size > 1 do\n    case consumer_info.config.ack_policy do\n      :all ->\n        :ok\n\n      other ->\n        {:error,\n         \"batch_size > 1 requires ack_policy: :all on the consumer, \" <>\n           \"got: #{inspect(other)}. With ack_policy: :explicit, \" <>\n           \"only the last message in each batch would be acknowledged and the \" <>\n           \"server would redeliver the rest\"}\n    end\n  end\n\n  defp validate_batch_ack_policy(_connection_options, _consumer_info), do: :ok\n\n  defp maybe_handle_connected(module, consumer_info, state) do\n    if function_exported?(module, :handle_connected, 2) do\n      {:ok, state} = module.handle_connected(consumer_info, state)\n      state\n    else\n      state\n    end\n  end\n\n  defp maybe_handle_status(message, %__MODULE__{module: module, state: state} = gen_state) do\n    if function_exported?(module, :handle_status, 2) do\n      {:ok, new_state} = module.handle_status(message, state)\n      %{gen_state | state: new_state}\n    else\n      gen_state\n    end\n  end\n\n  defp connection_pid(connection_name) when is_pid(connection_name) do\n    if Process.alive?(connection_name) do\n      {:ok, connection_name}\n    else\n      {:error, :not_alive}\n    end\n  end\n\n  defp connection_pid(connection_name) do\n    case Process.whereis(connection_name) do\n      nil -> {:error, :not_found}\n      pid -> {:ok, pid}\n    end\n  end\n\n  # -- Batch mode: 100 is an idle heartbeat — the pull is still alive, do\n  # nothing but invoke the user callback. --\n  def handle_info(\n        {:msg, %{status: \"100\"} = message},\n        %__MODULE__{connection_options: %ConnectionOptions{batch_size: batch_size}} = gen_state\n      )\n      when batch_size > 1 do\n    gen_state = maybe_handle_status(message, gen_state)\n    {:noreply, gen_state}\n  end\n\n  # -- Batch mode: any other status (404/408 terminators, 409 leadership\n  # change / max_ack_pending / max_waiting / consumer-deleted, etc.) ends\n  # the outstanding pull request. Process any partial buffer and issue a\n  # new pull so the consumer doesn't stall. --\n  def handle_info(\n        {:msg, %{status: status, gnat: gnat} = message},\n        %__MODULE__{\n          connection_options: %ConnectionOptions{batch_size: batch_size},\n          buffer: buffer\n        } = gen_state\n      )\n      when batch_size > 1 and is_binary(status) and status != \"\" do\n    gen_state = maybe_handle_status(message, gen_state)\n\n    case buffer do\n      [] ->\n        # Nothing buffered — long-poll for new messages.\n        request_batch(gnat, gen_state, :tailing)\n        {:noreply, gen_state}\n\n      _messages ->\n        # Partial batch — process what we have, then try for more.\n        gen_state = process_and_ack_batch(gen_state)\n        request_batch(gnat, gen_state, :catching_up)\n        {:noreply, gen_state}\n    end\n  end\n\n  # -- Single-message mode: informational status. Drop + re-pull so the\n  # consumer doesn't stall. Matches the nats.go convention of never exposing\n  # status messages to the user's message handler. --\n  def handle_info(\n        {:msg, %{status: status} = message},\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            stream_name: stream_name,\n            domain: domain\n          },\n          listening_topic: listening_topic,\n          consumer_name: consumer_name\n        } = gen_state\n      )\n      when is_binary(status) and status != \"\" do\n    gen_state = maybe_handle_status(message, gen_state)\n\n    next_message(\n      message.gnat,\n      stream_name,\n      consumer_name,\n      domain,\n      listening_topic\n    )\n\n    {:noreply, gen_state}\n  end\n\n  # -- Batch mode: data message — buffer until batch is full --\n  def handle_info(\n        {:msg, message},\n        %__MODULE__{\n          connection_options: %ConnectionOptions{batch_size: batch_size},\n          buffer: buffer\n        } = gen_state\n      )\n      when batch_size > 1 do\n    buffer = [message | buffer]\n    gen_state = %{gen_state | buffer: buffer}\n\n    if length(buffer) >= batch_size do\n      gen_state = process_and_ack_batch(gen_state)\n      request_batch(message.gnat, gen_state, :catching_up)\n      {:noreply, gen_state}\n    else\n      {:noreply, gen_state}\n    end\n  end\n\n  # -- Single-message mode (batch_size == 1, the default) --\n  def handle_info(\n        {:msg, message},\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            stream_name: stream_name,\n            connection_name: connection_name,\n            domain: domain\n          },\n          listening_topic: listening_topic,\n          subscription_id: subscription_id,\n          state: state,\n          module: module,\n          consumer_name: consumer_name\n        } = gen_state\n      ) do\n    Logger.debug(\n      \"\"\"\n      #{__MODULE__} for #{stream_name}.#{consumer_name} received a message: \\\n      #{inspect(message, pretty: true)}\n      \"\"\",\n      module: module,\n      listening_topic: listening_topic,\n      subscription_id: subscription_id,\n      connection_name: connection_name\n    )\n\n    case module.handle_message(message, state) do\n      {:ack, state} ->\n        Gnat.Jetstream.ack_next(message, listening_topic)\n\n        gen_state = %{gen_state | state: state}\n        {:noreply, gen_state}\n\n      {:nack, state} ->\n        Gnat.Jetstream.nack(message)\n\n        next_message(\n          message.gnat,\n          stream_name,\n          consumer_name,\n          domain,\n          listening_topic\n        )\n\n        gen_state = %{gen_state | state: state}\n        {:noreply, gen_state}\n\n      {:term, state} ->\n        Gnat.Jetstream.ack_term(message)\n\n        next_message(\n          message.gnat,\n          stream_name,\n          consumer_name,\n          domain,\n          listening_topic\n        )\n\n        gen_state = %{gen_state | state: state}\n        {:noreply, gen_state}\n\n      {:noreply, state} ->\n        next_message(\n          message.gnat,\n          stream_name,\n          consumer_name,\n          domain,\n          listening_topic\n        )\n\n        gen_state = %{gen_state | state: state}\n        {:noreply, gen_state}\n    end\n  end\n\n  def handle_info(\n        {:DOWN, ref, :process, _pid, _reason},\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            connection_name: connection_name,\n            stream_name: stream_name\n          },\n          subscription_id: subscription_id,\n          listening_topic: listening_topic,\n          module: module,\n          connection_monitor_ref: monitor_ref,\n          consumer_name: consumer_name\n        } = gen_state\n      )\n      when ref == monitor_ref do\n    Logger.debug(\n      \"\"\"\n      #{__MODULE__} for #{stream_name}.#{consumer_name}:\n      NATS connection has died. PullConsumer is reconnecting.\n      \"\"\",\n      module: module,\n      listening_topic: listening_topic,\n      subscription_id: subscription_id,\n      connection_name: connection_name\n    )\n\n    # Clear consumer name on reconnect so it gets recreated (for ephemeral and auto-cleanup consumers)\n    # Clear buffer to avoid processing stale messages from the dead connection\n    gen_state = %{\n      gen_state\n      | consumer_name: nil,\n        subscription_id: nil,\n        connection_monitor_ref: nil,\n        buffer: []\n    }\n\n    {:connect, :reconnect, gen_state}\n  end\n\n  def handle_info(\n        other,\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            connection_name: connection_name,\n            stream_name: stream_name\n          },\n          subscription_id: subscription_id,\n          listening_topic: listening_topic,\n          module: module,\n          consumer_name: consumer_name\n        } = gen_state\n      ) do\n    Logger.debug(\n      \"\"\"\n      #{__MODULE__} for #{stream_name}.#{consumer_name} received\n      unexpected message: #{inspect(other, pretty: true)}\n      \"\"\",\n      module: module,\n      listening_topic: listening_topic,\n      subscription_id: subscription_id,\n      connection_name: connection_name\n    )\n\n    {:noreply, gen_state}\n  end\n\n  def handle_call(\n        :close,\n        from,\n        %__MODULE__{\n          connection_options: %ConnectionOptions{\n            connection_name: connection_name,\n            stream_name: stream_name\n          },\n          subscription_id: subscription_id,\n          listening_topic: listening_topic,\n          module: module,\n          consumer_name: consumer_name\n        } = gen_state\n      ) do\n    Logger.debug(\"#{__MODULE__} for #{stream_name}.#{consumer_name} received :close call.\",\n      module: module,\n      listening_topic: listening_topic,\n      subscription_id: subscription_id,\n      connection_name: connection_name\n    )\n\n    {:disconnect, {:close, from}, gen_state}\n  end\n\n  defp next_message(conn, stream_name, consumer_name, domain, listening_topic) do\n    Gnat.Jetstream.API.Consumer.request_next_message(\n      conn,\n      stream_name,\n      consumer_name,\n      listening_topic,\n      domain\n    )\n  end\n\n  defp initial_fetch(gen_state, conn, stream_name, consumer_name, domain, listening_topic) do\n    if gen_state.connection_options.batch_size > 1 do\n      request_batch(conn, gen_state, :catching_up)\n    else\n      next_message(conn, stream_name, consumer_name, domain, listening_topic)\n    end\n  end\n\n  defp request_batch(conn, gen_state, mode) do\n    %{\n      connection_options: %ConnectionOptions{\n        stream_name: stream_name,\n        batch_size: batch_size,\n        domain: domain,\n        request_expires: expires\n      },\n      consumer_name: consumer_name,\n      listening_topic: listening_topic\n    } = gen_state\n\n    opts =\n      case mode do\n        :catching_up -> [batch: batch_size, no_wait: true]\n        :tailing -> [batch: batch_size, expires: expires]\n      end\n\n    Gnat.Jetstream.API.Consumer.request_next_message(\n      conn,\n      stream_name,\n      consumer_name,\n      listening_topic,\n      domain,\n      opts\n    )\n  end\n\n  defp process_and_ack_batch(%{buffer: buffer, module: module, state: state} = gen_state) do\n    messages = Enum.reverse(buffer)\n\n    new_state =\n      Enum.reduce(messages, state, fn message, acc_state ->\n        case module.handle_message(message, acc_state) do\n          {:ack, updated_state} ->\n            updated_state\n\n          {action, updated_state} ->\n            Logger.warning(\n              \"PullConsumer batch mode does not support #{inspect(action)}, treating as :ack\"\n            )\n\n            updated_state\n        end\n      end)\n\n    # With ack_policy: :all, acking the last message covers the entire batch\n    last = List.last(messages)\n    Gnat.Jetstream.ack(last)\n\n    %{gen_state | state: new_state, buffer: []}\n  end\nend\n"
  },
  {
    "path": "lib/gnat/jetstream/pull_consumer.ex",
    "content": "defmodule Gnat.Jetstream.PullConsumer do\n  @moduledoc \"\"\"\n  A behaviour which provides the NATS JetStream Pull Consumer functionalities.\n\n  When a Consumer is pull-based, it means that the messages will be delivered when the server\n  is asked for them.\n\n  ## Example\n\n  Declare a module which uses `Gnat.Jetstream.PullConsumer` and implements `c:init/1` and\n  `c:handle_message/2` callbacks.\n\n      defmodule MyApp.PullConsumer do\n        use Gnat.Jetstream.PullConsumer\n\n        def start_link(arg) do\n          Jetstream.PullConsumer.start_link(__MODULE__, arg)\n        end\n\n        @impl true\n        def init(_arg) do\n          {:ok, nil,\n            connection_name: :gnat,\n            stream_name: \"TEST_STREAM\",\n            consumer_name: \"TEST_CONSUMER\"}\n        end\n\n        @impl true\n        def handle_message(message, state) do\n          # Do some processing with the message.\n          {:ack, state}\n        end\n      end\n\n  You can then place your Pull Consumer in a supervision tree. Remember that you need to have the\n  `Gnat.ConnectionSupervisor` set up.\n\n      defmodule MyApp.Application do\n        use Application\n\n        @impl true\n        def start(_type, _args) do\n          children = [\n            # Create NATS connection\n            {Gnat.ConnectionSupervisor, ...},\n            # Start NATS Jetstream Pull Consumer\n            MyApp.PullConsumer,\n          ]\n          opts = [strategy: :one_for_one]\n          Supervisor.start_link(children, opts)\n        end\n      end\n\n  ## Connection Options\n\n  In order to establish consumer connection with NATS, you need to pass several connection options\n  via keyword list in third element of a tuple returned from `c:init/1` callback.\n\n  Following options **must** be provided. Omitting this options will cause the process to raise\n  errors upon initialization:\n\n  * `:connection_name` - Gnat connection or `Gnat.ConnectionSupervisor` name/PID.\n\n  For **durable consumers**, provide:\n  * `:stream_name` - name of an existing stream the consumer will consume messages from.\n  * `:consumer_name` - name of an existing consumer pointing at the stream.\n\n  For **ephemeral consumers**, provide:\n  * `:consumer` - a `Gnat.Jetstream.API.Consumer` struct for creating an ephemeral consumer.\n    The consumer struct must have `durable_name: nil` OR `inactive_threshold` set to ensure\n    that the server will clean it up. The `stream_name` field must also be set.\n\n  You can also pass the optional ones:\n  * `:connection_retry_timeout` - a duration in milliseconds after which the PullConsumer which\n    failed to establish NATS connection retries, defaults to `1000`\n  * `:connection_retries` - a number of attempts the PullConsumer will make to establish the NATS\n    connection. When this value is exceeded, the pull consumer stops with the `:timeout` reason,\n    defaults to `10`\n  * `:inbox_prefix` - allows the default `_INBOX.` prefix to be customized. Should end with a dot.\n  * `:domain` - use a JetStream domain, this is mostly used on leaf nodes.\n  * `:batch_size` - when set to a value greater than 1, enables batch mode. Messages are\n    buffered and delivered to `c:handle_message/2` in batches. Only the last message per\n    batch is acknowledged, so the underlying consumer should use `ack_policy: :all` for\n    correctness. This dramatically improves throughput for consumers that need to catch up\n    on large backlogs. In batch mode, `:nack` and `:term` returns from `c:handle_message/2`\n    are treated as `:ack` since `ack_policy: :all` cannot selectively reject messages.\n    Defaults to `1` (single-message mode).\n  * `:request_expires` - duration in nanoseconds a batch-mode pull request will linger on\n    the server while tailing (no backlog) before the server replies with a `408` terminator\n    and the consumer issues a fresh pull. Only used when `:batch_size` is greater than 1.\n    Defaults to `5_000_000_000` (5 seconds).\n\n  ## Dynamic Connection Options\n\n  It is possible that you have to determine some of the options dynamically depending on pull\n  consumer's init argument. To do so, it is recommended to derive these options values from some\n  init argument:\n\n      defmodule MyApp.PullConsumer do\n        use Gnat.Jetstream.PullConsumer\n\n        def start_link() do\n          Gnat.Jetstream.PullConsumer.start_link(__MODULE__, %{counter: counter})\n        end\n\n        @impl true\n        def init(%{counter: counter}) do\n          {:ok, nil,\n            connection_name: :gnat,\n            stream_name: \"TEST_STREAM_#\\{counter}\",\n            consumer_name: \"TEST_CONSUMER_#\\{counter}\"}\n        end\n\n        ...\n      end\n\n  ## Ephemeral Consumer Example\n\n  You can create ephemeral consumers by providing a `:consumer` struct with `durable_name: nil`.\n  These are automatically created and cleaned up with the connection lifecycle:\n\n      defmodule MyApp.EphemeralPullConsumer do\n        use Gnat.Jetstream.PullConsumer\n\n        def start_link(arg) do\n          Jetstream.PullConsumer.start_link(__MODULE__, arg)\n        end\n\n        @impl true\n        def init(_arg) do\n          consumer = %Gnat.Jetstream.API.Consumer{\n            stream_name: \"TEST_STREAM\",\n            durable_name: nil,  # Must be nil for ephemeral consumers\n            filter_subject: \"orders.*\"\n          }\n\n          {:ok, nil,\n            connection_name: :gnat,\n            consumer: consumer}\n        end\n\n        @impl true\n        def handle_message(message, state) do\n          # Do some processing with the message.\n          {:ack, state}\n        end\n      end\n\n  ## Auto-Cleanup Durable Consumer Example\n\n  For scenarios where you want persistence across reconnections but automatic cleanup\n  (e.g., Kubernetes pods), you can create durable consumers with `inactive_threshold`:\n\n      defmodule MyApp.AutoCleanupPullConsumer do\n        use Gnat.Jetstream.PullConsumer\n\n        def start_link(pod_name) do\n          Jetstream.PullConsumer.start_link(__MODULE__, pod_name)\n        end\n\n        @impl true\n        def init(pod_name) do\n          consumer = %Gnat.Jetstream.API.Consumer{\n            stream_name: \"ORDERS_STREAM\",\n            durable_name: \"orders-consumer-#{System.get_env(\"POD_NAME\")}\",  # Named after pod\n            inactive_threshold: 300_000_000_000,  # 5 minutes in nanoseconds\n            filter_subject: \"orders.*\"\n          }\n\n          {:ok, nil,\n            connection_name: :gnat,\n            consumer: consumer}\n        end\n\n        @impl true\n        def handle_message(message, state) do\n          # Process order message with state persistence\n          {:ack, state}\n        end\n      end\n\n  ## How to supervise\n\n  A `PullConsumer` is most commonly started under a supervision tree. When we invoke\n  `use Gnat.Jetstream.PullConsumer`, it automatically defines a `child_spec/1` function that allows us\n  to start the pull consumer directly under a supervisor. To start a pull consumer under\n  a supervisor with an initial argument of :example, one may do:\n\n      children = [\n        {MyPullConsumer, :example}\n      ]\n      Supervisor.start_link(children, strategy: :one_for_all)\n\n  While one could also simply pass the `MyPullConsumer` as a child to the supervisor, such as:\n\n      children = [\n        MyPullConsumer # Same as {MyPullConsumer, []}\n      ]\n      Supervisor.start_link(children, strategy: :one_for_all)\n\n  A common approach is to use a keyword list, which allows setting init argument and server options,\n  for example:\n\n      def start_link(opts) do\n        {initial_state, opts} = Keyword.pop(opts, :initial_state, nil)\n        Gnat.Jetstream.PullConsumer.start_link(__MODULE__, initial_state, opts)\n      end\n\n  and then you can use `MyPullConsumer`, `{MyPullConsumer, name: :my_consumer}` or even\n  `{MyPullConsumer, initial_state: :example, name: :my_consumer}` as a child specification.\n\n  `use Gnat.Jetstream.PullConsumer` also accepts a list of options which configures the child\n  specification and therefore how it runs under a supervisor. The generated `child_spec/1` can be\n  customized with the following options:\n\n    * `:id` - the child specification identifier, defaults to the current module\n    * `:restart` - when the child should be restarted, defaults to `:permanent`\n    * `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down\n\n  For example:\n\n      use Gnat.Jetstream.PullConsumer, restart: :transient, shutdown: 10_000\n\n  See the \"Child specification\" section in the `Supervisor` module for more detailed information.\n  The `@doc` annotation immediately preceding `use Jetstream.PullConsumer` will be attached to\n  the generated `child_spec/1` function.\n\n  ## Name registration\n\n  A pull consumer is bound to the same name registration rules as GenServers.\n  Read more about it in the `GenServer` documentation.\n  \"\"\"\n\n  @doc \"\"\"\n  Invoked when the server is started. `start_link/3` or `start/3` will block until it returns.\n\n  `init_arg` is the argument term (second argument) passed to `start_link/3`.\n\n  See `c:Connection.init/1` for more details.\n  \"\"\"\n  @callback init(init_arg :: term) ::\n              {:ok, state :: term(), connection_options()}\n              | :ignore\n              | {:stop, reason :: any}\n\n  @doc \"\"\"\n  Invoked to synchronously process a message pulled by the consumer.\n  Depending on the value it returns, the acknowledgement is or is not sent.\n\n  Only real stream messages reach this callback. JetStream informational\n  status messages (e.g. `100` heartbeat, `404`/`408` pull terminator, `409`\n  leadership change) are intercepted by the consumer and never passed here.\n  See `c:handle_status/2` if you want to observe them.\n\n  ## ACK actions\n\n  Possible ACK actions values explained:\n\n  * `:ack` - acknowledges the message was handled and requests delivery of the next message to\n    the reply subject.\n  * `:nack` - signals that the message will not be processed now and processing can move onto\n    the next message, NAK'd message will be retried.\n  * `:term` - instructs the server to stop redelivery of a message without acknowledging it as\n    successfully processed.\n  * `:noreply` - nothing is sent. You may send later asynchronously an ACK or NACK message using\n    the `Jetstream.ack/1` or `Jetstream.nack/1` and similar functions from `Jetstream` module.\n\n  ## Example\n\n      def handle_message(message, state) do\n        IO.inspect(message)\n        {:ack, state}\n      end\n\n  \"\"\"\n  @callback handle_message(message :: Gnat.message(), state :: term()) ::\n              {ack_action, new_state}\n            when ack_action: :ack | :nack | :term | :noreply, new_state: term()\n\n  @doc \"\"\"\n  Invoked after the consumer has been created or verified on the NATS server.\n\n  This callback is called during connection (and reconnection) after the JetStream\n  consumer has been successfully created or confirmed to exist. It receives the full\n  consumer info map returned by the server, which includes fields like `num_pending`\n  (the number of messages waiting to be delivered).\n\n  This is useful for detecting the initial state of the consumer. For example, if\n  `num_pending` is `0`, you know there are no existing messages to replay and can\n  mark the consumer as caught up immediately.\n\n  Returning `{:ok, state}` allows you to update the consumer's state based on the\n  consumer info.\n\n  This callback is optional. If not implemented, the state is passed through unchanged.\n\n  ## Example\n\n      @impl true\n      def handle_connected(consumer_info, state) do\n        if consumer_info.num_pending == 0 do\n          {:ok, mark_as_loaded(state)}\n        else\n          {:ok, state}\n        end\n      end\n\n  \"\"\"\n  @callback handle_connected(\n              consumer_info :: Gnat.Jetstream.API.Consumer.info(),\n              state :: term()\n            ) :: {:ok, new_state :: term()}\n\n  @doc \"\"\"\n  Invoked when the consumer receives an informational JetStream status message\n  instead of a real stream message.\n\n  JetStream delivers status messages on the same subscription as regular\n  messages — for example a `100` idle heartbeat, a `404`/`408` pull request\n  terminator, or a `409` leadership change. These are not stream records and\n  cannot be acked, so the PullConsumer never forwards them to `c:handle_message/2`.\n\n  By default they are silently dropped and the consumer continues fetching\n  the next message. Implement this callback if you want to observe them — for\n  example to log a warning on leadership changes, or to track heartbeat\n  arrival.\n\n  The callback receives the raw `Gnat.message()` (which includes `:status`\n  and optionally `:description`) and the current state. Returning\n  `{:ok, new_state}` updates the state; the consumer then proceeds the same\n  way it would have if the callback had not been defined.\n\n  This callback is optional.\n\n  ## Example\n\n      @impl true\n      def handle_status(%{status: \"409\", description: description}, state) do\n        Logger.warning(\"JetStream 409 from consumer: #\\{description}\")\n        {:ok, state}\n      end\n\n      def handle_status(_message, state), do: {:ok, state}\n\n  \"\"\"\n  @callback handle_status(message :: Gnat.message(), state :: term()) ::\n              {:ok, new_state :: term()}\n\n  @optional_callbacks [handle_connected: 2, handle_status: 2]\n\n  @typedoc \"\"\"\n  The pull consumer reference.\n  \"\"\"\n  @type consumer :: GenServer.server()\n\n  @typedoc \"\"\"\n  Connection option values used to connect the consumer to NATS server.\n  \"\"\"\n  @type connection_option ::\n          {:connection_name, GenServer.server()}\n          | {:stream_name, String.t()}\n          | {:consumer_name, String.t()}\n          | {:consumer, Gnat.Jetstream.API.Consumer.t()}\n          | {:connection_retry_timeout, non_neg_integer()}\n          | {:connection_retries, non_neg_integer()}\n          | {:domain, String.t()}\n          | {:batch_size, pos_integer()}\n          | {:request_expires, non_neg_integer()}\n\n  @typedoc \"\"\"\n  Connection options used to connect the consumer to NATS server.\n  \"\"\"\n  @type connection_options :: [connection_option()]\n\n  defmacro __using__(opts) do\n    quote location: :keep, bind_quoted: [opts: opts] do\n      @behaviour Gnat.Jetstream.PullConsumer\n\n      unless Module.has_attribute?(__MODULE__, :doc) do\n        @doc \"\"\"\n        Returns a specification to start this module under a supervisor.\n\n        See the \"Child specification\" section in the `Supervisor` module for more detailed\n        information.\n        \"\"\"\n      end\n\n      @spec child_spec(arg :: GenServer.options()) :: Supervisor.child_spec()\n      def child_spec(arg) do\n        default = %{\n          id: __MODULE__,\n          start: {__MODULE__, :start_link, [arg]}\n        }\n\n        Supervisor.child_spec(default, unquote(Macro.escape(opts)))\n      end\n\n      defoverridable child_spec: 1\n    end\n  end\n\n  @doc \"\"\"\n  Starts a pull consumer linked to the current process with the given function.\n\n  This is often used to start the pull consumer as part of a supervision tree.\n\n  Once the server is started, the `c:init/1` function of the given `module` is called with\n  `init_arg` as its argument to initialize the server. To ensure a synchronized start-up procedure,\n  this function does not return until `c:init/1` has returned.\n\n  See `GenServer.start_link/3` for more details.\n  \"\"\"\n  @spec start_link(module(), init_arg :: term(), options :: GenServer.options()) ::\n          GenServer.on_start()\n  def start_link(module, init_arg, options \\\\ []) when is_atom(module) and is_list(options) do\n    Connection.start_link(\n      Gnat.Jetstream.PullConsumer.Server,\n      %{module: module, init_arg: init_arg},\n      options\n    )\n  end\n\n  @doc \"\"\"\n  Starts a `Jetstream.PullConsumer` process without links (outside of a supervision tree).\n\n  See `start_link/3` for more information.\n  \"\"\"\n  @spec start(module(), init_arg :: term(), options :: GenServer.options()) ::\n          GenServer.on_start()\n  def start(module, init_arg, options \\\\ []) when is_atom(module) and is_list(options) do\n    Connection.start(\n      Gnat.Jetstream.PullConsumer.Server,\n      %{module: module, init_arg: init_arg},\n      options\n    )\n  end\n\n  @doc \"\"\"\n  Closes the pull consumer and stops underlying process.\n\n  ## Example\n\n      {:ok, consumer} =\n        PullConsumer.start_link(ExamplePullConsumer,\n          connection_name: :gnat,\n          stream_name: \"TEST_STREAM\",\n          consumer_name: \"TEST_CONSUMER\"\n        )\n\n      :ok = PullConsumer.close(consumer)\n\n  \"\"\"\n  @spec close(consumer :: consumer()) :: :ok\n  def close(consumer) do\n    Connection.call(consumer, :close)\n  end\nend\n"
  },
  {
    "path": "lib/gnat/parsec.ex",
    "content": "defmodule Gnat.Parsec do\n  @moduledoc false\n  defstruct partial: nil\n\n  import NimbleParsec\n\n  subject = ascii_string([?!..?~], min: 1)\n  length = integer(min: 1)\n  sid = integer(min: 1)\n\n  whitespace =\n    ascii_char([32, ?\\t])\n    |> times(min: 1)\n\n  op_msg =\n    ascii_char([?m, ?M])\n    |> ascii_char([?s, ?S])\n    |> ascii_char([?g, ?G])\n\n  op_hmsg =\n    ascii_char([?h, ?H])\n    |> ascii_char([?m, ?M])\n    |> ascii_char([?s, ?S])\n    |> ascii_char([?g, ?G])\n\n  op_err =\n    ascii_char([?-])\n    |> ascii_char([?e, ?E])\n    |> ascii_char([?r, ?R])\n    |> ascii_char([?r, ?R])\n\n  op_info =\n    ascii_char([?i, ?I])\n    |> ascii_char([?n, ?N])\n    |> ascii_char([?f, ?F])\n    |> ascii_char([?o, ?O])\n\n  op_ping =\n    ascii_char([?p, ?P])\n    |> ascii_char([?i, ?I])\n    |> ascii_char([?n, ?N])\n    |> ascii_char([?g, ?G])\n\n  op_pong =\n    ascii_char([?p, ?P])\n    |> ascii_char([?o, ?O])\n    |> ascii_char([?n, ?N])\n    |> ascii_char([?g, ?G])\n\n  op_ok =\n    ascii_char([?+])\n    |> ascii_char([?o, ?O])\n    |> ascii_char([?k, ?K])\n\n  err =\n    replace(op_err, :err)\n    |> ignore(whitespace)\n    |> ignore(string(\"'\"))\n    |> optional(utf8_string([not: ?'], min: 1))\n    |> ignore(string(\"'\\r\\n\"))\n\n  msg =\n    replace(op_msg, :msg)\n    |> ignore(whitespace)\n    |> concat(subject)\n    |> ignore(whitespace)\n    |> concat(sid)\n    |> ignore(whitespace)\n    |> choice([\n      subject |> ignore(whitespace) |> concat(length),\n      length\n    ])\n    |> ignore(string(\"\\r\\n\"))\n\n  hmsg =\n    replace(op_hmsg, :hmsg)\n    |> ignore(whitespace)\n    |> concat(subject)\n    |> ignore(whitespace)\n    |> concat(sid)\n    |> ignore(whitespace)\n    |> choice([\n      subject |> ignore(whitespace) |> concat(length) |> ignore(whitespace) |> concat(length),\n      length |> ignore(whitespace) |> concat(length)\n    ])\n    |> ignore(string(\"\\r\\n\"))\n\n  ok = replace(op_ok |> string(\"\\r\\n\"), :ok)\n  ping = replace(op_ping |> string(\"\\r\\n\"), :ping)\n  pong = replace(op_pong |> string(\"\\r\\n\"), :pong)\n\n  info =\n    replace(op_info, :info)\n    |> ignore(whitespace)\n    |> utf8_string([not: ?\\r], min: 2)\n    |> ignore(string(\"\\r\\n\"))\n\n  defparsecp(:command, choice([msg, hmsg, ok, ping, pong, info, err]))\n\n  def new, do: %__MODULE__{}\n\n  def parse(%__MODULE__{partial: nil} = state, string) do\n    {partial, commands} = parse_commands(string, [])\n    {%{state | partial: partial}, commands}\n  end\n\n  def parse(%__MODULE__{partial: partial} = state, string) do\n    {partial, commands} = parse_commands(partial <> string, [])\n    {%{state | partial: partial}, commands}\n  end\n\n  def parse_commands(\"\", list), do: {nil, Enum.reverse(list)}\n\n  def parse_commands(str, list) do\n    case parse_command(str) do\n      {:ok, command, rest} -> parse_commands(rest, [command | list])\n      {:error, partial} -> {partial, Enum.reverse(list)}\n    end\n  end\n\n  @spec parse_command(binary()) :: {:ok, tuple(), binary()} | {:error, binary()}\n  def parse_command(string) do\n    case command(string) do\n      {:ok, [:msg, subject, sid, length], rest, _, _, _} ->\n        finish_msg(subject, sid, nil, length, rest, string)\n\n      {:ok, [:msg, subject, sid, reply_to, length], rest, _, _, _} ->\n        finish_msg(subject, sid, reply_to, length, rest, string)\n\n      {:ok, [:hmsg, subject, sid, header_length, total_length], rest, _, _, _} ->\n        finish_hmsg(subject, sid, nil, header_length, total_length, rest, string)\n\n      {:ok, [:hmsg, subject, sid, reply_to, header_length, total_length], rest, _, _, _} ->\n        finish_hmsg(subject, sid, reply_to, header_length, total_length, rest, string)\n\n      {:ok, [atom], rest, _, _, _} ->\n        {:ok, atom, rest}\n\n      {:ok, [:info, json], rest, _, _, _} ->\n        {:ok, {:info, Jason.decode!(json, keys: :atoms)}, rest}\n\n      {:ok, [:err, msg], rest, _, _, _} ->\n        {:ok, {:error, msg}, rest}\n\n      {:error, _, _, _, _, _} ->\n        {:error, string}\n    end\n  end\n\n  def parse_headers(\"NATS/1.0\" <> rest) do\n    case String.split(rest, \"\\r\\n\", parts: 2) do\n      [\" \" <> status, headers] ->\n        case :cow_http.parse_headers(headers) do\n          {parsed, \"\"} ->\n            case String.split(status, \" \", parts: 2) do\n              [status, description] ->\n                {:ok, status, description, parsed}\n\n              [status] ->\n                {:ok, status, nil, parsed}\n            end\n\n          _other ->\n            {:error, \"Could not parse headers\"}\n        end\n\n      [_status_line, headers] ->\n        case :cow_http.parse_headers(headers) do\n          {parsed, \"\"} -> {:ok, nil, nil, parsed}\n          _other -> {:error, \"Could not parse headers\"}\n        end\n\n      _other ->\n        {:error, \"Could not parse status line\"}\n    end\n  end\n\n  def parse_headers(_other) do\n    {:error, \"Could not parse status line prefix\"}\n  end\n\n  def finish_msg(subject, sid, reply_to, length, rest, string) do\n    case rest do\n      <<body::size(length)-binary, \"\\r\\n\", rest::binary>> ->\n        {:ok, {:msg, subject, sid, reply_to, body}, rest}\n\n      _other ->\n        {:error, string}\n    end\n  end\n\n  def finish_hmsg(subject, sid, reply_to, header_length, total_length, rest, string) do\n    payload_length = total_length - header_length\n\n    case rest do\n      <<headers::size(header_length)-binary, payload::size(payload_length)-binary, \"\\r\\n\",\n        rest::binary>> ->\n        {:ok, status, description, headers} = parse_headers(headers)\n        {:ok, {:hmsg, subject, sid, reply_to, status, description, headers, payload}, rest}\n\n      _other ->\n        {:error, string}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/server.ex",
    "content": "defmodule Gnat.Server do\n  require Logger\n\n  @moduledoc \"\"\"\n  A behavior for acting as a server for nats messages.\n\n  You can use this behavior in your own module and then use the `Gnat.ConsumerSupervisor` to listen for and respond to nats messages.\n\n  ## Example\n\n      defmodule MyApp.RpcServer do\n        use Gnat.Server\n\n        def request(%{body: _body}) do\n          {:reply, \"hi\"}\n        end\n\n        # defining an error handler is optional, the default one will just call Logger.error for you\n        def error(%{gnat: gnat, reply_to: reply_to}, _error) do\n          Gnat.pub(gnat, reply_to, \"Something went wrong and I can't handle your request\")\n        end\n      end\n  \"\"\"\n\n  @doc \"\"\"\n  Called when a message is received from the broker\n  \"\"\"\n  @callback request(message :: Gnat.message()) :: :ok | {:reply, iodata()} | {:error, term()}\n\n  @doc \"\"\"\n  Called when an error occured during the `request/1`\n\n  If your `request/1` function returned `{:error, term}`, then the `term` you returned will be passed as the second argument.\n  If an exception was raised during your `request/1` function, then the exception will be passed as the second argument.\n  If your `request/1` function returned something other than the supported return types, then its return value will be passed as the second argument.\n  \"\"\"\n  @callback error(message :: Gnat.message(), error :: term()) :: :ok | {:reply, iodata()}\n\n  defmacro __using__(_opts) do\n    quote do\n      @behaviour Gnat.Server\n\n      def error(_message, error) do\n        require Logger\n\n        Logger.error(\n          \"Gnat.Server encountered an error while handling a request: #{inspect(error)}\",\n          type: :gnat_server_error,\n          error: error\n        )\n      end\n\n      defoverridable error: 2\n    end\n  end\n\n  # The functions below are not documented because they are used internally to run\n  # the callback modules\n\n  @doc false\n  def execute(module, message) do\n    try do\n      case apply(module, :request, [message]) do\n        :ok -> :done\n        {:reply, data} -> send_reply(message, data)\n        {:error, error} -> execute_error(module, message, error)\n        other -> execute_error(module, message, other)\n      end\n    rescue\n      e ->\n        execute_error(module, message, e)\n    end\n  end\n\n  @doc false\n  defp execute_error(module, message, error) do\n    try do\n      case apply(module, :error, [message, error]) do\n        :ok ->\n          :done\n\n        {:reply, data} ->\n          send_reply(message, data)\n\n        other ->\n          Logger.error(\n            \"error handler for #{module} returned something unexpected: #{inspect(other)}\",\n            type: :gnat_server_error\n          )\n      end\n    rescue\n      e ->\n        Logger.error(\n          \"error handler for #{module} encountered an error: #{inspect(e)}\",\n          type: :gnat_server_error\n        )\n    end\n  end\n\n  @doc false\n  def send_reply(%{gnat: gnat, reply_to: return_address}, iodata)\n      when is_binary(return_address) do\n    Gnat.pub(gnat, return_address, iodata)\n  end\n\n  def send_reply(_other, _iodata) do\n    Logger.error(\n      \"Could not send reply because no reply_to was provided with the original message\",\n      type: :gnat_server_error\n    )\n  end\nend\n"
  },
  {
    "path": "lib/gnat/services/server.ex",
    "content": "defmodule Gnat.Services.Server do\n  require Logger\n  alias Gnat.Services.{Service, ServiceResponder}\n\n  @moduledoc \"\"\"\n  A behavior for acting as a NATS service\n\n  Creating a service with this behavior works almost exactly the same as `Gnat.Server`,\n  with the bonus that this service keeps track of requests, errors, processing time, and\n  participates in service discovery and monitoring as defined by\n  the [NATS service protocol](https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-32.md).\n\n\n  ## Example\n\n      defmodule MyApp.Service do\n        use Gnat.Services.Server\n\n        # Classic subject matching\n        def request(%{body: _body, topic: \"myservice.req\"}, _, _) do\n          {:reply, \"handled request\"}\n        end\n\n        # Can also match on endpoint or group\n        def request(msg, \"add\", \"calculator\") do\n          {:reply, \"42\"}\n        end\n\n        # defining an error handler is optional, the default one will just call Logger.error for you\n        def error(%{gnat: gnat, reply_to: reply_to}, _error) do\n          Gnat.pub(gnat, reply_to, \"Something went wrong and I can't handle your request\")\n        end\n      end\n  \"\"\"\n\n  @doc \"\"\"\n  Called when a message is received from the broker. The endpoint on which the message arrived\n  is always supplied. If the endpoint is a member of a group, the group name will also be\n  provided.\n\n  Automatically increments the request time and processing time stats for this service.\n  \"\"\"\n  @callback request(message :: Gnat.message(), endpoint :: String.t(), group :: String.t() | nil) ::\n              :ok | {:reply, iodata()} | {:error, term()}\n\n  @doc \"\"\"\n  Called when an error occured during the `request/1`. Automatically increments the error count\n  and processing time stats for this service.\n\n  If your `request/1` function returned `{:error, term}`, then the `term` you returned will be passed as the second argument.\n  If an exception was raised during your `request/1` function, then the exception will be passed as the second argument.\n  If your `request/1` function returned something other than the supported return types, then its return value will be passed as the second argument.\n  \"\"\"\n  @callback error(message :: Gnat.message(), error :: term()) :: :ok | {:reply, iodata()}\n\n  defmacro __using__(_opts) do\n    quote do\n      @behaviour Gnat.Services.Server\n\n      def error(_message, error) do\n        require Logger\n\n        Logger.error(\n          \"Gnat.Server encountered an error while handling a request: #{inspect(error)}\",\n          type: :gnat_server_error,\n          error: error\n        )\n      end\n\n      defoverridable error: 2\n    end\n  end\n\n  @typedoc \"\"\"\n  Service configuration is provided as part of the consumer supervisor settings in the `service_definition` field.\n  You can specify _either_ the `subscription_topics` field for a regluar server or the `service_definition` field\n  for a new NATS service.\n\n  * `name` - The name of the service. Needs to conform to the rules for NATS service names\n  * `version` - A required version number (w/out \"v\" prefix) conforming to semver rules\n  * `queue_group` - An optional queue group for service subscriptions. If left off, \"q\" will be used.\n  * `description` - An optional description of the service\n  * `metadata` - An optional string->string map of service metadata\n  * `endpoints` - A required list of service endpoints. All services must have at least one endpoint\n  \"\"\"\n  @type service_configuration :: %{\n          required(:name) => binary(),\n          required(:version) => binary(),\n          required(:endpoints) => [endpoint_configuration()],\n          optional(:description) => binary(),\n          optional(:metadata) => map()\n        }\n\n  @typedoc \"\"\"\n  Each service configuration must contain at least one endpoint. Endpoints can manually specify their\n  subscription subjects or they can be derived from the endpoint name.\n\n  * `subject` - A specific subject for this endpoint to listen on. If this is not provided, then the endpoint name will be used.\n  * `name` - The required name of the endpoint\n  * `group_name` - An optional group to which this endpoint belongs\n  * `queue_group` - A queue group for this endpoint's subscription. If not supplied, \"q\" will be used (indicated by protocol spec).\n  * `metadata` - An optional string->string map containing metadata for this endpoint\n  \"\"\"\n  @type endpoint_configuration :: %{\n          required(:name) => binary(),\n          optional(:subject) => binary(),\n          optional(:group_name) => binary(),\n          optional(:queue_group) => binary(),\n          optional(:metadata) => map()\n        }\n\n  @doc false\n  def execute(_module, %{topic: \"$SRV\" <> _} = message, service) do\n    ServiceResponder.maybe_respond(message, service)\n  end\n\n  def execute(module, message, service) do\n    try do\n      endpoint = Map.get(service.subjects, message.topic)\n      %{group_name: group_name, name: endpoint_name} = endpoint\n      telemetry_tags = %{topic: message.topic, endpoint: endpoint_name, group: group_name}\n\n      case :timer.tc(fn -> apply(module, :request, [message, endpoint_name, group_name]) end) do\n        {_elapsed, :ok} ->\n          :done\n\n        {elapsed_micros, {:reply, data}} ->\n          send_reply(message, data)\n\n          :telemetry.execute(\n            [:gnat, :service_request],\n            %{latency: elapsed_micros},\n            telemetry_tags\n          )\n\n          Service.record_request(endpoint, elapsed_micros)\n\n        {elapsed_micros, {:error, error}} ->\n          execute_error(module, message, error)\n          :telemetry.execute([:gnat, :service_error], %{latency: elapsed_micros}, telemetry_tags)\n          Service.record_error(endpoint, elapsed_micros)\n\n        other ->\n          execute_error(module, message, other)\n      end\n    rescue\n      e ->\n        execute_error(module, message, e)\n    end\n  end\n\n  @doc false\n  defp execute_error(module, message, error) do\n    try do\n      case apply(module, :error, [message, error]) do\n        :ok ->\n          :done\n\n        {:reply, data} ->\n          send_reply(message, data)\n\n        other ->\n          Logger.error(\n            \"error handler for #{module} returned something unexpected: #{inspect(other)}\",\n            type: :gnat_server_error\n          )\n      end\n    rescue\n      e ->\n        Logger.error(\n          \"error handler for #{module} encountered an error: #{inspect(e)}\",\n          type: :gnat_server_error\n        )\n    end\n  end\n\n  @doc false\n  def send_reply(%{gnat: gnat, reply_to: return_address}, iodata)\n      when is_binary(return_address) do\n    Gnat.pub(gnat, return_address, iodata)\n  end\n\n  def send_reply(_other, _iodata) do\n    Logger.error(\n      \"Could not send reply because no reply_to was provided with the original message\",\n      type: :gnat_server_error\n    )\n  end\nend\n"
  },
  {
    "path": "lib/gnat/services/service.ex",
    "content": "defmodule Gnat.Services.Service do\n  @moduledoc false\n\n  @subscription_subject \"$SRV.>\"\n  # required default, see https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-32.md#request-handling\n  @default_service_queue_group \"q\"\n\n  @idx_requests 1\n  @idx_errors 2\n  @idx_processing_time 3\n\n  @name_regex ~r/^[a-zA-Z0-9_-]+$/\n  @version_regex ~r/^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/\n\n  alias Gnat.Services.WireProtocol\n\n  def init(configuration) do\n    with :ok <- validate_configuration(configuration) do\n      service = %{\n        name: configuration.name,\n        instance_id: :crypto.strong_rand_bytes(12) |> Base.encode64(),\n        description: configuration.description,\n        version: configuration.version,\n        subjects: build_subject_map(configuration.endpoints),\n        started: DateTime.to_iso8601(DateTime.utc_now()),\n        metadata: Map.get(configuration, :metadata, %{})\n      }\n\n      {:ok, service}\n    end\n  end\n\n  def info(service) do\n    endpoint_info =\n      service.subjects\n      |> Map.values()\n      |> Enum.map(&endpoint_info/1)\n\n    %WireProtocol.InfoResponse{\n      name: service.name,\n      description: service.description,\n      id: service.instance_id,\n      version: service.version,\n      metadata: service.metadata,\n      endpoints: endpoint_info\n    }\n  end\n\n  def ping(service) do\n    %WireProtocol.PingResponse{\n      name: service.name,\n      id: service.instance_id,\n      version: service.version,\n      metadata: service.metadata\n    }\n  end\n\n  def record_request(%{counters: counters} = _endpoint, elapsed_micros) do\n    :counters.add(counters, @idx_requests, 1)\n    :counters.add(counters, @idx_processing_time, elapsed_micros)\n  end\n\n  def record_error(%{counters: counters} = _endpoint, elapsed_micros) do\n    :counters.add(counters, @idx_errors, 1)\n    :counters.add(counters, @idx_processing_time, elapsed_micros)\n  end\n\n  def subscription_topics_with_queue_group(service) do\n    endpoint_subscriptions =\n      service.subjects\n      |> Enum.map(fn {topic, metadata} ->\n        {topic, metadata.queue_group}\n      end)\n\n    services_subscription = {@subscription_subject, nil}\n    [services_subscription | endpoint_subscriptions]\n  end\n\n  def stats(service) do\n    endpoint_stats =\n      service.subjects\n      |> Map.values()\n      |> Enum.map(&endpoint_stats/1)\n\n    %WireProtocol.StatsResponse{\n      name: service.name,\n      id: service.instance_id,\n      version: service.version,\n      started: service.started,\n      endpoints: endpoint_stats\n    }\n  end\n\n  defp build_subject_map(endpoints) do\n    Enum.reduce(endpoints, %{}, fn ep, map ->\n      subject = derive_subscription_subject(ep)\n\n      endpoint = %{\n        name: ep.name,\n        queue_group: Map.get(ep, :queue_group, @default_service_queue_group),\n        group_name: Map.get(ep, :group_name, nil),\n        metadata: Map.get(ep, :metadata, %{}),\n        subject: subject,\n        counters: :counters.new(3, [:atomics])\n      }\n\n      Map.put(map, subject, endpoint)\n    end)\n  end\n\n  @spec derive_subscription_subject(Gnat.Services.Server.endpoint_configuration()) :: String.t()\n  defp derive_subscription_subject(endpoint) do\n    group_prefix =\n      case Map.get(endpoint, :group_name) do\n        nil -> \"\"\n        prefix -> \"#{prefix}.\"\n      end\n\n    subject =\n      case Map.get(endpoint, :subject) do\n        nil -> endpoint.name\n        sub -> sub\n      end\n\n    \"#{group_prefix}#{subject}\"\n  end\n\n  defp endpoint_info(endpoint) do\n    %{\n      name: endpoint.name,\n      subject: endpoint.subject,\n      metadata: endpoint.metadata,\n      queue_group: endpoint.queue_group\n    }\n  end\n\n  defp endpoint_stats(%{counters: counters} = endpoint) do\n    micros = :counters.get(counters, @idx_processing_time)\n    nanos = 1000 * micros\n    num_errors = :counters.get(counters, @idx_errors)\n    num_requests = :counters.get(counters, @idx_requests)\n    total_calls = num_errors + num_requests\n\n    avg =\n      if total_calls > 0 do\n        trunc(ceil(nanos / total_calls))\n      else\n        0\n      end\n\n    %{\n      name: endpoint.name,\n      subject: endpoint.subject,\n      num_requests: num_requests,\n      num_errors: num_errors,\n      processing_time: nanos,\n      average_processing_time: avg,\n      queue_group: endpoint.queue_group\n    }\n  end\n\n  defp validate_configuration(configuration) when is_nil(configuration),\n    do: {:error, [\"Service definition cannot be null\"]}\n\n  defp validate_configuration(configuration) when not is_map(configuration),\n    do: {:error, [\"Service definition must be a map\"]}\n\n  defp validate_configuration(configuration) do\n    rules = [\n      {&valid_version?/1, configuration},\n      {&valid_name?/1, configuration},\n      {&valid_metadata?/1, Map.get(configuration, :metadata)}\n    ]\n\n    eprules =\n      configuration.endpoints\n      |> Enum.map(fn ep ->\n        {&valid_endpoint?/1, ep}\n      end)\n\n    results =\n      (rules ++ eprules)\n      |> Enum.map(fn {pred, input} ->\n        apply(pred, [input])\n      end)\n\n    {_good, bad} = Enum.split_with(results, fn e -> e == :ok end)\n\n    if length(bad) == 0 do\n      :ok\n    else\n      {:error, bad |> Enum.map(fn {:error, m} -> m end) |> Enum.to_list()}\n    end\n  end\n\n  defp valid_version?(service_definition) do\n    version = Map.get(service_definition, :version)\n\n    if String.match?(version, @version_regex) do\n      :ok\n    else\n      {:error, \"Version '#{version}' does not conform to semver specification\"}\n    end\n  end\n\n  defp valid_name?(service_definition) do\n    name = Map.get(service_definition, :name)\n\n    if String.match?(name, @name_regex) do\n      :ok\n    else\n      {:error, \"Service name '#{name}' is invalid. Check for illegal characters\"}\n    end\n  end\n\n  defp valid_metadata?(nil), do: :ok\n\n  defp valid_metadata?(md) do\n    bads =\n      Enum.filter(md, fn {k, v} ->\n        !is_binary(k) or !is_binary(v)\n      end)\n      |> length()\n\n    if bads == 0 do\n      :ok\n    else\n      {:error, \"At least one key or value found in metadata that was not a string\"}\n    end\n  end\n\n  defp valid_endpoint?(endpoint_definition) do\n    name = Map.get(endpoint_definition, :name)\n\n    with true <- String.match?(name, @name_regex),\n         :ok <- valid_metadata?(Map.get(endpoint_definition, :metadata)) do\n      :ok\n    else\n      false ->\n        {:error, \"Endpoint name '#{name}' is not valid\"}\n\n      e ->\n        e\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/services/service_responder.ex",
    "content": "defmodule Gnat.Services.ServiceResponder do\n  @moduledoc false\n\n  require Logger\n  alias Gnat.Services.Service\n\n  @op_ping \"PING\"\n  @op_stats \"STATS\"\n  @op_info \"INFO\"\n\n  def maybe_respond(%{topic: topic} = message, service) do\n    case String.split(topic, \".\") do\n      [\"$SRV\", @op_ping | rest] ->\n        handle_ping(rest, service, message)\n\n      [\"$SRV\", @op_info | rest] ->\n        handle_info(rest, service, message)\n\n      [\"$SRV\", @op_stats | rest] ->\n        handle_stats(rest, service, message)\n\n      _other ->\n        Logger.error(\"ServiceResponder received unexpected message #{topic}\")\n    end\n  end\n\n  defp handle_ping(tail, service, %{reply_to: rt, gnat: gnat}) do\n    if should_respond?(tail, service.name, service.instance_id) do\n      body = Service.ping(service) |> Jason.encode!()\n      Gnat.pub(gnat, rt, body)\n    end\n  end\n\n  defp handle_info(tail, service, %{reply_to: rt, gnat: gnat}) do\n    if should_respond?(tail, service.name, service.instance_id) do\n      body = Service.info(service) |> Jason.encode!()\n      Gnat.pub(gnat, rt, body)\n    end\n  end\n\n  defp handle_stats(tail, service, %{reply_to: rt, gnat: gnat}) do\n    if should_respond?(tail, service.name, service.instance_id) do\n      body = Service.stats(service) |> Jason.encode!()\n      Gnat.pub(gnat, rt, body)\n    end\n  end\n\n  @spec should_respond?(list, String.t(), String.t()) :: boolean()\n  defp should_respond?(tail, service_name, instance_id) do\n    case tail do\n      [] -> true\n      [^service_name] -> true\n      [^service_name, ^instance_id] -> true\n      _ -> false\n    end\n  end\nend\n"
  },
  {
    "path": "lib/gnat/services/wire_protocol.ex",
    "content": "defmodule Gnat.Services.WireProtocol do\n  @moduledoc false\n\n  defmodule InfoResponse do\n    @moduledoc false\n    @info_response_type \"io.nats.micro.v1.info_response\"\n\n    @type endpoint :: %{\n            name: String.t(),\n            subject: String.t(),\n            metadata: map(),\n            queue_group: String.t()\n          }\n\n    @type t :: %__MODULE__{\n            name: String.t(),\n            id: String.t(),\n            version: String.t(),\n            description: String.t(),\n            metadata: map(),\n            endpoints: [endpoint()],\n            type: String.t()\n          }\n\n    @derive Jason.Encoder\n    @enforce_keys [:name, :id, :version]\n    defstruct [\n      :name,\n      :id,\n      :version,\n      :metadata,\n      :description,\n      endpoints: [],\n      type: @info_response_type\n    ]\n  end\n\n  defmodule PingResponse do\n    @moduledoc false\n    @ping_response_type \"io.nats.micro.v1.ping_response\"\n\n    @type t :: %__MODULE__{\n            name: String.t(),\n            id: String.t(),\n            version: String.t(),\n            metadata: map()\n          }\n\n    @derive Jason.Encoder\n    @enforce_keys [:name, :id, :version]\n    defstruct [\n      :name,\n      :id,\n      :version,\n      :metadata,\n      type: @ping_response_type\n    ]\n  end\n\n  defmodule StatsResponse do\n    @moduledoc false\n    @stats_response_type \"io.nats.micro.v1.stats_response\"\n\n    @type endpoint :: %{\n            name: String.t(),\n            subject: String.t(),\n            num_requests: integer,\n            num_errors: integer,\n            last_error: String.t(),\n            processing_time: integer,\n            average_processing_time: integer,\n            queue_group: String.t(),\n            data: map()\n          }\n\n    @type t :: %__MODULE__{\n            name: String.t(),\n            id: String.t(),\n            version: String.t(),\n            metadata: map(),\n            started: String.t(),\n            endpoints: [endpoint()],\n            type: String.t()\n          }\n\n    @derive Jason.Encoder\n    @enforce_keys [:name, :id]\n    defstruct [\n      :name,\n      :id,\n      :version,\n      :metadata,\n      :started,\n      :endpoints,\n      type: @stats_response_type\n    ]\n  end\nend\n"
  },
  {
    "path": "lib/gnat.ex",
    "content": "# State transitions:\n#  :waiting_for_message => receive PING, send PONG => :waiting_for_message\n#  :waiting_for_message => receive MSG... -> :waiting_for_message\n\ndefmodule Gnat do\n  @moduledoc \"\"\"\n  The primary interface for interacting with NATS\n  \"\"\"\n  use GenServer\n  require Logger\n  alias Gnat.{Command, Parsec}\n\n  @type t :: GenServer.server()\n  @type headers :: [{binary(), iodata()}]\n\n  @typedoc \"\"\"\n  A message received from NATS will be delivered to your process in this form.\n  Please note that the `:reply_to` and `:headers` keys are optional.\n  They will only be present if the message was received from the NATS server with\n  headers or a `reply_to` topic\n\n  * `gnat` - The Gnat connection\n  * `topic` - The topic on which the message arrived\n  * `body` - The raw payload of the message\n  * `sid` - The subscription ID corresponding to this message. You generally won't need to use this value directly.\n  * `reply_to` - A topic supplied for expected replies\n  * `headers` - A set of NATS message headers on the message\n  * `status` - Similar to an HTTP status, this is present for messages with headers and can indicate the specific purpose of a message. Example `status: \"408\"`\n  * `description` - A string description of the `status`\n  \"\"\"\n  @type message :: %{\n          required(:gnat) => t(),\n          required(:topic) => binary(),\n          required(:body) => iodata(),\n          required(:sid) => non_neg_integer(),\n          optional(:reply_to) => binary(),\n          optional(:headers) => headers(),\n          optional(:status) => String.t(),\n          optional(:description) => String.t()\n        }\n  @type sent_message :: {:msg, message()}\n\n  @typedoc \"\"\"\n  * `connection_timeout` - limits how long it can take to establish a connection to a server\n  * `host` - The location of the NATS server\n  * `ping_interval` - The number of milliseconds between sending PING messages to the server to check the health of our connection\n  * `port` - The port the NATS server is listening on\n  * `ssl_opts` - Options for connecting over SSL\n  * `tcp_opts` - Options for connecting over TCP\n  * `tls` - If the server should use a TLS connection\n  * `inbox_prefix` - Prefix to use for the message inbox of this connection\n  * `no_responders` - Enable the no responders behavior (see `Gnat.request/4`)\n  \"\"\"\n  @type connection_settings :: %{\n          optional(:connection_timeout) => non_neg_integer(),\n          optional(:host) => binary(),\n          optional(:inbox_prefix) => binary(),\n          optional(:ping_interval) => non_neg_integer(),\n          optional(:port) => non_neg_integer(),\n          optional(:ssl_opts) => list(),\n          optional(:tcp_opts) => list(),\n          optional(:tls) => boolean(),\n          optional(:no_responders) => boolean()\n        }\n\n  @typedoc \"\"\"\n  [Info Protocol](https://docs.nats.io/reference/reference-protocols/nats-protocol#info)\n\n  * `client_id` - An optional unsigned integer (64 bits) representing the internal client identifier in the server. This can be used to filter client connections in monitoring, correlate with error logs, etc...\n  * `client_ip` - The IP address the client is connecting from\n  * `cluster` - The name of the cluster if any\n  * `cluster_dynamic` - If the cluster is dynamic\n  * `connect_urls` - An optional list of server urls that a client can connect to.\n  * `ws_connect_urls` - An optional list of server urls that a websocket client can connect to.\n  * `git_commit` - The git commit associated with this NATS version\n  * `go` - The version of golang the NATS server was built with\n  * `headers` - If messages can have headers in them\n  * `host` - The IP address used to start the NATS server, by default this will be 0.0.0.0 and can be configured with -client_advertise host:port\n  * `jetstream` - If the server is using JetStream features\n  * `max_payload` - Maximum payload size, in bytes, that the server will accept from the client\n  * `port` - The port number the NATS server is configured to listen on\n  * `proto` - An integer indicating the protocol version of the server. The server version 1.2.0 sets this to 1 to indicate that it supports the \"Echo\" feature.\n  * `server_id` - The unique identifier of the NATS server\n  * `server_name` - A name for the server\n  * `version` - The version of the NATS server\n  * `ldm` - If the server supports Lame Duck Mode notifications, and the current server has transitioned to lame duck, ldm will be set to true.\n  * `auth_required` - If this is set, then the client should try to authenticate upon connect.\n  * `tls_required` - If this is set, then the client must perform the TLS/1.2 handshake. Note, this used to be ssl_required and has been updated along with the protocol from SSL to TLS.\n  * `tls_verify` - If this is set, the client must provide a valid certificate during the TLS handshake.\n  * `tls_available` - If the server can use TLS\n  \"\"\"\n  @type server_info :: %{\n          :client_id => non_neg_integer(),\n          :client_ip => binary(),\n          optional(:ip) => binary(),\n          optional(:cluster) => binary(),\n          optional(:cluster_dynamic) => boolean(),\n          optional(:connect_urls) => list(binary()),\n          optional(:ws_connect_urls) => list(binary()),\n          optional(:git_commit) => binary(),\n          :go => binary(),\n          :headers => boolean(),\n          :host => binary(),\n          optional(:jetstream) => binary(),\n          :max_payload => integer(),\n          :port => non_neg_integer(),\n          :proto => integer(),\n          :server_id => binary(),\n          :server_name => binary(),\n          :version => binary(),\n          optional(:ldm) => boolean(),\n          optional(:tls_verify) => boolean(),\n          optional(:tls_available) => boolean(),\n          optional(:tls_required) => boolean(),\n          optional(:auth_required) => boolean()\n        }\n\n  @default_connection_settings %{\n    host: ~c\"localhost\",\n    ping_interval: 10_000,\n    port: 4222,\n    tcp_opts: [:binary],\n    connection_timeout: 3_000,\n    ssl_opts: [],\n    tls: false,\n    inbox_prefix: \"_INBOX.\",\n    no_responders: false\n  }\n\n  @request_sid 0\n\n  @doc \"\"\"\n  Starts a connection to a nats broker\n\n  ```\n  {:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222})\n  # if the server requires TLS you can start a connection with:\n  {:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, tls: true})\n  # if the server requires TLS and a client certificate you can start a connection with:\n  {:ok, gnat} = Gnat.start_link(%{tls: true, ssl_opts: [certfile: \"client-cert.pem\", keyfile: \"client-key.pem\"]})\n  # you can customize default \"_INBOX.\" inbox prefix with:\n  {:ok, gnat} = Gnat.start_link(%{host: \"127.0.0.1\", port: 4222, inbox_prefix: \"my_prefix._INBOX.\"})\n  # you can use IPv6 addresses too\n  {:ok, gnat} = Gnat.start_link(%{host: \"::1\", port: 4222, tcp_opts: [:inet6, :binary]})\n  ```\n\n  You can also pass arbitrary SSL or TCP options in the `tcp_opts` and `ssl_opts` keys.\n  If you pass custom TCP options please include `:binary`. Gnat uses binary matching to parse messages.\n\n  The final `opts` argument will be passed to the `GenServer.start_link` call so you can pass things like `[name: :gnat_connection]`.\n  \"\"\"\n  @spec start_link(connection_settings(), keyword()) :: GenServer.on_start()\n  def start_link(connection_settings \\\\ %{}, opts \\\\ []) do\n    GenServer.start_link(__MODULE__, connection_settings, opts)\n  end\n\n  @doc \"\"\"\n  Gracefully shuts down a connection\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  :ok = Gnat.stop(gnat)\n  ```\n  \"\"\"\n  @spec stop(t()) :: :ok\n  def stop(pid), do: GenServer.call(pid, :stop)\n\n  @doc \"\"\"\n  Subscribe to a topic\n\n  Supported options:\n    * queue_group: a string that identifies which queue group you want to join\n\n  By default each subscriber will receive a copy of every message on the topic.\n  When a queue_group is supplied messages will be spread among the subscribers\n  in the same group. (see [nats queueing](https://nats.io/documentation/concepts/nats-queueing/))\n\n  The subscribed process will begin receiving messages with a structure of `t:sent_message/0`\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  {:ok, subscription} = Gnat.sub(gnat, self(), \"topic\")\n  receive do\n    {:msg, %{topic: \"topic\", body: body}} ->\n      IO.puts \"Received: \\#\\{body\\}\"\n  end\n  ```\n  \"\"\"\n  @spec sub(t(), pid(), String.t(), keyword()) ::\n          {:ok, non_neg_integer()} | {:ok, String.t()} | {:error, String.t()}\n  def sub(pid, subscriber, topic, opts \\\\ []) do\n    start = :erlang.monotonic_time()\n    result = GenServer.call(pid, {:sub, subscriber, topic, opts})\n    latency = :erlang.monotonic_time() - start\n    :telemetry.execute([:gnat, :sub], %{latency: latency}, %{topic: topic})\n    result\n  end\n\n  @doc \"\"\"\n  Publish a message\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  :ok = Gnat.pub(gnat, \"characters\", \"Ron Swanson\")\n  ```\n\n  If you want to provide a reply address to receive a response you can pass it as an option.\n  [See request-reply pattern](https://docs.nats.io/nats-concepts/core-nats/reqreply).\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  :ok = Gnat.pub(gnat, \"characters\", \"Star Lord\", reply_to: \"me\")\n  ```\n\n  If you want to publish a message with headers you can pass the `:headers` key in the `opts` like this.\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  :ok = Gnat.pub(gnat, \"listen\", \"Yo\", headers: [{\"foo\", \"bar\"}])\n  ```\n\n  Headers must be passed as a `t:headers()` value (a list of tuples).\n  Sending and parsing headers has more overhead than typical nats messages\n  (see [the Nats 2.2 release notes for details](https://docs.nats.io/whats_new_22#message-headers)),\n  so only use them when they are really valuable.\n  \"\"\"\n  @spec pub(t(), String.t(), binary(), keyword()) :: :ok\n  def pub(pid, topic, message, opts \\\\ []) do\n    start = :erlang.monotonic_time()\n    opts = prepare_headers(opts)\n    result = GenServer.call(pid, {:pub, topic, message, opts})\n    latency = :erlang.monotonic_time() - start\n    :telemetry.execute([:gnat, :pub], %{latency: latency}, %{topic: topic})\n    result\n  end\n\n  @doc \"\"\"\n  Send a request and listen for a response synchronously\n\n  Following the nats [request-reply pattern](https://docs.nats.io/nats-concepts/core-nats/reqreply) this\n  function generates a one-time topic to receive replies and then sends a message to the provided topic.\n\n  Supported options:\n    * `receive_timeout` - An integer number of milliseconds to wait for a response. Defaults to 60_000\n    * `headers` - A set of headers you want to send with the request (see `Gnat.pub/4`)\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  case Gnat.request(gnat, \"i_can_haz_cheezburger\", \"plZZZZ?!?!?\") do\n    {:ok, %{body: delicious_cheezburger}} -> :yum\n    {:error, :timeout} -> :sad_cat\n  end\n  ```\n\n  ## No Responders\n\n  If you send a request to a topic that has no registered listeners, it is sometimes convenient to find out\n  right away, rather than waiting for a timeout to occur. In order to support this use-case, you can start\n  your Gnat connection with the `no_responders: true` option and this function will return very quickly with\n  an `{:error, :no_responders}` value. This behavior also works with `request_multi/4`\n  \"\"\"\n  @spec request(t(), String.t(), binary(), keyword()) ::\n          {:ok, message} | {:error, :timeout} | {:error, :no_responders}\n  def request(pid, topic, body, opts \\\\ []) do\n    start = :erlang.monotonic_time()\n    receive_timeout = Keyword.get(opts, :receive_timeout, 60_000)\n    req = %{recipient: self(), body: body, topic: topic}\n    opts = prepare_headers(opts)\n\n    req =\n      case Keyword.get(opts, :headers) do\n        nil -> req\n        headers -> Map.put(req, :headers, headers)\n      end\n\n    {:ok, subscription} = GenServer.call(pid, {:request, req})\n    response = receive_request_response(subscription, receive_timeout)\n    :ok = unsub(pid, subscription)\n    latency = :erlang.monotonic_time() - start\n    :telemetry.execute([:gnat, :request], %{latency: latency}, %{topic: topic})\n    response\n  end\n\n  @doc \"\"\"\n  Send a request and listen for multiple responses synchronously\n\n  This function makes it easy to do a scatter-gather operation where you wait for a limited time\n  and optionally a maximum number of replies.\n\n  Supported options:\n    * `receive_timeout` - An integer number of milliseconds to wait for responses. Defaults to 60_000\n    * `max_messages` - An integer number of messages to listen for. Defaults to -1 meaning unlimited\n    * `headers` - A set of headers you want to send with the request (see `Gnat.pub/4`)\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  {:ok, responses} = Gnat.request_multi(gnat, \"i_can_haz_fries\", \"plZZZZZ!?!?\", max_messages: 5)\n  Enum.count(responses) #=> 5\n  ```\n  \"\"\"\n  @spec request_multi(t(), String.t(), binary(), keyword()) ::\n          {:ok, list(message())} | {:error, :no_responders}\n  def request_multi(pid, topic, body, opts \\\\ []) do\n    start = :erlang.monotonic_time()\n    receive_timeout_ms = Keyword.get(opts, :receive_timeout, 60_000)\n    expiration = System.monotonic_time(:millisecond) + receive_timeout_ms\n    max_messages = Keyword.get(opts, :max_messages, -1)\n\n    req = %{recipient: self(), body: body, topic: topic}\n    opts = prepare_headers(opts)\n\n    req =\n      case Keyword.get(opts, :headers) do\n        nil -> req\n        headers -> Map.put(req, :headers, headers)\n      end\n\n    {:ok, subscription} = GenServer.call(pid, {:request, req})\n\n    result =\n      case receive_multi_request_responses(subscription, expiration, max_messages) do\n        {:error, :no_responders} -> {:error, :no_responders}\n        responses when is_list(responses) -> {:ok, responses}\n      end\n\n    :ok = unsub(pid, subscription)\n    latency = :erlang.monotonic_time() - start\n    :telemetry.execute([:gnat, :request_multi], %{latency: latency}, %{topic: topic})\n    result\n  end\n\n  @doc \"\"\"\n  Unsubscribe from a topic\n\n  Supported options:\n    * `max_messages` - Number of messages to be received before automatically unsubscribed\n\n  This correlates to the [UNSUB](https://docs.nats.io/reference/reference-protocols/nats-protocol#unsub) command in the nats protocol.\n  By default the unsubscribe is affected immediately, but an optional `max_messages` value can be provided which will allow\n  `max_messages` to be received before affecting the unsubscribe.\n  This is especially useful for request/reply patterns.\n\n  ```\n  {:ok, gnat} = Gnat.start_link()\n  {:ok, subscription} = Gnat.sub(gnat, self(), \"my_inbox\")\n  :ok = Gnat.unsub(gnat, subscription)\n  # OR\n  :ok = Gnat.unsub(gnat, subscription, max_messages: 2)\n  ```\n  \"\"\"\n  @spec unsub(t(), non_neg_integer() | String.t(), keyword()) :: :ok\n  def unsub(pid, sid, opts \\\\ []) do\n    start = :erlang.monotonic_time()\n    result = GenServer.call(pid, {:unsub, sid, opts})\n    :telemetry.execute([:gnat, :unsub], %{latency: :erlang.monotonic_time() - start})\n    result\n  end\n\n  @doc \"\"\"\n  Kept just for backward compatibility for now\n  \"\"\"\n  @deprecated \"Pinging is handled internally by the connection, this function is now a no-op until we remove the function in the 2.x series\"\n  def ping(_pid) do\n    :ok\n  end\n\n  @doc \"Get the number of active subscriptions\"\n  @spec active_subscriptions(t()) :: {:ok, non_neg_integer()}\n  def active_subscriptions(pid) do\n    GenServer.call(pid, :active_subscriptions)\n  end\n\n  @doc \"\"\"\n  Get information about the NATS server the connection is for\n  \"\"\"\n  @spec server_info(t()) :: server_info()\n  def server_info(name) do\n    GenServer.call(name, :server_info)\n  end\n\n  @impl GenServer\n  def init(connection_settings) do\n    connection_settings = Map.merge(@default_connection_settings, connection_settings)\n\n    case Gnat.Handshake.connect(connection_settings) do\n      {:ok, socket, server_info} ->\n        schedule_ping_check(connection_settings)\n\n        parser = Parsec.new()\n\n        request_inbox_prefix = Map.fetch!(connection_settings, :inbox_prefix) <> \"#{nuid()}.\"\n\n        state = %{\n          socket: socket,\n          connection_settings: connection_settings,\n          server_info: server_info,\n          next_sid: 1,\n          receivers: %{},\n          parser: parser,\n          request_receivers: %{},\n          request_inbox_prefix: request_inbox_prefix,\n          waiting_on_pong: false\n        }\n\n        state = create_request_subscription(state)\n        {:ok, state}\n\n      {:error, reason} ->\n        {:stop, reason}\n    end\n  end\n\n  @impl GenServer\n  def handle_info(:ping_check, %{waiting_on_pong: true} = state) do\n    error_message =\n      \"Closing connection because we did not receive a PONG back within #{state.connection_settings.ping_interval}ms\"\n\n    Logger.error(error_message)\n    {:stop, error_message}\n  end\n\n  def handle_info(:ping_check, %{waiting_on_pong: false} = state) do\n    :ok = socket_write(state, \"PING\\r\\n\")\n    schedule_ping_check(state.connection_settings)\n    {:noreply, %{state | waiting_on_pong: true}}\n  end\n\n  def handle_info({:tcp, socket, data}, %{socket: socket} = state) do\n    data_packets = receive_additional_tcp_data(socket, [data], 10)\n\n    new_state =\n      Enum.reduce(data_packets, state, fn data, %{parser: parser} = state ->\n        {new_parser, messages} = Parsec.parse(parser, data)\n        new_state = %{state | parser: new_parser}\n        Enum.reduce(messages, new_state, &process_message/2)\n      end)\n\n    {:noreply, new_state}\n  end\n\n  def handle_info({:ssl, socket, data}, state) do\n    handle_info({:tcp, socket, data}, state)\n  end\n\n  def handle_info({:tcp_closed, _}, state) do\n    {:stop, \"connection closed\", state}\n  end\n\n  def handle_info({:ssl_closed, _}, state) do\n    {:stop, \"connection closed\", state}\n  end\n\n  def handle_info({:tcp_error, _, reason}, state) do\n    {:stop, \"tcp transport error #{inspect(reason)}\", state}\n  end\n\n  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do\n    {sid, _receiver} =\n      Enum.find(state.receivers, fn {_sid, receiver} -> receiver.recipient == pid end)\n\n    state = unsub_sid(sid, [], state)\n\n    {:noreply, state}\n  end\n\n  def handle_info(other, state) do\n    Logger.error(\"#{__MODULE__} received unexpected message: #{inspect(other)}\")\n    {:noreply, state}\n  end\n\n  @impl GenServer\n  def handle_call(:stop, _from, state) do\n    socket_close(state)\n    {:stop, :normal, :ok, state}\n  end\n\n  def handle_call({:sub, receiver, topic, opts}, _from, %{next_sid: sid} = state) do\n    sub = Command.build(:sub, topic, sid, opts)\n    :ok = socket_write(state, sub)\n    ref = Process.monitor(receiver)\n\n    next_state =\n      add_subscription_to_state(state, sid, receiver, ref) |> Map.put(:next_sid, sid + 1)\n\n    {:reply, {:ok, sid}, next_state}\n  end\n\n  def handle_call({:pub, topic, message, opts}, from, state) do\n    commands = [Command.build(:pub, topic, message, opts)]\n    froms = [from]\n    {commands, froms} = receive_additional_pubs(commands, froms, 10)\n    :ok = socket_write(state, commands)\n    Enum.each(froms, fn from -> GenServer.reply(from, :ok) end)\n    {:noreply, state}\n  end\n\n  def handle_call({:request, request}, _from, state) do\n    inbox = make_new_inbox(state)\n\n    new_state = %{\n      state\n      | request_receivers: Map.put(state.request_receivers, inbox, request.recipient)\n    }\n\n    pub =\n      case request do\n        %{headers: headers} ->\n          Command.build(:pub, request.topic, request.body, headers: headers, reply_to: inbox)\n\n        _ ->\n          Command.build(:pub, request.topic, request.body, reply_to: inbox)\n      end\n\n    :ok = socket_write(new_state, [pub])\n    {:reply, {:ok, inbox}, new_state}\n  end\n\n  # When the SID is a string, it's a topic, which is used as a key in the request receiver map.\n  def handle_call({:unsub, topic, _opts}, _from, state) when is_binary(topic) do\n    if Map.has_key?(state.request_receivers, topic) do\n      request_receivers = Map.delete(state.request_receivers, topic)\n      new_state = %{state | request_receivers: request_receivers}\n      {:reply, :ok, new_state}\n    else\n      {:reply, :ok, state}\n    end\n  end\n\n  def handle_call({:unsub, sid, opts}, _from, state) do\n    state = unsub_sid(sid, opts, state)\n    {:reply, :ok, state}\n  end\n\n  def handle_call({:ping, pinger}, _from, state) do\n    :ok = socket_write(state, \"PING\\r\\n\")\n    {:reply, :ok, Map.put(state, :pinger, pinger)}\n  end\n\n  def handle_call(:active_subscriptions, _from, state) do\n    active_subscriptions = Enum.count(state.receivers)\n    {:reply, {:ok, active_subscriptions}, state}\n  end\n\n  def handle_call(:server_info, _from, state) do\n    {:reply, state.server_info, state}\n  end\n\n  defp unsub_sid(sid, opts, state) do\n    case Map.get(state.receivers, sid) do\n      nil ->\n        state\n\n      %{monitor_ref: ref} ->\n        command = Command.build(:unsub, sid, opts)\n        :ok = socket_write(state, command)\n        Process.demonitor(ref)\n        state = cleanup_subscription_from_state(state, sid, opts)\n        state\n    end\n  end\n\n  defp create_request_subscription(%{request_inbox_prefix: request_inbox_prefix} = state) do\n    # Example: \"_INBOX.Jhf7AcTGP3x4dAV9.*\"\n    wildcard_inbox_topic = request_inbox_prefix <> \"*\"\n    sub = Command.build(:sub, wildcard_inbox_topic, @request_sid, [])\n    :ok = socket_write(state, [sub])\n    add_subscription_to_state(state, @request_sid, self(), nil)\n  end\n\n  defp make_new_inbox(%{request_inbox_prefix: prefix}), do: prefix <> nuid()\n\n  defp nuid(), do: :crypto.strong_rand_bytes(12) |> Base.encode64()\n\n  defp prepare_headers(opts) do\n    if Keyword.has_key?(opts, :headers) do\n      headers = :cow_http.headers(Keyword.get(opts, :headers))\n      Keyword.put(opts, :headers, headers)\n    else\n      opts\n    end\n  end\n\n  defp socket_close(%{socket: socket, connection_settings: %{tls: true}}), do: :ssl.close(socket)\n  defp socket_close(%{socket: socket}), do: :gen_tcp.close(socket)\n\n  defp socket_write(%{socket: socket, connection_settings: %{tls: true}}, iodata) do\n    :ssl.send(socket, iodata)\n  end\n\n  defp socket_write(%{socket: socket}, iodata), do: :gen_tcp.send(socket, iodata)\n\n  defp add_subscription_to_state(%{receivers: receivers} = state, sid, pid, ref) do\n    receivers =\n      Map.put(receivers, sid, %{recipient: pid, monitor_ref: ref, unsub_after: :infinity})\n\n    %{state | receivers: receivers}\n  end\n\n  defp cleanup_subscription_from_state(%{receivers: receivers} = state, sid, []) do\n    receivers = Map.delete(receivers, sid)\n    %{state | receivers: receivers}\n  end\n\n  defp cleanup_subscription_from_state(%{receivers: receivers} = state, sid, max_messages: n) do\n    receivers = put_in(receivers, [sid, :unsub_after], n)\n    %{state | receivers: receivers}\n  end\n\n  defp process_message({:info, server_info}, state) do\n    %{state | server_info: server_info}\n  end\n\n  defp process_message({:msg, topic, @request_sid, reply_to, body}, state) do\n    if Map.has_key?(state.request_receivers, topic) do\n      send(\n        state.request_receivers[topic],\n        {:msg, %{topic: topic, body: body, reply_to: reply_to, gnat: self()}}\n      )\n\n      state\n    else\n      Logger.error(\"#{__MODULE__} got a response for a request, but that is no longer registered\")\n      state\n    end\n  end\n\n  defp process_message({:msg, topic, sid, reply_to, body}, state) do\n    unless is_nil(state.receivers[sid]) do\n      :telemetry.execute([:gnat, :message_received], %{count: 1}, %{topic: topic})\n\n      send(\n        state.receivers[sid].recipient,\n        {:msg, %{topic: topic, body: body, reply_to: reply_to, sid: sid, gnat: self()}}\n      )\n\n      update_subscriptions_after_delivering_message(state, sid)\n    else\n      Logger.error(\"#{__MODULE__} got message for sid #{sid}, but that is no longer registered\")\n      state\n    end\n  end\n\n  defp process_message(\n         {:hmsg, topic, @request_sid, reply_to, status, description, headers, body},\n         state\n       ) do\n    if Map.has_key?(state.request_receivers, topic) do\n      map = %{\n        topic: topic,\n        body: body,\n        reply_to: reply_to,\n        gnat: self(),\n        headers: headers,\n        status: status,\n        description: description\n      }\n\n      send(state.request_receivers[topic], {:msg, map})\n      state\n    else\n      Logger.error(\"#{__MODULE__} got a response for a request, but that is no longer registered\")\n      state\n    end\n  end\n\n  defp process_message({:hmsg, topic, sid, reply_to, status, description, headers, body}, state) do\n    unless is_nil(state.receivers[sid]) do\n      :telemetry.execute([:gnat, :message_received], %{count: 1}, %{topic: topic})\n\n      map = %{\n        topic: topic,\n        body: body,\n        reply_to: reply_to,\n        sid: sid,\n        gnat: self(),\n        headers: headers,\n        status: status,\n        description: description\n      }\n\n      send(state.receivers[sid].recipient, {:msg, map})\n      update_subscriptions_after_delivering_message(state, sid)\n    else\n      Logger.error(\"#{__MODULE__} got message for sid #{sid}, but that is no longer registered\")\n      state\n    end\n  end\n\n  defp process_message(:ping, state) do\n    socket_write(state, \"PONG\\r\\n\")\n    state\n  end\n\n  defp process_message(:pong, state) do\n    %{state | waiting_on_pong: false}\n  end\n\n  defp process_message({:error, message}, state) do\n    :error_logger.error_report(\n      type: :gnat_error_from_broker,\n      message: message\n    )\n\n    state\n  end\n\n  defp receive_additional_pubs(commands, froms, 0), do: {commands, froms}\n\n  defp receive_additional_pubs(commands, froms, how_many_more) do\n    receive do\n      {:\"$gen_call\", from, {:pub, topic, message, opts}} ->\n        commands = [Command.build(:pub, topic, message, opts) | commands]\n        froms = [from | froms]\n        receive_additional_pubs(commands, froms, how_many_more - 1)\n    after\n      0 -> {commands, froms}\n    end\n  end\n\n  defp receive_additional_tcp_data(_socket, packets, 0), do: Enum.reverse(packets)\n\n  defp receive_additional_tcp_data(socket, packets, n) do\n    receive do\n      {:tcp, ^socket, data} ->\n        receive_additional_tcp_data(socket, [data | packets], n - 1)\n    after\n      0 -> Enum.reverse(packets)\n    end\n  end\n\n  defp update_subscriptions_after_delivering_message(%{receivers: receivers} = state, sid) do\n    receivers =\n      case get_in(receivers, [sid, :unsub_after]) do\n        :infinity -> receivers\n        1 -> Map.delete(receivers, sid)\n        n -> put_in(receivers, [sid, :unsub_after], n - 1)\n      end\n\n    %{state | receivers: receivers}\n  end\n\n  defp receive_multi_request_responses(_sub, _exp, 0), do: []\n\n  defp receive_multi_request_responses(subscription, expiration, max_messages) do\n    timeout = expiration - :erlang.monotonic_time(:millisecond)\n\n    cond do\n      timeout < 1 ->\n        []\n\n      true ->\n        case receive_request_response(subscription, timeout) do\n          {:error, :no_responders} ->\n            {:error, :no_responders}\n\n          {:error, :timeout} ->\n            []\n\n          {:ok, msg} ->\n            [msg | receive_multi_request_responses(subscription, expiration, max_messages - 1)]\n        end\n    end\n  end\n\n  defp receive_request_response(subscription, timeout) do\n    receive do\n      {:msg, %{topic: ^subscription, status: \"503\"}} ->\n        {:error, :no_responders}\n\n      {:msg, %{topic: ^subscription} = msg} ->\n        {:ok, msg}\n    after\n      timeout ->\n        {:error, :timeout}\n    end\n  end\n\n  defp schedule_ping_check(connection_settings) do\n    Process.send_after(self(), :ping_check, connection_settings.ping_interval)\n  end\nend\n"
  },
  {
    "path": "mix.exs",
    "content": "defmodule Gnat.Mixfile do\n  use Mix.Project\n\n  @source_url \"https://github.com/nats-io/nats.ex\"\n  @version \"1.14.0\"\n\n  def project do\n    [\n      app: :gnat,\n      version: @version,\n      elixir: \"~> 1.14\",\n      elixirc_paths: elixirc_paths(Mix.env()),\n      build_embedded: Mix.env() == :prod,\n      start_permanent: Mix.env() == :prod,\n      package: package(),\n      propcheck: [counter_examples: \"test/counter_examples\"],\n      dialyzer: [\n        ignore_warnings: \".dialyzer_ignore.exs\",\n        plt_file: {:no_warn, \"priv/plts/project.plt\"},\n        plt_core_path: \"priv/plts/core.plt\"\n      ],\n      deps: deps(),\n      docs: docs()\n    ]\n  end\n\n  def application do\n    [extra_applications: [:logger, :ssl]]\n  end\n\n  defp deps do\n    [\n      {:benchee, \"~> 1.0\", only: :dev},\n      {:cowlib, \"~> 2.0\"},\n      {:dialyxir, \"~> 1.0\", only: [:dev, :test], runtime: false},\n      {:ex_doc, \"~> 0.36\", only: :dev},\n      {:jason, \"~> 1.1\"},\n      {:connection, \"~> 1.1\"},\n      {:nimble_parsec, \"~> 0.5 or ~> 1.0\"},\n      {:nkeys, \"~> 0.2\"},\n      {:propcheck, \"~> 1.0\", only: :test},\n      {:telemetry, \"~> 0.4 or ~> 1.0\"}\n    ]\n  end\n\n  defp docs do\n    [\n      main: \"readme\",\n      logo: \"nats-icon-color.svg\",\n      source_ref: \"v#{@version}\",\n      source_url: @source_url,\n      extras: [\n        \"README.md\",\n        \"docs/js/introduction/overview.md\",\n        \"docs/js/introduction/getting_started.md\",\n        \"docs/js/guides/managing.md\",\n        \"docs/js/guides/push_based_consumer.md\",\n        \"docs/js/guides/broadway.md\",\n        \"CHANGELOG.md\"\n      ],\n      groups_for_extras: [\n        \"JetStream Introduction\": Path.wildcard(\"docs/js/introduction/*.md\"),\n        \"JetStream Guides\": Path.wildcard(\"docs/js/guides/*.md\")\n      ]\n    ]\n  end\n\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  defp package do\n    [\n      description: \"A nats client in pure elixir. Resilience, Performance, Ease-of-Use.\",\n      licenses: [\"MIT\"],\n      links: %{\n        \"Changelog\" => \"#{@source_url}/blob/master/CHANGELOG.md\",\n        \"Github\" => @source_url\n      },\n      maintainers: [\n        \"Jon Carstens\",\n        \"Devin Christensen\",\n        \"Dave Hackett\",\n        \"Steve Newell\",\n        \"Michael Ries\",\n        \"Garrett Thornburg\",\n        \"Masahiro Tokioka\",\n        \"Kevin Hoffman\"\n      ],\n      exclude_patterns: [\"priv/plts\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "scripts/cluster/cluster.sh",
    "content": "#!/usr/bin/env bash\n# Minimal control script for a 3-node local nats cluster used for\n# manually exercising PullConsumer failover behavior.\n#\n# Usage:\n#   scripts/cluster/cluster.sh start           # start all 3\n#   scripts/cluster/cluster.sh start n1        # start just n1\n#   scripts/cluster/cluster.sh stop            # stop all 3 (SIGTERM)\n#   scripts/cluster/cluster.sh stop n2         # stop just n2\n#   scripts/cluster/cluster.sh kill n2         # SIGKILL n2 (hard fail)\n#   scripts/cluster/cluster.sh status          # show what's running\n#   scripts/cluster/cluster.sh clean           # stop all + wipe data dirs\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../..\" && pwd)\"\nPID_DIR=\"$SCRIPT_DIR/pids\"\nLOG_DIR=\"$SCRIPT_DIR/logs\"\nDATA_DIR=\"$SCRIPT_DIR/data\"\n\nmkdir -p \"$PID_DIR\" \"$LOG_DIR\" \"$DATA_DIR\"\n\nnodes=(n1 n2 n3)\n\nport_for() {\n  case \"$1\" in\n    n1) echo 4223 ;;\n    n2) echo 4224 ;;\n    n3) echo 4225 ;;\n    *)  echo \"unknown node: $1\" >&2; return 1 ;;\n  esac\n}\n\nis_running() {\n  local node=$1\n  local pidfile=\"$PID_DIR/$node.pid\"\n  [[ -f \"$pidfile\" ]] || return 1\n  local pid\n  pid=$(cat \"$pidfile\")\n  kill -0 \"$pid\" 2>/dev/null\n}\n\nstart_node() {\n  local node=$1\n  if is_running \"$node\"; then\n    echo \"$node already running (pid $(cat \"$PID_DIR/$node.pid\"))\"\n    return 0\n  fi\n\n  local conf=\"$SCRIPT_DIR/$node.conf\"\n  local log=\"$LOG_DIR/$node.log\"\n  local pidfile=\"$PID_DIR/$node.pid\"\n\n  # Run from repo root so the relative store_dir in the conf resolves.\n  (\n    cd \"$REPO_ROOT\"\n    nohup nats-server -c \"$conf\" >\"$log\" 2>&1 &\n    echo $! > \"$pidfile\"\n  )\n  sleep 0.3\n  if is_running \"$node\"; then\n    echo \"$node started (pid $(cat \"$pidfile\"), port $(port_for \"$node\"), log $log)\"\n  else\n    echo \"$node failed to start — check $log\" >&2\n    return 1\n  fi\n}\n\nstop_node() {\n  local node=$1\n  local signal=${2:-TERM}\n  if ! is_running \"$node\"; then\n    echo \"$node not running\"\n    rm -f \"$PID_DIR/$node.pid\"\n    return 0\n  fi\n  local pid\n  pid=$(cat \"$PID_DIR/$node.pid\")\n  kill \"-$signal\" \"$pid\"\n  echo \"$node sent SIG$signal (pid $pid)\"\n  rm -f \"$PID_DIR/$node.pid\"\n}\n\nstatus() {\n  for node in \"${nodes[@]}\"; do\n    if is_running \"$node\"; then\n      echo \"$node  RUNNING  pid=$(cat \"$PID_DIR/$node.pid\")  port=$(port_for \"$node\")\"\n    else\n      echo \"$node  stopped              port=$(port_for \"$node\")\"\n    fi\n  done\n}\n\ncmd=${1:-}\nshift || true\n\ntargets=()\nif (( $# > 0 )); then\n  targets=(\"$@\")\nelse\n  targets=(\"${nodes[@]}\")\nfi\n\ncase \"$cmd\" in\n  start)\n    for n in \"${targets[@]}\"; do start_node \"$n\"; done\n    ;;\n  stop)\n    for n in \"${targets[@]}\"; do stop_node \"$n\" TERM; done\n    ;;\n  kill)\n    for n in \"${targets[@]}\"; do stop_node \"$n\" KILL; done\n    ;;\n  status)\n    status\n    ;;\n  clean)\n    for n in \"${nodes[@]}\"; do stop_node \"$n\" KILL || true; done\n    rm -rf \"$DATA_DIR\" \"$LOG_DIR\" \"$PID_DIR\"\n    echo \"cleaned data, logs, pids\"\n    ;;\n  *)\n    echo \"usage: $0 {start|stop|kill|status|clean} [n1|n2|n3]...\" >&2\n    exit 1\n    ;;\nesac\n"
  },
  {
    "path": "scripts/cluster/driver.exs",
    "content": "# Manual failover driver for the 3-node cluster under scripts/cluster/.\n#\n# Usage (from repo root, after `scripts/cluster/cluster.sh start`):\n#\n#   mix run --no-halt scripts/cluster/driver.exs\n#\n# The script:\n#   * connects to 2 of the 3 cluster nodes via Gnat.ConnectionSupervisor\n#   * creates (if needed) a 3-replica stream \"FAILOVER_STREAM\" and a durable\n#     consumer \"FAILOVER_CONSUMER\"\n#   * starts a PullConsumer that logs every delivery\n#   * publishes a numbered message every second\n#\n# Watch the logs, then in another terminal run:\n#   scripts/cluster/cluster.sh kill n1\n#   scripts/cluster/cluster.sh start n1\n#   scripts/cluster/cluster.sh kill n2\n# …and verify the consumer keeps receiving messages.\n\nrequire Logger\nLogger.configure(level: :info)\n\nalias Gnat.Jetstream.API.{Consumer, Stream}\n\nstream_name = \"FAILOVER_STREAM\"\nconsumer_name = \"FAILOVER_CONSUMER\"\nsubject = \"failover.test\"\n\n# Deliberately point at only 2 of the 3 nodes so we can prove the connection\n# can still reach the cluster even when one of the listed nodes is down.\nconnection_settings = [\n  %{host: ~c\"127.0.0.1\", port: 4223},\n  %{host: ~c\"127.0.0.1\", port: 4224}\n]\n\n{:ok, _sup} =\n  Gnat.ConnectionSupervisor.start_link(\n    %{\n      name: :gnat,\n      backoff_period: 1_000,\n      connection_settings: connection_settings\n    },\n    name: :gnat_sup\n  )\n\n# Wait until the connection is actually established before issuing JS admin calls.\nwait_for_gnat = fn wait_for_gnat ->\n  case Process.whereis(:gnat) do\n    nil ->\n      Process.sleep(100)\n      wait_for_gnat.(wait_for_gnat)\n\n    pid when is_pid(pid) ->\n      :ok\n  end\nend\n\nwait_for_gnat.(wait_for_gnat)\nLogger.info(\"connected to cluster\")\n\n# Create (or reuse) a 3-replica stream + durable consumer.\nstream = %Stream{\n  name: stream_name,\n  subjects: [subject],\n  num_replicas: 3,\n  storage: :file\n}\n\ncase Stream.create(:gnat, stream) do\n  {:ok, _} -> Logger.info(\"created stream #{stream_name}\")\n  {:error, %{\"err_code\" => 10058}} -> Logger.info(\"stream #{stream_name} already exists\")\n  {:error, other} -> Logger.warning(\"stream create returned: #{inspect(other)}\")\nend\n\nconsumer = %Consumer{\n  stream_name: stream_name,\n  durable_name: consumer_name,\n  ack_policy: :all\n}\n\ncase Consumer.create(:gnat, consumer) do\n  {:ok, _} -> Logger.info(\"created consumer #{consumer_name}\")\n  {:error, %{\"err_code\" => 10148}} -> Logger.info(\"consumer #{consumer_name} already exists\")\n  {:error, other} -> Logger.warning(\"consumer create returned: #{inspect(other)}\")\nend\n\n# ---------- The PullConsumer under test ----------\ndefmodule FailoverConsumer do\n  use Gnat.Jetstream.PullConsumer\n  require Logger\n\n  def start_link(opts), do: Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n\n  @impl true\n  def init(opts) do\n    {:ok, %{count: 0, batch_size: 10}, opts}\n  end\n\n  @impl true\n  def handle_message(%{body: body, reply_to: reply_to}, state) do\n    # reply_to carries delivery metadata: $JS.ACK.<stream>.<consumer>.<delivered>.<stream_seq>.<consumer_seq>.<ts>.<pending>\n    meta =\n      case reply_to do\n        \"$JS.ACK.\" <> rest -> rest\n        other -> other\n      end\n\n    Logger.info(\"RECV ##{state.count + 1} body=#{inspect(body)} meta=#{meta}\")\n    {:ack, %{state | count: state.count + 1}}\n  end\n\n  @impl true\n  def handle_status(%{status: status, description: desc}, state) do\n    Logger.warning(\"STATUS #{status} #{desc}\")\n    {:ok, state}\n  end\n\n  def handle_status(%{status: status}, state) do\n    Logger.warning(\"STATUS #{status}\")\n    {:ok, state}\n  end\n\n  @impl true\n  def handle_connected(consumer_info, state) do\n    Logger.info(\"connected, num_pending=#{consumer_info.num_pending}\")\n    {:ok, state}\n  end\nend\n\n{:ok, _pc} =\n  FailoverConsumer.start_link(\n    connection_name: :gnat,\n    stream_name: stream_name,\n    consumer_name: consumer_name\n  )\n\nLogger.info(\"PullConsumer started; publishing one message per second\")\n\n# ---------- Publisher loop ----------\n# Uses Gnat.request so we wait for the JetStream publish ack. We only bump\n# the accepted counter when the server confirms the write — that way RECV\n# numbers track actual stream writes, not best-effort pub attempts.\ndefmodule Publisher do\n  require Logger\n\n  def loop(subject, attempt \\\\ 1, accepted \\\\ 0) do\n    body = \"msg-#{accepted + 1}-#{System.system_time(:millisecond)}\"\n\n    result =\n      try do\n        Gnat.request(:gnat, subject, body, receive_timeout: 2_000)\n      catch\n        :exit, reason -> {:error, {:exit, reason}}\n      end\n\n    case result do\n      {:ok, %{body: resp_body}} ->\n        case Jason.decode(resp_body) do\n          {:ok, %{\"stream\" => stream, \"seq\" => seq}} ->\n            Logger.info(\"PUB  ##{accepted + 1} ok stream=#{stream} seq=#{seq} body=#{body}\")\n            Process.sleep(1_000)\n            loop(subject, attempt + 1, accepted + 1)\n\n          {:ok, %{\"error\" => err}} ->\n            Logger.warning(\n              \"PUB attempt ##{attempt} rejected by server: #{inspect(err)} body=#{body}\"\n            )\n\n            Process.sleep(1_000)\n            loop(subject, attempt + 1, accepted)\n\n          other ->\n            Logger.warning(\"PUB attempt ##{attempt} unparsable ack: #{inspect(other)}\")\n            Process.sleep(1_000)\n            loop(subject, attempt + 1, accepted)\n        end\n\n      {:error, reason} ->\n        Logger.warning(\"PUB attempt ##{attempt} failed: #{inspect(reason)} body=#{body}\")\n        Process.sleep(1_000)\n        loop(subject, attempt + 1, accepted)\n    end\n  end\nend\n\nPublisher.loop(subject)\n"
  },
  {
    "path": "scripts/cluster/n1.conf",
    "content": "port: 4223\nhttp_port: 8223\nserver_name: n1\n\njetstream {\n  store_dir: \"./scripts/cluster/data/n1\"\n}\n\ncluster {\n  name: failover_test\n  listen: 127.0.0.1:6223\n  routes: [\n    nats-route://127.0.0.1:6224\n    nats-route://127.0.0.1:6225\n  ]\n}\n"
  },
  {
    "path": "scripts/cluster/n2.conf",
    "content": "port: 4224\nhttp_port: 8224\nserver_name: n2\n\njetstream {\n  store_dir: \"./scripts/cluster/data/n2\"\n}\n\ncluster {\n  name: failover_test\n  listen: 127.0.0.1:6224\n  routes: [\n    nats-route://127.0.0.1:6223\n    nats-route://127.0.0.1:6225\n  ]\n}\n"
  },
  {
    "path": "scripts/cluster/n3.conf",
    "content": "port: 4225\nhttp_port: 8225\nserver_name: n3\n\njetstream {\n  store_dir: \"./scripts/cluster/data/n3\"\n}\n\ncluster {\n  name: failover_test\n  listen: 127.0.0.1:6225\n  routes: [\n    nats-route://127.0.0.1:6223\n    nats-route://127.0.0.1:6224\n  ]\n}\n"
  },
  {
    "path": "test/command_test.exs",
    "content": "defmodule Gnat.CommandTest do\n  use ExUnit.Case, async: true\n  alias Gnat.Command\n\n  test \"formatting a simple pub message\" do\n    command = Command.build(:pub, \"topic\", \"payload\", []) |> IO.iodata_to_binary()\n    assert command == \"PUB topic 7\\r\\npayload\\r\\n\"\n  end\n\n  test \"formatting a pub with reply_to set\" do\n    command = Command.build(:pub, \"topic\", \"payload\", reply_to: \"INBOX\") |> IO.iodata_to_binary()\n    assert command == \"PUB topic INBOX 7\\r\\npayload\\r\\n\"\n  end\n\n  test \"formatting a basic sub message\" do\n    command = Command.build(:sub, \"foobar\", 4, []) |> IO.iodata_to_binary()\n    assert command == \"SUB foobar 4\\r\\n\"\n  end\n\n  test \"formatting a sub with a queue group\" do\n    command = Command.build(:sub, \"foobar\", 5, queue_group: \"us\") |> IO.iodata_to_binary()\n    assert command == \"SUB foobar us 5\\r\\n\"\n  end\n\n  test \"formatting a simple unsub message\" do\n    command = Command.build(:unsub, 12, []) |> IO.iodata_to_binary()\n    assert command == \"UNSUB 12\\r\\n\"\n  end\n\n  test \"formatting an unsub message with max messages\" do\n    command = Command.build(:unsub, 12, max_messages: 3) |> IO.iodata_to_binary()\n    assert command == \"UNSUB 12 3\\r\\n\"\n  end\nend\n"
  },
  {
    "path": "test/fixtures/ca.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDXDCCAkQCCQDI2Vsry8+BDDANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCQ0ExEDAOBgNVBAoMB1N5bmFkaWExEDAOBgNVBAsMB25hdHMu\naW8xEjAQBgNVBAMMCWxvY2FsaG9zdDEcMBoGCSqGSIb3DQEJARYNZGVyZWtAbmF0\ncy5pbzAeFw0xOTEwMTcxMzAzNThaFw0yOTEwMTQxMzAzNThaMHAxCzAJBgNVBAYT\nAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwHbmF0\ncy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJla0Bu\nYXRzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAohX2dXdHIDM5\nyZDWk96b0mwRTHhBIOKtMPTTs/zKmlAgjjDxW7kSg0JimTNds9YbJ33FhcEJKXtV\nKH3Cn0uyZPS1VcTzPr7XP2QI+9SqqLuahkHAhgqoRwK62fTFJgzdZO0f9w9WwzMi\ngGk/v7KkKFa/9xKLCa9DTEJ9FA34HuYoBxXMZvypDm8d+0kxOCdThpzhKeucE4ya\njFlvOP9/l7GyjlczzAD/nt/QhPfSeIx1MF0ICj5qzwPD/jB1ekoL9OShoHvoEyXo\nUO13GMdVmZqwJcS7Vk5XNEZoH0cxSw/SrZGCE9SFjR1t8TAe3QZiZ9E8EAg4IzJQ\njfR2II5LiQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBIwib+0xLth/1+URtgQFn8\ndvQNqnJjlqC27U48qiTCTC5vJWbQDqUg9o6gtwZyYEHQ7dMmn68ozDzcGTCxaikV\nn01Bj2ijODK96Jrm/P5aVkP5Cn06FfudluZI2Q/A1cqTsa8V4rj02PpwCcLEaDqX\nyhztlhbKypWrlGuWpVlDBWstyRar98vvRK1XEyBu2NHp2fy49cwJCub4Cmz920fh\noiIwzXIKtfnf1GEjUnsuFPMgCxvhjirYNPWWjqaBldrM/dBJqwTyZf/p6g40vufN\nJJDc65c4tyRwBSBdFn+Q4zD44M0AR/8THAeIfsT42lyl8fMV5A4fe1nAVJDC4Z/H\n-----END CERTIFICATE-----"
  },
  {
    "path": "test/fixtures/client-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDQjCCAiqgAwIBAgIJAJCSLX9jr5WzMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwH\nbmF0cy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJl\na0BuYXRzLmlvMB4XDTE5MTAxNzEzMjI0MloXDTI5MTAxNDEzMjI0MlowDTELMAkG\nA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsnD6dO3oS\nVoV4yt+c/Ax+XvJPIjNGgThT16clj9fuFhPiZ0mI9pSZ8Kmm2/56F8nj3zFzcThw\nOpYemXtdB+Nj5Oi/mfc9XCf1tzcp2u6CgADUyNMbNg2L04qbjhKhTQzFIvhWO2oa\n++k9CB4Tf1VuLmWTmpBUA20N5kTW98DX2OHHHsKbo26I8XxYCKKfE8xbuREsHSNv\nOq5Hmg9qzuWANAnm4/12Ss9aGLucxcF0SWd3G7oohjGm/BKvSoUbc1v01kL/DBxJ\n5zHyWioezYfLIv9wHEjtuuC+8Lye4NxZ26V0JVizYQT2MyhrByVgD3KTFmyfsK1K\nGPeeKR63YTQXAgMBAAGjQjBAMCkGA1UdEQQiMCCCCWxvY2FsaG9zdIcEfwAAAYEN\nZGVyZWtAbmF0cy5pbzATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQUF\nAAOCAQEAfGUnzmpXXigAJxcnVKQy1ago+GGOAGldlDKIcHoofkYibhWWrrojulHF\npRPRVKm2S/P4rRnSsjrPfpf6I2Icd+oVdVxrsWcN5itbul8Xymsjl2gMSJSHknYs\nwTYNjdM4opRioArK69aRa26xXlxRs8YpRErF8Nb5mkxgvtUgtM8t/T/28MBprc7x\n7NuYvohKlOcWbgdBYI+e3CA2XLRG/A+9EmOe8g66vW/uY0eaiWduBJSwXhd+stjg\nelXYnK+EEUpJIK9DeS7r6k6HreNZ2FPM90RxdbMP7Q+i3bJwic4cJG3QOdLl+IqK\ntME8kUPD/63mEDHHMJjgAktgaFX4bQ==\n-----END CERTIFICATE-----"
  },
  {
    "path": "test/fixtures/client-key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCsnD6dO3oSVoV4\nyt+c/Ax+XvJPIjNGgThT16clj9fuFhPiZ0mI9pSZ8Kmm2/56F8nj3zFzcThwOpYe\nmXtdB+Nj5Oi/mfc9XCf1tzcp2u6CgADUyNMbNg2L04qbjhKhTQzFIvhWO2oa++k9\nCB4Tf1VuLmWTmpBUA20N5kTW98DX2OHHHsKbo26I8XxYCKKfE8xbuREsHSNvOq5H\nmg9qzuWANAnm4/12Ss9aGLucxcF0SWd3G7oohjGm/BKvSoUbc1v01kL/DBxJ5zHy\nWioezYfLIv9wHEjtuuC+8Lye4NxZ26V0JVizYQT2MyhrByVgD3KTFmyfsK1KGPee\nKR63YTQXAgMBAAECggEBAKc6FHt2NPTxOAxn2C6aDmycBftesfiblnu8EWaVrmgu\noYMV+CsmYZ+mhmZu+mNFCsam5JzoUvp/+BKbNeZSjx2nl0qRmvOqhdhLcbkuLybl\nZmjAS64wNv2Bq+a6xRfaswWGtLuugkS0TCph4+mV0qmVb7mJ5ExQqWXu8kCl9QHn\nuKacp1wVFok9rmEI+byL1+Z01feKrkf/hcF6dk62U7zHNPajViJFTDww7hiHyfUH\n6qsxIe1UWSNKtE61haEHkzqbDIDAy79jX4t3JobLToeVNCbJ7BSPf2IQSPJxELVL\nsidIJhndEjsbDR2CLpIF/EjsiSIaP7jh2zC9fxFpgSkCgYEA1qH0PH1JD5FqRV/p\nn9COYa6EifvSymGo4u/2FHgtX7wNSIQvqAVXenrQs41mz9E65womeqFXT/AZglaM\n1PEjjwcFlDuLvUEYYJNgdXrIC515ZXS6TdvJ0JpQJLx28GzZ7h31tZXfwn68C3/i\nUGEHp+nN1BfBBQnsqvmGFFvHZFUCgYEAzeDlZHHijBlgHU+kGzKm7atJfAGsrv6/\ntw7CIMEsL+z/y7pl3nwDLdZF+mLIvGuKlwIRajEzbYcEuVymCyG2/SmPMQEUf6j+\nC1OmorX9CW8OwHmVCajkIgKn0ICFsF9iFv6aYZmm1kG48AIuYiQ7HOvY/MlilqFs\n1p8sw6ZpQrsCgYEAj7Z9fQs+omfxymYAXnwc+hcKtAGkENL3bIzULryRVSrrkgTA\njDaXbnFR0Qf7MWedkxnezfm+Js5TpkwhnGuiLaC8AZclaCFwGypTShZeYDifEmno\nXT2vkjfhNdfjo/Ser6vr3BxwaSDG9MQ6Wyu9HpeUtFD7c05D4++T8YnKpskCgYEA\npCkcoIAStcWSFy0m3K0B3+dBvAiVyh/FfNDeyEFf24Mt4CPsEIBwBH+j4ugbyeoy\nYwC6JCPBLyeHA8q1d5DVmX4m+Fs1HioBD8UOzRUyA/CzIZSQ21f5OIlHiIDCmQUl\ncNJpBUQAfT2AmpgSphzfqcsBhWeLHjLvVx8rEYLC0fsCgYAiHdPZ3C0f7rWZP93N\ngY4DuldiO4d+KVsWAdBxeNgPznisUI7/ZZ/9NvCxGvA5NynyZr0qlpiKzVvtFJG8\n1ZPUuFFRMAaWn9h5C+CwMPgk65tFC6lw/el0hpmcocSXVdiJEbkV0rnv9iGh0CYX\nHMACGrYlyZdDYM0CH/JAM+K/QQ==\n-----END PRIVATE KEY-----"
  },
  {
    "path": "test/fixtures/nkey_config",
    "content": "authorization: {\n  users: [\n    { nkey: UBSUDO5PNPFR72YUCWWSN4ADPIEU3WESNZ35S3VZAWERPXCZSQTDH7SS }\n  ]\n}"
  },
  {
    "path": "test/fixtures/nkey_seed",
    "content": "SUAMH3IDGSDQ2AVZFOWYAKNA7R2FXIZZSQ3BQMA5QNJRYP3ABIKDDP5DBA"
  },
  {
    "path": "test/fixtures/server-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDPTCCAiWgAwIBAgIJAJCSLX9jr5W7MA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwH\nbmF0cy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJl\na0BuYXRzLmlvMB4XDTE5MTAxNzEzNTcyNloXDTI5MTAxNDEzNTcyNlowDTELMAkG\nA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm+0dlzcmi\nLa+LzdVqeVQ8B1/rWnErK+VvvjH7FmVodg5Z5+RXyojpd9ZBrVd6QrLSVMQPfFvB\nvGGX4yI6Ph5KXUefa31vNOOMhp2FGSmaEVhETKGQ0xRh4VfaAerOP5Cunl0TbSyJ\nyjkVa7aeMtcqTEiFL7Ae2EtiMhTrMrYpBDQ8rzm2i1IyTb9DX5v7DUOmrSynQSlV\nyXCztRVGNL/kHlItpEku1SHt/AD3ogu8EgqQZFB8xRRw9fubYgh4Q0kx80e4k9Qt\nTKncF3B2NGb/ZcE5Z+mmHIBq8J2zKMijOrdd3m5TbQmzDbETEOjs4L1eoZRLcL/c\nvYu5gmXdr4F7AgMBAAGjPTA7MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAd\nBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQADggEB\nADQYaEjWlOb9YzUnFGjfDC06dRZjRmK8TW/4GiDHIDk5TyZ1ROtskvyhVyTZJ5Vs\nqXOKJwpps0jK2edtrvZ7xIGw+Y41oPgYYhr5TK2c+oi2UOHG4BXqRbuwz/5cU+nM\nZWOG1OrHBCbrMSeFsn7rzETnd8SZnw6ZE7LI62WstdoCY0lvNfjNv3kY/6hpPm+9\n0bVzurZ28pdJ6YEJYgbPcOvxSzGDXTw9LaKEmqknTsrBKI2qm+myVTbRTimojYTo\nrw/xjHESAue/HkpOwWnFTOiTT+V4hZnDXygiSy+LWKP4zLnYOtsn0lN9OmD0z+aa\ngpoVMSncu2jMIDZX63IkQII=\n-----END CERTIFICATE-----"
  },
  {
    "path": "test/fixtures/server-key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDm+0dlzcmiLa+L\nzdVqeVQ8B1/rWnErK+VvvjH7FmVodg5Z5+RXyojpd9ZBrVd6QrLSVMQPfFvBvGGX\n4yI6Ph5KXUefa31vNOOMhp2FGSmaEVhETKGQ0xRh4VfaAerOP5Cunl0TbSyJyjkV\na7aeMtcqTEiFL7Ae2EtiMhTrMrYpBDQ8rzm2i1IyTb9DX5v7DUOmrSynQSlVyXCz\ntRVGNL/kHlItpEku1SHt/AD3ogu8EgqQZFB8xRRw9fubYgh4Q0kx80e4k9QtTKnc\nF3B2NGb/ZcE5Z+mmHIBq8J2zKMijOrdd3m5TbQmzDbETEOjs4L1eoZRLcL/cvYu5\ngmXdr4F7AgMBAAECggEBAK4sr3MiEbjcsHJAvXyzjwRRH1Bu+8VtLW7swe2vvrpd\nw4aiKXrV/BXpSsRtvPgxkXyvdMSkpuBZeFI7cVTwAJFc86RQPt77x9bwr5ltFwTZ\nrXCbRH3b3ZPNhByds3zhS+2Q92itu5cPyanQdn2mor9/lHPyOOGZgobCcynELL6R\nwRElkeDyf5ODuWEd7ADC5IFyZuwb3azNVexIK+0yqnMmv+QzEW3hsycFmFGAeB7v\nMIMjb2BhLrRr6Y5Nh+k58yM5DCf9h/OJhDpeXwLkxyK4BFg+aZffEbUX0wHDMR7f\n/nMv1g6cKvDWiLU8xLzez4t2qNIBNdxw5ZSLyQRRolECgYEA+ySTKrBAqI0Uwn8H\nsUFH95WhWUXryeRyGyQsnWAjZGF1+d67sSY2un2W6gfZrxRgiNLWEFq9AaUs0MuH\n6syF4Xwx/aZgU/gvsGtkgzuKw1bgvekT9pS/+opmHRCZyQAFEHj0IEpzyB6rW1u/\nLdlR3ShEENnmXilFv/uF/uXP5tMCgYEA63LiT0w46aGPA/E+aLRWU10c1eZ7KdhR\nc3En6zfgIxgFs8J38oLdkOR0CF6T53DSuvGR/OprVKdlnUhhDxBgT1oQjK2GlhPx\nJV5uMvarJDJxAwsF+7T4H2QtZ00BtEfpyp790+TlypSG1jo/BnSMmX2uEbV722lY\nhzINLY49obkCgYBEpN2YyG4T4+PtuXznxRkfogVk+kiVeVx68KtFJLbnw//UGT4i\nEHjbBmLOevDT+vTb0QzzkWmh3nzeYRM4aUiatjCPzP79VJPsW54whIDMHZ32KpPr\nTQMgPt3kSdpO5zN7KiRIAzGcXE2n/e7GYGUQ1uWr2XMu/4byD5SzdCscQwJ/Ymii\nLoKtRvk/zWYHr7uwWSeR5dVvpQ3E/XtONAImrIRd3cRqXfJUqTrTRKxDJXkCmyBc\n5FkWg0t0LUkTSDiQCJqcUDA3EINFR1kwthxja72pfpwc5Be/nV9BmuuUysVD8myB\nqw8A/KsXsHKn5QrRuVXOa5hvLEXbuqYw29mX6QKBgDGDzIzpR9uPtBCqzWJmc+IJ\nz4m/1NFlEz0N0QNwZ/TlhyT60ytJNcmW8qkgOSTHG7RDueEIzjQ8LKJYH7kXjfcF\n6AJczUG5PQo9cdJKo9JP3e1037P/58JpLcLe8xxQ4ce03zZpzhsxR2G/tz8DstJs\nb8jpnLyqfGrcV2feUtIZ\n-----END PRIVATE KEY-----"
  },
  {
    "path": "test/gnat/consumer_supervisor_test.exs",
    "content": "defmodule Gnat.ConsumerSupervisorTest do\n  alias Gnat.ConsumerSupervisor\n  use ExUnit.Case, async: true\n\n  # these requests are being handled by `ExampleServer` in the `test_helper.exs` file\n\n  test \"successful requests work fine\" do\n    assert {:ok, %{body: \"Re: hi\"}} = Gnat.request(:test_connection, \"example.good\", \"hi\")\n  end\n\n  test \"catches returned errors\" do\n    assert {:ok, %{body: \"400 error\"}} = Gnat.request(:test_connection, \"example.error\", \"hi\")\n  end\n\n  test \"catches raised errors\" do\n    assert {:ok, %{body: \"500 error\"}} = Gnat.request(:test_connection, \"example.raise\", \"hi\")\n  end\n\n  # The happy path is setup in `test_helper.exs`\n  # check the ExampleService module for the implementation of the endpoints\n\n  test \"microservice endpoint add works\" do\n    assert {:ok, %{body: \"6\"}} = Gnat.request(:test_connection, \"calc.add\", \"foo\")\n  end\n\n  test \"microservice endpoint sub works\" do\n    assert {:ok, %{body: \"4\"}} = Gnat.request(:test_connection, \"calc.sub\", \"foo\")\n  end\n\n  test \"microservice endpoint errors properly\" do\n    assert {:ok, %{body: \"500 error\"}} = Gnat.request(:test_connection, \"calc.sub\", \"error\")\n  end\n\n  test \"service endpoint ping response\" do\n    {:ok, %{body: body}} = Gnat.request(:test_connection, \"$SRV.PING.exampleservice\", \"\")\n    payload = Jason.decode!(body, keys: :atoms)\n    assert payload.name == \"exampleservice\"\n    assert is_binary(payload.id)\n    assert payload.version == \"0.1.0\"\n    assert payload.metadata == %{}\n  end\n\n  test \"services info response\" do\n    {:ok, %{body: body}} = Gnat.request(:test_connection, \"$SRV.INFO.exampleservice\", \"\")\n    payload = Jason.decode!(body, keys: :atoms)\n    assert payload.name == \"exampleservice\"\n    assert is_binary(payload.id)\n    assert payload.version == \"0.1.0\"\n    assert payload.description == \"This is an example service\"\n    assert payload.metadata == %{}\n\n    [add, sub] = Enum.sort_by(payload.endpoints, & &1.name)\n\n    assert add == %{\n             name: \"add\",\n             subject: \"calc.add\",\n             queue_group: \"q\",\n             metadata: %{}\n           }\n\n    assert sub == %{\n             name: \"sub\",\n             subject: \"calc.sub\",\n             queue_group: \"q\",\n             metadata: %{}\n           }\n  end\n\n  test \"service endpoint stats response\" do\n    # at least 1 error, at least 1 request, non-zero processing time\n    assert {:ok, %{body: \"4\"}} = Gnat.request(:test_connection, \"calc.sub\", \"foo\")\n    assert {:ok, %{body: \"6\"}} = Gnat.request(:test_connection, \"calc.add\", \"foo\")\n    assert {:ok, %{body: \"500 error\"}} = Gnat.request(:test_connection, \"calc.sub\", \"error\")\n\n    {:ok, %{body: body}} = Gnat.request(:test_connection, \"$SRV.STATS.exampleservice\", \"\")\n    payload = Jason.decode!(body, keys: :atoms)\n    assert Enum.at(payload.endpoints, 0) |> Map.get(:processing_time) > 1000\n    assert Enum.at(payload.endpoints, 0) |> Map.get(:num_requests) > 0\n\n    assert Enum.at(payload.endpoints, 1) |> Map.get(:processing_time) > 1000\n    assert Enum.at(payload.endpoints, 1) |> Map.get(:num_requests) > 0\n  end\n\n  test \"validates the version of a service\" do\n    bad = %{\n      name: \"exampleservice\",\n      description: \"This is an example service\",\n      version: \"0.1\",\n      endpoints: [\n        %{\n          name: \"add\",\n          group_name: \"calc\"\n        }\n      ]\n    }\n\n    assert {:error, message} = start_service_supervisor(bad)\n    assert message =~ \"Version '0.1' does not conform to semver specification\"\n  end\n\n  test \"validates the name of a service\" do\n    bad = %{\n      name: \"example!\",\n      description: \"This is an example service\",\n      version: \"0.1.0\",\n      endpoints: [\n        %{\n          name: \"add\",\n          group_name: \"calc\"\n        }\n      ]\n    }\n\n    assert {:error, message} = start_service_supervisor(bad)\n    assert message =~ \"Service name 'example!' is invalid.\"\n  end\n\n  test \"validates the name of an endpoint\" do\n    bad = %{\n      name: \"example\",\n      description: \"This is an example service\",\n      version: \"0.1.0\",\n      endpoints: [\n        %{\n          name: \"add some stuff\",\n          group_name: \"calc\"\n        }\n      ]\n    }\n\n    assert {:error, message} = start_service_supervisor(bad)\n    assert message =~ \"Endpoint name 'add some stuff' is not valid\"\n  end\n\n  test \"validates metadata of a service\" do\n    bad = %{\n      name: \"exampleservice\",\n      description: \"This is an example service\",\n      version: \"0.0.1\",\n      endpoints: [\n        %{\n          name: \"add\",\n          group_name: \"calc\",\n          metadata: %{:blarg => :thisisbad}\n        }\n      ]\n    }\n\n    assert {:error, message} = start_service_supervisor(bad)\n    assert message =~ \"At least one key or value found in metadata that was not a string\"\n  end\n\n  # In OTP 26 the GenServer.init behavior changed to do a process EXIT when returning a\n  # {:stop, error} in GenServer.init\n  # We inherit this behavior, so for the purpose of testing, we trap those process exits\n  # to make sure we can process the `{:error, error}` tuple before the process exit kills\n  # our ExUnit test\n  defp start_service_supervisor(service_config) do\n    Process.flag(:trap_exit, true)\n\n    config = %{\n      connection_name: :something,\n      module: ExampleService,\n      service_definition: service_config\n    }\n\n    ConsumerSupervisor.start_link(config)\n  end\nend\n"
  },
  {
    "path": "test/gnat/handshake_test.exs",
    "content": "defmodule Gnat.HandshakeTest do\n  use ExUnit.Case, async: true\n  alias Gnat.Handshake\n\n  describe \"negotiate_settings/2\" do\n    test \"respects server auth_required setting\" do\n      server_settings = %{auth_required: true}\n      user_settings = %{username: \"test\", password: \"secret\"}\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      assert result[:user] == \"test\"\n      assert result[:pass] == \"secret\"\n    end\n\n    test \"allows client to force auth when server doesn't require it\" do\n      server_settings = %{}\n      user_settings = %{username: \"test\", password: \"secret\", auth_required: true}\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      assert result[:user] == \"test\"\n      assert result[:pass] == \"secret\"\n    end\n\n    test \"allows client to force auth with token when server doesn't require it\" do\n      server_settings = %{}\n      user_settings = %{token: \"my-secret-token\", auth_required: true}\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      assert result[:auth_token] == \"my-secret-token\"\n    end\n\n    test \"doesn't send auth when neither server nor client requires it\" do\n      server_settings = %{}\n      user_settings = %{username: \"test\", password: \"secret\"}\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      refute Map.has_key?(result, :user)\n      refute Map.has_key?(result, :pass)\n      refute Map.has_key?(result, :auth_token)\n    end\n\n    test \"client auth_required setting takes precedence over server setting being false\" do\n      server_settings = %{auth_required: false}\n      user_settings = %{username: \"test\", password: \"secret\", auth_required: true}\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      assert result[:user] == \"test\"\n      assert result[:pass] == \"secret\"\n    end\n\n    test \"works with nkey authentication when client forces auth\" do\n      nonce = \"test-nonce-value\"\n      server_settings = %{nonce: nonce}\n\n      user_settings = %{\n        nkey_seed: \"SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4\",\n        auth_required: true\n      }\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      assert Map.has_key?(result, :sig)\n      assert Map.has_key?(result, :nkey)\n      assert result[:protocol] == 1\n    end\n\n    test \"works with JWT+nkey authentication when client forces auth\" do\n      nonce = \"test-nonce-value\"\n      server_settings = %{nonce: nonce}\n\n      user_settings = %{\n        nkey_seed: \"SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4\",\n        jwt:\n          \"eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJPUkhQUERHQ1FHRVdPSkZOUVIzM0tFSzVYT0lHSElNNlFOTVFOUUVIVlJLWVpGUkQ3NFNBIiwiaWF0IjoxNjM4MzMzMjI4LCJpc3MiOiJBQlpMM1pSRkdYNTQzRkU1SkdDMkVFQkJRVVhSREQ1TFdWN1dYSEdCSEdOUko2Nks0VUNJUEFHMyIsIm5hbWUiOiJ0ZXN0LXVzZXIiLCJzdWIiOiJVQzJGRllPUTVQWUEyQU5aREFCV1daSEhNRE5JVVdLQ0VITldNSUNCNlo2U1hLNEdOVUFZUUdCUCIsIm5hdHMiOnsicHViIjp7fSwic3ViIjp7fSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.test-signature\",\n        auth_required: true\n      }\n\n      result = Handshake.negotiate_settings(server_settings, user_settings)\n\n      assert Map.has_key?(result, :sig)\n      assert Map.has_key?(result, :jwt)\n      assert result[:protocol] == 1\n    end\n  end\nend\n"
  },
  {
    "path": "test/gnat/parsec_property_test.exs",
    "content": "defmodule Gnat.ParsecPropertyTest do\n  use ExUnit.Case, async: true\n  use PropCheck\n  import Gnat.Generators, only: [message: 0, protocol_message: 0]\n  alias Gnat.Parsec\n  @numtests (System.get_env(\"N\") || \"100\") |> String.to_integer()\n\n  @tag :property\n  property \"can parse any message\" do\n    numtests(\n      @numtests * 10,\n      forall map <- message() do\n        {_parser, [parsed]} = Parsec.new() |> Parsec.parse(map.binary)\n        {:msg, parsed_subject, parsed_sid, parsed_reply_to, parsed_payload} = parsed\n\n        parsed_payload == map.payload &&\n          parsed_subject == map.subject &&\n          parsed_sid == map.sid &&\n          parsed_reply_to == map.reply_to\n      end\n    )\n  end\n\n  @tag :property\n  property \"can parse messages split into arbitrary chunks\" do\n    numtests(\n      @numtests,\n      forall messages <- list(message()) do\n        frames = messages |> Enum.reduce(\"\", fn %{binary: b}, acc -> acc <> b end) |> random_chunk\n        payloads = Enum.map(messages, fn msg -> msg.payload end)\n\n        {parser, parsed} =\n          Enum.reduce(frames, {Parsec.new(), []}, fn frame, {parser, acc} ->\n            {parser, parsed_messages} = Parsec.parse(parser, frame)\n            payloads = Enum.map(parsed_messages, &elem(&1, 4))\n            {parser, acc ++ payloads}\n          end)\n\n        parser.partial == nil &&\n          parsed == payloads\n      end\n    )\n  end\n\n  @tag :property\n  property \"can parse any sequence of protocol messages without exception\" do\n    numtests(\n      @numtests,\n      forall messages <- list(protocol_message()) do\n        parser =\n          Enum.reduce(messages, Parsec.new(), fn %{binary: bin}, parser ->\n            {parser, [_parsed]} = Parsec.parse(parser, bin)\n            parser\n          end)\n\n        parser.partial == nil\n      end\n    )\n  end\n\n  @tag :property\n  property \"can parse any sequence of protocol messages, randomly chunked without exception\" do\n    numtests(\n      @numtests,\n      forall messages <- list(protocol_message()) do\n        bin = Enum.reduce(messages, \"\", fn %{binary: part}, acc -> acc <> part end)\n        chunks = random_chunk(bin)\n\n        {parser, parsed} =\n          Enum.reduce(chunks, {Parsec.new(), []}, fn bin, {parser, acc} ->\n            {parser, parsed} = Parsec.parse(parser, bin)\n            {parser, acc ++ parsed}\n          end)\n\n        parser.partial == nil &&\n          Enum.count(parsed) == Enum.count(messages)\n      end\n    )\n  end\n\n  def random_chunk(binary), do: random_chunk(binary, [])\n  def random_chunk(\"\", list), do: Enum.reverse(list)\n\n  def random_chunk(binary, list) do\n    size = :rand.uniform(128)\n    remaining = byte_size(binary)\n\n    case remaining <= size do\n      true ->\n        random_chunk(\"\", [binary | list])\n\n      false ->\n        random_chunk(:binary.part(binary, size, remaining - size), [\n          :binary.part(binary, 0, size) | list\n        ])\n    end\n  end\nend\n"
  },
  {
    "path": "test/gnat/parsec_test.exs",
    "content": "defmodule Gnat.ParsecTest do\n  use ExUnit.Case, async: true\n  alias Gnat.Parsec\n\n  test \"parsing a complete message\" do\n    {parser_state, [parsed_message]} = Parsec.new() |> Parsec.parse(\"MSG topic 13 4\\r\\ntest\\r\\n\")\n    assert parser_state.partial == nil\n    assert parsed_message == {:msg, \"topic\", 13, nil, \"test\"}\n  end\n\n  test \"parsing a complete message with newlines in it\" do\n    {parser_state, [parsed_message]} =\n      Parsec.new() |> Parsec.parse(\"MSG topic 13 10\\r\\ntest\\r\\nline\\r\\n\")\n\n    assert parser_state.partial == nil\n    assert parsed_message == {:msg, \"topic\", 13, nil, \"test\\r\\nline\"}\n  end\n\n  test \"parsing multiple messages\" do\n    {parser_state, [msg1, msg2]} =\n      Parsec.new() |> Parsec.parse(\"MSG t1 1 3\\r\\nwat\\r\\nMSG t2 2 4\\r\\ndawg\\r\\n\")\n\n    assert parser_state.partial == nil\n    assert msg1 == {:msg, \"t1\", 1, nil, \"wat\"}\n    assert msg2 == {:msg, \"t2\", 2, nil, \"dawg\"}\n  end\n\n  test \"parsing a message with a reply to\" do\n    {parser_state, [parsed_message]} =\n      Parsec.new() |> Parsec.parse(\"MSG topic 13 me 10\\r\\ntest\\r\\nline\\r\\n\")\n\n    assert parser_state.partial == nil\n    assert parsed_message == {:msg, \"topic\", 13, \"me\", \"test\\r\\nline\"}\n  end\n\n  test \"handling _INBOX subjects\" do\n    inbox = \"_INBOX.Rf+MI+V1+9pUCgC+.BChhlI06WHyCTYor\"\n\n    {parser_state, [parsed_message]} =\n      Parsec.new() |> Parsec.parse(\"MSG topic 13 #{inbox} 10\\r\\ntest\\r\\nline\\r\\n\")\n\n    assert parser_state.partial == nil\n    assert parsed_message == {:msg, \"topic\", 13, inbox, \"test\\r\\nline\"}\n  end\n\n  test \"parsing messages with headers - single header no reply\" do\n    binary = \"HMSG SUBJECT 1 23 30\\r\\nNATS/1.0\\r\\nHeader: X\\r\\n\\r\\nPAYLOAD\\r\\n\"\n\n    {state, [parsed]} = Parsec.new() |> Parsec.parse(binary)\n    assert state.partial == nil\n    assert parsed == {:hmsg, \"SUBJECT\", 1, nil, nil, nil, [{\"header\", \"X\"}], \"PAYLOAD\"}\n  end\n\n  test \"parsing messages with headers - single header\" do\n    binary = \"HMSG SUBJECT 1 REPLY 23 30\\r\\nNATS/1.0\\r\\nHeader: X\\r\\n\\r\\nPAYLOAD\\r\\n\"\n\n    {state, [parsed]} = Parsec.new() |> Parsec.parse(binary)\n    assert state.partial == nil\n    assert parsed == {:hmsg, \"SUBJECT\", 1, \"REPLY\", nil, nil, [{\"header\", \"X\"}], \"PAYLOAD\"}\n  end\n\n  # This example comes from https://github.com/nats-io/nats-architecture-and-design/blob/cb8f68af6ba730c00a6aa174dedaa217edd9edc6/adr/ADR-9.md\n  test \"parsing idle heartbeat messages\" do\n    binary =\n      \"HMSG my.messages 2  75 75\\r\\nNATS/1.0 100 Idle Heartbeat\\r\\nNats-Last-Consumer: 0\\r\\nNats-Last-Stream: 0\\r\\n\\r\\n\\r\\n\"\n\n    {state, [parsed]} = Parsec.new() |> Parsec.parse(binary)\n    assert state.partial == nil\n\n    assert parsed ==\n             {:hmsg, \"my.messages\", 2, nil, \"100\", \"Idle Heartbeat\",\n              [{\"nats-last-consumer\", \"0\"}, {\"nats-last-stream\", \"0\"}], \"\"}\n  end\n\n  # This example comes from https://github.com/nats-io/nats-architecture-and-design/blob/682d5cd5f21d18502da70025727128a407655250/adr/ADR-13.md\n  test \"parsing no wait pull request responses\" do\n    binary =\n      \"HMSG _INBOX.x7tkDPDLCOEknrfB4RH1V7.OgY4M7 2  28 28\\r\\nNATS/1.0 404 No Messages\\r\\n\\r\\n\\r\\n\"\n\n    {state, [parsed]} = Parsec.new() |> Parsec.parse(binary)\n    assert state.partial == nil\n\n    assert parsed ==\n             {:hmsg, \"_INBOX.x7tkDPDLCOEknrfB4RH1V7.OgY4M7\", 2, nil, \"404\", \"No Messages\", [], \"\"}\n  end\n\n  test \"parsing no_responders messages\" do\n    binary = \"HMSG _INBOX.10ahfXw89Nx5htVf.7Il73yuah/RHW6w8 0 16 16\\r\\nNATS/1.0 503\\r\\n\\r\\n\\r\\n\"\n\n    {state, [parsed]} = Parsec.new() |> Parsec.parse(binary)\n    assert state.partial == nil\n\n    assert parsed ==\n             {:hmsg, \"_INBOX.10ahfXw89Nx5htVf.7Il73yuah/RHW6w8\", 0, nil, \"503\", nil, [], \"\"}\n  end\n\n  test \"parsing PING message\" do\n    {parser_state, [parsed_message]} = Parsec.new() |> Parsec.parse(\"PING\\r\\n\")\n    assert parser_state.partial == nil\n    assert parsed_message == :ping\n  end\n\n  test \"parsing a complete message with case insensitive command\" do\n    {parser_state, [parsed_message]} = Parsec.new() |> Parsec.parse(\"msg topic 13 4\\r\\ntest\\r\\n\")\n    assert parser_state.partial == nil\n    assert parsed_message == {:msg, \"topic\", 13, nil, \"test\"}\n  end\n\n  test \"parsing case insensitive ping message\" do\n    {parser_state, [parsed_message]} = Parsec.new() |> Parsec.parse(\"ping\\r\\n\")\n    assert parser_state.partial == nil\n    assert parsed_message == :ping\n  end\n\n  test \"parsing partial messages\" do\n    {parser_state, [parsed_message]} =\n      Parsec.new() |> Parsec.parse(\"PING\\r\\nMSG topic 11 4\\r\\nOH\")\n\n    assert parsed_message == :ping\n    assert parser_state.partial == \"MSG topic 11 4\\r\\nOH\"\n\n    {parser_state, [msg1, msg2]} =\n      Parsec.parse(parser_state, \"AI\\r\\nMSG topic 11 3\\r\\nWAT\\r\\nMSG topic\")\n\n    assert msg1 == {:msg, \"topic\", 11, nil, \"OHAI\"}\n    assert msg2 == {:msg, \"topic\", 11, nil, \"WAT\"}\n    assert parser_state.partial == \"MSG topic\"\n  end\n\n  test \"parsing INFO message\" do\n    {parser_state, [parsed_message]} =\n      Parsec.new()\n      |> Parsec.parse(\n        \"INFO {\\\"server_id\\\":\\\"1ec445b504f4edfb4cf7927c707dd717\\\",\\\"version\\\":\\\"0.6.6\\\",\\\"go\\\":\\\"go1.4.2\\\",\\\"host\\\":\\\"0.0.0.0\\\",\\\"port\\\":4222,\\\"auth_required\\\":false,\\\"ssl_required\\\":false,\\\"max_payload\\\":1048576}\\r\\n\"\n      )\n\n    assert parser_state.partial == nil\n\n    assert parsed_message ==\n             {:info,\n              %{\n                server_id: \"1ec445b504f4edfb4cf7927c707dd717\",\n                version: \"0.6.6\",\n                go: \"go1.4.2\",\n                host: \"0.0.0.0\",\n                port: 4222,\n                auth_required: false,\n                ssl_required: false,\n                max_payload: 1_048_576\n              }}\n  end\n\n  test \"parsing PONG message\" do\n    {parser_state, [parsed_message]} = Parsec.new() |> Parsec.parse(\"PONG\\r\\n\")\n    assert parser_state.partial == nil\n    assert parsed_message == :pong\n  end\n\n  test \"parsing -ERR message\" do\n    {parser_state, [parsed_message]} =\n      Parsec.new() |> Parsec.parse(\"-ERR 'Unknown Protocol Operation'\\r\\n\")\n\n    assert parser_state.partial == nil\n    assert parsed_message == {:error, \"Unknown Protocol Operation\"}\n  end\n\n  test \"parsing -ERR messages in the middle of other traffic\" do\n    assert {parser, [:ping]} = Parsec.new() |> Parsec.parse(\"PING\\r\\n-ERR 'Unknown Pro\")\n\n    assert {_, [{:error, \"Unknown Protocol Operation\"}]} =\n             parser |> Parsec.parse(\"tocol Operation'\\r\\nMSG\")\n  end\nend\n"
  },
  {
    "path": "test/gnat_property_test.exs",
    "content": "defmodule GnatPropertyTest do\n  use ExUnit.Case, async: true\n  use PropCheck\n  import Gnat.Generators, only: [message: 0]\n  @numtests (System.get_env(\"N\") || \"100\") |> String.to_integer()\n\n  @tag :property\n  property \"can publish to random subjects\" do\n    numtests(\n      @numtests * 2,\n      forall %{subject: subject, payload: payload} <- message() do\n        Gnat.pub(:test_connection, subject, payload) == :ok\n      end\n    )\n  end\n\n  @tag :property\n  property \"can subscribe, publish, receive and unsubscribe from subjects\" do\n    numtests(\n      @numtests * 2,\n      forall %{subject: subject, payload: payload} <- message() do\n        {:ok, ref} = Gnat.sub(:test_connection, self(), subject)\n        :ok = Gnat.pub(:test_connection, subject, payload)\n        assert_receive {:msg, %{topic: ^subject, body: ^payload, reply_to: nil}}, 500\n        Gnat.unsub(:test_connection, ref) == :ok\n      end\n    )\n  end\n\n  @tag :property\n  property \"can make requests to an echo endpoint\" do\n    numtests(\n      @numtests * 2,\n      forall %{subject: subject, payload: payload} <- message() do\n        {:ok, msg} = Gnat.request(:test_connection, \"rpc.#{subject}\", payload)\n        msg.body == payload\n      end\n    )\n  end\nend\n"
  },
  {
    "path": "test/gnat_test.exs",
    "content": "defmodule GnatTest do\n  use ExUnit.Case, async: true\n  doctest Gnat\n\n  setup context do\n    CheckForExpectedNatsServers.check(Map.keys(context))\n    :ok\n  end\n\n  test \"connect to a server\" do\n    {:ok, pid} = Gnat.start_link()\n    assert Process.alive?(pid)\n    :ok = Gnat.stop(pid)\n  end\n\n  # We have to skip this test in CI builds because CircleCI doesn't enable IPv6 in it's docker\n  # configuration. See https://circleci.com/docs/faq#can-i-use-ipv6-in-my-tests\n  @tag :ci_skip\n  test \"connect to a server over IPv6\" do\n    {:ok, pid} = Gnat.start_link(%{host: ~c\"::1\", tcp_opts: [:binary, :inet6]})\n    assert Process.alive?(pid)\n    :ok = Gnat.stop(pid)\n  end\n\n  @tag :multi_server\n  test \"connect to a server with user/pass authentication\" do\n    connection_settings = %{\n      host: \"localhost\",\n      port: 4223,\n      tcp_opts: [:binary],\n      username: \"bob\",\n      password: \"alice\"\n    }\n\n    {:ok, pid} = Gnat.start_link(connection_settings)\n    assert Process.alive?(pid)\n    :ok = Gnat.stop(pid)\n  end\n\n  @tag :multi_server\n  test \"connect to a server with token authentication\" do\n    connection_settings = %{\n      host: \"localhost\",\n      port: 4226,\n      tcp_opts: [:binary],\n      token: \"SpecialToken\",\n      auth_required: true\n    }\n\n    {:ok, pid} = Gnat.start_link(connection_settings)\n    assert Process.alive?(pid)\n    :ok = Gnat.stop(pid)\n  end\n\n  @tag :multi_server\n  test \"connet to a server which requires TLS\" do\n    connection_settings = %{port: 4224, tls: true}\n    {:ok, gnat} = Gnat.start_link(connection_settings)\n    assert Gnat.stop(gnat) == :ok\n  end\n\n  @tag :multi_server\n  test \"connect to a server which requires TLS with a client certificate\" do\n    connection_settings = %{\n      port: 4225,\n      tls: true,\n      ssl_opts: [\n        certfile: \"test/fixtures/client-cert.pem\",\n        keyfile: \"test/fixtures/client-key.pem\"\n      ]\n    }\n\n    {:ok, gnat} = Gnat.start_link(connection_settings)\n    assert Gnat.stop(gnat) == :ok\n  end\n\n  @tag :multi_server\n  test \"connect to a server which requires nkeys\" do\n    connection_settings = %{\n      port: 4227,\n      nkey_seed: File.read!(\"test/fixtures/nkey_seed\")\n    }\n\n    {:ok, gnat} = Gnat.start_link(connection_settings)\n    assert Gnat.stop(gnat) == :ok\n  end\n\n  test \"subscribe to topic and receive a message\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _ref} = Gnat.sub(pid, self(), \"test\")\n    :ok = Gnat.pub(pid, \"test\", \"yo dawg\")\n\n    assert_receive {:msg, %{topic: \"test\", body: \"yo dawg\", reply_to: nil}}, 1000\n    :ok = Gnat.stop(pid)\n  end\n\n  test \"subscribe to topic and receive a message with headers\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _ref} = Gnat.sub(pid, self(), \"sub_with_headers\")\n    headers = [{\"X\", \"foo\"}]\n    :ok = Gnat.pub(pid, \"sub_with_headers\", \"yo dawg\", headers: headers)\n\n    assert_receive {:msg,\n                    %{\n                      topic: \"sub_with_headers\",\n                      body: \"yo dawg\",\n                      reply_to: nil,\n                      headers: [{\"x\", \"foo\"}]\n                    }},\n                   1000\n\n    :ok = Gnat.stop(pid)\n  end\n\n  test \"subscribe receive a message with a reply_to\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _ref} = Gnat.sub(pid, self(), \"with_reply\")\n    :ok = Gnat.pub(pid, \"with_reply\", \"yo dawg\", reply_to: \"me\")\n\n    assert_receive {:msg, %{topic: \"with_reply\", reply_to: \"me\", body: \"yo dawg\"}}, 1000\n    :ok = Gnat.stop(pid)\n  end\n\n  test \"subscribe receive a message with a reply_to and headers\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _ref} = Gnat.sub(pid, self(), \"with_reply\")\n    headers = [{\"x\", \"y\"}]\n    :ok = Gnat.pub(pid, \"with_reply\", \"yo dawg\", reply_to: \"me\", headers: headers)\n\n    assert_receive {:msg,\n                    %{topic: \"with_reply\", reply_to: \"me\", body: \"yo dawg\", headers: ^headers}},\n                   1000\n\n    :ok = Gnat.stop(pid)\n  end\n\n  test \"receive multiple messages\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _ref} = Gnat.sub(pid, self(), \"test\")\n    :ok = Gnat.pub(pid, \"test\", \"message 1\")\n    :ok = Gnat.pub(pid, \"test\", \"message 2\")\n    :ok = Gnat.pub(pid, \"test\", \"message 3\")\n\n    assert_receive {:msg, %{topic: \"test\", body: \"message 1\", reply_to: nil}}, 500\n    assert_receive {:msg, %{topic: \"test\", body: \"message 2\", reply_to: nil}}, 500\n    assert_receive {:msg, %{topic: \"test\", body: \"message 3\", reply_to: nil}}, 500\n    :ok = Gnat.stop(pid)\n  end\n\n  test \"subscribing to the same topic multiple times\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _sub1} = Gnat.sub(pid, self(), \"dup\")\n    {:ok, _sub2} = Gnat.sub(pid, self(), \"dup\")\n    :ok = Gnat.pub(pid, \"dup\", \"yo\")\n    :ok = Gnat.pub(pid, \"dup\", \"ma\")\n    assert_receive {:msg, %{topic: \"dup\", body: \"yo\"}}, 500\n    assert_receive {:msg, %{topic: \"dup\", body: \"yo\"}}, 500\n    assert_receive {:msg, %{topic: \"dup\", body: \"ma\"}}, 500\n    assert_receive {:msg, %{topic: \"dup\", body: \"ma\"}}, 500\n  end\n\n  test \"subscribing to the same topic multiple times with a queue group\" do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _sub1} = Gnat.sub(pid, self(), \"dup\", queue_group: \"us\")\n    {:ok, _sub2} = Gnat.sub(pid, self(), \"dup\", queue_group: \"us\")\n    :ok = Gnat.pub(pid, \"dup\", \"yo\")\n    :ok = Gnat.pub(pid, \"dup\", \"ma\")\n    assert_receive {:msg, %{topic: \"dup\", body: \"yo\"}}, 500\n    assert_receive {:msg, %{topic: \"dup\", body: \"ma\"}}, 500\n\n    receive do\n      {:msg, %{topic: _topic}} = msg -> flunk(\"Received duplicate message: #{inspect(msg)}\")\n    after\n      200 -> :ok\n    end\n  end\n\n  test \"unsubscribing from a topic\" do\n    topic = \"testunsub\"\n    {:ok, pid} = Gnat.start_link()\n    {:ok, sub_ref} = Gnat.sub(pid, self(), topic)\n    :ok = Gnat.pub(pid, topic, \"msg1\")\n    assert_receive {:msg, %{topic: ^topic, body: \"msg1\"}}, 500\n    :ok = Gnat.unsub(pid, sub_ref)\n    :ok = Gnat.pub(pid, topic, \"msg2\")\n\n    receive do\n      {:msg, %{topic: _topic, body: _body}} = msg ->\n        flunk(\"Received message after unsubscribe: #{inspect(msg)}\")\n    after\n      200 -> :ok\n    end\n  end\n\n  test \"unsubscribing from a topic after a maximum number of messages\" do\n    topic = \"testunsub_maxmsg\"\n    {:ok, pid} = Gnat.start_link()\n    {:ok, sub_ref} = Gnat.sub(pid, self(), topic)\n    :ok = Gnat.unsub(pid, sub_ref, max_messages: 2)\n    :ok = Gnat.pub(pid, topic, \"msg1\")\n    :ok = Gnat.pub(pid, topic, \"msg2\")\n    :ok = Gnat.pub(pid, topic, \"msg3\")\n    assert_receive {:msg, %{topic: ^topic, body: \"msg1\"}}, 500\n    assert_receive {:msg, %{topic: ^topic, body: \"msg2\"}}, 500\n\n    receive do\n      {:msg, _topic, _msg} = msg -> flunk(\"Received message after unsubscribe: #{inspect(msg)}\")\n    after\n      200 -> :ok\n    end\n  end\n\n  test \"subscription is cleaned up when the subscribing process dies\" do\n    topic = \"testcleanup\"\n    test_pid = self()\n    {:ok, pid} = Gnat.start_link()\n\n    # one subscription created at boot\n    assert {:ok, 1} = Gnat.active_subscriptions(pid)\n\n    %Task{pid: task_pid} =\n      Task.async(fn ->\n        Gnat.sub(pid, self(), topic)\n        assert {:ok, 2} = Gnat.active_subscriptions(pid)\n        send(test_pid, \"subscribed\")\n\n        receive do\n          :done -> :ok\n        end\n      end)\n\n    assert_receive \"subscribed\", 1_000\n    Gnat.server_info(pid)\n    Process.monitor(task_pid)\n    send(task_pid, :done)\n    assert_receive {:DOWN, _ref, :process, ^task_pid, _reason}, 1_000\n\n    assert {:ok, 1} = Gnat.active_subscriptions(pid)\n  end\n\n  test \"request-reply convenience function\" do\n    topic = \"req-resp\"\n    {:ok, pid} = Gnat.start_link()\n    spin_up_echo_server_on_topic(self(), pid, topic)\n    # Wait for server to spawn and subscribe.\n    assert_receive(true, 100)\n    {:ok, msg} = Gnat.request(pid, topic, \"ohai\", receive_timeout: 500)\n    assert msg.body == \"ohai\"\n  end\n\n  test \"request-reply convenience function with headers\" do\n    topic = \"req-resp\"\n    {:ok, pid} = Gnat.start_link()\n    spin_up_echo_server_on_topic(self(), pid, topic)\n    # Wait for server to spawn and subscribe.\n    assert_receive(true, 100)\n    headers = [{\"accept\", \"json\"}]\n    {:ok, msg} = Gnat.request(pid, topic, \"ohai\", receive_timeout: 500, headers: headers)\n    assert msg.body == \"ohai\"\n    assert msg.headers == headers\n  end\n\n  @tag timeout: 100\n  test \"request-reply no_responders\" do\n    topic = \"nobody-is-listening-to-this-topic\"\n    {:ok, pid} = Gnat.start_link(%{no_responders: true})\n    assert {:error, :no_responders} = Gnat.request(pid, topic, \"ohai\")\n  end\n\n  @tag timeout: 100\n  test \"request-reply timeout\" do\n    topic = \"nobody-is-listening-to-this-topic\"\n    {:ok, pid} = Gnat.start_link()\n    assert {:error, :timeout} = Gnat.request(pid, topic, \"ohai\", receive_timeout: 5)\n  end\n\n  test \"request_multi convenience function with no maximum messages\" do\n    topic = \"req.multi\"\n    {:ok, pid} = Gnat.start_link()\n    # start 4 servers to get 4 responses\n    Enum.each(1..4, fn _i ->\n      spin_up_echo_server_on_topic(self(), pid, topic)\n      assert_receive(true, 100)\n    end)\n\n    {:ok, messages} = Gnat.request_multi(pid, topic, \"ohai\", receive_timeout: 500)\n    assert Enum.count(messages) == 4\n    assert Enum.all?(messages, fn msg -> msg.body == \"ohai\" end)\n  end\n\n  test \"request_multi convenience function with maximum messages\" do\n    topic = \"req.multi2\"\n    {:ok, pid} = Gnat.start_link()\n    # start 4 servers to get 4 responses\n    Enum.each(1..4, fn _i ->\n      spin_up_echo_server_on_topic(self(), pid, topic)\n      assert_receive(true, 100)\n    end)\n\n    {:ok, messages} =\n      Gnat.request_multi(pid, topic, \"ohai\", max_messages: 4, receive_timeout: 500)\n\n    assert Enum.count(messages) == 4\n    assert Enum.all?(messages, fn msg -> msg.body == \"ohai\" end)\n  end\n\n  test \"request_multi convenience function with maximum messages not met\" do\n    topic = \"req.multi2\"\n    {:ok, pid} = Gnat.start_link()\n    # start 4 servers to get 4 responses\n    Enum.each(1..4, fn _i ->\n      spin_up_echo_server_on_topic(self(), pid, topic)\n      assert_receive(true, 100)\n    end)\n\n    {:ok, messages} =\n      Gnat.request_multi(pid, topic, \"ohai\", max_messages: 8, receive_timeout: 500)\n\n    assert Enum.count(messages) == 4\n    assert Enum.all?(messages, fn msg -> msg.body == \"ohai\" end)\n  end\n\n  test \"request_multi with no_responders\" do\n    topic = \"nobody.is.home\"\n    {:ok, pid} = Gnat.start_link(%{no_responders: true})\n    assert {:error, :no_responders} = Gnat.request_multi(pid, topic, \"ohai\", max_messages: 2)\n  end\n\n  defp spin_up_echo_server_on_topic(ready, gnat, topic) do\n    spawn(fn ->\n      {:ok, subscription} = Gnat.sub(gnat, self(), topic)\n      :ok = Gnat.unsub(gnat, subscription, max_messages: 1)\n      send(ready, true)\n\n      receive do\n        {:msg, %{topic: ^topic, body: body, reply_to: reply_to, headers: headers}} ->\n          Gnat.pub(gnat, reply_to, body, headers: headers)\n\n        {:msg, %{topic: ^topic, body: body, reply_to: reply_to}} ->\n          Gnat.pub(gnat, reply_to, body)\n      end\n    end)\n  end\n\n  test \"recording errors from the broker\" do\n    import ExUnit.CaptureLog\n    {:ok, gnat} = Gnat.start_link()\n\n    assert capture_log(fn ->\n             Process.flag(:trap_exit, true)\n             Gnat.sub(gnat, self(), \"invalid. subject\")\n             # errors are reported asynchronously so we need to wait a moment\n             Process.sleep(20)\n           end) =~ \"Invalid Subject\"\n  end\n\n  test \"connection timeout\" do\n    start = System.monotonic_time(:millisecond)\n    connection_settings = %{host: ~c\"169.33.33.33\", connection_timeout: 200}\n    {:stop, :timeout} = Gnat.init(connection_settings)\n    assert_in_delta System.monotonic_time(:millisecond) - start, 200, 10\n  end\n\n  test \"request-reply with custom inbox prefix\" do\n    topic = \"req-resp\"\n    {:ok, pid} = Gnat.start_link(%{inbox_prefix: \"custom._INBOX.\"})\n    spin_up_echo_server_on_topic(self(), pid, topic)\n    # Wait for server to spawn and subscribe.\n    assert_receive(true, 100)\n    headers = [{\"accept\", \"json\"}]\n    {:ok, msg} = Gnat.request(pid, topic, \"ohai\", receive_timeout: 500, headers: headers)\n    assert \"custom._INBOX.\" <> _ = msg.topic\n  end\n\n  test \"server_info/1 returns server info\" do\n    {:ok, pid} = Gnat.start_link()\n    info = Gnat.server_info(pid)\n    assert Map.has_key?(info, :version)\n    assert is_binary(info.version)\n  end\nend\n"
  },
  {
    "path": "test/jetstream/api/consumer_doc_test.exs",
    "content": "defmodule Gnat.Jetstream.API.ConsumerDocTest do\n  use Gnat.Jetstream.ConnCase\n  @moduletag with_gnat: :gnat\n  doctest Gnat.Jetstream.API.Consumer\nend\n"
  },
  {
    "path": "test/jetstream/api/consumer_test.exs",
    "content": "defmodule Gnat.Jetstream.API.ConsumerTest do\n  use Gnat.Jetstream.ConnCase\n  alias Gnat.Jetstream.API.{Consumer, Stream}\n\n  @moduletag with_gnat: :gnat\n\n  test \"listing, creating, and deleting consumers\" do\n    stream = %Stream{name: \"STREAM1\", subjects: [\"STREAM1\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n\n    assert {:ok, consumers} = Consumer.list(:gnat, \"STREAM1\")\n\n    assert consumers == %{\n             total: 0,\n             offset: 0,\n             limit: 1024,\n             consumers: []\n           }\n\n    consumer = %Consumer{stream_name: \"STREAM1\", durable_name: \"STREAM1\"}\n    assert {:ok, consumer_response} = Consumer.create(:gnat, consumer)\n\n    assert consumer_response.ack_floor == %{\n             consumer_seq: 0,\n             stream_seq: 0\n           }\n\n    assert consumer_response.delivered == %{\n             consumer_seq: 0,\n             stream_seq: 0\n           }\n\n    assert %DateTime{} = consumer_response.created\n\n    assert consumer_response.config == %{\n             ack_policy: :explicit,\n             ack_wait: 30_000_000_000,\n             deliver_policy: :all,\n             deliver_subject: nil,\n             durable_name: \"STREAM1\",\n             filter_subject: nil,\n             opt_start_seq: nil,\n             opt_start_time: nil,\n             replay_policy: :instant,\n             backoff: nil,\n             deliver_group: nil,\n             description: nil,\n             flow_control: nil,\n             headers_only: nil,\n             idle_heartbeat: nil,\n             inactive_threshold: nil,\n             max_ack_pending: 20000,\n             max_batch: nil,\n             max_deliver: -1,\n             max_expires: nil,\n             max_waiting: 512,\n             rate_limit_bps: nil,\n             sample_freq: nil\n           }\n\n    assert consumer_response.num_pending == 0\n    assert consumer_response.num_redelivered == 0\n\n    assert {:ok, consumers} = Consumer.list(:gnat, \"STREAM1\")\n\n    assert consumers == %{\n             total: 1,\n             offset: 0,\n             limit: 1024,\n             consumers: [\"STREAM1\"]\n           }\n\n    assert :ok = Consumer.delete(:gnat, \"STREAM1\", \"STREAM1\")\n    assert {:ok, consumers} = Consumer.list(:gnat, \"STREAM1\")\n\n    assert consumers == %{\n             total: 0,\n             offset: 0,\n             limit: 1024,\n             consumers: []\n           }\n  end\n\n  test \"failed creates\" do\n    consumer = %Consumer{durable_name: \"STREAM2\", stream_name: \"STREAM2\"}\n\n    assert {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} =\n             Consumer.create(:gnat, consumer)\n  end\n\n  test \"failed deletes\" do\n    assert {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} =\n             Consumer.delete(:gnat, \"STREAM3\", \"STREAM3\")\n  end\n\n  test \"getting consumer info\" do\n    stream = %Stream{name: \"STREAM4\", subjects: [\"STREAM4\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n\n    consumer = %Consumer{\n      stream_name: \"STREAM4\",\n      durable_name: \"STREAM4\",\n      deliver_subject: \"consumer.STREAM4\"\n    }\n\n    assert {:ok, _consumer_response} = Consumer.create(:gnat, consumer)\n\n    assert {:ok, consumer_response} = Consumer.info(:gnat, \"STREAM4\", \"STREAM4\")\n\n    assert consumer_response.ack_floor == %{\n             consumer_seq: 0,\n             stream_seq: 0\n           }\n\n    assert consumer_response.delivered == %{\n             consumer_seq: 0,\n             stream_seq: 0\n           }\n\n    assert %DateTime{} = consumer_response.created\n\n    assert consumer_response.config == %{\n             ack_policy: :explicit,\n             ack_wait: 30_000_000_000,\n             deliver_policy: :all,\n             deliver_subject: \"consumer.STREAM4\",\n             durable_name: \"STREAM4\",\n             filter_subject: nil,\n             opt_start_seq: nil,\n             opt_start_time: nil,\n             replay_policy: :instant,\n             backoff: nil,\n             deliver_group: nil,\n             description: nil,\n             flow_control: nil,\n             headers_only: nil,\n             idle_heartbeat: nil,\n             inactive_threshold: nil,\n             max_ack_pending: 20000,\n             max_batch: nil,\n             max_deliver: -1,\n             max_expires: nil,\n             max_waiting: nil,\n             rate_limit_bps: nil,\n             sample_freq: nil\n           }\n\n    assert consumer_response.num_pending == 0\n    assert consumer_response.num_redelivered == 0\n\n    assert :ok = Consumer.delete(:gnat, \"STREAM4\", \"STREAM4\")\n    assert :ok = Stream.delete(:gnat, \"STREAM4\")\n  end\n\n  test \"validating stream and consumer names\" do\n    assert {:error, reason} =\n             Consumer.create(:gnat, %Consumer{stream_name: \"test.periods\", durable_name: \"foo\"})\n\n    assert reason == \"invalid stream_name: cannot contain '.', '>', '*', spaces or tabs\"\n\n    assert {:error, reason} =\n             Consumer.create(:gnat, %Consumer{stream_name: nil, durable_name: \"foo\"})\n\n    assert reason == \"must have a :stream_name set\"\n\n    assert {:error, reason} =\n             Consumer.create(:gnat, %Consumer{stream_name: :foo, durable_name: \"foo\"})\n\n    assert reason == \"stream_name must be a string\"\n\n    assert {:error, reason} =\n             Consumer.create(:gnat, %Consumer{stream_name: \"TEST_STREAM\", durable_name: \"foo.bar\"})\n\n    assert reason == \"invalid durable_name: cannot contain '.', '>', '*', spaces or tabs\"\n\n    assert {:error, reason} =\n             Consumer.create(:gnat, %Consumer{stream_name: \"TEST_STREAM\", durable_name: :ohai})\n\n    assert reason == \"durable_name must be a string\"\n  end\n\n  describe \"request_next_message/5\" do\n    setup do\n      stream_name = \"REQUEST_MESSAGE_TEST_STREAM\"\n      subject = \"request_test_subject\"\n      consumer_name = \"REQUEST_MESSAGE_TEST_CONSUMER\"\n\n      Stream.delete(:gnat, stream_name)\n\n      stream = %Stream{name: stream_name, subjects: [subject]}\n      {:ok, _response} = Stream.create(:gnat, stream)\n\n      consumer = %Consumer{stream_name: stream_name, durable_name: consumer_name}\n      assert {:ok, _response} = Consumer.create(:gnat, consumer)\n\n      reply_subject = \"reply\"\n\n      Gnat.sub(:gnat, self(), reply_subject)\n\n      %{\n        stream_name: stream_name,\n        subject: subject,\n        consumer_name: consumer_name,\n        reply_subject: reply_subject\n      }\n    end\n\n    test \"requests a single message with default options\", %{\n      stream_name: stream_name,\n      subject: subject,\n      consumer_name: consumer_name,\n      reply_subject: reply_subject\n    } do\n      Consumer.request_next_message(:gnat, stream_name, consumer_name, reply_subject)\n\n      Gnat.pub(:gnat, subject, \"message 1\")\n\n      assert_receive {:msg, %{body: \"message 1\", topic: ^subject}}\n\n      Gnat.pub(:gnat, subject, \"message 2\")\n\n      refute_receive {:msg, %{body: \"message 2\"}}\n    end\n\n    test \"requests batch messages\", %{\n      stream_name: stream_name,\n      subject: subject,\n      consumer_name: consumer_name,\n      reply_subject: reply_subject\n    } do\n      Consumer.request_next_message(:gnat, stream_name, consumer_name, reply_subject, nil,\n        batch: 10\n      )\n\n      Gnat.pub(:gnat, subject, \"message 1\")\n\n      assert_receive {:msg, %{body: \"message 1\", topic: ^subject}}\n\n      for i <- 2..10, do: Gnat.pub(:gnat, subject, \"message #{i}\")\n\n      for i <- 2..10 do\n        expected_body = \"message #{i}\"\n\n        assert_receive {:msg, %{body: ^expected_body, topic: ^subject}}\n      end\n\n      Gnat.pub(:gnat, subject, \"message 11\")\n\n      refute_receive {:msg, %{body: \"message 11\"}}\n    end\n\n    test \"doesn't wait for messages when `no_wait` option is set to true\", %{\n      stream_name: stream_name,\n      subject: subject,\n      consumer_name: consumer_name,\n      reply_subject: reply_subject\n    } do\n      Consumer.request_next_message(:gnat, stream_name, consumer_name, reply_subject, nil,\n        no_wait: true\n      )\n\n      assert_receive {:msg, %{body: \"\", topic: ^reply_subject}}\n\n      Gnat.pub(:gnat, subject, \"message 1\")\n\n      refute_receive {:msg, %{body: \"message 1\"}}\n    end\n\n    test \"doesn't wait for messages to complete the batch size with `no_wait`\", %{\n      stream_name: stream_name,\n      subject: subject,\n      consumer_name: consumer_name,\n      reply_subject: reply_subject\n    } do\n      for i <- 1..9 do\n        # Using `request` to make sure messages get in the stream before we move on to consume them\n        assert {:ok, _} = Gnat.request(:gnat, subject, \"message #{i}\")\n      end\n\n      Consumer.request_next_message(:gnat, stream_name, consumer_name, reply_subject, nil,\n        batch: 10,\n        no_wait: true\n      )\n\n      for i <- 1..9 do\n        expected_body = \"message #{i}\"\n\n        assert_receive {:msg, %{body: ^expected_body, topic: ^subject}}, 2_000\n      end\n\n      assert_receive {:msg, %{body: \"\", topic: ^reply_subject}}, 2_000\n\n      Gnat.pub(:gnat, subject, \"message 10\")\n\n      refute_receive {:msg, %{body: \"message 10\"}}\n    end\n  end\nend\n"
  },
  {
    "path": "test/jetstream/api/kv/entry_test.exs",
    "content": "defmodule Gnat.Jetstream.API.KV.EntryTest do\n  use ExUnit.Case, async: true\n\n  alias Gnat.Jetstream.API.KV.Entry\n\n  @bucket \"my_bucket\"\n  @reply_to \"$JS.ACK.KV_my_bucket.consumer.1.42.7.1750928948439739269.3\"\n\n  describe \"from_message/2\" do\n    test \"parses a put message with no headers\" do\n      message = %{topic: \"$KV.my_bucket.some.key\", body: \"hello\", reply_to: @reply_to}\n\n      assert {:ok, entry} = Entry.from_message(message, @bucket)\n\n      assert %Entry{\n               bucket: \"my_bucket\",\n               key: \"some.key\",\n               value: \"hello\",\n               operation: :put,\n               revision: 42,\n               delta: 3\n             } = entry\n\n      assert %DateTime{} = entry.created\n    end\n\n    test \"parses a put message when headers are present but not KV-operation\" do\n      message = %{\n        topic: \"$KV.my_bucket.foo\",\n        body: \"bar\",\n        headers: [{\"some-other\", \"value\"}]\n      }\n\n      assert {:ok, %Entry{operation: :put, key: \"foo\", value: \"bar\"}} =\n               Entry.from_message(message, @bucket)\n    end\n\n    test \"parses a DEL message as :delete\" do\n      message = %{\n        topic: \"$KV.my_bucket.foo\",\n        body: \"\",\n        headers: [{\"kv-operation\", \"DEL\"}]\n      }\n\n      assert {:ok, %Entry{operation: :delete, key: \"foo\", value: \"\"}} =\n               Entry.from_message(message, @bucket)\n    end\n\n    test \"parses a PURGE message as :purge\" do\n      message = %{\n        topic: \"$KV.my_bucket.foo\",\n        body: \"\",\n        headers: [{\"kv-operation\", \"PURGE\"}]\n      }\n\n      assert {:ok, %Entry{operation: :purge, key: \"foo\"}} =\n               Entry.from_message(message, @bucket)\n    end\n\n    test \"treats a nats-marker-reason tombstone as :delete\" do\n      message = %{\n        topic: \"$KV.my_bucket.foo\",\n        body: \"\",\n        headers: [{\"nats-marker-reason\", \"MaxAge\"}]\n      }\n\n      assert {:ok, %Entry{operation: :delete, key: \"foo\"}} =\n               Entry.from_message(message, @bucket)\n    end\n\n    test \"recovers keys that include dots\" do\n      message = %{topic: \"$KV.my_bucket.a.b.c\", body: \"v\"}\n\n      assert {:ok, %Entry{key: \"a.b.c\"}} = Entry.from_message(message, @bucket)\n    end\n\n    test \"leaves revision/created/delta nil when no reply_to is present\" do\n      message = %{topic: \"$KV.my_bucket.foo\", body: \"bar\"}\n\n      assert {:ok, %Entry{revision: nil, created: nil, delta: nil}} =\n               Entry.from_message(message, @bucket)\n    end\n\n    test \"returns :ignore when the subject does not belong to the bucket\" do\n      message = %{topic: \"$KV.other_bucket.foo\", body: \"bar\"}\n\n      assert :ignore = Entry.from_message(message, @bucket)\n    end\n\n    test \"returns :ignore for a 409 leadership-change status message\" do\n      message = %{\n        topic: \"_INBOX.foo\",\n        body: \"\",\n        status: \"409\",\n        description: \"Leadership Change\"\n      }\n\n      assert :ignore = Entry.from_message(message, @bucket)\n    end\n\n    test \"returns :ignore for a 100 idle heartbeat with no description\" do\n      message = %{topic: \"_INBOX.foo\", body: \"\", status: \"100\"}\n\n      assert :ignore = Entry.from_message(message, @bucket)\n    end\n  end\nend\n"
  },
  {
    "path": "test/jetstream/api/kv/watcher_test.exs",
    "content": "defmodule Gnat.Jetstream.API.KV.WatcherTest do\n  use Gnat.Jetstream.ConnCase, min_server_version: \"2.6.2\"\n\n  alias Gnat.Jetstream.API.KV\n\n  @moduletag with_gnat: :gnat\n\n  describe \"status messages\" do\n    setup do\n      bucket = \"WATCHER_STATUS_TEST\"\n      {:ok, _} = KV.create_bucket(:gnat, bucket)\n\n      on_exit(fn ->\n        {:ok, pid} = Gnat.start_link()\n        :ok = KV.delete_bucket(pid, bucket)\n        Gnat.stop(pid)\n      end)\n\n      test_pid = self()\n\n      {:ok, watcher} =\n        KV.watch(:gnat, bucket, fn action, key, value ->\n          send(test_pid, {action, key, value})\n        end)\n\n      on_exit(fn ->\n        if Process.alive?(watcher), do: KV.unwatch(watcher)\n      end)\n\n      %{watcher: watcher}\n    end\n\n    test \"409 status messages are dropped\", %{watcher: watcher} do\n      send(\n        watcher,\n        {:msg, %{status: \"409\", description: \"Leadership Change\", body: \"\", topic: \"ignored\"}}\n      )\n\n      refute_receive {:key_added, _, _}, 100\n      refute_receive {:key_deleted, _, _}, 100\n      assert Process.alive?(watcher)\n    end\n\n    test \"100 idle heartbeat (no reply_to) is dropped\", %{watcher: watcher} do\n      send(watcher, {:msg, %{status: \"100\", body: \"\", topic: \"ignored\"}})\n\n      refute_receive {:key_added, _, _}, 100\n      assert Process.alive?(watcher)\n    end\n\n    test \"100 flow-control message is answered with an empty publish\", %{watcher: watcher} do\n      reply_to = \"_WATCHER_TEST.fc.#{System.unique_integer([:positive])}\"\n      {:ok, _sub} = Gnat.sub(:gnat, self(), reply_to)\n\n      send(\n        watcher,\n        {:msg,\n         %{\n           status: \"100\",\n           description: \"FlowControl Request\",\n           body: \"\",\n           reply_to: reply_to,\n           topic: \"ignored\"\n         }}\n      )\n\n      assert_receive {:msg, %{topic: ^reply_to, body: \"\"}}, 500\n    end\n  end\nend\n"
  },
  {
    "path": "test/jetstream/api/kv_test.exs",
    "content": "defmodule Gnat.Jetstream.API.KVTest do\n  use Gnat.Jetstream.ConnCase, min_server_version: \"2.6.2\"\n  alias Gnat.Jetstream.API.KV\n  alias Gnat.Jetstream.API.Stream\n\n  @moduletag with_gnat: :gnat\n\n  describe \"create_bucket/3\" do\n    test \"creates a bucket\" do\n      assert {:ok, %{config: config}} = KV.create_bucket(:gnat, \"BUCKET_TEST\")\n      assert config.name == \"KV_BUCKET_TEST\"\n      assert config.subjects == [\"$KV.BUCKET_TEST.>\"]\n      assert config.max_msgs_per_subject == 1\n      assert config.discard == :new\n      assert config.allow_rollup_hdrs == true\n\n      assert :ok = KV.delete_bucket(:gnat, \"BUCKET_TEST\")\n    end\n\n    test \"creates a bucket with duplicate window < 2min\" do\n      assert {:ok, %{config: config}} = KV.create_bucket(:gnat, \"TTL_TEST\", ttl: 1_000_000_000)\n      assert config.max_age == 1_000_000_000\n      assert config.duplicate_window == 1_000_000_000\n\n      assert :ok = KV.delete_bucket(:gnat, \"TTL_TEST\")\n    end\n\n    test \"creates a bucket with duplicate window > 2min\" do\n      assert {:ok, %{config: config}} =\n               KV.create_bucket(:gnat, \"OTHER_TTL_TEST\", ttl: 130_000_000_000)\n\n      assert config.max_age == 130_000_000_000\n      assert config.duplicate_window == 120_000_000_000\n\n      assert :ok = KV.delete_bucket(:gnat, \"OTHER_TTL_TEST\")\n    end\n\n    @tag :message_ttl\n    test \"creates a bucket with limit_marker_ttl\" do\n      assert {:ok, %{config: config}} =\n               KV.create_bucket(:gnat, \"LIMIT_MARKER_TTL_TEST\", limit_marker_ttl: 1_000_000_000)\n\n      assert config.subject_delete_marker_ttl == 1_000_000_000\n\n      assert :ok = KV.delete_bucket(:gnat, \"LIMIT_MARKER_TTL_TEST\")\n    end\n  end\n\n  test \"create_key/4 creates a key\" do\n    assert {:ok, _} = KV.create_bucket(:gnat, \"KEY_CREATE_TEST\")\n    assert :ok = KV.create_key(:gnat, \"KEY_CREATE_TEST\", \"foo\", \"bar\")\n    assert \"bar\" = KV.get_value(:gnat, \"KEY_CREATE_TEST\", \"foo\")\n    assert :ok = KV.delete_bucket(:gnat, \"KEY_CREATE_TEST\")\n  end\n\n  test \"create_key/4 returns error\" do\n    assert {:error, :timeout} = KV.create_key(:gnat, \"KEY_CREATE_TEST\", \"foo\", \"bar\", timeout: 1)\n  end\n\n  test \"delete_key/3 deletes a key\" do\n    assert {:ok, _} = KV.create_bucket(:gnat, \"KEY_DELETE_TEST\")\n    assert :ok = KV.create_key(:gnat, \"KEY_DELETE_TEST\", \"foo\", \"bar\")\n    assert :ok = KV.delete_key(:gnat, \"KEY_DELETE_TEST\", \"foo\")\n    assert KV.get_value(:gnat, \"KEY_DELETE_TEST\", \"foo\") == nil\n    assert :ok = KV.delete_bucket(:gnat, \"KEY_DELETE_TEST\")\n  end\n\n  test \"delete_key/3 returns error\" do\n    assert {:error, :timeout} = KV.delete_key(:gnat, \"KEY_DELETE_TEST\", \"foo\", timeout: 1)\n  end\n\n  test \"purge_key/3 purges a key\" do\n    assert {:ok, _} = KV.create_bucket(:gnat, \"KEY_PURGE_TEST\")\n    assert :ok = KV.create_key(:gnat, \"KEY_PURGE_TEST\", \"foo\", \"bar\")\n    assert :ok = KV.purge_key(:gnat, \"KEY_PURGE_TEST\", \"foo\")\n    assert KV.get_value(:gnat, \"KEY_PURGE_TEST\", \"foo\") == nil\n    assert :ok = KV.delete_bucket(:gnat, \"KEY_PURGE_TEST\")\n  end\n\n  test \"purge_key/3 returns error\" do\n    assert {:error, :timeout} = KV.purge_key(:gnat, \"KEY_PURGE_TEST\", \"foo\", timeout: 1)\n  end\n\n  test \"put_value/4 updates a key\" do\n    assert {:ok, _} = KV.create_bucket(:gnat, \"KEY_PUT_TEST\")\n    assert :ok = KV.create_key(:gnat, \"KEY_PUT_TEST\", \"foo\", \"bar\")\n    assert :ok = KV.put_value(:gnat, \"KEY_PUT_TEST\", \"foo\", \"baz\")\n    assert \"baz\" = KV.get_value(:gnat, \"KEY_PUT_TEST\", \"foo\")\n    assert :ok = KV.delete_bucket(:gnat, \"KEY_PUT_TEST\")\n  end\n\n  test \"put_value/4 returns error\" do\n    assert {:error, :timeout} = KV.put_value(:gnat, \"KEY_PUT_TEST\", \"foo\", \"baz\", timeout: 1)\n  end\n\n  @tag :message_ttl\n  test \"detects key removed based on limit_marker_ttl\" do\n    assert {:ok, _} =\n             KV.create_bucket(:gnat, \"LIMIT_MARKER_TTL_TEST\",\n               limit_marker_ttl: 1_000_000_000,\n               ttl: 1_000_000_000\n             )\n\n    test_pid = self()\n\n    {:ok, watcher_pid} =\n      KV.watch(:gnat, \"LIMIT_MARKER_TTL_TEST\", fn action, key, value ->\n        send(test_pid, {action, key, value})\n      end)\n\n    KV.put_value(:gnat, \"LIMIT_MARKER_TTL_TEST\", \"foo\", \"bar\")\n    assert_receive({:key_added, \"foo\", \"bar\"})\n\n    assert_receive({:key_deleted, \"foo\", \"\"}, 1500)\n\n    KV.unwatch(watcher_pid)\n    assert :ok = KV.delete_bucket(:gnat, \"LIMIT_MARKER_TTL_TEST\")\n  end\n\n  describe \"watch/3\" do\n    setup do\n      bucket = \"KEY_WATCH_TEST\"\n      {:ok, _} = KV.create_bucket(:gnat, bucket)\n      %{bucket: bucket}\n    end\n\n    test \"detects key added and removed keys\", %{bucket: bucket} do\n      test_pid = self()\n\n      {:ok, watcher_pid} =\n        KV.watch(:gnat, bucket, fn action, key, value ->\n          send(test_pid, {action, key, value})\n        end)\n\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      assert_receive({:key_added, \"foo\", \"bar\"})\n\n      KV.put_value(:gnat, bucket, \"baz\", \"quz\")\n      assert_receive({:key_added, \"baz\", \"quz\"})\n\n      KV.delete_key(:gnat, bucket, \"baz\")\n      # key deletions don't carry the data removed\n      assert_receive({:key_deleted, \"baz\", \"\"})\n\n      KV.put_value(:gnat, bucket, \"foo\", \"buzz\")\n      assert_receive({:key_added, \"foo\", \"buzz\"})\n\n      KV.purge_key(:gnat, bucket, \"foo\")\n      assert_receive({:key_purged, \"foo\", \"\"})\n\n      KV.unwatch(watcher_pid)\n\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n  end\n\n  describe \"contents/2\" do\n    setup do\n      bucket = \"KEY_LIST_TEST\"\n      {:ok, _} = KV.create_bucket(:gnat, bucket)\n      %{bucket: bucket}\n    end\n\n    test \"provides all keys\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"baz\", \"quz\")\n      assert {:ok, %{\"foo\" => \"bar\", \"baz\" => \"quz\"}} == KV.contents(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"deleted keys not included\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"baz\", \"quz\")\n      KV.delete_key(:gnat, bucket, \"baz\")\n      assert {:ok, %{\"foo\" => \"bar\"}} == KV.contents(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"updated keys use most recent\", %{bucket: bucket} do\n      :ok = KV.delete_bucket(:gnat, bucket)\n      {:ok, _} = KV.create_bucket(:gnat, bucket, history: 5)\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"foo\", \"baz\")\n      assert {:ok, %{\"foo\" => \"baz\"}} == KV.contents(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"empty for no keys\", %{bucket: bucket} do\n      assert {:ok, %{}} == KV.contents(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"error tuple if problem\", %{bucket: bucket} do\n      assert {:error, _message} = KV.contents(:gnat, \"NOT_REAL_BUCKET\")\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n  end\n\n  describe \"keys/2\" do\n    setup do\n      bucket = \"KEY_KEYS_TEST\"\n      {:ok, _} = KV.create_bucket(:gnat, bucket)\n      %{bucket: bucket}\n    end\n\n    test \"provides all keys\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"baz\", \"quz\")\n      KV.put_value(:gnat, bucket, \"alpha\", \"beta\")\n      assert {:ok, [\"alpha\", \"baz\", \"foo\"]} == KV.keys(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"deleted keys not included\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"baz\", \"quz\")\n      KV.put_value(:gnat, bucket, \"alpha\", \"beta\")\n      KV.delete_key(:gnat, bucket, \"baz\")\n      assert {:ok, [\"alpha\", \"foo\"]} == KV.keys(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"purged keys not included\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"baz\", \"quz\")\n      KV.purge_key(:gnat, bucket, \"foo\")\n      assert {:ok, [\"baz\"]} == KV.keys(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"updated keys only appear once\", %{bucket: bucket} do\n      :ok = KV.delete_bucket(:gnat, bucket)\n      {:ok, _} = KV.create_bucket(:gnat, bucket, history: 5)\n      KV.put_value(:gnat, bucket, \"foo\", \"bar\")\n      KV.put_value(:gnat, bucket, \"foo\", \"baz\")\n      KV.put_value(:gnat, bucket, \"foo\", \"qux\")\n      assert {:ok, [\"foo\"]} == KV.keys(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"empty list for no keys\", %{bucket: bucket} do\n      assert {:ok, []} == KV.keys(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"keys are sorted alphabetically\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"zebra\", \"value1\")\n      KV.put_value(:gnat, bucket, \"apple\", \"value2\")\n      KV.put_value(:gnat, bucket, \"middle\", \"value3\")\n      assert {:ok, [\"apple\", \"middle\", \"zebra\"]} == KV.keys(:gnat, bucket)\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"error tuple if bucket does not exist\", %{bucket: bucket} do\n      assert {:error, _message} = KV.keys(:gnat, \"NOT_REAL_BUCKET\")\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n\n    test \"handles keys with special characters\", %{bucket: bucket} do\n      KV.put_value(:gnat, bucket, \"key.with.dots\", \"value1\")\n      KV.put_value(:gnat, bucket, \"key-with-dashes\", \"value2\")\n      KV.put_value(:gnat, bucket, \"key_with_underscores\", \"value3\")\n\n      assert {:ok, [\"key-with-dashes\", \"key.with.dots\", \"key_with_underscores\"]} ==\n               KV.keys(:gnat, bucket)\n\n      :ok = KV.delete_bucket(:gnat, bucket)\n    end\n  end\n\n  describe \"list_buckets/2\" do\n    test \"list buckets when none exists\" do\n      assert {:ok, []} = KV.list_buckets(:gnat)\n    end\n\n    test \"list buckets properly\" do\n      assert {:ok, %{config: _config}} = KV.create_bucket(:gnat, \"TEST_BUCKET_1\")\n      assert {:ok, %{config: _config}} = KV.create_bucket(:gnat, \"TEST_BUCKET_2\")\n      assert {:ok, [\"TEST_BUCKET_1\", \"TEST_BUCKET_2\"]} = KV.list_buckets(:gnat)\n      :ok = KV.delete_bucket(:gnat, \"TEST_BUCKET_1\")\n      :ok = KV.delete_bucket(:gnat, \"TEST_BUCKET_2\")\n    end\n\n    test \"ignore streams that are not buckets\" do\n      assert {:ok, %{config: _config}} = KV.create_bucket(:gnat, \"TEST_BUCKET_1\")\n\n      stream = %Stream{\n        name: \"TEST_STREAM_1\",\n        subjects: [\"TEST_STREAM_1.subject1\", \"TEST_STREAM_1.subject2\"]\n      }\n\n      assert {:ok, _response} = Stream.create(:gnat, stream)\n      assert {:ok, [\"TEST_BUCKET_1\"]} = KV.list_buckets(:gnat)\n      :ok = KV.delete_bucket(:gnat, \"TEST_BUCKET_1\")\n    end\n  end\n\n  describe \"info/3\" do\n    test \"returns bucket info\" do\n      assert {:ok, _} = KV.create_bucket(:gnat, \"TEST_BUCKET_1\")\n      assert {:ok, %{config: %{name: \"KV_TEST_BUCKET_1\"}}} = KV.info(:gnat, \"TEST_BUCKET_1\")\n      :ok = KV.delete_bucket(:gnat, \"TEST_BUCKET_1\")\n      assert {:error, %{\"code\" => 404}} = KV.info(:gnat, \"NOT_A_BUCKET\")\n    end\n  end\nend\n"
  },
  {
    "path": "test/jetstream/api/object_test.exs",
    "content": "defmodule Gnat.Jetstream.API.ObjectTest do\n  use Gnat.Jetstream.ConnCase, min_server_version: \"2.6.2\"\n  alias Gnat.Jetstream.API.{Object, Stream}\n  import Gnat.Jetstream.API.Util, only: [nuid: 0]\n\n  @moduletag with_gnat: :gnat\n  @changelog_path Path.join([Path.dirname(__DIR__), \"..\", \"..\", \"CHANGELOG.md\"])\n  @readme_path Path.join([Path.dirname(__DIR__), \"..\", \"..\", \"README.md\"])\n\n  describe \"create_bucket/3\" do\n    test \"create/delete a bucket\" do\n      assert {:ok, %{config: config}} = Object.create_bucket(:gnat, \"MY-STORE\")\n      assert config.name == \"OBJ_MY-STORE\"\n      assert config.max_age == 0\n      assert config.max_bytes == -1\n      assert config.storage == :file\n      assert config.allow_rollup_hdrs == true\n\n      assert config.subjects == [\n               \"$O.MY-STORE.C.>\",\n               \"$O.MY-STORE.M.>\"\n             ]\n\n      assert :ok = Object.delete_bucket(:gnat, \"MY-STORE\")\n    end\n\n    test \"creating a bucket with TTL\" do\n      bucket = nuid()\n      # 10s in nanoseconds\n      ttl = 10 * 1_000_000_000\n      assert {:ok, %{config: config}} = Object.create_bucket(:gnat, bucket, ttl: ttl)\n      assert config.max_age == ttl\n\n      assert :ok = Object.delete_bucket(:gnat, bucket)\n    end\n\n    test \"bucket names are validated\" do\n      assert {:error, \"invalid bucket name\"} = Object.create_bucket(:gnat, \"\")\n      assert {:error, \"invalid bucket name\"} = Object.create_bucket(:gnat, \"MY.STORE\")\n      assert {:error, \"invalid bucket name\"} = Object.create_bucket(:gnat, \"(*!&@($%*&))\")\n    end\n  end\n\n  describe \"delete_bucket/2\" do\n    test \"create/delete a bucket\" do\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, \"MY-STORE\")\n      assert :ok = Object.delete_bucket(:gnat, \"MY-STORE\")\n    end\n  end\n\n  describe \"delete/3\" do\n    test \"delete an object\" do\n      bucket = nuid()\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, bucket)\n      {:ok, _} = put_filepath(@readme_path, bucket, \"README.md\")\n      {:ok, _} = put_filepath(@readme_path, bucket, \"OTHER.md\")\n      assert :ok = Object.delete(:gnat, bucket, \"README.md\")\n\n      assert {:ok, objects} = Object.list(:gnat, bucket)\n      assert Enum.count(objects) == 1\n      assert Enum.map(objects, & &1.name) == [\"OTHER.md\"]\n      assert {:ok, objects} = Object.list(:gnat, bucket, show_deleted: true)\n      assert Enum.count(objects) == 2\n      assert Enum.map(objects, & &1.name) |> Enum.sort() == [\"OTHER.md\", \"README.md\"]\n\n      assert :ok = Object.delete_bucket(:gnat, bucket)\n    end\n  end\n\n  describe \"get/4\" do\n    test \"retrieves and object chunk-by-chunk\" do\n      nuid = nuid()\n      assert {:ok, _} = Object.create_bucket(:gnat, nuid)\n      readme_content = File.read!(@readme_path)\n      assert {:ok, _meta} = put_filepath(@readme_path, nuid, \"README.md\")\n\n      assert :ok =\n               Object.get(:gnat, nuid, \"README.md\", fn chunk ->\n                 assert chunk == readme_content\n                 send(self(), :got_chunk)\n               end)\n\n      assert_received :got_chunk\n\n      :ok = Object.delete_bucket(:gnat, nuid)\n    end\n  end\n\n  describe \"info/3\" do\n    test \"lookup meta information about an object\" do\n      assert {:ok, %{config: _stream}} = Object.create_bucket(:gnat, \"INF\")\n      assert {:ok, io} = File.open(@readme_path, [:read])\n      assert {:ok, initial_meta} = Object.put(:gnat, \"INF\", \"README.md\", io)\n\n      assert {:ok, lookup_meta} = Object.info(:gnat, \"INF\", \"README.md\")\n      assert lookup_meta == initial_meta\n\n      assert :ok = Object.delete_bucket(:gnat, \"INF\")\n    end\n  end\n\n  describe \"list/3\" do\n    test \"list an empty bucket\" do\n      bucket = nuid()\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, bucket)\n      assert {:ok, []} = Object.list(:gnat, bucket)\n      assert :ok = Object.delete_bucket(:gnat, bucket)\n    end\n\n    test \"list a bucket with two files\" do\n      bucket = nuid()\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, bucket)\n      assert {:ok, io} = File.open(@readme_path, [:read])\n      assert {:ok, _object} = Object.put(:gnat, bucket, \"README.md\", io)\n      assert {:ok, io} = File.open(@readme_path, [:read])\n      assert {:ok, _object} = Object.put(:gnat, bucket, \"SOMETHING.md\", io)\n\n      assert {:ok, objects} = Object.list(:gnat, bucket)\n      [readme, something] = Enum.sort_by(objects, & &1.name)\n      assert readme.name == \"README.md\"\n      assert readme.size == something.size\n      assert readme.digest == something.digest\n\n      assert :ok = Object.delete_bucket(:gnat, bucket)\n    end\n  end\n\n  describe \"put/4\" do\n    test \"creates an object\" do\n      assert {:ok, %{config: _stream}} = Object.create_bucket(:gnat, \"MY-STORE\")\n\n      expected_sha = @readme_path |> File.read!() |> then(&:crypto.hash(:sha256, &1))\n      assert {:ok, object_meta} = put_filepath(@readme_path, \"MY-STORE\", \"README.md\")\n      assert object_meta.name == \"README.md\"\n      assert object_meta.bucket == \"MY-STORE\"\n      assert object_meta.chunks == 1\n      assert \"SHA-256=\" <> encoded = object_meta.digest\n      assert Base.url_decode64!(encoded) == expected_sha\n\n      assert :ok = Object.delete_bucket(:gnat, \"MY-STORE\")\n    end\n\n    test \"overwriting a file\" do\n      bucket = nuid()\n      assert {:ok, %{config: _stream}} = Object.create_bucket(:gnat, bucket)\n      assert {:ok, _} = put_filepath(@readme_path, bucket, \"WAT\")\n      size_after_readme = stream_byte_size(bucket)\n      assert {:ok, _} = put_filepath(@changelog_path, bucket, \"WAT\")\n      size_after_changelog = stream_byte_size(bucket)\n      assert size_after_changelog < size_after_readme\n      assert {:ok, [meta]} = Object.list(:gnat, bucket)\n      assert meta.name == \"WAT\"\n\n      assert :ok = Object.delete_bucket(:gnat, bucket)\n    end\n\n    test \"return an error if the object store doesn't exist\" do\n      assert {:error, err} = put_filepath(@readme_path, \"I_DONT_EXIST\", \"foo\")\n      assert %{\"code\" => 404, \"description\" => \"stream not found\"} = err\n    end\n  end\n\n  @tag :tmp_dir\n  test \"storing and retrieving larger files\", %{tmp_dir: tmp_dir} do\n    assert {:ok, path, sha} = generate_big_file(tmp_dir)\n    bucket = nuid()\n    assert {:ok, %{config: _stream}} = Object.create_bucket(:gnat, bucket)\n    assert {:ok, meta} = put_filepath(path, bucket, \"big\")\n    assert meta.chunks == 800\n    assert meta.size == 800 * 128 * 1024\n    assert \"SHA-256=\" <> encoded = meta.digest\n    assert Base.url_decode64!(encoded) == sha\n\n    Process.put(:buffer, \"\")\n\n    Object.get(:gnat, bucket, \"big\", fn chunk ->\n      Process.put(:buffer, Process.get(:buffer) <> chunk)\n    end)\n\n    file_contents = Process.get(:buffer)\n    assert byte_size(file_contents) == meta.size\n    assert :crypto.hash(:sha256, file_contents) == sha\n    assert stream_byte_size(bucket) > 1024 * 1024\n\n    assert :ok = Object.delete(:gnat, bucket, \"big\")\n    assert stream_byte_size(bucket) < 1024\n    :ok = Object.delete_bucket(:gnat, bucket)\n  end\n\n  @tag :tmp_dir\n  test \"control messages don't affect chunk count\", %{tmp_dir: tmp_dir} do\n    assert {:ok, path, _sha} = generate_big_file(tmp_dir)\n    bucket = nuid()\n    assert {:ok, %{config: _stream}} = Object.create_bucket(:gnat, bucket)\n    assert {:ok, meta} = put_filepath(path, bucket, \"test_chunks\")\n\n    Process.put(:chunk_count, 0)\n\n    :ok =\n      Object.get(:gnat, bucket, \"test_chunks\", fn _chunk ->\n        Process.put(:chunk_count, Process.get(:chunk_count) + 1)\n      end)\n\n    chunk_count = Process.get(:chunk_count)\n\n    assert chunk_count == meta.chunks\n\n    :ok = Object.delete_bucket(:gnat, bucket)\n  end\n\n  describe \"list_buckets/2\" do\n    test \"list buckets when none exists\" do\n      assert {:ok, []} = Object.list_buckets(:gnat)\n    end\n\n    test \"list buckets properly\" do\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, \"TEST_BUCKET_1\")\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, \"TEST_BUCKET_2\")\n      assert {:ok, [\"TEST_BUCKET_1\", \"TEST_BUCKET_2\"]} = Object.list_buckets(:gnat)\n      :ok = Object.delete_bucket(:gnat, \"TEST_BUCKET_1\")\n      :ok = Object.delete_bucket(:gnat, \"TEST_BUCKET_2\")\n    end\n\n    test \"ignore streams that are not buckets\" do\n      assert {:ok, %{config: _config}} = Object.create_bucket(:gnat, \"TEST_BUCKET_1\")\n\n      stream = %Stream{\n        name: \"TEST_STREAM_1\",\n        subjects: [\"TEST_STREAM_1.subject1\", \"TEST_STREAM_1.subject2\"]\n      }\n\n      assert {:ok, _response} = Stream.create(:gnat, stream)\n      assert {:ok, [\"TEST_BUCKET_1\"]} = Object.list_buckets(:gnat)\n      :ok = Object.delete_bucket(:gnat, \"TEST_BUCKET_1\")\n    end\n  end\n\n  # create a random 100MB binary file\n  # re-use it on subsequent test runs if it already exists\n  defp generate_big_file(tmp_dir) do\n    filepath = Path.join(tmp_dir, \"big_file.bin\")\n    sha = :crypto.hash_init(:sha256)\n    {:ok, fh} = File.open(filepath, [:write])\n\n    sha =\n      Enum.reduce(1..800, sha, fn _, digest ->\n        rand_chunk = :crypto.strong_rand_bytes(128) |> String.duplicate(1024)\n        :ok = IO.binwrite(fh, rand_chunk)\n        :crypto.hash_update(digest, rand_chunk)\n      end)\n\n    :ok = File.close(fh)\n    {:ok, filepath, :crypto.hash_final(sha)}\n  end\n\n  defp put_filepath(path, bucket, name) do\n    {:ok, io} = File.open(path, [:read])\n    Object.put(:gnat, bucket, name, io)\n  end\n\n  defp stream_byte_size(bucket) do\n    {:ok, %{state: state}} = Stream.info(:gnat, \"OBJ_#{bucket}\")\n    state.bytes\n  end\nend\n"
  },
  {
    "path": "test/jetstream/api/stream_doc_test.exs",
    "content": "defmodule Gnat.Jetstream.API.StreamDocTest do\n  use Gnat.Jetstream.ConnCase\n  @moduletag with_gnat: :gnat\n  doctest Gnat.Jetstream.API.Stream\nend\n"
  },
  {
    "path": "test/jetstream/api/stream_test.exs",
    "content": "defmodule Gnat.Jetstream.API.StreamTest do\n  use Gnat.Jetstream.ConnCase\n  alias Gnat.Jetstream.API.Stream\n\n  @moduletag with_gnat: :gnat\n\n  test \"listing and creating, and deleting streams\" do\n    {:ok, %{streams: streams}} = Stream.list(:gnat)\n    assert streams == nil || !(\"LIST_TEST\" in streams)\n\n    stream = %Stream{name: \"LIST_TEST\", subjects: [\"STREAM_TEST\"]}\n    {:ok, response} = Stream.create(:gnat, stream)\n    assert response.config == stream\n\n    assert response.state == %{\n             bytes: 0,\n             consumer_count: 0,\n             first_seq: 0,\n             first_ts: ~U[0001-01-01 00:00:00Z],\n             last_seq: 0,\n             last_ts: ~U[0001-01-01 00:00:00Z],\n             messages: 0,\n             deleted: nil,\n             lost: nil,\n             num_deleted: nil,\n             num_subjects: nil,\n             subjects: nil\n           }\n\n    {:ok, %{streams: streams}} = Stream.list(:gnat)\n    assert \"LIST_TEST\" in streams\n\n    assert :ok = Stream.delete(:gnat, \"LIST_TEST\")\n    {:ok, %{streams: streams}} = Stream.list(:gnat)\n    assert streams == nil || !(\"LIST_TEST\" in streams)\n  end\n\n  test \"list/2 includes multiple streams\" do\n    stream = %Stream{name: \"LIST_SUBJECT_TEST_ONE\", subjects: [\"LIST_SUBJECT_TEST.subject1\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n    stream = %Stream{name: \"LIST_SUBJECT_TEST_TWO\", subjects: [\"LIST_SUBJECT_TEST.subject2\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n\n    {:ok, %{streams: streams}} = Stream.list(:gnat)\n    assert \"LIST_SUBJECT_TEST_ONE\" in streams\n    assert \"LIST_SUBJECT_TEST_TWO\" in streams\n    assert :ok = Stream.delete(:gnat, \"LIST_SUBJECT_TEST_ONE\")\n    assert :ok = Stream.delete(:gnat, \"LIST_SUBJECT_TEST_TWO\")\n  end\n\n  test \"list/2 can filter by subject\" do\n    stream = %Stream{name: \"LIST_SUBJECT_TEST_ONE\", subjects: [\"LIST_SUBJECT_TEST.subject1\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n    stream = %Stream{name: \"LIST_SUBJECT_TEST_TWO\", subjects: [\"LIST_SUBJECT_TEST.subject2\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n\n    {:ok, %{streams: [stream]}} = Stream.list(:gnat, subject: \"LIST_SUBJECT_TEST.subject2\")\n    assert stream == \"LIST_SUBJECT_TEST_TWO\"\n    assert :ok = Stream.delete(:gnat, \"LIST_SUBJECT_TEST_ONE\")\n    assert :ok = Stream.delete(:gnat, \"LIST_SUBJECT_TEST_TWO\")\n  end\n\n  test \"list/2 includes accepts offset\" do\n    stream = %Stream{name: \"LIST_OFFSET_TEST_ONE\", subjects: [\"LIST_OFFSET_TEST.subject1\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n    stream = %Stream{name: \"LIST_OFFSET_TEST_TWO\", subjects: [\"LIST_OFFSET_TEST.subject2\"]}\n    {:ok, _response} = Stream.create(:gnat, stream)\n\n    {:ok, %{streams: streams}} = Stream.list(:gnat)\n    num_no_offset = Enum.count(streams)\n\n    {:ok, %{streams: streams}} = Stream.list(:gnat, offset: 1)\n    num_with_offset = Enum.count(streams)\n\n    # Checking offset functionality without a strict pattern match to keep this\n    # test passing even if another test forgets to delete a stream after it's done\n    assert num_no_offset - num_with_offset == 1\n    assert :ok = Stream.delete(:gnat, \"LIST_OFFSET_TEST_ONE\")\n    assert :ok = Stream.delete(:gnat, \"LIST_OFFSET_TEST_TWO\")\n  end\n\n  test \"create stream with discard_new_per_subject: true\" do\n    stream = %Stream{\n      name: \"DISCARD_NEW_PER_SUBJECT_TEST\",\n      subjects: [\"STREAM_TEST\"],\n      max_msgs_per_subject: 1,\n      discard_new_per_subject: true,\n      discard: :new\n    }\n\n    assert {:ok, _response} = Stream.create(:gnat, stream)\n\n    assert {:ok, _} = Gnat.request(:gnat, \"STREAM_TEST\", \"first message\")\n\n    assert {:ok, response} =\n             Stream.get_message(:gnat, \"DISCARD_NEW_PER_SUBJECT_TEST\", %{\n               last_by_subj: \"STREAM_TEST\"\n             })\n\n    %{\n      data: \"first message\",\n      hdrs: nil,\n      subject: \"STREAM_TEST\",\n      time: %DateTime{}\n    } = response\n\n    assert {:ok, _} = Gnat.request(:gnat, \"STREAM_TEST\", \"second message\")\n\n    assert {:ok, response} =\n             Stream.get_message(:gnat, \"DISCARD_NEW_PER_SUBJECT_TEST\", %{\n               last_by_subj: \"STREAM_TEST\"\n             })\n\n    %{\n      data: \"first message\",\n      hdrs: nil,\n      subject: \"STREAM_TEST\",\n      time: %DateTime{}\n    } = response\n\n    Stream.purge(:gnat, \"DISCARD_NEW_PER_SUBJECT_TEST\")\n\n    assert {:ok, _} = Gnat.request(:gnat, \"STREAM_TEST\", \"second message\")\n\n    assert {:ok, response} =\n             Stream.get_message(:gnat, \"DISCARD_NEW_PER_SUBJECT_TEST\", %{\n               last_by_subj: \"STREAM_TEST\"\n             })\n\n    %{\n      data: \"second message\",\n      hdrs: nil,\n      subject: \"STREAM_TEST\",\n      time: %DateTime{}\n    } = response\n\n    assert :ok = Stream.delete(:gnat, \"DISCARD_NEW_PER_SUBJECT_TEST\")\n  end\n\n  @tag :message_ttl\n  test \"create a stream with messages TTL\" do\n    stream = %Stream{\n      name: \"STREAM-ALLOW_MSG-TTL\",\n      subjects: [\"STREAM_TTL_TEST\"],\n      allow_msg_ttl: true\n    }\n\n    assert {:ok, _response} = Stream.create(:gnat, stream)\n\n    :ok =\n      Gnat.pub(:gnat, \"STREAM_TTL_TEST\", \"message-should-be-dropped-by-ttl\",\n        headers: [{\"Nats-TTL\", \"1\"}]\n      )\n\n    {:ok, %{data: \"message-should-be-dropped-by-ttl\"}} =\n      Stream.get_message(:gnat, \"STREAM-ALLOW_MSG-TTL\", %{last_by_subj: \"STREAM_TTL_TEST\"})\n\n    :timer.sleep(1500)\n\n    {:error, %{\"code\" => 404}} =\n      Stream.get_message(:gnat, \"STREAM-ALLOW_MSG-TTL\", %{last_by_subj: \"STREAM_TTL_TEST\"})\n\n    assert :ok = Stream.delete(:gnat, \"STREAM-ALLOW_MSG-TTL\")\n  end\n\n  @tag :message_ttl\n  test \"create a stream with subject delete marker TTL\" do\n    stream = %Stream{\n      name: \"STREAM-SUBJECT-DELETE-MARKER-TTL\",\n      subjects: [\"STREAM_TTL_TEST\"],\n      max_age: 1_000_000_000,\n      duplicate_window: 1_000_000_000,\n      subject_delete_marker_ttl: 1_000_000_000\n    }\n\n    assert {:ok, _response} = Stream.create(:gnat, stream)\n\n    :ok = Gnat.pub(:gnat, \"STREAM_TTL_TEST\", \"message-should-be-dropped-by-ttl\")\n\n    {:ok, %{data: \"message-should-be-dropped-by-ttl\"}} =\n      Stream.get_message(:gnat, \"STREAM-SUBJECT-DELETE-MARKER-TTL\", %{\n        last_by_subj: \"STREAM_TTL_TEST\"\n      })\n\n    :timer.sleep(1500)\n\n    {:ok, %{data: nil, hdrs: headers}} =\n      Stream.get_message(:gnat, \"STREAM-SUBJECT-DELETE-MARKER-TTL\", %{\n        last_by_subj: \"STREAM_TTL_TEST\"\n      })\n\n    assert true === String.contains?(headers, \"Nats-Marker-Reason: MaxAge\")\n\n    assert :ok = Stream.delete(:gnat, \"STREAM-SUBJECT-DELETE-MARKER-TTL\")\n  end\n\n  test \"updating a stream\" do\n    stream = %Stream{name: \"UPDATE_TEST\", subjects: [\"STREAM_TEST\"]}\n    assert {:ok, _response} = Stream.create(:gnat, stream)\n    updated_stream = %Stream{name: \"UPDATE_TEST\", subjects: [\"STREAM_TEST\", \"NEW_SUBJECT\"]}\n    assert {:ok, response} = Stream.update(:gnat, updated_stream)\n    assert response.config.subjects == [\"STREAM_TEST\", \"NEW_SUBJECT\"]\n    assert :ok = Stream.delete(:gnat, \"UPDATE_TEST\")\n  end\n\n  test \"failed deletes\" do\n    assert {:error, %{\"code\" => 404, \"description\" => \"stream not found\"}} =\n             Stream.delete(:gnat, \"NaN\")\n  end\n\n  test \"getting stream info\" do\n    stream = %Stream{name: \"INFO_TEST\", subjects: [\"INFO_TEST.*\"]}\n    assert {:ok, _response} = Stream.create(:gnat, stream)\n\n    assert {:ok, response} = Stream.info(:gnat, \"INFO_TEST\")\n    assert response.config == stream\n\n    assert response.state == %{\n             bytes: 0,\n             consumer_count: 0,\n             first_seq: 0,\n             first_ts: ~U[0001-01-01 00:00:00Z],\n             last_seq: 0,\n             last_ts: ~U[0001-01-01 00:00:00Z],\n             messages: 0,\n             deleted: nil,\n             lost: nil,\n             num_deleted: nil,\n             num_subjects: nil,\n             subjects: nil\n           }\n\n    assert :ok = Stream.delete(:gnat, \"INFO_TEST\")\n  end\n\n  test \"creating a stream with non-standard settings\" do\n    stream = %Stream{\n      name: \"ARGS_TEST\",\n      subjects: [\"ARGS_TEST.*\"],\n      retention: :workqueue,\n      duplicate_window: 100_000_000,\n      storage: :memory,\n      compression: \"s2\"\n    }\n\n    assert {:ok, %{config: result}} = Stream.create(:gnat, stream)\n    assert result.name == \"ARGS_TEST\"\n    assert result.duplicate_window == 100_000_000\n    assert result.retention == :workqueue\n    assert result.storage == :memory\n    assert result.compression == \"s2\"\n    assert :ok = Stream.delete(:gnat, \"ARGS_TEST\")\n  end\n\n  test \"validating stream names\" do\n    assert {:error, reason} =\n             Stream.create(:gnat, %Stream{name: \"test.periods\", subjects: [\"foo\"]})\n\n    assert reason == \"invalid name: cannot contain '.', '>', '*', spaces or tabs\"\n\n    assert {:error, reason} =\n             Stream.create(:gnat, %Stream{name: \"test>greater\", subjects: [\"foo\"]})\n\n    assert reason == \"invalid name: cannot contain '.', '>', '*', spaces or tabs\"\n\n    assert {:error, reason} = Stream.create(:gnat, %Stream{name: \"test_star*\", subjects: [\"foo\"]})\n    assert reason == \"invalid name: cannot contain '.', '>', '*', spaces or tabs\"\n\n    assert {:error, reason} =\n             Stream.create(:gnat, %Stream{name: \"test-space \", subjects: [\"foo\"]})\n\n    assert reason == \"invalid name: cannot contain '.', '>', '*', spaces or tabs\"\n\n    assert {:error, reason} = Stream.create(:gnat, %Stream{name: \"\\ttest-tab\", subjects: [\"foo\"]})\n    assert reason == \"invalid name: cannot contain '.', '>', '*', spaces or tabs\"\n  end\n\n  describe \"get_message/3\" do\n    test \"error if both seq and last_by_subj are used\" do\n      assert {:error, reason} = Stream.get_message(:gnat, \"foo\", %{seq: 1, last_by_subj: \"bar\"})\n      assert reason == \"To get a message you must use only one of `seq` or `last_by_subj`\"\n    end\n\n    test \"decodes message data\" do\n      stream = %Stream{name: \"GET_MESSAGE_TEST\", subjects: [\"GET_MESSAGE_TEST.foo\"]}\n      assert {:ok, _response} = Stream.create(:gnat, stream)\n      assert {:ok, _} = Gnat.request(:gnat, \"GET_MESSAGE_TEST.foo\", \"hi there\")\n\n      assert {:ok, response} =\n               Stream.get_message(:gnat, \"GET_MESSAGE_TEST\", %{\n                 last_by_subj: \"GET_MESSAGE_TEST.foo\"\n               })\n\n      %{\n        data: \"hi there\",\n        hdrs: nil,\n        subject: \"GET_MESSAGE_TEST.foo\",\n        time: %DateTime{}\n      } = response\n\n      assert is_number(response.seq)\n\n      assert :ok = Stream.delete(:gnat, \"GET_MESSAGE_TEST\")\n    end\n\n    test \"decodes message data with headers\" do\n      stream = %Stream{\n        name: \"GET_MESSAGE_TEST_WITH_HEADERS\",\n        subjects: [\"GET_MESSAGE_TEST_WITH_HEADERS.bar\"]\n      }\n\n      assert {:ok, _response} = Stream.create(:gnat, stream)\n\n      assert {:ok, %{body: _body}} =\n               Gnat.request(:gnat, \"GET_MESSAGE_TEST_WITH_HEADERS.bar\", \"hi there\",\n                 headers: [{\"foo\", \"bar\"}]\n               )\n\n      assert {:ok, response} =\n               Stream.get_message(:gnat, \"GET_MESSAGE_TEST_WITH_HEADERS\", %{\n                 last_by_subj: \"GET_MESSAGE_TEST_WITH_HEADERS.bar\"\n               })\n\n      assert response.hdrs =~ \"foo: bar\"\n\n      assert :ok = Stream.delete(:gnat, \"GET_MESSAGE_TEST_WITH_HEADERS\")\n    end\n  end\n\n  describe \"purge/2\" do\n    test \"clears the stream\" do\n      stream = %Stream{name: \"PURGE_TEST\", subjects: [\"PURGE_TEST.foo\"]}\n      assert {:ok, _response} = Stream.create(:gnat, stream)\n      assert {:ok, _} = Gnat.request(:gnat, \"PURGE_TEST.foo\", \"hi there\")\n\n      assert :ok = Stream.purge(:gnat, \"PURGE_TEST\")\n\n      assert {:error, %{\"description\" => description}} =\n               Stream.get_message(:gnat, \"PURGE_TEST\", %{\n                 last_by_subj: \"PURGE_TEST.foo\"\n               })\n\n      assert description in [\"no message found\", \"stream store EOF\"]\n\n      assert :ok = Stream.delete(:gnat, \"PURGE_TEST\")\n    end\n  end\nend\n"
  },
  {
    "path": "test/jetstream/message_test.exs",
    "content": "defmodule Gnat.Jetstream.MessageTest do\n  use ExUnit.Case, async: true\n\n  test \"message metadata without domain\" do\n    assert {:ok,\n            %Gnat.Jetstream.API.Message.Metadata{\n              stream_seq: 1_428_472,\n              consumer_seq: 20,\n              num_pending: 3283,\n              num_delivered: 4,\n              timestamp: ~U[2025-06-26 09:09:08.439739Z],\n              stream: \"STREAM\",\n              consumer: \"consumer\",\n              domain: nil\n            }} =\n             Gnat.Jetstream.API.Message.metadata(%{\n               reply_to: \"$JS.ACK.STREAM.consumer.4.1428472.20.1750928948439739269.3283\"\n             })\n  end\n\n  test \"message metadata with domain\" do\n    assert {:ok,\n            %Gnat.Jetstream.API.Message.Metadata{\n              stream_seq: 1_428_472,\n              consumer_seq: 20,\n              num_pending: 3283,\n              num_delivered: 4,\n              timestamp: ~U[2025-06-26 09:09:08.439739Z],\n              stream: \"STREAM\",\n              consumer: \"consumer\",\n              domain: \"test\"\n            }} =\n             Gnat.Jetstream.API.Message.metadata(%{\n               reply_to:\n                 \"$JS.ACK.test.$G.STREAM.consumer.4.1428472.20.1750928948439739269.3283.279619330\"\n             })\n  end\nend\n"
  },
  {
    "path": "test/jetstream/pager_test.exs",
    "content": "defmodule Gnat.Jetstream.PagerTest do\n  use Gnat.Jetstream.ConnCase\n  alias Gnat.Jetstream.Pager\n  alias Gnat.Jetstream.API.Stream\n\n  @moduletag with_gnat: :gnat\n\n  test \"paging over an entire stream vs from a sequence\" do\n    {:ok, _stream} = create_stream(\"pager_a\")\n\n    Enum.each(1..100, fn i ->\n      :ok = Gnat.pub(:gnat, \"input.pager_a\", \"#{i}\")\n    end)\n\n    {:ok, res} =\n      Pager.reduce(:gnat, \"pager_a\", [], 0, fn msg, total ->\n        total + String.to_integer(msg.body)\n      end)\n\n    assert res == 5050\n\n    {:ok, res} =\n      Pager.reduce(:gnat, \"pager_a\", [from_seq: 51], 0, fn msg, total ->\n        total + String.to_integer(msg.body)\n      end)\n\n    assert res == 3775\n\n    Stream.delete(:gnat, \"pager_a\")\n  end\n\n  test \"paging from a datetime\" do\n    {:ok, _stream} = create_stream(\"pager_b\")\n\n    Enum.each(1..50, fn i ->\n      :ok = Gnat.pub(:gnat, \"input.pager_b\", \"#{i}\")\n    end)\n\n    timestamp = DateTime.utc_now()\n\n    Enum.each(51..100, fn i ->\n      :ok = Gnat.pub(:gnat, \"input.pager_b\", \"#{i}\")\n    end)\n\n    {:ok, res} =\n      Pager.reduce(:gnat, \"pager_b\", [from_datetime: timestamp], 0, fn msg, total ->\n        total + String.to_integer(msg.body)\n      end)\n\n    # The datetime isn't exactly synced between nats and the client\n    # so we use a pretty fuzzy check here. This test is mostly to\n    # provide coverage for accepting the option\n    assert res <= 5050\n    assert res >= 3775\n  end\n\n  defp create_stream(name) do\n    stream = %Stream{\n      name: name,\n      subjects: [\"input.#{name}\"]\n    }\n\n    Stream.create(:gnat, stream)\n  end\nend\n"
  },
  {
    "path": "test/pull_consumer/batch_test.exs",
    "content": "defmodule Gnat.Jetstream.PullConsumer.BatchTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Consumer, Stream}\n\n  @stream_name \"BATCH_TEST_STREAM\"\n  @subject \"batch_test.*\"\n\n  defmodule BatchPullConsumer do\n    use Gnat.Jetstream.PullConsumer\n\n    def start_link(opts) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n    end\n\n    @impl true\n    def init(opts) do\n      batch_size = Keyword.fetch!(opts, :batch_size)\n\n      connection_opts =\n        [connection_name: :gnat, batch_size: batch_size, request_expires: 500_000_000]\n        |> maybe_put(:consumer, opts)\n        |> maybe_put(:stream_name, opts)\n        |> maybe_put(:consumer_name, opts)\n        |> maybe_put(:connection_retry_timeout, opts)\n        |> maybe_put(:connection_retries, opts)\n\n      state = %{\n        test_pid: Keyword.fetch!(opts, :test_pid),\n        messages: [],\n        call_count: 0\n      }\n\n      {:ok, state, connection_opts}\n    end\n\n    defp maybe_put(conn_opts, key, opts) do\n      case Keyword.fetch(opts, key) do\n        {:ok, value} -> Keyword.put(conn_opts, key, value)\n        :error -> conn_opts\n      end\n    end\n\n    @impl true\n    def handle_connected(consumer_info, state) do\n      send(state.test_pid, {:connected, consumer_info})\n      {:ok, state}\n    end\n\n    @impl true\n    def handle_message(message, state) do\n      call_count = state.call_count + 1\n      messages = [message.body | state.messages]\n      send(state.test_pid, {:handled, call_count, message.body})\n      {:ack, %{state | messages: messages, call_count: call_count}}\n    end\n  end\n\n  # A consumer whose handle_message returns :nack or :term based on message body\n  defmodule NonAckBatchConsumer do\n    use Gnat.Jetstream.PullConsumer\n\n    def start_link(opts) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n    end\n\n    @impl true\n    def init(opts) do\n      consumer = Keyword.fetch!(opts, :consumer)\n      batch_size = Keyword.fetch!(opts, :batch_size)\n\n      connection_opts = [\n        connection_name: :gnat,\n        consumer: consumer,\n        batch_size: batch_size\n      ]\n\n      state = %{test_pid: Keyword.fetch!(opts, :test_pid)}\n      {:ok, state, connection_opts}\n    end\n\n    @impl true\n    def handle_message(%{body: \"nack_me\"}, state) do\n      send(state.test_pid, {:returned_nack})\n      {:nack, state}\n    end\n\n    def handle_message(%{body: \"term_me\"}, state) do\n      send(state.test_pid, {:returned_term})\n      {:term, state}\n    end\n\n    def handle_message(message, state) do\n      send(state.test_pid, {:handled, message.body})\n      {:ack, state}\n    end\n  end\n\n  describe \"batch mode\" do\n    @describetag with_gnat: :gnat\n\n    setup do\n      stream = %Stream{name: @stream_name, subjects: [@subject]}\n      {:ok, _} = Stream.create(:gnat, stream)\n\n      on_exit(fn ->\n        {:ok, pid} = Gnat.start_link()\n        Stream.delete(pid, @stream_name)\n        Gnat.stop(pid)\n      end)\n\n      :ok\n    end\n\n    test \"processes a full batch of messages\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      # Publish exactly batch_size messages before starting consumer\n      for i <- 1..3 do\n        :ok = Gnat.pub(:gnat, \"batch_test.full\", \"msg-#{i}\")\n      end\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 3, test_pid: self()})\n\n      assert_receive {:connected, _consumer_info}\n\n      # All 3 messages should be delivered individually to handle_message\n      assert_receive {:handled, 1, \"msg-1\"}\n      assert_receive {:handled, 2, \"msg-2\"}\n      assert_receive {:handled, 3, \"msg-3\"}\n    end\n\n    test \"processes partial batch when fewer messages than batch_size\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      # Only 2 messages with batch_size of 5\n      :ok = Gnat.pub(:gnat, \"batch_test.partial\", \"partial-1\")\n      :ok = Gnat.pub(:gnat, \"batch_test.partial\", \"partial-2\")\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 5, test_pid: self()})\n\n      assert_receive {:connected, _}\n\n      # Partial batch should still be processed when terminal signal arrives\n      assert_receive {:handled, 1, \"partial-1\"}, 3_000\n      assert_receive {:handled, 2, \"partial-2\"}, 3_000\n    end\n\n    test \"empty stream does not hang the consumer\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      pid =\n        start_supervised!(\n          {BatchPullConsumer, consumer: consumer, batch_size: 5, test_pid: self()}\n        )\n\n      assert_receive {:connected, consumer_info}\n      assert consumer_info.num_pending == 0\n\n      # Consumer should not hang — it should be alive and responsive\n      assert Process.alive?(pid)\n\n      # Now publish a message — consumer should still pick it up\n      :ok = Gnat.pub(:gnat, \"batch_test.empty\", \"late-arrival\")\n\n      # In batch mode with batch_size 5, a single message will arrive as a\n      # partial batch (terminal signal triggers processing of the 1-message buffer)\n      assert_receive {:handled, 1, \"late-arrival\"}, 10_000\n    end\n\n    test \"continues processing after multiple batches\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      # Publish 6 messages with batch_size 3 — should produce 2 full batches\n      for i <- 1..6 do\n        :ok = Gnat.pub(:gnat, \"batch_test.multi\", \"multi-#{i}\")\n      end\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 3, test_pid: self()})\n\n      assert_receive {:connected, _}\n\n      # All 6 messages processed across 2 batches\n      for i <- 1..6 do\n        assert_receive {:handled, ^i, _body}, 5_000\n      end\n    end\n\n    test \"handles messages arriving after initial catch-up\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      # Start with some messages to catch up on\n      for i <- 1..3 do\n        :ok = Gnat.pub(:gnat, \"batch_test.live\", \"catchup-#{i}\")\n      end\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 3, test_pid: self()})\n\n      assert_receive {:connected, _}\n\n      # Wait for catch-up batch\n      for i <- 1..3 do\n        assert_receive {:handled, ^i, _body}, 5_000\n      end\n\n      # Now publish new messages — consumer should transition to tailing mode\n      # and still pick these up\n      :ok = Gnat.pub(:gnat, \"batch_test.live\", \"live-1\")\n      :ok = Gnat.pub(:gnat, \"batch_test.live\", \"live-2\")\n\n      assert_receive {:handled, 4, \"live-1\"}, 10_000\n      assert_receive {:handled, 5, \"live-2\"}, 10_000\n    end\n\n    test \"nack and term returns are treated as ack with warning logged\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      :ok = Gnat.pub(:gnat, \"batch_test.nonack\", \"normal\")\n      :ok = Gnat.pub(:gnat, \"batch_test.nonack\", \"nack_me\")\n      :ok = Gnat.pub(:gnat, \"batch_test.nonack\", \"term_me\")\n\n      import ExUnit.CaptureLog\n\n      log =\n        capture_log(fn ->\n          start_supervised!(\n            {NonAckBatchConsumer, consumer: consumer, batch_size: 3, test_pid: self()}\n          )\n\n          assert_receive {:handled, \"normal\"}, 5_000\n          assert_receive {:returned_nack}, 5_000\n          assert_receive {:returned_term}, 5_000\n\n          # Give time for the log to flush\n          Process.sleep(100)\n        end)\n\n      # The warning should mention that batch mode doesn't support nack/term\n      assert log =~ \"batch mode does not support\"\n    end\n\n    test \"consumer does not get stuck after processing batch\" do\n      # This test verifies the critical flow: after a batch is processed and acked,\n      # the consumer must issue another fetch request. If it doesn't, messages\n      # published after the batch will never arrive.\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 2, test_pid: self()})\n\n      assert_receive {:connected, _}\n\n      # First batch\n      :ok = Gnat.pub(:gnat, \"batch_test.stuck\", \"a\")\n      :ok = Gnat.pub(:gnat, \"batch_test.stuck\", \"b\")\n\n      assert_receive {:handled, 1, \"a\"}, 10_000\n      assert_receive {:handled, 2, \"b\"}, 10_000\n\n      # Wait a moment, then send another batch — consumer must not be stuck\n      Process.sleep(200)\n\n      :ok = Gnat.pub(:gnat, \"batch_test.stuck\", \"c\")\n      :ok = Gnat.pub(:gnat, \"batch_test.stuck\", \"d\")\n\n      assert_receive {:handled, 3, \"c\"}, 10_000\n      assert_receive {:handled, 4, \"d\"}, 10_000\n\n      # Third batch to confirm sustained flow\n      Process.sleep(200)\n\n      :ok = Gnat.pub(:gnat, \"batch_test.stuck\", \"e\")\n\n      assert_receive {:handled, 5, \"e\"}, 10_000\n    end\n\n    test \"batch_size 1 uses single-message mode (sends +NXT on ack)\" do\n      consumer_name = \"BATCH_COMPAT_CONSUMER\"\n\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        durable_name: consumer_name,\n        inactive_threshold: 30_000_000_000,\n        ack_policy: :explicit\n      }\n\n      # Subscribe to the ack subject to verify +NXT is sent (single-message pipeline)\n      # rather than an empty body (batch-mode ack).\n      {:ok, _} = Gnat.sub(:gnat, self(), \"$JS.ACK.#{@stream_name}.#{consumer_name}.>\")\n\n      :ok = Gnat.pub(:gnat, \"batch_test.compat\", \"compat-1\")\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 1, test_pid: self()})\n\n      assert_receive {:handled, 1, \"compat-1\"}, 5_000\n      assert_receive {:msg, %{body: \"+NXT\"}}, 5_000\n    end\n\n    test \"batch mode with batch_size 2 and odd number of messages\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      # 5 messages with batch_size 2: batches of [2, 2, 1(partial)]\n      for i <- 1..5 do\n        :ok = Gnat.pub(:gnat, \"batch_test.odd\", \"odd-#{i}\")\n      end\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 2, test_pid: self()})\n\n      assert_receive {:connected, _}\n\n      for i <- 1..5 do\n        assert_receive {:handled, ^i, _body}, 5_000\n      end\n    end\n\n    test \"large batch processes many messages correctly\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      msg_count = 50\n\n      for i <- 1..msg_count do\n        :ok = Gnat.pub(:gnat, \"batch_test.large\", \"large-#{i}\")\n      end\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 10, test_pid: self()})\n\n      assert_receive {:connected, _}\n\n      # All 50 messages should be delivered (5 batches of 10)\n      for i <- 1..msg_count do\n        assert_receive {:handled, ^i, _body}, 10_000\n      end\n    end\n\n    test \"can be closed cleanly during batch mode\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      pid =\n        start_supervised!(\n          {BatchPullConsumer, consumer: consumer, batch_size: 5, test_pid: self()}\n        )\n\n      assert_receive {:connected, _}\n\n      ref = Process.monitor(pid)\n      assert :ok = Gnat.Jetstream.PullConsumer.close(pid)\n      assert_receive {:DOWN, ^ref, :process, ^pid, :shutdown}\n    end\n\n    test \"handle_connected receives consumer info in batch mode\" do\n      # Publish messages before consumer starts, so num_pending > 0\n      for i <- 1..3 do\n        {:ok, _} = Gnat.request(:gnat, \"batch_test.connected\", \"pre-#{i}\")\n      end\n\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      start_supervised!({BatchPullConsumer, consumer: consumer, batch_size: 5, test_pid: self()})\n\n      assert_receive {:connected, consumer_info}\n      assert consumer_info.num_pending == 3\n    end\n\n    test \"rejects ephemeral consumer with ack_policy :explicit and batch_size > 1\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :explicit,\n        deliver_policy: :all\n      }\n\n      Process.flag(:trap_exit, true)\n\n      assert {:error, {%ArgumentError{message: message}, _}} =\n               BatchPullConsumer.start_link(consumer: consumer, batch_size: 3, test_pid: self())\n\n      assert message =~ \"batch_size > 1 requires ack_policy: :all\"\n    end\n\n    test \"rejects ephemeral consumer with default ack_policy and batch_size > 1\" do\n      # Consumer defaults to ack_policy: :explicit, so forgetting to set it should fail\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        deliver_policy: :all\n      }\n\n      Process.flag(:trap_exit, true)\n\n      assert {:error, {%ArgumentError{message: message}, _}} =\n               BatchPullConsumer.start_link(consumer: consumer, batch_size: 3, test_pid: self())\n\n      assert message =~ \"batch_size > 1 requires ack_policy: :all\"\n    end\n\n    test \"durable consumer with ack_policy :explicit fails to connect in batch mode\" do\n      # Create a durable consumer with explicit ack policy\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        durable_name: \"BATCH_EXPLICIT_CONSUMER\",\n        ack_policy: :explicit\n      }\n\n      {:ok, _} = Consumer.create(:gnat, consumer)\n\n      pid =\n        start_supervised!(\n          {BatchPullConsumer,\n           [\n             stream_name: @stream_name,\n             consumer_name: \"BATCH_EXPLICIT_CONSUMER\",\n             batch_size: 3,\n             test_pid: self(),\n             connection_retry_timeout: 50,\n             connection_retries: 1\n           ]},\n          restart: :temporary\n        )\n\n      ref = Process.monitor(pid)\n\n      # Should fail to connect and eventually stop after retries\n      assert_receive {:DOWN, ^ref, :process, ^pid, :timeout}, 5_000\n    end\n\n    test \"allows batch_size 1 with any ack_policy (no validation needed)\" do\n      consumer = %Consumer{\n        stream_name: @stream_name,\n        ack_policy: :explicit,\n        deliver_policy: :all\n      }\n\n      # batch_size: 1 should not trigger the ack_policy validation\n      pid =\n        start_supervised!(\n          {BatchPullConsumer, consumer: consumer, batch_size: 1, test_pid: self()}\n        )\n\n      assert_receive {:connected, _}\n      assert Process.alive?(pid)\n    end\n  end\nend\n"
  },
  {
    "path": "test/pull_consumer/connectivity_test.exs",
    "content": "defmodule Gnat.Jetstream.PullConsumer.ConnectivityTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Consumer, Stream}\n\n  defmodule ExamplePullConsumer do\n    use Gnat.Jetstream.PullConsumer\n\n    def start_link(opts) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n    end\n\n    @impl true\n    def init(opts) do\n      {:ok, nil, Keyword.merge([connection_name: :gnat], opts)}\n    end\n\n    @impl true\n    def handle_message(%{topic: \"ackable\"}, state) do\n      {:ack, state}\n    end\n\n    def handle_message(%{topic: \"non-ackable\", reply_to: reply_to}, state) do\n      [_, _, _, _, delivered_count, _, _, _, _] = String.split(reply_to, \".\")\n\n      # NACK on first delivery\n      if delivered_count == \"1\" do\n        {:nack, state}\n      else\n        {:ack, state}\n      end\n    end\n\n    def handle_message(%{topic: \"terminatable\"}, state) do\n      {:term, state}\n    end\n\n    def handle_message(%{topic: \"skippable\"}, state) do\n      {:noreply, state}\n    end\n  end\n\n  describe \"Jetstream.PullConsumer\" do\n    @describetag with_gnat: :gnat\n\n    setup do\n      stream_name = \"TEST_STREAM\"\n      stream_subjects = [\"ackable\", \"non-ackable\", \"terminatable\", \"skippable\"]\n      consumer_name = \"TEST_CONSUMER\"\n\n      stream = %Stream{name: stream_name, subjects: stream_subjects}\n      {:ok, _response} = Stream.create(:gnat, stream)\n\n      consumer = %Consumer{stream_name: stream_name, durable_name: consumer_name}\n      {:ok, _response} = Consumer.create(:gnat, consumer)\n\n      on_exit(fn ->\n        cleanup()\n      end)\n\n      %{\n        stream_name: stream_name,\n        consumer_name: consumer_name\n      }\n    end\n\n    test \"ignores messages with :noreply\", %{\n      stream_name: stream_name,\n      consumer_name: consumer_name\n    } do\n      start_supervised!(\n        {ExamplePullConsumer, stream_name: stream_name, consumer_name: consumer_name}\n      )\n\n      :ok = Gnat.pub(:gnat, \"skippable\", \"hello\")\n\n      refute_receive {:msg, _}\n    end\n\n    test \"consumes JetStream messages\", %{stream_name: stream_name, consumer_name: consumer_name} do\n      start_supervised!(\n        {ExamplePullConsumer, stream_name: stream_name, consumer_name: consumer_name}\n      )\n\n      Gnat.sub(:gnat, self(), \"$JS.ACK.#{stream_name}.#{consumer_name}.>\")\n\n      :ok = Gnat.pub(:gnat, \"ackable\", \"hello\")\n\n      assert_receive {:msg, %{body: \"+NXT\", topic: topic}}\n      assert String.starts_with?(topic, \"$JS.ACK.#{stream_name}.#{consumer_name}.1\")\n\n      :ok = Gnat.pub(:gnat, \"ackable\", \"hello\")\n\n      assert_receive {:msg, %{body: \"+NXT\", topic: topic}}\n      assert String.starts_with?(topic, \"$JS.ACK.#{stream_name}.#{consumer_name}.1\")\n\n      :ok = Gnat.pub(:gnat, \"non-ackable\", \"hello\")\n\n      assert_receive {:msg, %{body: \"-NAK\", topic: topic}}\n      assert String.starts_with?(topic, \"$JS.ACK.#{stream_name}.#{consumer_name}.1\")\n\n      assert_receive {:msg, %{body: \"+NXT\", topic: topic}}\n      assert String.starts_with?(topic, \"$JS.ACK.#{stream_name}.#{consumer_name}.2\")\n\n      :ok = Gnat.pub(:gnat, \"ackable\", \"hello\")\n\n      assert_receive {:msg, %{body: \"+NXT\", topic: topic}}\n      assert String.starts_with?(topic, \"$JS.ACK.#{stream_name}.#{consumer_name}.1\")\n\n      :ok = Gnat.pub(:gnat, \"terminatable\", \"hello\")\n\n      assert_receive {:msg, %{body: \"+TERM\", topic: topic}}\n      assert String.starts_with?(topic, \"$JS.ACK.#{stream_name}.#{consumer_name}.1\")\n    end\n\n    test \"can be manually closed\", %{stream_name: stream_name, consumer_name: consumer_name} do\n      pid =\n        start_supervised!(\n          {ExamplePullConsumer, stream_name: stream_name, consumer_name: consumer_name}\n        )\n\n      ref = Process.monitor(pid)\n\n      assert :ok = Gnat.Jetstream.PullConsumer.close(pid)\n\n      assert_receive {:DOWN, ^ref, :process, ^pid, :shutdown}\n    end\n\n    test \"retries on unsuccessful connection\", %{\n      stream_name: stream_name,\n      consumer_name: consumer_name\n    } do\n      pid =\n        start_supervised!(\n          {ExamplePullConsumer,\n           connection_name: :non_existent,\n           stream_name: stream_name,\n           consumer_name: consumer_name,\n           connection_retry_timeout: 50,\n           connection_retries: 2},\n          restart: :temporary\n        )\n\n      ref = Process.monitor(pid)\n\n      assert_receive {:DOWN, ^ref, :process, ^pid, :timeout}, 1_000\n    end\n\n    test \"allows setting custom inbox prefix\", %{\n      stream_name: stream_name,\n      consumer_name: consumer_name\n    } do\n      start_supervised!(\n        {ExamplePullConsumer,\n         inbox_prefix: \"CUSTOM_PREFIX.\", stream_name: stream_name, consumer_name: consumer_name}\n      )\n\n      Gnat.sub(:gnat, self(), \"$JS.API.CONSUMER.MSG.NEXT.#{stream_name}.#{consumer_name}\")\n\n      :ok = Gnat.pub(:gnat, \"ackable\", \"hello\")\n\n      expected_body = %{batch: 1} |> Jason.encode!()\n\n      assert_receive {:msg, %{body: ^expected_body, reply_to: \"CUSTOM_PREFIX.\" <> _}}\n    end\n  end\n\n  defp cleanup do\n    # Manage connection on our own here, because all supervised processes will be\n    # closed by the time `on_exit` runs\n    {:ok, pid} = Gnat.start_link()\n    :ok = Stream.delete(pid, \"TEST_STREAM\")\n    Gnat.stop(pid)\n  end\nend\n"
  },
  {
    "path": "test/pull_consumer/ephemeral_test.exs",
    "content": "defmodule Gnat.Jetstream.PullConsumer.EphemeralTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Consumer, Stream}\n\n  defmodule ExamplePullConsumer do\n    use Gnat.Jetstream.PullConsumer\n\n    def start_link(opts) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n    end\n\n    @impl true\n    def init(opts) do\n      consumer = Keyword.fetch!(opts, :consumer)\n\n      connection_opts = [\n        connection_name: :gnat,\n        consumer: consumer\n      ]\n\n      state = %{test_pid: Keyword.fetch!(opts, :test_pid)}\n      {:ok, state, connection_opts}\n    end\n\n    @impl true\n    def handle_connected(consumer_info, state) do\n      send(state.test_pid, {:connected, consumer_info})\n      {:ok, state}\n    end\n\n    @impl true\n    def handle_message(message, state) do\n      send(state.test_pid, {:pulled, message})\n      {:ack, state}\n    end\n  end\n\n  describe \"Jetstream.PullConsumer\" do\n    @describetag with_gnat: :gnat\n\n    setup do\n      stream_name = \"TEST_STREAM_2\"\n      stream_subjects = [\"stream_2.*\"]\n\n      stream = %Stream{name: stream_name, subjects: stream_subjects}\n      {:ok, _response} = Stream.create(:gnat, stream)\n\n      on_exit(fn ->\n        cleanup()\n      end)\n\n      %{\n        stream_name: stream_name\n      }\n    end\n\n    test \"ephemeral consumer receives messages\", %{\n      stream_name: stream_name\n    } do\n      consumer = %Consumer{stream_name: stream_name}\n\n      {:ok, _resp} = Gnat.request(:gnat, \"stream_2.ohai\", \"whatsup\")\n\n      start_supervised!({ExamplePullConsumer, consumer: consumer, test_pid: self()})\n\n      assert_receive {:connected, consumer_info}\n      assert consumer_info.num_pending == 1\n\n      assert_receive {:pulled, message}\n      assert %{topic: \"stream_2.ohai\", body: \"whatsup\"} = message\n\n      {:ok, _resp} = Gnat.request(:gnat, \"stream_2.ohai\", \"second\")\n\n      assert_receive {:pulled, message}\n      assert %{topic: \"stream_2.ohai\", body: \"second\"} = message\n    end\n  end\n\n  defp cleanup do\n    # Manage connection on our own here, because all supervised processes will be\n    # closed by the time `on_exit` runs\n    {:ok, pid} = Gnat.start_link()\n    :ok = Stream.delete(pid, \"TEST_STREAM_2\")\n    Gnat.stop(pid)\n  end\nend\n"
  },
  {
    "path": "test/pull_consumer/status_messages_test.exs",
    "content": "defmodule Gnat.Jetstream.PullConsumer.StatusMessagesTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Consumer, Stream}\n\n  defmodule ObservingConsumer do\n    use Gnat.Jetstream.PullConsumer\n\n    def start_link(opts) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n    end\n\n    @impl true\n    def init(opts) do\n      {test_pid, opts} = Keyword.pop!(opts, :test_pid)\n      state = Keyword.merge([connection_name: :gnat], opts)\n\n      {:ok, %{test_pid: test_pid}, state}\n    end\n\n    @impl true\n    def handle_message(message, state) do\n      send(state.test_pid, {:handle_message, message})\n      {:ack, state}\n    end\n\n    @impl true\n    def handle_status(message, state) do\n      send(state.test_pid, {:handle_status, message})\n      {:ok, state}\n    end\n  end\n\n  defmodule SilentConsumer do\n    use Gnat.Jetstream.PullConsumer\n\n    def start_link(opts) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n    end\n\n    @impl true\n    def init(opts) do\n      {test_pid, opts} = Keyword.pop!(opts, :test_pid)\n\n      {:ok, test_pid, Keyword.merge([connection_name: :gnat], opts)}\n    end\n\n    @impl true\n    def handle_message(message, test_pid) do\n      send(test_pid, {:handle_message, message})\n      {:ack, test_pid}\n    end\n  end\n\n  describe \"JetStream status messages\" do\n    @describetag with_gnat: :gnat\n\n    setup do\n      stream_name = \"STATUS_TEST_STREAM\"\n      consumer_name = \"STATUS_TEST_CONSUMER\"\n\n      stream = %Stream{name: stream_name, subjects: [\"status.test\"]}\n      {:ok, _} = Stream.create(:gnat, stream)\n\n      consumer = %Consumer{stream_name: stream_name, durable_name: consumer_name}\n      {:ok, _} = Consumer.create(:gnat, consumer)\n\n      on_exit(fn ->\n        {:ok, pid} = Gnat.start_link()\n        :ok = Stream.delete(pid, stream_name)\n        Gnat.stop(pid)\n      end)\n\n      %{stream_name: stream_name, consumer_name: consumer_name}\n    end\n\n    test \"are not forwarded to handle_message when no handle_status is defined\",\n         %{stream_name: stream_name, consumer_name: consumer_name} do\n      pid =\n        start_supervised!(\n          {SilentConsumer,\n           test_pid: self(), stream_name: stream_name, consumer_name: consumer_name}\n        )\n\n      send(pid, {:msg, %{status: \"409\", description: \"Leadership Change\", body: \"\", gnat: :gnat}})\n      send(pid, {:msg, %{status: \"100\", body: \"\", gnat: :gnat}})\n\n      refute_receive {:handle_message, _}, 100\n    end\n\n    test \"are forwarded to handle_status when the callback is defined\",\n         %{stream_name: stream_name, consumer_name: consumer_name} do\n      pid =\n        start_supervised!(\n          {ObservingConsumer,\n           test_pid: self(), stream_name: stream_name, consumer_name: consumer_name}\n        )\n\n      send(pid, {:msg, %{status: \"409\", description: \"Leadership Change\", body: \"\", gnat: :gnat}})\n\n      assert_receive {:handle_status, %{status: \"409\", description: \"Leadership Change\"}}\n      refute_receive {:handle_message, _}, 100\n    end\n\n    test \"single-message mode issues a new pull after a 409\",\n         %{stream_name: stream_name, consumer_name: consumer_name} do\n      # Subscribe to the next-message subject to observe outbound pulls.\n      {:ok, _sid} =\n        Gnat.sub(:gnat, self(), \"$JS.API.CONSUMER.MSG.NEXT.#{stream_name}.#{consumer_name}\")\n\n      pid =\n        start_supervised!(\n          {SilentConsumer,\n           test_pid: self(), stream_name: stream_name, consumer_name: consumer_name}\n        )\n\n      # Drain the initial pull issued on connect.\n      assert_receive {:msg, %{topic: \"$JS.API.CONSUMER.MSG.NEXT.\" <> _}}, 1_000\n\n      send(pid, {:msg, %{status: \"409\", description: \"Leadership Change\", body: \"\", gnat: :gnat}})\n\n      # A new pull must be issued or the consumer is stuck.\n      assert_receive {:msg, %{topic: \"$JS.API.CONSUMER.MSG.NEXT.\" <> _}}, 1_000\n    end\n  end\n\n  describe \"batch-mode JetStream status messages\" do\n    @describetag with_gnat: :gnat\n\n    setup do\n      stream_name = \"STATUS_BATCH_STREAM\"\n      subject = \"status.batch\"\n\n      stream = %Stream{name: stream_name, subjects: [subject]}\n      {:ok, _} = Stream.create(:gnat, stream)\n\n      on_exit(fn ->\n        {:ok, pid} = Gnat.start_link()\n        :ok = Stream.delete(pid, stream_name)\n        Gnat.stop(pid)\n      end)\n\n      %{stream_name: stream_name, subject: subject}\n    end\n\n    defmodule BatchObservingConsumer do\n      use Gnat.Jetstream.PullConsumer\n\n      def start_link(opts) do\n        Gnat.Jetstream.PullConsumer.start_link(__MODULE__, opts)\n      end\n\n      @impl true\n      def init(opts) do\n        {test_pid, opts} = Keyword.pop!(opts, :test_pid)\n        state = Keyword.merge([connection_name: :gnat], opts)\n        {:ok, %{test_pid: test_pid}, state}\n      end\n\n      @impl true\n      def handle_message(message, state) do\n        send(state.test_pid, {:handled, message.body})\n        {:ack, state}\n      end\n\n      @impl true\n      def handle_status(message, state) do\n        send(state.test_pid, {:status, message})\n        {:ok, state}\n      end\n    end\n\n    test \"batch mode issues a new pull after 409 with empty buffer\",\n         %{stream_name: stream_name} do\n      consumer = %Gnat.Jetstream.API.Consumer{\n        stream_name: stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      # Subscribe to the next-message subject before starting the consumer so\n      # we see the pull the consumer issues at connect time and any re-pulls.\n      {:ok, _sid} = Gnat.sub(:gnat, self(), \"$JS.API.CONSUMER.MSG.NEXT.#{stream_name}.>\")\n\n      pid =\n        start_supervised!(\n          {BatchObservingConsumer, consumer: consumer, batch_size: 5, test_pid: self()}\n        )\n\n      # Drain pulls issued during connect (at minimum the initial catch-up fetch).\n      # Absorb any that arrive within a short window so the assert below only\n      # fires on the post-409 re-pull.\n      drain_pulls(200)\n\n      send(pid, {:msg, %{status: \"409\", description: \"Leadership Change\", body: \"\", gnat: :gnat}})\n\n      # A new pull must be issued or the consumer is stuck.\n      assert_receive {:msg, %{topic: \"$JS.API.CONSUMER.MSG.NEXT.\" <> _}}, 1_000\n      # And handle_status should have observed the 409.\n      assert_receive {:status, %{status: \"409\", description: \"Leadership Change\"}}, 1_000\n    end\n\n    test \"batch mode processes partial buffer and re-pulls on 409\",\n         %{stream_name: stream_name, subject: subject} do\n      consumer = %Gnat.Jetstream.API.Consumer{\n        stream_name: stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      {:ok, _sid} = Gnat.sub(:gnat, self(), \"$JS.API.CONSUMER.MSG.NEXT.#{stream_name}.>\")\n\n      # Publish a single message so the consumer buffers 1 of a 5-message batch.\n      :ok = Gnat.pub(:gnat, subject, \"buffered-1\")\n\n      pid =\n        start_supervised!(\n          {BatchObservingConsumer, consumer: consumer, batch_size: 5, test_pid: self()}\n        )\n\n      # The buffered message should arrive at the server-issued 404 terminator\n      # and be processed. Once that's settled, drain any further pulls.\n      assert_receive {:handled, \"buffered-1\"}, 5_000\n      drain_pulls(200)\n\n      # Now stuff a new message into the buffer via direct send and inject a 409.\n      fake_msg = %{\n        topic: subject,\n        body: \"partial-1\",\n        reply_to: \"$JS.ACK.#{stream_name}.FAKE.1.1.1.0.0\",\n        gnat: :gnat\n      }\n\n      send(pid, {:msg, fake_msg})\n\n      # A 409 should cause the partial buffer to be processed (handle_message\n      # called for \"partial-1\") and a fresh pull to be issued.\n      send(pid, {:msg, %{status: \"409\", description: \"Leadership Change\", body: \"\", gnat: :gnat}})\n\n      assert_receive {:handled, \"partial-1\"}, 1_000\n      assert_receive {:msg, %{topic: \"$JS.API.CONSUMER.MSG.NEXT.\" <> _}}, 1_000\n      assert_receive {:status, %{status: \"409\", description: \"Leadership Change\"}}, 1_000\n    end\n\n    test \"batch mode treats 100 heartbeat as a no-op (no re-pull)\",\n         %{stream_name: stream_name} do\n      consumer = %Gnat.Jetstream.API.Consumer{\n        stream_name: stream_name,\n        ack_policy: :all,\n        deliver_policy: :all\n      }\n\n      {:ok, _sid} = Gnat.sub(:gnat, self(), \"$JS.API.CONSUMER.MSG.NEXT.#{stream_name}.>\")\n\n      pid =\n        start_supervised!(\n          {BatchObservingConsumer, consumer: consumer, batch_size: 5, test_pid: self()}\n        )\n\n      drain_pulls(200)\n\n      # 100 is a keep-alive on the existing pull — must NOT cause a re-pull.\n      send(pid, {:msg, %{status: \"100\", body: \"\", gnat: :gnat}})\n\n      refute_receive {:msg, %{topic: \"$JS.API.CONSUMER.MSG.NEXT.\" <> _}}, 200\n      # But handle_status should still be invoked so users can observe heartbeats.\n      assert_receive {:status, %{status: \"100\"}}, 500\n    end\n  end\n\n  defp drain_pulls(timeout) do\n    receive do\n      {:msg, %{topic: \"$JS.API.CONSUMER.MSG.NEXT.\" <> _}} -> drain_pulls(timeout)\n    after\n      timeout -> :ok\n    end\n  end\nend\n"
  },
  {
    "path": "test/pull_consumer/using_macro_test.exs",
    "content": "defmodule Gnat.Jetstream.PullConsumer.UsingMacroTest do\n  use Gnat.Jetstream.ConnCase\n\n  defmodule ExamplePullConsumer do\n    use Gnat.Jetstream.PullConsumer, restart: :temporary, shutdown: 12345\n\n    def start_link(_) do\n      Gnat.Jetstream.PullConsumer.start_link(__MODULE__, [])\n    end\n\n    @impl true\n    def init([]) do\n      {:ok, nil, []}\n    end\n\n    @impl true\n    def handle_message(%{}, state) do\n      {:ack, state}\n    end\n  end\n\n  describe \"use Jetstream.PullConsumer\" do\n    test \"allows specifing child specification options via argument\" do\n      assert ExamplePullConsumer.child_spec(:arg) == %{\n               id: ExamplePullConsumer,\n               start: {ExamplePullConsumer, :start_link, [:arg]},\n               restart: :temporary,\n               shutdown: 12345\n             }\n    end\n  end\nend\n"
  },
  {
    "path": "test/support/conn_case.ex",
    "content": "defmodule Gnat.Jetstream.ConnCase do\n  @moduledoc \"\"\"\n  This module defines the test case to be used by tests that require setting up a connection.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  using(opts) do\n    tags = module_tags(opts)\n\n    quote do\n      import Gnat.Jetstream.ConnCase\n\n      @moduletag unquote(tags)\n    end\n  end\n\n  defp module_tags(opts) do\n    Enum.reduce(opts, [capture_log: true], fn opt, acc ->\n      add_module_tag(acc, opt)\n    end)\n  end\n\n  defp add_module_tag(tags, {:min_server_version, min_version}) do\n    if server_version_incompatible?(min_version) do\n      Keyword.put(tags, :incompatible, true)\n    else\n      tags\n    end\n  end\n\n  defp add_module_tag(tags, _opt), do: tags\n\n  defp get_server_version(conn) do\n    Gnat.server_info(conn).version\n  end\n\n  defp server_version_incompatible?(min_version) do\n    {:ok, conn} = Gnat.start_link()\n    match? = Version.match?(get_server_version(conn), \">= #{min_version}\")\n    :ok = Gnat.stop(conn)\n    !match?\n  end\n\n  setup tags do\n    if arg = Map.get(tags, :with_gnat) do\n      conn = start_gnat!(arg)\n\n      %{conn: conn}\n    else\n      :ok\n    end\n  end\n\n  def start_gnat!(name) when is_atom(name) do\n    start_gnat!(%{}, name: name)\n  end\n\n  def start_gnat!(connection_settings, options)\n      when is_map(connection_settings) and is_list(options) do\n    {Gnat, %{}}\n    |> Supervisor.child_spec(start: {Gnat, :start_link, [connection_settings, options]})\n    |> start_supervised!()\n  end\nend\n"
  },
  {
    "path": "test/support/generators.ex",
    "content": "defmodule Gnat.Generators do\n  use PropCheck\n\n  # Character classes useful for generating text\n  def alphanumeric_char do\n    elements(~c\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n  end\n\n  def alphanumeric_space_char do\n    elements(~c\" 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n  end\n\n  def numeric_char do\n    elements(~c\"0123456789\")\n  end\n\n  @doc \"protocol delimiter. The protocol can use one ore more of space or tab characters as delimiters between fields\"\n  def delimiter, do: let(chunks <- non_empty(list(delimiter_char())), do: Enum.join(chunks, \"\"))\n\n  def delimiter_char, do: union([\" \", \"\\t\"])\n\n  def error do\n    let chars <- list(alphanumeric_space_char()) do\n      %{binary: \"-ERR '#{List.to_string(chars)}'\\r\\n\"}\n    end\n  end\n\n  def host_port do\n    let(\n      {ip, port} <- {[byte(), byte(), byte(), byte()], non_neg_integer()},\n      do: \"#{Enum.join(ip, \".\")}:#{port}\"\n    )\n  end\n\n  def info do\n    let options <- info_options() do\n      %{binary: \"INFO #{Jason.encode!(options)}\\r\\n\"}\n    end\n  end\n\n  def info_options do\n    let(\n      {server_id, version, go, host, port, auth_required, ssl_required, max_payload, connect_urls} <-\n        {list(alphanumeric_space_char()), list(list(numeric_char())), list(alphanumeric_char()),\n         list(alphanumeric_char()), non_neg_integer(), boolean(), boolean(),\n         integer(1024, 1_048_576), list(host_port())}\n    ) do\n      %{\n        server_id: List.to_string(server_id),\n        version: version |> Enum.map(&List.to_string/1) |> Enum.join(\".\"),\n        go: List.to_string(go),\n        host: List.to_string(host),\n        port: port,\n        auth_required: auth_required,\n        ssl_required: ssl_required,\n        max_payload: max_payload,\n        connect_urls: connect_urls\n      }\n    end\n  end\n\n  def ok, do: %{binary: \"+OK\\r\\n\"}\n\n  def ping, do: %{binary: \"PING\\r\\n\"}\n\n  def pong, do: %{binary: \"PONG\\r\\n\"}\n\n  # generates a map containing the binary encoded message and attributes for which generated\n  # sid, subject, payload and reply_to topic were used in the encoded message\n  def message, do: sized(size, message(size))\n  def message(size), do: union([message_without_reply(size), message_with_reply(size)])\n\n  def message_with_reply(size) do\n    let(\n      {p, su, si, r, d1, d2, d3, d4} <-\n        {payload(size), subject(), sid(), reply_to(), delimiter(), delimiter(), delimiter(),\n         delimiter()}\n    ) do\n      parts = [\"MSG\", d1, su, d2, si, d3, r, d4, byte_size(p), \"\\r\\n\", p, \"\\r\\n\"]\n\n      %{\n        binary: Enum.join(parts),\n        reply_to: r,\n        sid: si,\n        subject: su,\n        payload: p\n      }\n    end\n  end\n\n  def message_without_reply(size) do\n    let(\n      {p, su, si, d1, d2, d3} <-\n        {payload(size), subject(), sid(), delimiter(), delimiter(), delimiter()}\n    ) do\n      parts = [\"MSG\", d1, su, d2, si, d3, byte_size(p), \"\\r\\n\", p, \"\\r\\n\"]\n\n      %{\n        binary: Enum.join(parts),\n        reply_to: nil,\n        sid: si,\n        subject: su,\n        payload: p\n      }\n    end\n  end\n\n  def payload(size), do: binary(size)\n\n  def protocol_message do\n    union([ok(), ping(), pong(), error(), info(), message()])\n  end\n\n  # according to the spec sid's can be alphanumeric, but our client only generates\n  # non-negative integers and we only receive back our own sids\n  def sid, do: non_neg_integer()\n\n  def subject do\n    let(chunks <- subject_chunks(), do: Enum.join(chunks, \".\"))\n  end\n\n  def subject_chunks do\n    non_empty(list(non_empty(list(alphanumeric_char()))))\n  end\n\n  # TODO subsription names are like subject names, but they can have wildcards\n  # There is a special case where a user can subscribe to \">\" to subscribe to all topics\n\n  def reply_to, do: subject()\nend\n"
  },
  {
    "path": "test/test_helper.exs",
    "content": "ExUnit.configure(exclude: [:pending, :property, :multi_server, :message_ttl])\n\nExUnit.start()\n\n# set assert_receive default timeout\nApplication.put_env(:ex_unit, :assert_receive_timeout, 1_000)\n\n# cleanup any streams left over by previous test runs\n{:ok, conn} = Gnat.start_link(%{}, name: :jstest)\n{:ok, %{streams: streams}} = Gnat.Jetstream.API.Stream.list(conn)\nstreams = streams || []\n\nEnum.each(streams, fn stream ->\n  :ok = Gnat.Jetstream.API.Stream.delete(conn, stream)\nend)\n\n:ok = Gnat.stop(conn)\n\ncase :gen_tcp.connect(~c\"localhost\", 4222, [:binary]) do\n  {:ok, socket} ->\n    :gen_tcp.close(socket)\n\n  {:error, reason} ->\n    Mix.raise(\n      \"Cannot connect to nats-server\" <>\n        \" (http://localhost:4222):\" <>\n        \" #{:inet.format_error(reason)}\\n\" <>\n        \"You probably need to start nats-server.\"\n    )\nend\n\n# this is used by some property tests, see test/gnat_property_test.exs\nGnat.start_link(%{}, name: :test_connection)\n\ndefmodule RpcEndpoint do\n  def init do\n    {:ok, pid} = Gnat.start_link()\n    {:ok, _ref} = Gnat.sub(pid, self(), \"rpc.>\")\n    loop(pid)\n  end\n\n  def loop(pid) do\n    receive do\n      {:msg, %{body: body, reply_to: topic}} ->\n        Gnat.pub(pid, topic, body)\n        loop(pid)\n    end\n  end\nend\n\nspawn(&RpcEndpoint.init/0)\n\ndefmodule ExampleService do\n  use Gnat.Services.Server\n\n  def request(%{topic: \"calc.add\", body: body}, _endpoint, _group) when body == \"foo\" do\n    :timer.sleep(10)\n\n    {:reply, \"6\"}\n  end\n\n  def request(%{body: body}, \"sub\", \"calc\") when body == \"foo\" do\n    # want some processing time to show up\n    :timer.sleep(10)\n    {:reply, \"4\"}\n  end\n\n  def request(_, _, _) do\n    {:error, \"oops\"}\n  end\n\n  def error(_msg, \"oops\") do\n    {:reply, \"500 error\"}\n  end\nend\n\ndefmodule ExampleServer do\n  use Gnat.Server\n\n  def request(%{topic: \"example.good\", body: body}) do\n    {:reply, \"Re: #{body}\"}\n  end\n\n  def request(%{topic: \"example.error\"}) do\n    {:error, \"oops\"}\n  end\n\n  def request(%{topic: \"example.raise\"}) do\n    raise \"oops\"\n  end\n\n  def error(_msg, \"oops\") do\n    {:reply, \"400 error\"}\n  end\n\n  def error(_msg, %RuntimeError{message: \"oops\"}) do\n    {:reply, \"500 error\"}\n  end\n\n  def error(msg, other) do\n    require Logger\n    Logger.error(\"#{msg.topic} failed #{inspect(other)}\")\n  end\nend\n\n{:ok, _pid} =\n  Gnat.ConsumerSupervisor.start_link(%{\n    connection_name: :test_connection,\n    module: ExampleServer,\n    subscription_topics: [\n      %{topic: \"example.*\"}\n    ]\n  })\n\n{:ok, _pid} =\n  Gnat.ConsumerSupervisor.start_link(%{\n    connection_name: :test_connection,\n    module: ExampleService,\n    service_definition: %{\n      name: \"exampleservice\",\n      description: \"This is an example service\",\n      version: \"0.1.0\",\n      endpoints: [\n        %{\n          name: \"add\",\n          group_name: \"calc\"\n        },\n        %{\n          name: \"sub\",\n          group_name: \"calc\"\n        }\n      ]\n    }\n  })\n\ndefmodule CheckForExpectedNatsServers do\n  def check(tags) do\n    check_for_default()\n    Enum.each(tags, &check_for_tag/1)\n  end\n\n  def check_for_default do\n    case :gen_tcp.connect(~c\"localhost\", 4222, [:binary]) do\n      {:ok, socket} ->\n        :gen_tcp.close(socket)\n\n      {:error, reason} ->\n        Mix.raise(\n          \"Cannot connect to nats-server\" <>\n            \" (tcp://localhost:4222):\" <>\n            \" #{:inet.format_error(reason)}\\n\" <>\n            \"You probably need to start nats-server.\"\n        )\n    end\n  end\n\n  def check_for_tag(:multi_server) do\n    case :gen_tcp.connect(~c\"localhost\", 4223, [:binary]) do\n      {:ok, socket} ->\n        :gen_tcp.close(socket)\n\n      {:error, reason} ->\n        Mix.raise(\n          \"Cannot connect to nats-server\" <>\n            \" (tcp://localhost:4223):\" <>\n            \" #{:inet.format_error(reason)}\\n\" <>\n            \"You probably need to start a nats-server \" <>\n            \"server that requires authentication with \" <>\n            \"the following command `nats-server -p 4223 \" <>\n            \"--user bob --pass alice`.\"\n        )\n    end\n\n    case :gen_tcp.connect(~c\"localhost\", 4224, [:binary]) do\n      {:ok, socket} ->\n        :gen_tcp.close(socket)\n\n      {:error, reason} ->\n        Mix.raise(\n          \"Cannot connect to nats-server\" <>\n            \" (tcp://localhost:4224):\" <>\n            \" #{:inet.format_error(reason)}\\n\" <>\n            \"You probably need to start a nats-server \" <>\n            \"server that requires tls with \" <>\n            \"a command like `nats-server -p 4224 \" <>\n            \"--tls --tlscert test/fixtures/server-cert.pem \" <>\n            \"--tlskey test/fixtures/server-key.pem`.\"\n        )\n    end\n\n    case :gen_tcp.connect(~c\"localhost\", 4225, [:binary]) do\n      {:ok, socket} ->\n        :gen_tcp.close(socket)\n\n      {:error, reason} ->\n        Mix.raise(\n          \"Cannot connect to nats-server\" <>\n            \" (tcp://localhost:4225):\" <>\n            \" #{:inet.format_error(reason)}\\n\" <>\n            \"You probably need to start a nats-server \" <>\n            \"server that requires tls with \" <>\n            \"a command like `nats-server -p 4225 --tls \" <>\n            \"--tlscert test/fixtures/server-cert.pem \" <>\n            \"--tlskey test/fixtures/server-key.pem \" <>\n            \"--tlscacert test/fixtures/ca.pem --tlsverify\"\n        )\n    end\n\n    case :gen_tcp.connect(~c\"localhost\", 4226, [:binary]) do\n      {:ok, socket} ->\n        :gen_tcp.close(socket)\n\n      {:error, reason} ->\n        Mix.raise(\n          \"Cannot connect to nats-server\" <>\n            \" (tcp://localhost:4226):\" <>\n            \" #{:inet.format_error(reason)}\\n\" <>\n            \"You probably need to start a nats-server \" <>\n            \"server that requires authentication with \" <>\n            \"the following command `nats-server -p 4226 \" <>\n            \"--auth SpecialToken`.\"\n        )\n    end\n\n    case :gen_tcp.connect(~c\"localhost\", 4227, [:binary]) do\n      {:ok, socket} ->\n        :gen_tcp.close(socket)\n\n      {:error, reason} ->\n        Mix.raise(\n          \"Cannot connect to nats-server\" <>\n            \" (tcp://localhost:4227):\" <>\n            \" #{:inet.format_error(reason)}\\n\" <>\n            \"You probably need to start a nats-server \" <>\n            \"server that requires authentication with \" <>\n            \"the following command `nats-server -p 4227 \" <>\n            \"-c test/fixtures/nkey_config`.\"\n        )\n    end\n  end\n\n  def check_for_tag(_), do: :ok\nend\n"
  }
]