Full Code of nats-io/nats.ex for AI

main b6d2f37e8901 cached
85 files
384.2 KB
109.9k tokens
493 symbols
1 requests
Download .txt
Showing preview only (409K chars total). Download the full file or copy to clipboard to get everything.
Repository: nats-io/nats.ex
Branch: main
Commit: b6d2f37e8901
Files: 85
Total size: 384.2 KB

Directory structure:
gitextract_y72cyuck/

├── .dialyzer_ignore.exs
├── .formatter.exs
├── .github/
│   └── workflows/
│       └── CI.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── MAINTAINERS.md
├── README.md
├── bench/
│   ├── client.exs
│   ├── kv_consume.exs
│   ├── parse.exs
│   ├── publish.exs
│   ├── request.exs
│   ├── request_multi.exs
│   ├── server.exs
│   └── service_bench.exs
├── dependencies.md
├── docs/
│   └── js/
│       ├── guides/
│       │   ├── broadway.md
│       │   ├── managing.md
│       │   └── push_based_consumer.md
│       └── introduction/
│           ├── getting_started.md
│           └── overview.md
├── lib/
│   ├── gnat/
│   │   ├── command.ex
│   │   ├── connection_supervisor.ex
│   │   ├── consumer_supervisor.ex
│   │   ├── handshake.ex
│   │   ├── jetstream/
│   │   │   ├── api/
│   │   │   │   ├── consumer.ex
│   │   │   │   ├── kv/
│   │   │   │   │   ├── entry.ex
│   │   │   │   │   └── watcher.ex
│   │   │   │   ├── kv.ex
│   │   │   │   ├── message.ex
│   │   │   │   ├── object/
│   │   │   │   │   └── meta.ex
│   │   │   │   ├── object.ex
│   │   │   │   ├── stream.ex
│   │   │   │   └── util.ex
│   │   │   ├── jetstream.ex
│   │   │   ├── pager.ex
│   │   │   ├── pull_consumer/
│   │   │   │   ├── connection_options.ex
│   │   │   │   └── server.ex
│   │   │   └── pull_consumer.ex
│   │   ├── parsec.ex
│   │   ├── server.ex
│   │   └── services/
│   │       ├── server.ex
│   │       ├── service.ex
│   │       ├── service_responder.ex
│   │       └── wire_protocol.ex
│   └── gnat.ex
├── mix.exs
├── scripts/
│   └── cluster/
│       ├── cluster.sh
│       ├── driver.exs
│       ├── n1.conf
│       ├── n2.conf
│       └── n3.conf
└── test/
    ├── command_test.exs
    ├── fixtures/
    │   ├── ca.pem
    │   ├── client-cert.pem
    │   ├── client-key.pem
    │   ├── nkey_config
    │   ├── nkey_seed
    │   ├── server-cert.pem
    │   └── server-key.pem
    ├── gnat/
    │   ├── consumer_supervisor_test.exs
    │   ├── handshake_test.exs
    │   ├── parsec_property_test.exs
    │   └── parsec_test.exs
    ├── gnat_property_test.exs
    ├── gnat_test.exs
    ├── jetstream/
    │   ├── api/
    │   │   ├── consumer_doc_test.exs
    │   │   ├── consumer_test.exs
    │   │   ├── kv/
    │   │   │   ├── entry_test.exs
    │   │   │   └── watcher_test.exs
    │   │   ├── kv_test.exs
    │   │   ├── object_test.exs
    │   │   ├── stream_doc_test.exs
    │   │   └── stream_test.exs
    │   ├── message_test.exs
    │   └── pager_test.exs
    ├── pull_consumer/
    │   ├── batch_test.exs
    │   ├── connectivity_test.exs
    │   ├── ephemeral_test.exs
    │   ├── status_messages_test.exs
    │   └── using_macro_test.exs
    ├── support/
    │   ├── conn_case.ex
    │   └── generators.ex
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .dialyzer_ignore.exs
================================================
[
]


================================================
FILE: .formatter.exs
================================================
[
  inputs: ["{.formatter,mix}.exs", "{lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: .github/workflows/CI.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 0 1 */1 *"

permissions:
  contents: read

jobs:
  test:
    name: Elixir ${{ matrix.pair.elixir }} / OTP ${{ matrix.pair.otp }} / NATS ${{ matrix.pair.nats }}
    runs-on: ubuntu-latest

    strategy:
      matrix:
        pair:
          - otp: "25"
            elixir: "1.14"
            nats: "2.10.0"

          - otp: "26"
            elixir: "1.16"
            nats: "2.10.24"

          - otp: "27"
            elixir: "1.17"
            nats: "2.10.24"

          - otp: "27"
            elixir: "1.18"
            nats: "2.10.24"

          - otp: "28"
            elixir: "1.18"
            nats: "2.11.11"

          - otp: "28"
            elixir: "1.18"
            nats: "2.10.24"

          # the main pair handles things like format checking and running dialyzer
          - main: true
            otp: "28"
            elixir: "1.19"
            nats: "2.10.24"

    env:
      MIX_ENV: test

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Elixir
        uses: erlef/setup-beam@v1
        with:
          elixir-version: ${{ matrix.pair.elixir }}
          otp-version: ${{ matrix.pair.otp }}

      - name: Start NATS Jetstream
        run: docker run --rm -d --network host nats:${{ matrix.pair.nats }} -js

      - name: Restore deps cache
        uses: actions/cache@v4
        with:
          path: |
            deps
            _build
          key: deps-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-${{ hashFiles('mix.lock') }}

      - name: Install package dependencies
        run: mix deps.get

      - name: Check for valid formatting
        if: matrix.pair.main
        run: mix format --check-formatted

      - name: Run unit tests
        run: mix test --color

      - name: Run NATS 2.11 specific tests
        if: ${{ matrix.pair.nats == '2.11.11' }}
        run: mix test --only message_ttl

      - name: Cache Dialyzer PLTs
        if: matrix.pair.main
        uses: actions/cache@v4
        with:
          path: |
            priv/plts/*.plt
            priv/plts/*.plt.hash
          key: dialyzer-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-${{ hashFiles('mix.lock') }}
          restore-keys: |
            dialyzer-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-

      - name: Run Dialyzer
        if: matrix.pair.main
        env:
          MIX_ENV: dev
        run: |
          mix dialyzer


================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build

# If you run "mix test --cover", coverage assets end up here.
/cover

# The directory Mix downloads your dependencies sources to.
/deps

/tmp

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc

# Dialyzer PLT files
priv/plts/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Do not commit the counterexamples stored during property testing
/test/counter_examples

# ASDF file to for specifying which erlang/elixir version to use for local development
.tool-versions

# Ignore Cluster details
scripts/cluster/data
scripts/cluster/logs
scripts/cluster/pids

================================================
FILE: CHANGELOG.md
================================================
# Changelog

## 1.14

* Add `PullConsumer.handle_connected/2` optional callback to get consumer info
* Add `PullConsumer.handle_status/2` optional callback to observe status messages
* Added support for `batch_size` option in PullConsumer options to pull messages
  and acknowledge them in batches.
* Add `Gnat.Jetstream.API.KV.Entry` with `from_message/2` for parsing a raw
  NATS message from a KV bucket's underlying stream into a structured entry
  (operation, key, value, revision, created, delta). Intended to be shared
  between the built-in `KV.Watcher` and user-supplied `PullConsumer`
  implementations (e.g. caches that need to detect when they are caught up
  with the stream). Returns `:ignore` for messages that are not KV records.
* `KV.Watcher` now uses `KV.Entry` internally; its public callback API is
  unchanged. The push consumer it creates now enables server-driven flow
  control and a 5s idle heartbeat (matching nats.go's ordered-consumer
  defaults), so slow handlers apply backpressure instead of being dropped
  as slow consumers.
* **Behavior change (bugfix):** `PullConsumer` no longer forwards JetStream
  informational status messages (e.g. `100` idle heartbeat, `409` leadership
  change) to `c:handle_message/2`. These are not stream records and cannot
  be acked. In single-message mode the consumer now drops them and re-issues
  a pull request.
* Add an optional `c:handle_status/2` callback to `Gnat.Jetstream.PullConsumer`
  for users who want to observe status messages (e.g. log on `409`).

## 1.11

* Allow clients to force authentication without server auth_required by @mmmries in #205
* Support ephemeral consumers and auto-cleanup consumers in the PullConsumer module by @mmmries in #202
* Implement regularly scheduled PING/PONG health check by @mmmries in #200
* Add Erlang 28 with Elixir 1.18 to the build matrix by @davydog187 in #201
* Use Pager module for KV.contents by @mmmries in #198
* Hint `:timeout` option in KV's typespec by @rixmann in #193

## 1.10

* Clarify authentication setup during test by @davydog187 in #187
* Test on Elixir 1.18 and NATS 2.10.24 by @davydog187 in #188
* Gnat.Jetstream.API.KV.info/3 by @davydog187 in #189
* Remove function_exported? check for Keyword.validate!/2 by @davydog187 in #186
* Tiny optimization to KV.list_buckets/1 by @davydog187 in #185
* make KV-watcher emit :key_added events when the message has a header by @rixmann in #191
* add :compression to stream attributes by @rixmann in #192
* fix: unknown field domain in Stream.create (#194) by @c0deaddict
* feat: add jetstream message metadata helper (#197) by @c0deaddict
* fix: deliver policy (#196) by @c0deaddict

## 1.9

* Housecleaning by @mmmries in #176
  * switch to charlist sigils
  * update to newest nkeys
  * require elixir 1.14 and erlang 25+
* Fix incorrect useage of charlist by @davydog187 in #179
* Soft deprecate is_kv_bucket_stream?/1 in favor of kv_bucket_stream?/1 by @davydog187 in #183
* Clean up examples in KV by @davydog187 in #181
* Document options for Gnat.Jetstream.API.KV by @davydog187 in #180

## 1.8

* Integrated the jetstream functionality into this client directly https://github.com/nats-io/nats.ex/pull/146
* Add ability to list KV buckets https://github.com/nats-io/nats.ex/pull/152
* Improve CI Reliability https://github.com/nats-io/nats.ex/pull/154
* Bugfix to treat no streams as an empty list rather than a null https://github.com/nats-io/nats.ex/pull/155
* Added supported for `allow_direct` and `mirror_direct` attributes of streams https://github.com/nats-io/nats.ex/pull/161
* Added support for `discard_new_per_subject` attribute of streams https://github.com/nats-io/nats.ex/pull/163
* Added support for `Object.list_buckets` https://github.com/nats-io/nats.ex/pull/169

## 1.7

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

## 1.6

* added the `no_responders` behavior https://github.com/nats-io/nats.ex/pull/137

## 1.5

* add the `inbox_prefix` option https://github.com/nats-io/nats.ex/pull/121
* add the `Gnat.server_info/1` function https://github.com/nats-io/nats.ex/pull/124
* fix header parsing issue https://github.com/nats-io/nats.ex/pull/125

## 1.4

* add the `Gnat.request_multi/4` function https://github.com/nats-io/nats.ex/pull/120
* add elixir 1.13 to the test matrix

## 1.3

* adding support for sending and receiving headers https://github.com/nats-io/nats.ex/pull/116

## 1.2

* `Gnat.Server` behaviour with support in the `ConsumerSupervisor` https://github.com/nats-io/nats.ex/compare/1b1adc85e4b28231218ef87c7fc3445fce854377...b24a7e14325b51fbb93fde7e3d891d18b4fa8afb
* avoid logging sensitive credentials https://github.com/nats-io/nats.ex/pull/105
* deprecate Gnat.ping, improved typespecs https://github.com/nats-io/nats.ex/pull/103 
* relax the version constraint on nimble_parsec https://github.com/nats-io/nats.ex/issues/112

## 1.1

* add support for nkeys and NGS https://github.com/nats-io/nats.ex/pull/101
* Fix supervisor ConsumerSuperivsor crash https://github.com/nats-io/nats.ex/pull/96

## 1.0

* Make supervisors officially supported https://github.com/nats-io/nats.ex/pull/96

## 0.7.0

* update to telemetry 0.4 https://github.com/nats-io/nats.ex/pull/86 and https://github.com/nats-io/nats.ex/pull/87
* support for token authentication https://github.com/nats-io/nats.ex/pull/92
* support elixir 1.9 https://github.com/nats-io/nats.ex/pull/93

## 0.6.0

* Dropped support for Erlang < 19 and Elixir <= 1.5
* Added Telemetry to the project (thanks @rubysolo)
* Switched to nimble_parsec for parsing
  * Updated benchmarking/performance information. We can now do 170k requests per second on a 16-core server.
* Fixed a bug around re-subscribing for the `ConsumerSupervisor`
* Pass `sid` when delivering message (thanks @entone)
* Documentation fixes from @deini and @johannestroeger

## 0.5.0

* Dropped support for Elixir 1.4 and OTP 18 releases. You will need to use Elixir 1.5+ and OTP 19+.
* Switched to running our tests against gnatsd `1.3.0`


================================================
FILE: LICENSE.txt
================================================
Copyright 2017 Michael Ries

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: MAINTAINERS.md
================================================
# Maintainers

Maintainership is on a per project basis.

### Maintainers
  - Colin Sullivan <colin@nats.io> [@ColinSullivan1](https://github.com/ColinSullivan1)
  - Michael Ries <riesmmm@gmail.com> [@mmmries](https://github.com/mmmries)
  - Kevin Hoffman <alothien@gmail.com> [@autodidaddict](https//github.com/autodidaddict)


================================================
FILE: README.md
================================================
[![hex.pm](https://img.shields.io/hexpm/v/gnat.svg)](https://hex.pm/packages/gnat)
[![hex.pm](https://img.shields.io/hexpm/dt/gnat.svg)](https://hex.pm/packages/gnat)
[![hex.pm](https://img.shields.io/hexpm/l/gnat.svg)](https://hex.pm/packages/gnat)
[![github.com](https://img.shields.io/github/last-commit/nats-io/nats.ex.svg)](https://github.com/nats-io/nats.ex)

![NATS](https://nats.io/img/logos/nats-horizontal-color.png)

# Gnat

A [nats.io](https://nats.io/) client for Elixir.
The goals of the project are resiliency, performance, and ease of use.

> Hex documentation available here: https://hex.pm/packages/gnat

## Usage

``` elixir
{:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222})
# Or if the server requires TLS you can start a connection with:
# {:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, tls: true})

{:ok, subscription} = Gnat.sub(gnat, self(), "pawnee.*")
:ok = Gnat.pub(gnat, "pawnee.news", "Leslie Knope recalled from city council (Jammed)")
receive do
  {:msg, %{body: body, topic: "pawnee.news", reply_to: nil}} ->
    IO.puts(body)
end
```

## Authentication

``` elixir
# with user and password
{:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, username: "joe", password: "123", auth_required: true})

# with token
{:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, token: "secret", auth_required: true})

# with an nkey seed
{:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, nkey_seed: "SUAM...", auth_required: true})

# with decentralized user credentials (JWT)
{:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, nkey_seed: "SUAM...", jwt: "eyJ0eX...", auth_required: true})

# connect to NGS with JWT
{:ok, gnat} = Gnat.start_link(%{host: "connect.ngs.global", tls: true, jwt: "ey...", nkey_seed: "SUAM..."})
```

## TLS Connections

[NATS Server](https://github.com/nats-io/nats-server) is often configured to accept or require TLS connections.
In order to connect to these clusters you'll want to pass some extra TLS settings to your `Gnat` connection.

``` elixir
# using a basic TLS connection
{:ok, gnat} = Gnat.start_link(%{host: "127.0.0.1", port: 4222, tls: true})

# Passing a Client Certificate for verification
{:ok, gnat} = Gnat.start_link(%{tls: true, ssl_opts: [certfile: "client-cert.pem", keyfile: "client-key.pem"]})
```

## Resiliency

If you would like to stay connected to a cluster of nats servers, you should consider using `Gnat.ConnectionSupervisor` .
This can be added to your supervision tree in your project and will handle automatically re-connecting to the cluster.

For long-lived subscriptions consider using `Gnat.ConsumerSupervisor` .
This can also be added to your supervision tree and use a supervised connection to re-establish a subscription.
It also handles details like handling each message in a supervised process so you isolate failures and get OTP logs when an unexpected error occurs.

## Services
If you supply a module that implements the `Gnat.Services.Server` behavior and the `service_definition` 
configuration field to a `Gnat.ConsumerSupervisor`, then this client will automatically take care
of exposing the service to discovery, responding to pings, and maintaining and exposing statistics like request and error counts, and processing times.

## Instrumentation

Gnat uses [telemetry](https://hex.pm/packages/telemetry) to make instrumentation data available to clients.
If 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:

``` elixir
iex(1)> metrics_function = fn(event_name, measurements, event_meta, config) ->
  IO.inspect([event_name, measurements, event_meta, config])
  :ok
end
#Function<4.128620087/4 in :erl_eval.expr/5>
iex(2)> names = [[:gnat, :pub], [:gnat, :sub], [:gnat, :message_received], [:gnat, :request], [:gnat, :unsub]]
[
  [:gnat, :pub],
  [:gnat, :sub],
  [:gnat, :message_received],
  [:gnat, :request],
  [:gnat, :unsub],
  [:gnat, :service_request],
  [:gnat, :service_error]
]
iex(3)> :telemetry.attach_many("my listener", names, metrics_function, %{my_config: true})
:ok
iex(4)> {:ok, gnat} = Gnat.start_link()
{:ok, #PID<0.203.0>}
iex(5)> Gnat.sub(gnat, self(), "topic")
[[:gnat, :sub], %{latency: 128000}, %{topic: "topic"}, %{my_config: true}]
{:ok, 1}
iex(6)> Gnat.pub(gnat, "topic", "ohai")
[[:gnat, :pub], %{latency: 117000}, %{topic: "topic"}, %{my_config: true}]
[[:gnat, :message_received], %{count: 1}, %{topic: "topic"}, %{my_config: true}]
:ok
```

The `pub` , `sub` , `request` , and `unsub` events all report the latency of those respective calls.
The `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.

All of the events (except `unsub` ) include metadata with a `:topic` key so you can split your metrics by topic.

## Benchmarks

Part of the motivation for building this library is to get better performance.
To 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.

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

The `bench/*.exs` files also contain some straight-line single-CPU performance tests.
As of this commit my 2018 MacBook pro shows.

|               | ips      | average   | deviation | median |
| ------------- | -------- | --------- | --------- | ------ |
| parse-128     | 487.67 K | 2.19 μs   | ±1701.54% | 2 μs   |
| pub - 128     | 96.37 K  | 10.38 μs  | ±102.94%  | 10 μs  |
| req-reply-128 | 8.32 K   | 120.16 μs | ±23.68%   | 114 μs |

## Development

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

We currently use version `2.10.24` in CI, but anything higher than `2.2.0` should be fine.
Versions from `0.9.6` up to `2.2.0` should work fine for everything except header support.
Make 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`).
The typical `mix test` will run all the basic unit tests.

You can also run the `multi_server` set of tests that test connectivity to different
`nats-server` configurations. You can run these with `mix test --only multi_server` .
The tests will tell you how to start the different configurations.

There are also some property-based tests that generate a lot of test cases.
You can tune how many test cases by setting the environment variable `N=200 mix test --only property` (default it 100).

For more details you can look at how Github runs these things in the CI flow.


================================================
FILE: bench/client.exs
================================================
Application.put_env(:client, :num_connections, 4)
num_requesters = 16
requests_per_requester = 500

defmodule Client do
  require Logger

  def setup(id) do
    num_connections = Application.get_env(:client, :num_connections)
    partition = rem(id, num_connections)
    String.to_atom("gnat#{partition}")
  end

  def send_request(gnat, request) do
    {:ok, _} = Gnat.request(gnat, "echo", request)# |> IO.inspect
  end

  def send_requests(gnat, how_many, request) do
    :lists.seq(1, how_many)
    |> Enum.each(fn(_) ->
      {micro_seconds, _result} = :timer.tc(fn() -> send_request(gnat, request) end)
      Benchmark.record_rpc_time(micro_seconds)
    end)
  end
end

defmodule Benchmark do
  def benchmark(num_actors, requests_per_actor, request) do
    {:ok, _pid} = Agent.start_link(fn -> [] end, name: __MODULE__)
    {total_micros, _result} = time_benchmark(num_actors, requests_per_actor, request)
    total_requests = num_actors * requests_per_actor
    total_bytes = total_requests * byte_size(request) * 2
    print_statistics(total_requests, total_bytes, total_micros)
    Agent.stop(__MODULE__, :normal)
  end

  def record_rpc_time(micro_seconds) do
    Agent.update(__MODULE__, fn(list) -> [micro_seconds | list] end)
  end

  def print_statistics(total_requests, total_bytes, total_micros) do
    total_seconds = total_micros / 1_000_000.0
    req_throughput = total_requests / total_seconds
    kilobyte_throughput = total_bytes / 1024 / total_seconds
    IO.puts "It took #{total_seconds}sec"
    IO.puts "\t#{req_throughput} req/sec"
    IO.puts "\t#{kilobyte_throughput} kb/sec"
    Agent.get(__MODULE__, fn(list_of_rpc_times) ->
      tc_l = list_of_rpc_times
      tc_n = Enum.count(list_of_rpc_times)
      tc_min = :lists.min(tc_l)
      tc_max = :lists.max(tc_l)
      sorted = :lists.sort(tc_l)
      tc_med = :lists.nth(round(tc_n * 0.5), sorted)
      tc_90th = :lists.nth(round(tc_n * 0.9), sorted)
      tc_avg = round(Enum.sum(tc_l) / tc_n)
      IO.puts "\tmin: #{tc_min}µs"
      IO.puts "\tmax: #{tc_max}µs"
      IO.puts "\tmedian: #{tc_med}µs"
      IO.puts "\t90th percentile: #{tc_90th}µs"
      IO.puts "\taverage: #{tc_avg}µs"
      IO.puts "\t#{tc_min},#{tc_max},#{tc_med},#{tc_90th},#{tc_avg},#{req_throughput},#{kilobyte_throughput}"
    end)
  end

  def time_benchmark(num_actors, requests_per_actor, request) do
    :timer.tc(fn() ->
      (1..num_actors) |> Enum.map(fn(i) ->
        parent = self()
        spawn(fn() ->
          gnat = Client.setup(i)
          #IO.puts "starting requests #{i}"
          Client.send_requests(gnat, requests_per_actor, request)
          #IO.puts "done with requests #{i}"
          send parent, :ack
        end)
      end)
      wait_for_times(num_actors)
    end)
  end

  def wait_for_times(0), do: :done
  def wait_for_times(n) do
    receive do
      :ack ->
        wait_for_times(n-1)
    end
  end
end

num_connections = Application.get_env(:client, :num_connections)
Enum.each(0..(num_connections - 1), fn(i) ->
  name = :"gnat#{i}"
  {:ok, _pid} = Gnat.start_link(%{}, name: name)
end)
:timer.sleep(500) # let the connections get started

#request = "ping"
request = :crypto.strong_rand_bytes(16)
Benchmark.benchmark(num_requesters, requests_per_requester, request)


================================================
FILE: bench/kv_consume.exs
================================================
# bench/kv_consume.exs
#
# Compares three approaches for consuming all messages from a KV bucket into ETS:
#
#   1. Pager (batch 500, ack_policy: :all) — fetch a page, process, ack last, repeat
#   2. Pull + ack_next pipeline (initial batch 500, ack_policy: :explicit) — prime the
#      pipeline with a batch request, then ack_next each message to keep flow continuous
#   3. PullConsumer with batch_size (ack_policy: :all) — the new batch mode using the
#      actual PullConsumer behaviour, batches messages and acks only the last per batch
#
# Prerequisites:
#   - NATS server with JetStream enabled: nats-server -js
#   - Run with: mix run bench/kv_consume.exs
#
# Optional env vars:
#   - BENCH_COUNT: number of messages (default 100000)
#   - BENCH_BATCH: batch size (default 500)
#   - BENCH_TIME: seconds per scenario (default 60)

Logger.configure(level: :warning)

require Logger
Logger.configure(level: :warning)

alias Gnat.Jetstream.API.{Consumer, KV}
alias Gnat.Jetstream.API.Util

defmodule BenchBatchPullConsumer do
  use Gnat.Jetstream.PullConsumer

  def start(args) do
    Gnat.Jetstream.PullConsumer.start(__MODULE__, args)
  end

  @impl true
  def init(%{tab: tab, notify: pid, expected: expected, batch_size: batch_size}) do
    consumer = %Consumer{
      stream_name: "KV_BENCH_KV",
      ack_policy: :all,
      ack_wait: 30_000_000_000,
      deliver_policy: :all,
      replay_policy: :instant
    }

    {:ok, %{tab: tab, notify: pid, expected: expected, received: 0},
     connection_name: :gnat_bench, consumer: consumer, batch_size: batch_size}
  end

  @impl true
  def handle_message(message, state) do
    :ets.insert(state.tab, {message.topic, message.body})
    received = state.received + 1

    if received >= state.expected do
      send(state.notify, {:done, received})
    end

    {:ack, %{state | received: received}}
  end
end

defmodule KVConsumeBench do
  @bucket "BENCH_KV"
  @stream "KV_BENCH_KV"
  @value_size 64

  def setup(conn, count) do
    # Clean up previous state
    KV.delete_bucket(conn, @bucket)
    :timer.sleep(500)

    {:ok, _} = KV.create_bucket(conn, @bucket, history: 1)

    IO.puts("Populating #{count} messages (#{@value_size} byte values)...")
    start = System.monotonic_time(:millisecond)

    Enum.each(1..count, fn i ->
      key = "key.#{String.pad_leading(Integer.to_string(i), 7, "0")}"
      value = :crypto.strong_rand_bytes(@value_size) |> Base.encode64()
      :ok = KV.put_value(conn, @bucket, key, value)

      if rem(i, 10_000) == 0 do
        elapsed = System.monotonic_time(:millisecond) - start
        rate = round(i / elapsed * 1000)
        IO.puts("  #{i}/#{count} (#{rate} msg/s)")
      end
    end)

    elapsed = System.monotonic_time(:millisecond) - start
    IO.puts("Setup complete: #{count} messages in #{div(elapsed, 1000)}s\n")
  end

  # ---------------------------------------------------------------------------
  # Strategy 1: Pager (ack_policy: :all, batch fetch, ack last per page)
  # ---------------------------------------------------------------------------
  def pager_consume(conn, batch_size) do
    tab = :ets.new(:pager_cache, [:set])

    {:ok, _} =
      Gnat.Jetstream.Pager.reduce(conn, @stream, [batch: batch_size], nil, fn msg, acc ->
        :ets.insert(tab, {msg.topic, msg.body})
        acc
      end)

    count = :ets.info(tab, :size)
    :ets.delete(tab)
    count
  end

  # ---------------------------------------------------------------------------
  # Strategy 2: Pull + ack_next pipeline (ack_policy: :explicit, continuous)
  # ---------------------------------------------------------------------------
  def pull_ack_next_consume(conn, batch_size) do
    {:ok, consumer_info} =
      Consumer.create(conn, %Consumer{
        stream_name: @stream,
        ack_policy: :explicit,
        deliver_policy: :all,
        replay_policy: :instant,
        inactive_threshold: 30_000_000_000
      })

    total = consumer_info.num_pending
    inbox = Util.reply_inbox()
    {:ok, sub} = Gnat.sub(conn, self(), inbox)

    :ok =
      Consumer.request_next_message(
        conn,
        @stream,
        consumer_info.name,
        inbox,
        nil,
        batch: batch_size,
        no_wait: true
      )

    tab = :ets.new(:pull_cache, [:set])
    receive_with_ack_next(sub, inbox, tab, 0, total)

    count = :ets.info(tab, :size)
    :ets.delete(tab)

    Gnat.unsub(conn, sub)
    Consumer.delete(conn, @stream, consumer_info.name)

    count
  end

  @terminals ["404", "408"]

  defp receive_with_ack_next(_sub, _inbox, _tab, total, total), do: :ok

  defp receive_with_ack_next(sub, inbox, tab, count, total) do
    receive do
      {:msg, %{sid: ^sub, status: status}} when status in @terminals ->
        receive_with_ack_next(sub, inbox, tab, count, total)

      {:msg, %{sid: ^sub, reply_to: nil}} ->
        receive_with_ack_next(sub, inbox, tab, count, total)

      {:msg, %{sid: ^sub} = message} ->
        :ets.insert(tab, {message.topic, message.body})

        if count + 1 < total do
          Gnat.Jetstream.ack_next(message, inbox)
        else
          Gnat.Jetstream.ack(message)
        end

        receive_with_ack_next(sub, inbox, tab, count + 1, total)
    after
      30_000 ->
        IO.puts("WARNING: timeout after receiving #{count}/#{total} messages")
        :timeout
    end
  end

  # ---------------------------------------------------------------------------
  # Strategy 3: PullConsumer with batch_size (ack_policy: :all, batch mode)
  #
  # Uses the actual PullConsumer behaviour with the new batch_size option.
  # This is the real-world usage pattern we want to validate.
  # ---------------------------------------------------------------------------
  def batch_pull_consumer_consume(expected, batch_size) do
    tab = :ets.new(:batch_pc_cache, [:set, :public])

    {:ok, pid} =
      BenchBatchPullConsumer.start(%{
        tab: tab,
        notify: self(),
        expected: expected,
        batch_size: batch_size
      })

    receive do
      {:done, _received} -> :ok
    after
      60_000 ->
        IO.puts("WARNING: PullConsumer timeout")
    end

    count = :ets.info(tab, :size)
    Gnat.Jetstream.PullConsumer.close(pid)
    :ets.delete(tab)
    count
  end
end

# -- Configuration -----------------------------------------------------------

count = String.to_integer(System.get_env("BENCH_COUNT", "100000"))
batch = String.to_integer(System.get_env("BENCH_BATCH", "500"))
time = String.to_integer(System.get_env("BENCH_TIME", "60"))

IO.puts("""
KV Consume Benchmark
====================
Messages:   #{count}
Batch size: #{batch}
Time/scenario: #{time}s
""")

# -- Setup --------------------------------------------------------------------

# Named connection for the PullConsumer
conn_settings = %{
  name: :gnat_bench,
  backoff_period: 1_000,
  connection_settings: [%{host: '127.0.0.1', port: 4222}]
}

{:ok, _} = Gnat.ConnectionSupervisor.start_link(conn_settings)
:timer.sleep(500)

# Direct connection for Pager and manual pull
{:ok, conn} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})

KVConsumeBench.setup(conn, count)

# -- Verify all approaches produce correct results ----------------------------

IO.puts("Verifying approaches...")

count1 = KVConsumeBench.pager_consume(conn, batch)
IO.puts("  Pager:              #{count1} entries")

count2 = KVConsumeBench.pull_ack_next_consume(conn, batch)
IO.puts("  Pull+ack_next:      #{count2} entries")

count3 = KVConsumeBench.batch_pull_consumer_consume(count, batch)
IO.puts("  Batch PullConsumer: #{count3} entries")

expected_counts = [count1, count2, count3]

if Enum.any?(expected_counts, &(&1 != count)) do
  IO.puts("\nERROR: expected #{count} entries from each approach")
  Gnat.stop(conn)
  System.halt(1)
end

IO.puts("\nAll approaches verified. Starting benchmark...\n")

# -- Benchmark ---------------------------------------------------------------

Benchee.run(
  %{
    "pager (batch #{batch})" => fn ->
      KVConsumeBench.pager_consume(conn, batch)
    end,
    "pull+ack_next (initial batch #{batch})" => fn ->
      KVConsumeBench.pull_ack_next_consume(conn, batch)
    end,
    "batch_pull_consumer (batch #{batch})" => fn ->
      KVConsumeBench.batch_pull_consumer_consume(count, batch)
    end
  },
  time: time,
  warmup: 0,
  memory_time: 0,
  formatters: [{Benchee.Formatters.Console, comparisons: true}]
)

Gnat.stop(conn)


================================================
FILE: bench/parse.exs
================================================
msg1024 = :crypto.strong_rand_bytes(1024)
msg128  = :crypto.strong_rand_bytes(128)
msg16   = :crypto.strong_rand_bytes(16)

inputs = %{
  "16 byte" => "MSG topic 1 16\r\n#{msg16}\r\n",
  "128 byte" => "MSG topic 1 128\r\n#{msg128}\r\n",
  "1024 byte" => "MSG topic 1 1024\r\n#{msg1024}\r\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"
}

parsec = Gnat.Parsec.new()
Benchee.run(%{
  "parsec" => fn(tcp_packet) -> {_parse, [_msg]} = Gnat.Parsec.parse(parsec, tcp_packet) end,
}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])


================================================
FILE: bench/publish.exs
================================================
inputs = %{
  "16 byte" => :crypto.strong_rand_bytes(16),
  "128 byte" => :crypto.strong_rand_bytes(128),
  "1024 byte" => :crypto.strong_rand_bytes(1024),
}

{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})

Benchee.run(%{
  "pub" => fn(msg) -> :ok = Gnat.pub(client_pid, "echo", msg) end,
}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])


================================================
FILE: bench/request.exs
================================================
defmodule EchoServer do
  def run(gnat) do
    spawn(fn -> init(gnat) end)
  end

  def init(gnat) do
    Gnat.sub(gnat, self(), "echo")
    loop(gnat)
  end

  def loop(gnat) do
    receive do
      {:msg, %{topic: "echo", reply_to: reply_to, body: msg}} ->
        Gnat.pub(gnat, reply_to, msg)
      other ->
        IO.puts "server received: #{inspect other}"
    end

    loop(gnat)
  end
end

{:ok, server_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})
EchoServer.run(server_pid)

inputs = %{
  "16 byte" => :crypto.strong_rand_bytes(16),
  "128 byte" => :crypto.strong_rand_bytes(128),
  "1024 byte" => :crypto.strong_rand_bytes(1024),
}

{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})

Benchee.run(%{
  "request" => fn(msg) -> {:ok, %{body: _}} = Gnat.request(client_pid, "echo", msg) end,
}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])


================================================
FILE: bench/request_multi.exs
================================================
defmodule EchoServer do
  def run(gnat) do
    spawn(fn -> init(gnat) end)
  end

  def init(gnat) do
    Gnat.sub(gnat, self(), "echo")
    loop(gnat)
  end

  def loop(gnat) do
    receive do
      {:msg, %{topic: "echo", reply_to: reply_to, body: msg}} ->
        Gnat.pub(gnat, reply_to, msg)
      other ->
        IO.puts "server received: #{inspect other}"
    end

    loop(gnat)
  end
end

{:ok, server_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})
# run 3 servers to get 3 responses
EchoServer.run(server_pid)
EchoServer.run(server_pid)
EchoServer.run(server_pid)

inputs = %{
  "16 byte" => :crypto.strong_rand_bytes(16),
  "128 byte" => :crypto.strong_rand_bytes(128),
  "1024 byte" => :crypto.strong_rand_bytes(1024),
}

{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})

Benchee.run(%{
  "request_multi" => fn(msg) -> {:ok, [%{body: _}, %{}, %{}]} = Gnat.request_multi(client_pid, "echo", msg, max_messages: 3) end,
}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])


================================================
FILE: bench/server.exs
================================================
num_connections = 4
num_subscribers = 4

Enum.each(0..(num_connections - 1), fn(i) ->
  name = :"gnat#{i}"
  {:ok, _pid} = Gnat.start_link(%{}, name: name)
end)

Enum.each(0..(num_subscribers - 1), fn(i) ->
  name = :"consumer#{i}"
  conn_name = :"gnat#{rem(i, num_connections)}"
  IO.puts "#{name} will use #{conn_name}"
  {:ok, _pid} = Gnat.ConsumerSupervisor.start_link(%{connection_name: conn_name, consuming_function: {EchoServer, :handle}, subscription_topics: [%{topic: "echo", queue_group: "echo"}]})
end)

defmodule EchoServer do
  def handle(%{body: body, reply_to: reply_to, gnat: gnat_pid}) do
    Gnat.pub(gnat_pid, reply_to, body)
  end

  def wait_loop do
    :timer.sleep(1_000)
    wait_loop()
  end
end

EchoServer.wait_loop()


================================================
FILE: bench/service_bench.exs
================================================
defmodule EchoService do
  use Gnat.Services.Server

  def request(%{body: body}, "echo", _group) do
    {:reply, body}
  end

  def definition do
    %{
      name: "echo",
      description: "This is an example service",
      version: "0.0.1",
      endpoints: [
        %{
          name: "echo",
          group_name: "mygroup",
        }
      ]
    }
  end
end

conn_supervisor_settings = %{
  name: :gnat, # (required) the registered named you want to give the Gnat connection
  backoff_period: 1_000, # number of milliseconds to wait between consecutive reconnect attempts (default: 2_000)
  connection_settings: [
    %{host: '127.0.0.1', port: 4222},
  ]
}
{:ok, _pid} = Gnat.ConnectionSupervisor.start_link(conn_supervisor_settings)

# let the connection get established
:timer.sleep(100)

consumer_supervisor_settings = %{
  connection_name: :gnat,
  module: EchoService, # a module that implements the Gnat.Services.Server behaviour
  service_definition: EchoService.definition()
}

{:ok, _pid} = Gnat.ConsumerSupervisor.start_link(consumer_supervisor_settings)

# wait for the connection and consumer to be ready
:timer.sleep(2000)

{:ok, client_pid} = Gnat.start_link(%{host: '127.0.0.1', port: 4222})

inputs = %{
  "16 byte" => :crypto.strong_rand_bytes(16),
  "256 byte" => :crypto.strong_rand_bytes(256),
  "1024 byte" => :crypto.strong_rand_bytes(1024),
}

Benchee.run(%{
  "service" => fn(msg) -> {:ok, %{body: ^msg}} = Gnat.request(client_pid, "mygroup.echo", msg) end,
}, time: 10, parallel: 1, inputs: inputs, formatters: [{Benchee.Formatters.Console, comparisons: false}])


================================================
FILE: dependencies.md
================================================
# Project Dependencies

This 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.
This list of dependencies was produced on October 13 2023.

| Dependency | License |
|-|-|
| ed25519 1.4.0 | MIT |
| cowlib 2.11.0 | ISC |
| deep_merge 1.0.0 | MIT |
| jason 1.2.2  | Apache 2.0 |
| nimble_parsec 1.2.0  | Apache 2.0 |
| nkeys 0.2.1  | MIT |
| telemetry 1.0.0 (rebar3) | Apache 2.0 |

# Development Dependencies

This is a list of dependencies used to build and test this library.

| Dependency | License |
|-|-|
| propcheck 1.4.1 | GPL 3.0 |
| proper 1.4.0 | GPL 3.0 |
| erlex 0.2.6  | Apache 2.0 |
| makeup 1.0.5  | BSD |
| makeup_elixir 0.15.2 | BSD |
| makeup_erlang 0.1.1 | BSD |
| deep_merge 0.2.0  | MIT |
| benchee 1.0.1  | MIT |
| dialyxir 1.1.0 | Apache 2.0 |
| earmark 1.3.1  | Apache 2.0 |
| makeup_elixir 0.13.0  | BSD |
| earmark_parser 1.4.18 | Apache-2.0 |
| ex_doc 0.26.0  | Apache 2.0  |


================================================
FILE: docs/js/guides/broadway.md
================================================
# Using Broadway with Jetstream

Broadway is a library which allows building concurrent and multi-stage data ingestion and data
processing pipelines with Elixir easily. You can learn about it more in
[Broadway documentation](https://hexdocs.pm/broadway/introduction.html).

Jetstream library comes with tools necessary to use NATS Jetstream with Broadway.

## Getting started

In order to use Broadway with NATS Jetstream you need to:

1. Setup a NATS Server with JetStream turned on
2. Create stream and consumer on NATS server
3. Configure Gnat connection in your Elixir project
4. Configure your project to use Broadway

In this guide, we are going to focus on the fourth point. To learn how to start Jetstream locally
with Docker Compose and then add Gnat and Jetstream to your application, see the Starting Jetstream
section in [Getting Started guide](../introduction/getting_started.md).

### Adding Broadway to your application

Once we have NATS with JetStream running and the stream and consumer we are going to use are
created, we can proceed to adding Broadway to our project. First, put `:broadway` to the list of
dependencies in `mix.exs`.

```elixir
defp deps do
  [
    ...
    {:broadway, ...version...},
    ...
  ]
end
```

Visit [Broadway page on Hex.pm](https://hex.pm/packages/broadway) to check for current version
to put in `deps`.

To install the dependencies, run:

```shell
mix deps.get
```

### Defining the pipeline configuration

The next step is to define your Broadway module. We need to implement three functions in order
to define a Broadway pipeline: `start_link/1`, `handle_message/3` and `handle_batch/4`.
Let's create `start_link/1` first:

```elixir
defmodule MyBroadway do
  use Broadway

  alias Broadway.Message

  def start_link(_opts) do
    Broadway.start_link(
      __MODULE__,
      name: MyBroadwayExample,
      producer: [
        module: {
          OffBroadway.Jetstream.Producer,
          connection_name: :gnat,
          stream_name: "TEST_STREAM",
          consumer_name: "TEST_CONSUMER"
        },
        concurrency: 10
      ],
      processors: [
        default: [concurrency: 10]
      ],
      batchers: [
        default: [
          concurrency: 5,
          batch_size: 10,
          batch_timeout: 2_000
        ]
      ]
      ...
    )
  end

  ...callbacks..
end
```

All `start_link/1` does is just delegating to `Broadway.start_link/2`.

To understand what all these options mean and to learn about other possible settings, visit
[Broadway documentation](https://hexdocs.pm/broadway/Broadway.html).

The part that interests us the most in this guide is the `producer.module`. Here we're choosing
`OffBroadway.Jetstream.Producer` as the producer module and passing the connection options,
such as Gnat process name and stream name. For full list of available options, visit
[Producer](`OffBroadway.Jetstream.Producer`) documentation.

### Implementing Broadway callbacks

Broadway requires some callbacks to be implemented in order to process messages. For full list
of available callbacks visit
[Broadway documentation](https://hexdocs.pm/broadway/Broadway.html#callbacks).

A simple example:

```elixir
defmodule MyBroadway do
  use Broadway

  alias Broadway.Message

  ...start_link...

  def handle_message(_processor_name, message, _context) do
    message
    |> Message.update_data(&process_data/1)
    |> case do
      "FOO" -> Message.configure_ack(on_success: :term)
      "BAR" -> Message.configure_ack(on_success: :nack)
      message -> message
    end
  end

  defp process_data(data) do
    String.upcase(data)
  end

  def handle_batch(_, messages, _, _) do
    list = messages |> Enum.map(fn e -> e.data end)
    IO.puts("Got a batch: #{inspect(list)}. Sending acknowledgements...")
    messages
  end
```

First, in `handle_message/3` we update our messages' data individually by converting them to
uppercase. Then, in the same callback, we're changing the success ack option of the message
to `:term` if its content is `"FOO"` or to `:nack` if the message is `"BAR"`. In the end we
print each batch in `handle_batch/4`. It's not quite useful but should be enough for this
guide.

## Running the Broadway pipeline

Once we have our pipeline fully defined, we need to add it as a child in the supervision tree.
Most applications have a supervision tree defined at `lib/my_app/application.ex`.

```elixir
children = [
  {MyBroadway, []}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

You can now test the pipeline. Let's start the application:

```shell
iex -S mix
```

Use Gnat API to send messages to your stream:

```elixir
Gnat.pub(:gnat, "test_subject", "foo")
Gnat.pub(:gnat, "test_subject", "bar")
Gnat.pub(:gnat, "test_subject", "baz")
```

Batcher should then print:

```
Got a batch: ["FOO", "BAR", "BAZ"]. Sending acknowledgements...
```

================================================
FILE: docs/js/guides/managing.md
================================================
# Managing Streams and Consumers

Jetstream provides a JSON API for managing streams and consumers.
This library exposes this API via interactions with the `Jetstream.Api.Stream` and `Jetstream.Api.Consumer` modules.

These modules act as native wrappers for the API and do not attempt to simplify any of the common use-cases.
As 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.

================================================
FILE: docs/js/guides/push_based_consumer.md
================================================
# Push based consumer

```elixir
# Start a nats server with jetstream enabled and default configs
# Now run the following snippets in an IEx terminal
alias Jetstream.API.{Consumer,Stream}

# Setup a connection to the nats server and create the stream/consumer
# This is the equivalent of these two nats cli commands
#   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
#   nats consumer add TEST TEST --target consumer.greetings --replay instant --deliver=all --ack all --wait=5s --filter="" --max-deliver=10
{:ok, connection} = Gnat.start_link()
stream = %Stream{name: "TEST", subjects: ["greetings"]}
{:ok, _response} = Stream.create(connection, stream)
consumer = %Consumer{stream_name: "TEST", name: "TEST", deliver_subject: "consumer.greetings", ack_wait: 5_000_000_000, max_deliver: 10}
{:ok, _response} = Consumer.create(connection, consumer)

# Setup Consuming Function
defmodule Subscriber do
  def handle(msg) do
    IO.inspect(msg)
    case msg.body do
      "hola" -> Jetstream.ack(msg)
      "bom dia" -> Jetstream.nack(msg)
      _ -> nil
    end
  end
end

# normally you would add the `ConnectionSupervisor` and `ConsumerSupervisor` to your supervisrion tree
# here we start them up manually in an IEx session
{:ok, _pid} = Gnat.ConnectionSupervisor.start_link(%{
  name: :gnat,
  backoff_period: 4_000,
  connection_settings: [
    %{}
  ]
})
{:ok, _pid} = Gnat.ConsumerSupervisor.start_link(%{
  connection_name: :gnat,
  consuming_function: {Subscriber, :handle},
  subscription_topics: [
    %{topic: "consumer.greetings"}
  ]
})

# now publish some messages into the stream
Gnat.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
Gnat.pub(:gnat, "greetings", "hola") # an ack is sent back so this will only be received once
Gnat.pub(:gnat, "greetings", "bom dia") # a -NAK is sent back so you'll see this received 10 times very quickly
```

================================================
FILE: docs/js/introduction/getting_started.md
================================================
# Getting Started

In this guide, we're going to learn how to install Jetstream in your project and start consuming
messages from your streams.

## Starting Jetstream

The following Docker Compose file will do the job:

```yaml
version: "3"
services:
  nats:
    image: nats:latest
    command:
      - -js
    ports:
      - 4222:4222
```

Save this snippet as `docker-compose.yml` and run the following command:

```shell
docker compose up -d
```

Let's also create Jetstream stream where we will publish our hello world messages:

```shell
nats stream add HELLO --subjects="greetings"
```

> #### Tip {: .tip}
>
> You can also manage Jetstream streams and consumers via Elixir. You can see more details in
> [this guide](../guides/managing.md).

## Adding Jetstream and Gnat to an application

To start off with, we'll generate a new Elixir application by running this command:

```
mix new hello_jetstream --sup
```

We need to have [a supervision tree](http://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html)
up and running in your app, and the `--sup` option ensures that.

To add Jetstream to this application, you need to add [Jetstream](https://hex.pm/packages/jetstream)
and [Gnat](https://hex.pm/packages/gnat) libraries to your `deps` definition in our `mix.exs` file.
**Fill exact version requirements from each package Hex.pm pages.**

```elixir
defp deps do
  [
    {:gnat, ...},
    {:jetstream, ...}
  ]
end
```

To install these dependencies, we will run this command:

```shell
mix deps.get
```

Now let's connect to our NATS server. To do this, you need to start `Gnat.ConnectionSupervisor`
under our application's supervision tree. Add following to `lib/hello_jetstream/application.ex`:

```elixir
def start(_type, _args) do
  children = [
    ...

    # Create NATS connection
    {Gnat.ConnectionSupervisor,
      %{
        name: :gnat,
        connection_settings: [
          %{host: "localhost", port: 4222}
        ]
      }},
  ]

  ...
```

This piece of configuration will start Gnat processes that connect to the NATS server and allow
publishing and subscribing to any subjects. Jetstream operates using plain NATS subjects which
follow specific naming and message format conventions.

Let's now create a _pull consumer_ which will subscribe a specific Jetstream stream and print
incoming messages to standard output.

## Creating a pull consumer

Jetstream requires us to allocate a view/cursor of the stream that our consumer will operate on.
In Jetstream terminology, this view is called a _consumer_ (Funnily enough we've just implemented
a consumer in our code, coincidence?). [Jetstream](https://docs.nats.io/nats-concepts/jetstream/consumers)
[documentation](https://docs.nats.io/nats-concepts/jetstream/consumers/example_configuration)
offers great insights on benefits of having this separate concept so we won't duplicate work here.

Jetstream offers two stream consuming modes: _push_ and _pull_.

In _push_ mode, Jetstream will simply send messages to selected consumers immediately when they are
received. This approach does offer congestion control, so it is not recommended for high-volume
and/or reliability sensitive streams. You do not really need this library to implement push
consumer because all building blocks are in `Gnat` library. You can read more about push consumers
in [this guide](../guides/push_based_consumer.md).

On the other hand, in _pull_ mode consumers ask Jetstream for more messages when they are ready
to process them. This is the recommended approach for most use cases and we will proceed with it
in this guide.

> #### This is just a brief outline {: .tip}
>
> For more details about differences between consumer modes, consult
> [Jetstream documentation](https://docs.nats.io/nats-concepts/jetstream/consumers).

Let's create a pull consumer module within our application at
`lib/hello_jetstream/logger_pull_consumer.ex`:

```elixir
defmodule HelloJetstream.LoggerPullConsumer do
  use Jetstream.PullConsumer

  def start_link([]) do
    Jetstream.PullConsumer.start_link(__MODULE__, [])
  end

  @impl true
  def init([]) do
    {:ok, nil, connection_name: :gnat, stream_name: "HELLO", consumer_name: "LOGGER"}
  end

  @impl true
  def handle_message(message, state) do
    IO.inspect(message)
    {:ack, state}
  end
end
```

Pull Consumer is a regular `GenServer` and it takes a reference to `Gnat.ConnectionSupervisor`
along with names of Jetstream stream and consumer as options passed to
`Jetstream.PullConsumer.start*` functions. These options are passed as keyword list in third element
of tuple returned from the `c:Jetstream.PullConsumer.init/1` callback.

The only required callbacks are well known gen server's `c:Jetstream.PullConsumer.init/1` and
`c:Jetstream.PullConsumer.handle_message/2`, which takes new message as its first argument and
is expected to return an _ACK action_ instructing underlying process loop what to do with this
message. Here we are asking it to automatically send for us an ACK message back to Jetstream.

Let's now create a consumer in our NATS server. We will call it `LOGGER` as we plan to let it simply
log everything published to the stream.

```shell
nats consumer add --pull --deliver=all HELLO LOGGER
```

Now, let's start our pull consumer under application's supervision tree.

```elixir
def start(_type, _args) do
  children = [
    ...

    # Jetstream Pull Consumer
    HelloJetstream.LoggerPullConsumer,
  ]

  ...
```

Let's now publish some messages to our `HELLO` stream, so something will be waiting for our
application to be read when it starts.

## Publishing messages to streams

Jetstream listens on regular NATS subjects, so publishing messages is dead simple with `Gnat.pub/3`:

```elixir
Gnat.pub(:gnat, "greetings", "Hello World")
```

Or via NATS CLI:

```shell
nats pub greetings "Hello World"
```

That's it! When you run your app, you should see your messages being read by your application.

================================================
FILE: docs/js/introduction/overview.md
================================================
# Overview

[Jetstream](https://docs.nats.io/nats-concepts/jetstream) is a distributed persistence system
built-in to [NATS](https://nats.io/). It provides a streaming system that lets you capture streams
of events from various sources and persist these into persistent stores, which you can immediately
or later replay for processing.

This library exposes interfaces for publishing, consuming and managing Jetstream services. It builds
on top of [Gnat](https://hex.pm/packages/gnat), the officially supported Elixir client for NATS.

* [Let's get Jetstream up and running](./getting_started.md)
* [Using Broadway with Jetstream](../guides/broadway.md)
* [Pull Consumer API](`Gnat.Jetstream.PullConsumer`)
* [Create, update and delete Jetstream streams and consumers via Elixir](../guides/managing.md)

================================================
FILE: lib/gnat/command.ex
================================================
defmodule Gnat.Command do
  @moduledoc false

  @newline "\r\n"
  @hpub "HPUB"
  @pub "PUB"
  @sub "SUB"
  @unsub "UNSUB"

  def build(:pub, topic, payload, []),
    do: [@pub, " ", topic, " #{IO.iodata_length(payload)}", @newline, payload, @newline]

  def build(:pub, topic, payload, reply_to: reply),
    do: [
      @pub,
      " ",
      topic,
      " ",
      reply,
      " #{IO.iodata_length(payload)}",
      @newline,
      payload,
      @newline
    ]

  def build(:pub, topic, payload, headers: headers) do
    # it takes 10 bytes to add the nats header version line
    # and 2 more for the newline between headers and payload
    header_len = IO.iodata_length(headers) + 12
    total_len = IO.iodata_length(payload) + header_len

    [
      @hpub,
      " ",
      topic,
      " ",
      Integer.to_string(header_len),
      " ",
      Integer.to_string(total_len),
      "\r\nNATS/1.0\r\n",
      headers,
      @newline,
      payload,
      @newline
    ]
  end

  def build(:pub, topic, payload, headers: headers, reply_to: reply) do
    # it takes 10 bytes to add the nats header version line
    # and 2 more for the newline between headers and payload
    header_len = IO.iodata_length(headers) + 12
    total_len = IO.iodata_length(payload) + header_len

    [
      @hpub,
      " ",
      topic,
      " ",
      reply,
      " ",
      Integer.to_string(header_len),
      " ",
      Integer.to_string(total_len),
      "\r\nNATS/1.0\r\n",
      headers,
      @newline,
      payload,
      @newline
    ]
  end

  def build(:sub, topic, sid, []), do: [@sub, " ", topic, " ", Integer.to_string(sid), @newline]

  def build(:sub, topic, sid, queue_group: qg),
    do: [@sub, " ", topic, " ", qg, " ", Integer.to_string(sid), @newline]

  def build(:unsub, sid, []), do: [@unsub, " #{sid}", @newline]
  def build(:unsub, sid, max_messages: max), do: [@unsub, " #{sid}", " #{max}", @newline]
end


================================================
FILE: lib/gnat/connection_supervisor.ex
================================================
defmodule Gnat.ConnectionSupervisor do
  use GenServer
  require Logger

  @moduledoc """
  A process that can supervise a named connection for you

  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.
  It takes a map with the following data:

  ```
  gnat_supervisor_settings = %{
    name: :gnat, # (required) the registered named you want to give the Gnat connection
    backoff_period: 4_000, # number of milliseconds to wait between consecutive reconnect attempts (default: 2_000)
    connection_settings: [
      %{host: '10.0.0.100', port: 4222},
      %{host: '10.0.0.101', port: 4222},
    ]
  }
  ```

  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.

  To use this in your supervision tree add an entry like this:

  ```
  import Supervisor.Spec
  worker(Gnat.ConnectionSupervisor, [gnat_supervisor_settings, [name: :my_connection_supervisor]])
  ```

  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:

  ```
  :ok = Gnat.pub(:gnat, "subject", "message")
  ```

  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.
  """
  @spec start_link(map(), keyword()) :: GenServer.on_start()
  def start_link(settings, options \\ []) do
    GenServer.start_link(__MODULE__, settings, options)
  end

  @impl GenServer
  def init(options) do
    state = %{
      backoff_period: Map.get(options, :backoff_period, 2000),
      connection_settings: Map.fetch!(options, :connection_settings),
      name: Map.fetch!(options, :name),
      gnat: nil
    }

    Process.flag(:trap_exit, true)
    send(self(), :attempt_connection)
    {:ok, state}
  end

  @impl GenServer
  def handle_info(:attempt_connection, state) do
    connection_config = random_connection_config(state)
    Logger.debug("connecting to #{inspect(connection_config)}")

    case Gnat.start_link(connection_config, name: state.name) do
      {:ok, gnat} ->
        {:noreply, %{state | gnat: gnat}}

      {:error, err} ->
        Logger.error("failed to connect #{inspect(err)}")
        Process.send_after(self(), :attempt_connection, state.backoff_period)
        {:noreply, %{state | gnat: nil}}
    end
  end

  # in OTP 25 and below, we will get back an EXIT message in addition to receiving the {:error, reason}
  # tuple on from the start_link call above. So if we get an exit message when there is no connection tracked
  # it means will have already scheduled a new attempt_connection
  def handle_info({:EXIT, _pid, _reason}, %{gnat: nil} = state) do
    {:noreply, state}
  end

  def handle_info({:EXIT, _pid, reason}, state) do
    Logger.error("connection failed #{inspect(reason)}")
    send(self(), :attempt_connection)
    {:noreply, state}
  end

  def handle_info(msg, state) do
    Logger.error("#{__MODULE__} received unexpected message #{inspect(msg)}")
    {:noreply, state}
  end

  defp random_connection_config(%{connection_settings: connection_settings}) do
    connection_settings |> Enum.random()
  end
end


================================================
FILE: lib/gnat/consumer_supervisor.ex
================================================
defmodule Gnat.ConsumerSupervisor do
  use GenServer
  require Logger
  alias Gnat.Services.Service

  @moduledoc """
  A process that can supervise consumers for you

  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.
  It also spawns a supervised `Task` for each message it receives.
  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.

  To use this just add an entry to your supervision tree like this:

  ```
  consumer_supervisor_settings = %{
    connection_name: :name_of_supervised_connection,
    module: MyApp.Server, # a module that implements the Gnat.Server behaviour
    subscription_topics: [
      %{topic: "rpc.MyApp.search", queue_group: "rpc.MyApp.search"},
      %{topic: "rpc.MyApp.create", queue_group: "rpc.MyApp.create"},
    ],
  }
  worker(Gnat.ConsumerSupervisor, [consumer_supervisor_settings, [name: :rpc_consumer]], shutdown: 30_000)
  ```

  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.

  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.

  If you want this consumer supervisor to host a NATS service, then you can specify a module that
  implements the `Gnat.Services.Server` behavior. You'll need to specify the `service_definition` field in the consumer
  supervisor settings and conforms to the `Gnat.Services.Server.service_configuration` type. Here is an example of configuring
  the consumer supervisor to manage a service:

  ```
  consumer_supervisor_settings = %{
    connection_name: :name_of_supervised_connection,
    module: MyApp.Service, # a module that implements the Gnat.Services.Server behaviour
    service_definition: %{
      name: "exampleservice",
      description: "This is an example service",
      version: "0.1.0",
      endpoints: [
        %{
          name: "add",
          group_name: "calc",
        },
        %{
          name: "sub",
          group_name: "calc"
        }
      ]
    }
  }
  worker(Gnat.ConsumerSupervisor, [consumer_supervisor_settings, [name: :myservice_consumer]], shutdown: 30_000)
  ```

  It's also possible to pass a `%{consuming_function: {YourModule, :your_function}}` rather than a `:module` in your settings.
  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.
  """
  @spec start_link(map(), keyword()) :: GenServer.on_start()
  def start_link(settings, options \\ []) do
    GenServer.start_link(__MODULE__, settings, options)
  end

  @impl GenServer
  def init(settings) do
    Process.flag(:trap_exit, true)
    {:ok, task_supervisor_pid} = Task.Supervisor.start_link()
    connection_name = Map.get(settings, :connection_name)
    subscription_topics = Map.get(settings, :subscription_topics)

    state = %{
      connection_name: connection_name,
      connection_pid: nil,
      svc_responder_pid: nil,
      status: :disconnected,
      subscription_topics: subscription_topics,
      subscriptions: [],
      task_supervisor_pid: task_supervisor_pid
    }

    with {:ok, state} <- maybe_append_service(state, settings),
         {:ok, state} <- maybe_append_module(state, settings),
         {:ok, state} <- maybe_append_consuming_function(state, settings),
         :ok <- validate_state(state) do
      send(self(), :connect)
      {:ok, state}
    end
  end

  @impl GenServer
  def handle_info(:connect, %{connection_name: name} = state) do
    case Process.whereis(name) do
      nil ->
        Process.send_after(self(), :connect, 2_000)
        {:noreply, state}

      connection_pid ->
        _ref = Process.monitor(connection_pid)
        subscriptions = subscribe_to_topics(state, connection_pid)

        {:noreply,
         %{
           state
           | status: :connected,
             connection_pid: connection_pid,
             subscriptions: subscriptions
         }}
    end
  end

  def handle_info(
        {:DOWN, _ref, :process, connection_pid, _reason},
        %{connection_pid: connection_pid} = state
      ) do
    Process.send_after(self(), :connect, 2_000)
    {:noreply, %{state | status: :disconnected, connection_pid: nil, subscriptions: []}}
  end

  # Ignore DOWN and task result messages from the spawned tasks
  def handle_info({:DOWN, _ref, :process, _task_pid, _reason}, state), do: {:noreply, state}
  def handle_info({ref, _result}, state) when is_reference(ref), do: {:noreply, state}

  def handle_info(
        {:EXIT, supervisor_pid, _reason},
        %{task_supervisor_pid: supervisor_pid} = state
      ) do
    {:ok, task_supervisor_pid} = Task.Supervisor.start_link()
    {:noreply, Map.put(state, :task_supervisor_pid, task_supervisor_pid)}
  end

  def handle_info({:msg, gnat_message}, %{service: service, module: module} = state) do
    Task.Supervisor.async_nolink(state.task_supervisor_pid, Gnat.Services.Server, :execute, [
      module,
      gnat_message,
      service
    ])

    {:noreply, state}
  end

  def handle_info({:msg, gnat_message}, %{module: module} = state) do
    Task.Supervisor.async_nolink(state.task_supervisor_pid, Gnat.Server, :execute, [
      module,
      gnat_message
    ])

    {:noreply, state}
  end

  def handle_info({:msg, gnat_message}, %{consuming_function: {mod, fun}} = state) do
    Task.Supervisor.async_nolink(state.task_supervisor_pid, mod, fun, [gnat_message])
    {:noreply, state}
  end

  def handle_info(other, state) do
    Logger.error("#{__MODULE__} received unexpected message #{inspect(other)}")
    {:noreply, state}
  end

  @impl GenServer
  def terminate(:shutdown, state) do
    Logger.info("#{__MODULE__} starting graceful shutdown")

    Enum.each(state.subscriptions, fn subscription ->
      :ok = Gnat.unsub(state.connection_pid, subscription)
    end)

    # wait for final messages from broker
    Process.sleep(500)
    receive_final_broker_messages(state)
    wait_for_empty_task_supervisor(state)
    Logger.info("#{__MODULE__} finished graceful shutdown")
  end

  def terminate(reason, _state) do
    Logger.error("#{__MODULE__} unexpected shutdown #{inspect(reason)}")
  end

  defp receive_final_broker_messages(state) do
    receive do
      info ->
        handle_info(info, state)
        receive_final_broker_messages(state)
    after
      0 ->
        :done
    end
  end

  defp wait_for_empty_task_supervisor(%{task_supervisor_pid: pid} = state) do
    case Task.Supervisor.children(pid) do
      [] ->
        :ok

      children ->
        Logger.info("#{__MODULE__}\t\t#{Enum.count(children)} tasks remaining")
        Process.sleep(1_000)
        wait_for_empty_task_supervisor(state)
    end
  end

  defp subscribe_to_topics(%{service: service}, connection_pid) do
    Service.subscription_topics_with_queue_group(service)
    |> Enum.map(fn
      {topic, nil} ->
        {:ok, subscription} = Gnat.sub(connection_pid, self(), topic)
        subscription

      {topic, queue_group} ->
        {:ok, subscription} = Gnat.sub(connection_pid, self(), topic, queue_group: queue_group)
        subscription
    end)
  end

  defp subscribe_to_topics(state, connection_pid) do
    Enum.map(state.subscription_topics, fn topic_and_queue_group ->
      topic = Map.fetch!(topic_and_queue_group, :topic)

      {:ok, subscription} =
        case Map.get(topic_and_queue_group, :queue_group) do
          nil -> Gnat.sub(connection_pid, self(), topic)
          queue_group -> Gnat.sub(connection_pid, self(), topic, queue_group: queue_group)
        end

      subscription
    end)
  end

  defp maybe_append_service(state, %{service_definition: config}) do
    case Service.init(config) do
      {:ok, service} ->
        {:ok, Map.put(state, :service, service)}

      {:error, errors} ->
        {:stop, "Invalid service configuration: #{Enum.join(errors, ",")}"}
    end
  end

  defp maybe_append_service(state, _), do: {:ok, state}

  defp maybe_append_module(state, %{module: module}) do
    {:ok, Map.put(state, :module, module)}
  end

  defp maybe_append_module(state, _), do: {:ok, state}

  defp maybe_append_consuming_function(state, %{consuming_function: consuming_function}) do
    {:ok, Map.put(state, :consuming_function, consuming_function)}
  end

  defp maybe_append_consuming_function(state, _), do: {:ok, state}

  defp validate_state(state) do
    partial = Map.take(state, [:module, :consuming_function])

    case Enum.count(partial) do
      0 ->
        {:stop, "You must provide a module or consuming function for the consumer supervisor"}

      1 ->
        :ok

      _ ->
        {:stop,
         "You cannot provide both a module and consuming function. Please specify one or the other."}
    end
  end
end


================================================
FILE: lib/gnat/handshake.ex
================================================
defmodule Gnat.Handshake do
  @moduledoc false
  alias Gnat.Parsec

  @doc """
  This function handles all of the variations of establishing a connection to
  a nats server and just returns {:ok, socket} or {:error, reason}
  """
  def connect(settings) do
    host = settings.host |> to_charlist

    case :gen_tcp.connect(host, settings.port, settings.tcp_opts, settings.connection_timeout) do
      {:ok, tcp} -> perform_handshake(tcp, settings)
      result -> result
    end
  end

  def negotiate_settings(server_settings, user_settings) do
    auth_required = server_settings[:auth_required] || user_settings[:auth_required] || false

    %{verbose: false}
    |> negotiate_auth(server_settings, user_settings, auth_required)
    |> negotiate_headers(server_settings, user_settings)
    |> negotiate_no_responders(server_settings, user_settings)
  end

  defp perform_handshake(tcp, user_settings) do
    receive do
      {:tcp, ^tcp, operation} ->
        {_, [{:info, server_settings}]} = Parsec.parse(Parsec.new(), operation)
        {:ok, socket} = upgrade_connection(tcp, user_settings)
        settings = negotiate_settings(server_settings, user_settings)
        :ok = send_connect(user_settings, settings, socket)
        {:ok, socket, server_settings}
    after
      1000 ->
        {:error, "timed out waiting for info"}
    end
  end

  defp send_connect(%{tls: true}, settings, socket) do
    :ssl.send(socket, "CONNECT " <> Jason.encode!(settings, maps: :strict) <> "\r\n")
  end

  defp send_connect(_, settings, socket) do
    :gen_tcp.send(socket, "CONNECT " <> Jason.encode!(settings, maps: :strict) <> "\r\n")
  end

  defp negotiate_auth(
         settings,
         _server,
         %{username: username, password: password} = _user,
         true = _auth_required
       ) do
    Map.merge(settings, %{user: username, pass: password})
  end

  defp negotiate_auth(settings, _server, %{token: token} = _user, true = _auth_required) do
    Map.merge(settings, %{auth_token: token})
  end

  defp negotiate_auth(
         settings,
         %{nonce: nonce} = _server,
         %{nkey_seed: seed, jwt: jwt} = _user,
         true = _auth_required
       ) do
    {:ok, nkey} = NKEYS.from_seed(seed)
    signature = NKEYS.sign(nkey, nonce) |> Base.url_encode64() |> String.replace("=", "")

    Map.merge(settings, %{sig: signature, protocol: 1, jwt: jwt})
  end

  defp negotiate_auth(
         settings,
         %{nonce: nonce} = _server,
         %{nkey_seed: seed} = _user,
         true = _auth_required
       ) do
    {:ok, nkey} = NKEYS.from_seed(seed)
    signature = NKEYS.sign(nkey, nonce) |> Base.url_encode64() |> String.replace("=", "")
    public = NKEYS.public_nkey(nkey)

    Map.merge(settings, %{sig: signature, protocol: 1, nkey: public})
  end

  defp negotiate_auth(settings, _server, _user, _auth_required) do
    settings
  end

  defp negotiate_headers(settings, %{headers: true} = _server, user_settings) do
    if Map.get(user_settings, :headers, true) do
      Map.put(settings, :headers, true)
    else
      Map.put(settings, :headers, false)
    end
  end

  defp negotiate_headers(_settings, _server, %{headers: true} = _user) do
    raise "NATS Server does not support headers, but your connection settings specify header support"
  end

  defp negotiate_headers(settings, _server, _user) do
    settings
  end

  defp negotiate_no_responders(%{headers: true} = settings, _server_settings, %{
         no_responders: true
       }) do
    Map.put(settings, :no_responders, true)
  end

  defp negotiate_no_responders(settings, _server_settings, _user_settings) do
    settings
  end

  defp upgrade_connection(tcp, %{tls: true, ssl_opts: opts}) do
    :ok = :inet.setopts(tcp, active: true)
    :ssl.connect(tcp, opts, 1_000)
  end

  defp upgrade_connection(tcp, _settings), do: {:ok, tcp}
end


================================================
FILE: lib/gnat/jetstream/api/consumer.ex
================================================
defmodule Gnat.Jetstream.API.Consumer do
  @moduledoc """
  A module representing a NATS JetStream Consumer.

  Learn more about consumers: https://docs.nats.io/nats-concepts/jetstream/consumers

  ## The Jetstream.API.Consumer struct

  The struct's only mandatory field to set is the `:stream_name`. The rest will have
  the NATS default values set.

  Note that consumers are ephemeral by default. Set the `:durable_name` to make it durable.

  Consumer struct fields explanation:

  * `:stream_name` - name of a stream the consumer is pointing at.
  * `:domain` - JetStream domain the stream is on.
  * `:ack_policy` - how the messages should be acknowledged. It has the following options:
      - `:explicit` - the default policy. It means that each individual message must be acknowledged.
        It is the only allowed option for pull consumers.
      - `:none` - no need to ack messages, the server will assume ack on delivery.
      - `:all` - only the last received message needs to be acked, all the previous messages received
        are automatically acknowledged.
  * `:ack_wait` - time in nanoseconds that server will wait for an ack for any individual. If an ack
     is not received in time, the message will be redelivered.
  * `:backoff` - list of durations that represents a retry timescale for NAK'd messages or those being
     normally retried.
  * `:deliver_group` - when set, will only deliver messages to subscriptions matching that group.
  * `:deliver_policy` - specifies where in the stream it wants to start receiving messages. It has the
     following options:
       - `:all` - the default policy. The consumer will start receiving from the earliest available
         message.
       - `:last` - the consumer will start receiving messages with the last message added to the stream.
       - `:new` - the consumer will only start receiving messages that were created after the customer
         was created.
       - `:by_start_sequence` - the consumer is required to specify `:opt_start_seq`, the sequence number
         to start on. It will receive the closest available message moving forward in the sequence
         should the message specified have been removed based on the stream limit policy.
       - `:by_start_time` - the consumer will start with messages on or after this time. The consumer is
         required to specify `:opt_start_time`, the time in the stream to start at.
       - `:last_per_subject` - the consumer will start with the latest one for each filtered subject
         currently  in the stream.
  * `:deliver_subject` - the subject to deliver observed messages. Not allowed for pull subscriptions.
    A delivery subject is required for queue subscribing as it configures a subject that all the queue
    consumers should listen on.
  * `:description` - a short description of the purpose of this customer.
  * `:durable_name` - the name of the consumer, which the server will track, allowing resuming consumption
    where left off. By default, a consumer is ephemeral. To make the consumer durable, set the name.
    See [naming](https://docs.nats.io/running-a-nats-service/nats_admin/jetstream_admin/naming).
  * `:filter_subject` - when consuming from a stream with a wildcard subject, this allows you to select
    a subset of the full wildcard subject to receive messages from.
  * `:flow_control` - when set to true, an empty message with Status header 100 and a reply subject will
    be sent. Consumers must reply to these messages to control the rate of message delivery.
  * `:headers_only` - delivers only the headers of messages in the stream and not the bodies. Additionally
    adds the Nats-Msg-Size header to indicate the size of the removed payload.
  * `:idle_heartbeat` - if set, the server will regularly send a status message to the client while there
    are  no new messages to send. This lets the client know that the JetStream service is still up and
    running, even when there is no activity on the stream. The message status header will have a code of 100.
    Unlike `:flow_control`, it will have no reply to address. It may have a description like
    "Idle Heartbeat".
  * `:inactive_threshold` - duration that instructs the server to clean up ephemeral consumers that are
    inactive for that long.
  * `:max_ack_pending` - it sets the maximum number of messages without an acknowledgement that can be
    outstanding, once this limit is reached, message delivery will be suspended. It cannot be used with
    `:ack_none` ack policy. This maximum number of pending acks applies for all the consumer's
    subscriber processes. A value of -1 means there can be any number of pending acks (i.e. no flow
    control).
  * `:max_batch` - the largest batch property that may be specified when doing a pull on a Pull consumer.
  * `:max_deliver` - the maximum number of times a specific message will be delivered. Applies to any
    message that is re-sent due to ack policy.
  * `:max_expires` - the maximum expires value that may be set when doing a pull on a Pull consumer.
  * `:max_waiting` - the number of pulls that can be outstanding on a pull consumer, pulls received after
    this is reached are ignored.
  * `:opt_start_seq` - use with `:deliver_policy` set to `:by_start_sequence`. It represents the sequence
    number to start consuming on.
  * `:opt_start_time` - use with `:deliver_policy` set to `:by_start_time`. It represents the time to start
    consuming at.
  * `:rate_limit_bps` - used to throttle the delivery of messages to the consumer, in bits per second.
  * `:replay_policy` - it applies when the `:deliver_policy` is set to `:all`, `:by_start_sequence` or
    `:by_start_time`. It has the following options:
       - `:instant` - the default policy. The messages will be pushed to the client as fast as possible.
       - `:original` - the messages in the stream will be pushed to the client at the same rate that they
         were originally received.
  * `:sample_freq` - Sets the percentage of acknowledgements that should be sampled for observability, 0-100.
    This value is a binary and for example allows both `30` and `30%` as valid values.
  """

  import Gnat.Jetstream.API.Util

  @enforce_keys [:stream_name]
  defstruct [
    :backoff,
    :deliver_group,
    :deliver_subject,
    :description,
    :domain,
    :durable_name,
    :filter_subject,
    :flow_control,
    :headers_only,
    :idle_heartbeat,
    :inactive_threshold,
    :max_batch,
    :max_expires,
    :max_waiting,
    :opt_start_seq,
    :opt_start_time,
    :rate_limit_bps,
    :sample_freq,
    :stream_name,
    ack_policy: :explicit,
    ack_wait: 30_000_000_000,
    deliver_policy: :all,
    max_ack_pending: 20_000,
    max_deliver: -1,
    replay_policy: :instant
  ]

  @type t :: %__MODULE__{
          stream_name: binary(),
          domain: nil | binary(),
          ack_policy: :none | :all | :explicit,
          ack_wait: nil | non_neg_integer(),
          backoff: nil | [non_neg_integer()],
          deliver_group: nil | binary(),
          deliver_policy:
            :all | :last | :new | :by_start_sequence | :by_start_time | :last_per_subject,
          deliver_subject: nil | binary(),
          description: nil | binary(),
          durable_name: nil | binary(),
          filter_subject: nil | binary(),
          flow_control: nil | boolean(),
          headers_only: nil | boolean(),
          idle_heartbeat: nil | non_neg_integer(),
          inactive_threshold: nil | non_neg_integer(),
          max_ack_pending: nil | integer(),
          max_batch: nil | integer(),
          max_deliver: nil | integer(),
          max_expires: nil | non_neg_integer(),
          max_waiting: nil | integer(),
          opt_start_seq: nil | non_neg_integer(),
          opt_start_time: nil | DateTime.t(),
          rate_limit_bps: nil | non_neg_integer(),
          replay_policy: :instant | :original,
          sample_freq: nil | binary()
        }

  @type info :: %{
          ack_floor: %{
            consumer_seq: non_neg_integer(),
            stream_seq: non_neg_integer()
          },
          cluster:
            nil
            | %{
                optional(:name) => binary(),
                optional(:leader) => binary(),
                optional(:replicas) => [
                  %{
                    :active => non_neg_integer(),
                    :current => boolean(),
                    :name => binary(),
                    optional(:lag) => non_neg_integer(),
                    optional(:offline) => boolean()
                  }
                ]
              },
          config: config(),
          created: DateTime.t(),
          delivered: %{
            consumer_seq: non_neg_integer(),
            stream_seq: non_neg_integer()
          },
          name: binary(),
          num_ack_pending: non_neg_integer(),
          num_pending: non_neg_integer(),
          num_redelivered: non_neg_integer(),
          num_waiting: non_neg_integer(),
          push_bound: nil | boolean(),
          stream_name: binary()
        }

  @type config :: %{
          ack_policy: :none | :all | :explicit,
          ack_wait: nil | non_neg_integer(),
          backoff: nil | [non_neg_integer()],
          deliver_group: nil | binary(),
          deliver_policy:
            :all | :last | :new | :by_start_sequence | :by_start_time | :last_per_subject,
          deliver_subject: nil | binary(),
          description: nil | binary(),
          durable_name: nil | binary(),
          filter_subject: nil | binary(),
          flow_control: nil | boolean(),
          headers_only: nil | boolean(),
          idle_heartbeat: nil | non_neg_integer(),
          inactive_threshold: nil | non_neg_integer(),
          max_ack_pending: nil | integer(),
          max_batch: nil | integer(),
          max_deliver: nil | integer(),
          max_expires: nil | non_neg_integer(),
          max_waiting: nil | integer(),
          opt_start_seq: nil | non_neg_integer(),
          opt_start_time: nil | DateTime.t(),
          rate_limit_bps: nil | non_neg_integer(),
          replay_policy: :instant | :original,
          sample_freq: nil | binary()
        }

  @type consumers :: %{
          consumers: list(binary()),
          limit: non_neg_integer(),
          offset: non_neg_integer(),
          total: non_neg_integer()
        }

  @doc """
  Creates a consumer. When consumer's `:durable_name` field is not set, the function
  creates an ephemeral consumer. Otherwise, it creates a durable consumer.

  ## Examples

      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "astream", subjects: ["subject"]})
      iex> {:ok, %{name: "consumer", stream_name: "astream"}} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: "consumer", stream_name: "astream"})

      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "astream", subjects: ["subject"]})
      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})

  """
  @spec create(conn :: Gnat.t(), consumer :: t()) :: {:ok, info()} | {:error, term()}
  def create(conn, %__MODULE__{durable_name: name} = consumer) when not is_nil(name) do
    create_topic =
      "#{js_api(consumer.domain)}.CONSUMER.DURABLE.CREATE.#{consumer.stream_name}.#{name}"

    with :ok <- validate_durable(consumer),
         {:ok, raw_response} <- request(conn, create_topic, create_payload(consumer)) do
      {:ok, to_info(raw_response)}
    end
  end

  def create(conn, %__MODULE__{} = consumer) do
    create_topic = "#{js_api(consumer.domain)}.CONSUMER.CREATE.#{consumer.stream_name}"

    with :ok <- validate(consumer),
         {:ok, raw_response} <- request(conn, create_topic, create_payload(consumer)) do
      {:ok, to_info(raw_response)}
    end
  end

  @doc """
  Deletes a consumer.

  ## Examples

      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "astream", subjects: ["subject"]})
      iex> {:ok, _response} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: "consumer", stream_name: "astream"})
      iex> Gnat.Jetstream.API.Consumer.delete(:gnat, "astream", "consumer")
      :ok

      iex> {:error, %{"code" => 404, "description" => "stream not found"}} = Gnat.Jetstream.API.Consumer.delete(:gnat, "wrong_stream", "consumer")

  """
  @spec delete(
          conn :: Gnat.t(),
          stream_name :: binary(),
          consumer_name :: binary(),
          domain :: nil | binary()
        ) ::
          :ok | {:error, any()}
  def delete(conn, stream_name, consumer_name, domain \\ nil) do
    topic = "#{js_api(domain)}.CONSUMER.DELETE.#{stream_name}.#{consumer_name}"

    with {:ok, _response} <- request(conn, topic, "") do
      :ok
    end
  end

  @doc """
  Information about the consumer.

  ## Examples

      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "astream", subjects: ["subject"]})
      iex> {:ok, _response} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: "consumer", stream_name: "astream"})
      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Consumer.info(:gnat, "astream", "consumer")

      iex>  {:error, %{"code" => 404, "description" => "stream not found"}} = Gnat.Jetstream.API.Consumer.info(:gnat, "wrong_stream", "consumer")

  """
  @spec info(
          conn :: Gnat.t(),
          stream_name :: binary(),
          consumer_name :: binary(),
          domain :: nil | binary()
        ) ::
          {:ok, info()} | {:error, any()}
  def info(conn, stream_name, consumer_name, domain \\ nil) do
    topic = "#{js_api(domain)}.CONSUMER.INFO.#{stream_name}.#{consumer_name}"

    with {:ok, raw} <- request(conn, topic, "") do
      {:ok, to_info(raw)}
    end
  end

  @doc """
  Paged list of known consumers, including their current info.

  ## Examples

      iex> {:ok, _response} =  Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "astream", subjects: ["subject"]})
      iex> {:ok, %{consumers: _, limit: 1024, offset: 0, total: _}} = Gnat.Jetstream.API.Consumer.list(:gnat, "astream")

      iex> {:error, %{"code" => 404, "description" => "stream not found"}} = Gnat.Jetstream.API.Consumer.list(:gnat, "wrong_stream")

  """
  @spec list(
          conn :: Gnat.t(),
          stream_name :: binary(),
          params :: [offset: non_neg_integer(), domain: nil | binary()]
        ) ::
          {:ok, consumers()} | {:error, term()}
  def list(conn, stream_name, params \\ []) do
    domain = Keyword.get(params, :domain)

    payload =
      Jason.encode!(%{
        offset: Keyword.get(params, :offset, 0)
      })

    with {:ok, raw} <- request(conn, "#{js_api(domain)}.CONSUMER.NAMES.#{stream_name}", payload) do
      response = %{
        consumers: Map.get(raw, "consumers"),
        limit: Map.get(raw, "limit"),
        offset: Map.get(raw, "offset"),
        total: Map.get(raw, "total")
      }

      {:ok, response}
    end
  end

  @doc """
  Requests a next message from a stream to be consumed. The response (consumed message)will be sent
  on the subject given as the `reply_to` parameter.

  ## Options

  * `batch` - How many messages to receive. Messages will be sent to the `reply_to` subject
    separately. Defaults to 1.

  * `expires` - Time in nanoseconds the request will be kept in the server. Once this time passes
    a message with empty body and topic set to `reply_to` subject is sent. Useful when polling
    the server frequently and not wanting the pull requests to accumulate. By default, the pull
    request stays in the server until a message comes.

  * `no_wait` - Boolean value which indicates whether the pull request should be accumulated on
    the server. When set to true and no message is present to be consumed, a message with empty
    body and topic value set to `reply_to` is sent. Defaults to false.

  ## Example

      iex> {:ok, _response} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "astream", subjects: ["subject"]})
      iex> {:ok, _response} = Gnat.Jetstream.API.Consumer.create(:gnat, %Gnat.Jetstream.API.Consumer{durable_name: "consumer", stream_name: "astream"})
      iex> {:ok, _sid} = Gnat.sub(:gnat, self(), "reply_subject")
      iex> :ok = Gnat.Jetstream.API.Consumer.request_next_message(:gnat, "astream", "consumer", "reply_subject")
      iex> :ok = Gnat.pub(:gnat, "subject", "message1")
      iex> assert_receive {:msg, %{body: "message1", topic: "subject"}}
  """
  @spec request_next_message(
          conn :: Gnat.t(),
          stream_name :: binary(),
          consumer_name :: binary(),
          reply_to :: String.t(),
          domain :: nil | binary(),
          opts :: keyword()
        ) :: :ok
  def request_next_message(
        conn,
        stream_name,
        consumer_name,
        reply_to,
        domain \\ nil,
        opts \\ []
      ) do
    default_payload = %{batch: 1}

    put_option_if_not_nil = fn payload, option_key ->
      if option_value = opts[option_key] do
        Map.put(payload, option_key, option_value)
      else
        payload
      end
    end

    payload =
      default_payload
      |> put_option_if_not_nil.(:batch)
      |> put_option_if_not_nil.(:no_wait)
      |> put_option_if_not_nil.(:expires)
      |> Jason.encode!()

    Gnat.pub(
      conn,
      "#{js_api(domain)}.CONSUMER.MSG.NEXT.#{stream_name}.#{consumer_name}",
      payload,
      reply_to: reply_to
    )
  end

  # https://docs.nats.io/running-a-nats-service/configuration/leafnodes/jetstream_leafnodes
  defp js_api(nil), do: "$JS.API"
  defp js_api(""), do: "$JS.API"
  defp js_api(domain), do: "$JS.#{domain}.API"

  defp create_payload(%__MODULE__{} = cons) do
    %{
      config: %{
        ack_policy: cons.ack_policy,
        ack_wait: cons.ack_wait,
        backoff: cons.backoff,
        deliver_group: cons.deliver_group,
        deliver_policy: cons.deliver_policy,
        deliver_subject: cons.deliver_subject,
        description: cons.description,
        durable_name: cons.durable_name,
        filter_subject: cons.filter_subject,
        flow_control: cons.flow_control,
        headers_only: cons.headers_only,
        idle_heartbeat: cons.idle_heartbeat,
        inactive_threshold: cons.inactive_threshold,
        max_ack_pending: cons.max_ack_pending,
        max_batch: cons.max_batch,
        max_deliver: cons.max_deliver,
        max_expires: cons.max_expires,
        max_waiting: cons.max_waiting,
        opt_start_seq: cons.opt_start_seq,
        opt_start_time: cons.opt_start_time,
        rate_limit_bps: cons.rate_limit_bps,
        replay_policy: cons.replay_policy,
        sample_freq: cons.sample_freq
      },
      stream_name: cons.stream_name
    }
    |> Jason.encode!()
  end

  defp to_config(raw) do
    %{
      ack_policy: raw |> Map.get("ack_policy") |> to_sym(),
      ack_wait: raw |> Map.get("ack_wait"),
      backoff: Map.get(raw, "backoff"),
      deliver_group: Map.get(raw, "deliver_group"),
      deliver_policy: raw |> Map.get("deliver_policy") |> to_sym(),
      deliver_subject: raw |> Map.get("deliver_subject"),
      description: Map.get(raw, "description"),
      durable_name: Map.get(raw, "durable_name"),
      filter_subject: raw |> Map.get("filter_subject"),
      flow_control: Map.get(raw, "flow_control"),
      headers_only: Map.get(raw, "headers_only"),
      idle_heartbeat: Map.get(raw, "idle_heartbeat"),
      inactive_threshold: Map.get(raw, "inactive_threshold"),
      max_ack_pending: Map.get(raw, "max_ack_pending"),
      max_batch: Map.get(raw, "max_batch"),
      max_deliver: Map.get(raw, "max_deliver"),
      max_expires: Map.get(raw, "max_expires"),
      max_waiting: Map.get(raw, "max_waiting"),
      opt_start_seq: raw |> Map.get("opt_start_seq"),
      opt_start_time: raw |> Map.get("opt_start_time") |> to_datetime(),
      rate_limit_bps: Map.get(raw, "rate_limit_bps"),
      replay_policy: raw |> Map.get("replay_policy") |> to_sym(),
      sample_freq: Map.get(raw, "sample_freq")
    }
  end

  defp to_info(raw) do
    %{
      ack_floor: %{
        consumer_seq: get_in(raw, ["ack_floor", "consumer_seq"]),
        stream_seq: get_in(raw, ["ack_floor", "stream_seq"])
      },
      cluster: Map.get(raw, "cluster"),
      config: to_config(Map.get(raw, "config")),
      created: raw |> Map.get("created") |> to_datetime(),
      delivered: %{
        consumer_seq: get_in(raw, ["delivered", "consumer_seq"]),
        stream_seq: get_in(raw, ["delivered", "stream_seq"])
      },
      name: Map.get(raw, "name"),
      num_ack_pending: Map.get(raw, "num_ack_pending"),
      num_pending: Map.get(raw, "num_pending"),
      num_redelivered: Map.get(raw, "num_redelivered"),
      num_waiting: Map.get(raw, "num_waiting"),
      push_bound: Map.get(raw, "push_bound"),
      stream_name: Map.get(raw, "stream_name")
    }
  end

  defp validate(consumer) do
    cond do
      consumer.stream_name == nil ->
        {:error, "must have a :stream_name set"}

      is_binary(consumer.stream_name) == false ->
        {:error, "stream_name must be a string"}

      valid_name?(consumer.stream_name) == false ->
        {:error, "invalid stream_name: " <> invalid_name_message()}

      consumer.deliver_policy not in [
        :all,
        :last,
        :new,
        :by_start_sequence,
        :by_start_time,
        :last_per_subject
      ] ->
        {:error, "invalid deliver policy: #{consumer.deliver_policy}"}

      consumer.replay_policy not in [:instant, :original] ->
        {:error, "invalid replay policy: #{consumer.replay_policy}"}

      true ->
        :ok
    end
  end

  defp validate_durable(consumer) do
    with :ok <- validate(consumer) do
      cond do
        is_binary(consumer.durable_name) == false ->
          {:error, "durable_name must be a string"}

        valid_name?(consumer.durable_name) == false ->
          {:error, "invalid durable_name: " <> invalid_name_message()}

        true ->
          :ok
      end
    end
  end
end


================================================
FILE: lib/gnat/jetstream/api/kv/entry.ex
================================================
defmodule Gnat.Jetstream.API.KV.Entry do
  @moduledoc """
  A parsed view of a single message from a Key/Value bucket's underlying stream.

  Messages delivered from a KV bucket's stream encode three different operations
  (put, delete, purge) using a combination of the `kv-operation` header, the
  `nats-marker-reason` header, and the absence of any headers. Recovering the
  original key also requires stripping the `$KV.<bucket>.` subject prefix.

  This module captures that convention in one place so both the built-in
  `Gnat.Jetstream.API.KV.Watcher` (push consumer) and user-supplied
  `Gnat.Jetstream.PullConsumer` implementations can share it.

  ## Using with a custom PullConsumer

  A common use case is hydrating a local cache from a KV bucket by driving a
  `Gnat.Jetstream.PullConsumer`. Inside `c:handle_message/2`, convert the raw
  message into an `Entry` and branch on the operation:

      defmodule MyApp.KVCache do
        use Gnat.Jetstream.PullConsumer

        alias Gnat.Jetstream.API.KV

        @bucket "my_bucket"

        @impl true
        def handle_message(message, state) do
          case KV.Entry.from_message(message, @bucket) do
            {:ok, %KV.Entry{operation: :put, key: key, value: value}} ->
              {:ack, put_in(state.cache[key], value)}

            {:ok, %KV.Entry{operation: op, key: key}} when op in [:delete, :purge] ->
              {:ack, update_in(state.cache, &Map.delete(&1, key))}

            :ignore ->
              {:ack, state}
          end
        end
      end

  The returned struct also carries the JetStream `revision` (stream sequence),
  `created` timestamp, and `delta` (`num_pending`) when the message includes
  JetStream metadata, which is useful for detecting when the consumer has
  caught up with the tail of the stream (`delta == 0`).

  ## Messages that are not KV records

  `from_message/2` returns `:ignore` when the input is not a KV record — for
  example a JetStream status message (`100` heartbeat, `404`/`408` pull
  terminator, `409` leadership change) or a message whose subject does not
  belong to the given bucket. In normal operation the `Watcher` and
  `PullConsumer` layers filter status messages out before they reach user
  code, so this is a defensive fallback rather than something consumers are
  expected to rely on.
  """

  alias Gnat.Jetstream.API.Message

  @operation_header "kv-operation"
  @operation_del "DEL"
  @operation_purge "PURGE"
  @nats_marker_reason_header "nats-marker-reason"
  @subject_prefix "$KV."

  @type operation :: :put | :delete | :purge

  @type t :: %__MODULE__{
          bucket: String.t(),
          key: String.t(),
          value: binary(),
          operation: operation(),
          revision: non_neg_integer() | nil,
          created: DateTime.t() | nil,
          delta: non_neg_integer() | nil
        }

  defstruct [:bucket, :key, :value, :operation, :revision, :created, :delta]

  @doc """
  Parse a NATS message delivered from a KV bucket's underlying stream into an
  `Entry`.

  `bucket_name` must match the bucket the message was published to; it is used
  to strip the `$KV.<bucket>.` subject prefix and recover the key.

  Returns `:ignore` if the message is not a KV record for the given bucket
  (JetStream status message, wrong subject, etc.).

  The `:revision`, `:created`, and `:delta` fields are populated when the
  message carries a JetStream `$JS.ACK...` reply subject. For messages without
  one (e.g. direct get responses), those fields are `nil`.
  """
  @spec from_message(Gnat.message(), bucket_name :: String.t()) :: {:ok, t()} | :ignore
  def from_message(message, bucket_name) do
    with false <- status_message?(message),
         {:ok, key} <- extract_key(message, bucket_name) do
      entry = %__MODULE__{
        bucket: bucket_name,
        key: key,
        value: Map.get(message, :body, ""),
        operation: operation(message)
      }

      {:ok, apply_metadata(entry, message)}
    else
      _ -> :ignore
    end
  end

  defp status_message?(%{status: status}) when is_binary(status) and status != "", do: true
  defp status_message?(_), do: false

  defp extract_key(%{topic: topic}, bucket_name) do
    prefix = @subject_prefix <> bucket_name <> "."

    if String.starts_with?(topic, prefix) do
      {:ok, binary_part(topic, byte_size(prefix), byte_size(topic) - byte_size(prefix))}
    else
      :error
    end
  end

  defp operation(%{headers: headers}) when is_list(headers) do
    Enum.find_value(headers, :put, fn
      {@operation_header, @operation_del} -> :delete
      {@operation_header, @operation_purge} -> :purge
      {@nats_marker_reason_header, _} -> :delete
      _ -> false
    end)
  end

  defp operation(_message), do: :put

  defp apply_metadata(entry, message) do
    case Message.metadata(message) do
      {:ok, metadata} ->
        %__MODULE__{
          entry
          | revision: metadata.stream_seq,
            created: metadata.timestamp,
            delta: metadata.num_pending
        }

      {:error, _} ->
        entry
    end
  end
end


================================================
FILE: lib/gnat/jetstream/api/kv/watcher.ex
================================================
defmodule Gnat.Jetstream.API.KV.Watcher do
  @moduledoc """
  The watcher server establishes a subscription to the changes that occur to a given key-value bucket. The
  consumer-supplied handler function will be sent an indicator as to whether the change is a delete or an add,
  as well as the key being changed and the value (if it was added).

  Ensure that you call `stop` with a watcher pid when you no longer need to be notified about key changes
  """
  use GenServer

  alias Gnat.Jetstream.API.{Consumer, KV, Util}
  alias Gnat.Jetstream.API.KV.Entry

  # Matches the ordered-consumer defaults used by the nats.go KV watcher:
  # a 5s idle heartbeat and server-driven flow control. The flow-control
  # messages arrive as 100-status messages with a reply subject — the client
  # is expected to publish an empty reply so the server releases backpressure.
  @flow_control_heartbeat_ns 5_000_000_000

  @type keywatch_handler ::
          (action :: :key_deleted | :key_added, key :: String.t(), value :: any() -> nil)

  @type watcher_options ::
          {:conn, Gnat.t()}
          | {:bucket_name, String.t()}
          | {:handler, keywatch_handler()}

  @spec start_link(opts :: [watcher_options()]) :: GenServer.on_start()
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def stop(pid) do
    GenServer.stop(pid)
  end

  def init(opts) do
    {:ok, {sub, consumer_name}} = subscribe(opts[:conn], opts[:bucket_name])

    {:ok,
     %{
       handler: opts[:handler],
       conn: opts[:conn],
       bucket_name: opts[:bucket_name],
       sub: sub,
       consumer_name: consumer_name,
       domain: Keyword.get(opts, :domain)
     }}
  end

  def terminate(_reason, state) do
    stream = KV.stream_name(state.bucket_name)
    :ok = Gnat.unsub(state.conn, state.sub)
    :ok = Consumer.delete(state.conn, stream, state.consumer_name, state.domain)
  end

  # Flow-control request: the server is asking us to acknowledge that we're
  # keeping up. Responding releases backpressure so the server continues
  # delivering messages to slow handlers rather than dropping us as a slow
  # consumer.
  def handle_info({:msg, %{status: "100", reply_to: reply_to}}, state)
      when is_binary(reply_to) and reply_to != "" do
    :ok = Gnat.pub(state.conn, reply_to, "")
    {:noreply, state}
  end

  # Idle heartbeat (status 100 with no reply_to) and any other informational
  # status message (404, 408, 409, etc.) — not a stream record, drop it.
  def handle_info({:msg, %{status: status}}, state)
      when is_binary(status) and status != "" do
    {:noreply, state}
  end

  def handle_info({:msg, message}, state) do
    case Entry.from_message(message, state.bucket_name) do
      {:ok, entry} ->
        state.handler.(action(entry.operation), entry.key, entry.value)

      :ignore ->
        :ok
    end

    {:noreply, state}
  end

  defp action(:put), do: :key_added
  defp action(:delete), do: :key_deleted
  defp action(:purge), do: :key_purged

  defp subscribe(conn, bucket_name) do
    stream = KV.stream_name(bucket_name)
    inbox = Util.reply_inbox()
    consumer_name = "all_key_values_watcher_#{Util.nuid()}"

    with {:ok, sub} <- Gnat.sub(conn, self(), inbox),
         {:ok, _consumer} <-
           Consumer.create(conn, %Consumer{
             durable_name: consumer_name,
             deliver_subject: inbox,
             stream_name: stream,
             ack_policy: :none,
             max_ack_pending: -1,
             max_deliver: 1,
             flow_control: true,
             idle_heartbeat: @flow_control_heartbeat_ns
           }) do
      {:ok, {sub, consumer_name}}
    end
  end
end


================================================
FILE: lib/gnat/jetstream/api/kv.ex
================================================
defmodule Gnat.Jetstream.API.KV do
  @moduledoc """
  API for interacting with the Key/Value store functionality in Nats Jetstream.

  Learn about the Key/Value store: https://docs.nats.io/nats-concepts/jetstream/key-value-store

  ## Consuming KV changes from a custom PullConsumer

  `watch/3` covers the common "push consumer" use case for reacting to bucket
  changes. If you need a pull-based consumer instead (e.g. to hydrate a local
  cache and detect when it is caught up with the stream), see
  `Gnat.Jetstream.API.KV.Entry` for a shared helper that parses a raw NATS
  message into a KV operation/key/value triple with optional revision
  metadata.
  """
  alias Gnat.Jetstream.API.{Stream}

  @stream_prefix "KV_"
  @subject_prefix "$KV."
  @two_minutes_in_nanoseconds 120_000_000_000

  @type bucket_options ::
          {:history, non_neg_integer()}
          | {:ttl, non_neg_integer()}
          | {:limit_marker_ttl, non_neg_integer()}
          | {:max_bucket_size, non_neg_integer()}
          | {:max_value_size, non_neg_integer()}
          | {:description, binary()}
          | {:replicas, non_neg_integer()}
          | {:storage, :file | :memory}
          | {:placement, Stream.placement()}

  @doc """
  Create a new Key/Value bucket. Can include the following options

  * `:history` - How many historic values to keep per key (defaults to 1, max of 64)
  * `:ttl` - How long to keep values for (in nanoseconds)
  * `:limit_marker_ttl` - How long the bucket keeps markers when keys are removed by the TTL setting.
  * `:max_bucket_size` - The max number of bytes the bucket can hold
  * `:max_value_size` - The max number of bytes a value may be
  * `:description` - A description for the bucket
  * `:replicas` - How many replicas of the data to store
  * `:storage` - Storage backend to use (:file, :memory)
  * `:placement` - A map with :cluster (required) and :tags (optional)

  ## Examples

     iex> {:ok, info} = Jetstream.API.KV.create_bucket(:gnat, "my_bucket")
  """
  @spec create_bucket(conn :: Gnat.t(), bucket_name :: binary(), params :: [bucket_options()]) ::
          {:ok, Stream.info()} | {:error, any()}
  def create_bucket(conn, bucket_name, params \\ []) do
    # The primary NATS docs don't provide information about how to interact
    # with Key-Value functionality over the wire. Turns out the KV store is
    # just a Stream under-the-hood
    # Discovered these settings from looking at the `nats-server -js -DV` logs
    # as well as the GoLang implementation https://github.com/nats-io/nats.go/blob/dd91b86bc4f7fa0f061fefe11506aaee413bfafd/kv.go#L339
    # If the settings aren't correct, NATS will not consider it a valid KV store
    stream = %Stream{
      name: stream_name(bucket_name),
      subjects: stream_subjects(bucket_name),
      description: Keyword.get(params, :description),
      max_msgs_per_subject: Keyword.get(params, :history, 1),
      discard: :new,
      deny_delete: true,
      allow_rollup_hdrs: true,
      max_age: Keyword.get(params, :ttl, 0),
      max_bytes: Keyword.get(params, :max_bucket_size, -1),
      max_msg_size: Keyword.get(params, :max_value_size, -1),
      num_replicas: Keyword.get(params, :replicas, 1),
      storage: Keyword.get(params, :storage, :file),
      placement: Keyword.get(params, :placement),
      duplicate_window: adjust_duplicate_window(Keyword.get(params, :ttl, 0)),
      subject_delete_marker_ttl: Keyword.get(params, :limit_marker_ttl, 0)
    }

    Stream.create(conn, stream)
  end

  # The `duplicate_window` can't be greater than the `max_age`. The default `duplicate_window`
  # is 2 minutes. We'll keep the 2 minute window UNLESS the ttl is less than 2 minutes
  defp adjust_duplicate_window(ttl) when ttl > 0 and ttl < @two_minutes_in_nanoseconds, do: ttl
  defp adjust_duplicate_window(_ttl), do: @two_minutes_in_nanoseconds

  @doc """
  Delete a Key/Value bucket

  ## Examples

     iex> :ok = Jetstream.API.KV.delete_bucket(:gnat, "my_bucket")
  """
  @spec delete_bucket(conn :: Gnat.t(), bucket_name :: binary()) :: :ok | {:error, any()}
  def delete_bucket(conn, bucket_name) do
    Stream.delete(conn, stream_name(bucket_name))
  end

  @doc """
  Create a Key in a Key/Value Bucket

  ## Options

  * `:timeout` - receive timeout for the request

  ## Examples

      iex> :ok = Jetstream.API.KV.create_key(:gnat, "my_bucket", "my_key", "my_value")
  """
  @spec create_key(
          conn :: Gnat.t(),
          bucket_name :: binary(),
          key :: binary(),
          value :: binary(),
          opts :: keyword()
        ) ::
          :ok | {:error, any()}
  def create_key(conn, bucket_name, key, value, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, 5_000)

    reply = Gnat.request(conn, key_name(bucket_name, key), value, receive_timeout: timeout)

    case reply do
      {:ok, _} -> :ok
      error -> error
    end
  end

  @doc """
  Delete a Key from a K/V Bucket

  ## Examples

      iex> :ok = Jetstream.API.KV.delete_key(:gnat, "my_bucket", "my_key")
  """
  @spec delete_key(
          conn :: Gnat.t(),
          bucket_name :: binary(),
          key :: binary(),
          opts :: keyword()
        ) ::
          :ok | {:error, any()}
  def delete_key(conn, bucket_name, key, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, 5_000)

    reply =
      Gnat.request(conn, key_name(bucket_name, key), "",
        headers: [{"KV-Operation", "DEL"}],
        receive_timeout: timeout
      )

    case reply do
      {:ok, _} -> :ok
      error -> error
    end
  end

  @doc """
  Purge a Key from a K/V bucket. This will remove any revision history the key had

  ## Examples

      iex> :ok = Jetstream.API.KV.purge_key(:gnat, "my_bucket", "my_key")
  """
  @spec purge_key(
          conn :: Gnat.t(),
          bucket_name :: binary(),
          key :: binary(),
          opts :: keyword()
        ) ::
          :ok | {:error, any()}
  def purge_key(conn, bucket_name, key, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, 5_000)

    reply =
      Gnat.request(conn, key_name(bucket_name, key), "",
        headers: [{"KV-Operation", "PURGE"}, {"Nats-Rollup", "sub"}],
        receive_timeout: timeout
      )

    case reply do
      {:ok, _} -> :ok
      error -> error
    end
  end

  @doc """
  Put a value into a Key in a K/V Bucket

  ## Examples

      iex> :ok = Jetstream.API.KV.put_value(:gnat, "my_bucket", "my_key", "my_value")
  """
  @spec put_value(
          conn :: Gnat.t(),
          bucket_name :: binary(),
          key :: binary(),
          value :: binary(),
          opts :: keyword()
        ) ::
          :ok | {:error, any()}
  def put_value(conn, bucket_name, key, value, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, 5_000)

    reply = Gnat.request(conn, key_name(bucket_name, key), value, receive_timeout: timeout)

    case reply do
      {:ok, _} -> :ok
      error -> error
    end
  end

  @doc """
  Get the value for a key in a particular K/V bucket

  ## Examples

      iex> "my_value" = Jetstream.API.KV.get_value(:gnat, "my_bucket", "my_key")
  """
  @spec get_value(conn :: Gnat.t(), bucket_name :: binary(), key :: binary()) ::
          binary() | {:error, any()} | nil
  def get_value(conn, bucket_name, key) do
    case Stream.get_message(conn, stream_name(bucket_name), %{
           last_by_subj: key_name(bucket_name, key)
         }) do
      {:ok, message} -> message.data
      error -> error
    end
  end

  @doc """
  Get all the non-deleted key-value pairs for a Bucket

  ## Options

  * `:batch` - Number of messages to fetch per request (default: 250)
  * `:domain` - JetStream domain (default: nil)

  ## Examples

      iex> {:ok, %{"key1" => "value1"}} = Jetstream.API.KV.contents(:gnat, "my_bucket")
      iex> {:ok, contents} = Jetstream.API.KV.contents(:gnat, "my_bucket", batch: 500)
  """
  @spec contents(conn :: Gnat.t(), bucket_name :: binary(), opts :: keyword()) ::
          {:ok, map()} | {:error, binary()}
  def contents(conn, bucket_name, opts \\ []) do
    alias Gnat.Jetstream.Pager
    stream = stream_name(bucket_name)
    domain = Keyword.get(opts, :domain)
    batch_size = Keyword.get(opts, :batch, 250)

    pager_opts = [domain: domain, batch: batch_size]

    Pager.reduce(conn, stream, pager_opts, %{}, fn msg, acc ->
      case msg do
        %{topic: key, body: body, headers: headers} ->
          if {"kv-operation", "DEL"} in headers do
            acc
          else
            Map.put(acc, subject_to_key(key, bucket_name), body)
          end

        %{topic: key, body: body} ->
          Map.put(acc, subject_to_key(key, bucket_name), body)
      end
    end)
  end

  @doc """
  Get all the non-deleted keys for a Bucket

  ## Options

  * `:batch` - Number of messages to fetch per request (default: 250)
  * `:domain` - JetStream domain (default: nil)

  ## Examples

      iex> {:ok, ["key1", "key2"]} = Jetstream.API.KV.keys(:gnat, "my_bucket")
      iex> {:ok, keys} = Jetstream.API.KV.keys(:gnat, "my_bucket", batch: 500)
  """
  @spec keys(conn :: Gnat.t(), bucket_name :: binary(), opts :: keyword()) ::
          {:ok, list(binary())} | {:error, binary()}
  def keys(conn, bucket_name, opts \\ []) do
    alias Gnat.Jetstream.Pager
    stream = stream_name(bucket_name)
    domain = Keyword.get(opts, :domain)
    batch_size = Keyword.get(opts, :batch, 250)

    pager_opts = [domain: domain, headers_only: true, batch: batch_size]

    result =
      Pager.reduce(conn, stream, pager_opts, MapSet.new(), fn msg, acc ->
        case msg do
          %{topic: key, headers: headers} ->
            cond do
              {"kv-operation", "DEL"} in headers ->
                MapSet.delete(acc, subject_to_key(key, bucket_name))

              {"kv-operation", "PURGE"} in headers ->
                MapSet.delete(acc, subject_to_key(key, bucket_name))

              true ->
                MapSet.put(acc, subject_to_key(key, bucket_name))
            end

          %{topic: key} ->
            MapSet.put(acc, subject_to_key(key, bucket_name))
        end
      end)

    case result do
      {:ok, key_set} -> {:ok, Enum.sort(MapSet.to_list(key_set))}
      error -> error
    end
  end

  @doc """
  Information about the state of the bucket's Stream.

  ## Opts
  * `:domain` - (default `nil`) the domain of the bucket
  """
  @spec info(conn :: Gnat.t(), bucket_name :: binary(), keyword()) ::
          {:ok, Stream.info()} | {:error, any()}
  def info(conn, bucket_name, opts \\ []) do
    Stream.info(conn, @stream_prefix <> bucket_name, Keyword.get(opts, :domain))
  end

  @doc ~S"""
  Starts a monitor for key changes in a given bucket. Supply a handler that will receive
  key change notifications.

  ## Examples

      iex> {:ok, _pid} = Jetstream.API.KV.watch(:gnat, "my_bucket", fn action, key, value ->
      ...>  IO.puts("#{action} taken on #{key}")
      ...> end)
  """
  def watch(conn, bucket_name, handler) do
    Gnat.Jetstream.API.KV.Watcher.start_link(
      conn: conn,
      bucket_name: bucket_name,
      handler: handler
    )
  end

  @doc ~S"""
  Stops a previously running monitor. This will unsubscribe from the key changes and remove the
  ephemeral consumer

  ## Examples

      iex> :ok = Jetstream.API.KV.unwatch(pid)
  """
  def unwatch(pid) do
    Gnat.Jetstream.API.KV.Watcher.stop(pid)
  end

  @spec is_kv_bucket_stream?(stream_name :: binary()) :: boolean()
  @deprecated "Use Gnat.Jetstream.API.KV.kv_bucket_stream?/1 instead"
  def is_kv_bucket_stream?(stream_name) do
    kv_bucket_stream?(stream_name)
  end

  @doc """
  Returns true if the provided stream is a KV bucket, false otherwise

  ## Parameters
  * `stream_name` - the stream name to test
  """
  @spec kv_bucket_stream?(stream_name :: binary()) :: boolean()
  def kv_bucket_stream?(stream_name) do
    String.starts_with?(stream_name, "KV_")
  end

  @doc """
  Returns a list of all the buckets in the KV
  """
  @spec list_buckets(conn :: Gnat.t()) :: {:error, term()} | {:ok, list(String.t())}
  def list_buckets(conn) do
    with {:ok, %{streams: streams}} <- Stream.list(conn) do
      stream_names =
        for bucket <- streams, kv_bucket_stream?(bucket) do
          String.trim_leading(bucket, @stream_prefix)
        end

      {:ok, stream_names}
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc false
  def stream_name(bucket_name) do
    "#{@stream_prefix}#{bucket_name}"
  end

  defp stream_subjects(bucket_name) do
    ["#{@subject_prefix}#{bucket_name}.>"]
  end

  defp key_name(bucket_name, key) do
    "#{@subject_prefix}#{bucket_name}.#{key}"
  end

  @doc false
  def subject_to_key(subject, bucket_name) do
    String.replace(subject, "#{@subject_prefix}#{bucket_name}.", "")
  end
end


================================================
FILE: lib/gnat/jetstream/api/message.ex
================================================
defmodule Gnat.Jetstream.API.Message do
  @moduledoc """
  This module provides a way to parse the `reply_to` received by a PullConsumer
  and get some useful information about the state of the consumer.
  """

  # Based on:
  # https://github.com/nats-io/nats.py/blob/d9f24b4beae541b7723873ba0a786ea7c0ecb3d5/nats/aio/msg.py#L182

  defmodule Metadata do
    @type t :: %__MODULE__{
            :stream_seq => integer(),
            :consumer_seq => integer(),
            :num_pending => integer(),
            :num_delivered => integer(),
            :timestamp => DateTime.t(),
            :stream => String.t(),
            :consumer => String.t(),
            :domain => String.t() | nil
          }

    defstruct [
      :stream_seq,
      :consumer_seq,
      :num_pending,
      :num_delivered,
      :timestamp,
      :stream,
      :consumer,
      :domain
    ]
  end

  @spec metadata(message :: Gnat.message()) :: {:ok, Metadata.t()} | {:error, term()}
  def metadata(%{reply_to: "$JS.ACK." <> ack_topic}),
    do: decode_reply_to(String.split(ack_topic, "."))

  def metadata(_), do: {:error, :no_jetstream_message}

  # # Subject without domain:
  # $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
  defp decode_reply_to([
         stream,
         consumer,
         num_delivered,
         stream_seq,
         consumer_seq,
         ts,
         num_pending
       ]) do
    with {ts, ""} <- Integer.parse(ts),
         {:ok, ts} <- DateTime.from_unix(ts, :nanosecond),
         {stream_seq, ""} <- Integer.parse(stream_seq),
         {consumer_seq, ""} <- Integer.parse(consumer_seq),
         {num_delivered, ""} <- Integer.parse(num_delivered),
         {num_pending, ""} <- Integer.parse(num_pending) do
      {:ok,
       %Metadata{
         stream_seq: stream_seq,
         consumer_seq: consumer_seq,
         num_delivered: num_delivered,
         num_pending: num_pending,
         timestamp: ts,
         stream: stream,
         consumer: consumer
       }}
    else
      _ ->
        {:error, :invalid_ack_reply_to}
    end
  end

  # Subject with domain:
  # $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.
  #   <cseq>.<tm>.<pending>.<a token with a random value>
  defp decode_reply_to([
         domain,
         _account_hash,
         stream,
         consumer,
         num_delivered,
         stream_seq,
         consumer_seq,
         ts,
         num_pending,
         _random_value
       ]) do
    case decode_reply_to([
           stream,
           consumer,
           num_delivered,
           stream_seq,
           consumer_seq,
           ts,
           num_pending
         ]) do
      {:ok, metadata} ->
        domain = if domain == "_", do: nil, else: domain
        {:ok, %{metadata | domain: domain}}

      err = {:error, _} ->
        err
    end
  end

  defp decode_reply_to(_), do: {:error, :invalid_ack_reply_to}
end


================================================
FILE: lib/gnat/jetstream/api/object/meta.ex
================================================
defmodule Gnat.Jetstream.API.Object.Meta do
  @enforce_keys [:bucket, :chunks, :digest, :name, :nuid, :size]
  defstruct bucket: nil,
            chunks: nil,
            deleted: false,
            digest: nil,
            name: nil,
            nuid: nil,
            size: nil

  @type t :: %__MODULE__{
          bucket: String.t(),
          chunks: non_neg_integer(),
          deleted: boolean(),
          digest: String.t(),
          name: String.t(),
          nuid: String.t(),
          size: non_neg_integer()
        }
end

defimpl Jason.Encoder, for: Gnat.Jetstream.API.Object.Meta do
  alias Gnat.Jetstream.API.Object.Meta

  def encode(%Meta{deleted: true} = meta, opts) do
    Map.take(meta, [:bucket, :chunks, :deleted, :digest, :name, :nuid, :size])
    |> Jason.Encode.map(opts)
  end

  def encode(meta, opts) do
    Map.take(meta, [:bucket, :chunks, :digest, :name, :nuid, :size])
    |> Jason.Encode.map(opts)
  end
end


================================================
FILE: lib/gnat/jetstream/api/object.ex
================================================
defmodule Gnat.Jetstream.API.Object do
  @moduledoc """
  API for interacting with the JetStream Object Store

  Learn more about Object Store: https://docs.nats.io/nats-concepts/jetstream/obj_store
  """
  alias Gnat.Jetstream.API.{Consumer, Stream, Util}
  alias Gnat.Jetstream.API.Object.Meta

  @stream_prefix "OBJ_"
  @subject_prefix "$O."

  @type bucket_opt ::
          {:description, String.t()}
          | {:max_bucket_size, integer()}
          | {:max_chunk_size, integer()}
          | {:placement, Stream.placement()}
          | {:replicas, non_neg_integer()}
          | {:storage, :file | :memory}
          | {:ttl, non_neg_integer()}
  @spec create_bucket(Gnat.t(), String.t(), list(bucket_opt)) ::
          {:ok, Stream.info()} | {:error, any()}
  def create_bucket(conn, bucket_name, params \\ []) do
    with :ok <- validate_bucket_name(bucket_name) do
      stream = %Stream{
        name: stream_name(bucket_name),
        subjects: stream_subjects(bucket_name),
        description: Keyword.get(params, :description),
        discard: :new,
        allow_rollup_hdrs: true,
        max_age: Keyword.get(params, :ttl, 0),
        max_bytes: Keyword.get(params, :max_bucket_size, -1),
        max_msg_size: Keyword.get(params, :max_chunk_size, -1),
        num_replicas: Keyword.get(params, :replicas, 1),
        storage: Keyword.get(params, :storage, :file),
        placement: Keyword.get(params, :placement),
        duplicate_window: adjust_duplicate_window(Keyword.get(params, :ttl, 0))
      }

      Stream.create(conn, stream)
    end
  end

  @spec delete_bucket(Gnat.t(), String.t()) :: :ok | {:error, any}
  def delete_bucket(conn, bucket_name) do
    Stream.delete(conn, stream_name(bucket_name))
  end

  @spec delete(Gnat.t(), String.t(), String.t()) :: :ok | {:error, any}
  def delete(conn, bucket_name, object_name) do
    with {:ok, meta = %Meta{}} <- info(conn, bucket_name, object_name),
         meta <- %Meta{meta | deleted: true},
         topic <- meta_stream_topic(bucket_name, object_name),
         {:ok, body} <- Jason.encode(meta),
         {:ok, _msg} <- Gnat.request(conn, topic, body, headers: [{"Nats-Rollup", "sub"}]) do
      filter = chunk_stream_topic(meta)
      Stream.purge(conn, stream_name(bucket_name), nil, %{filter: filter})
    end
  end

  @spec get(Gnat.t(), String.t(), String.t(), (binary -> any())) :: :ok | {:error, any}
  def get(conn, bucket_name, object_name, chunk_fun) do
    with {:ok, %{config: _stream}} <- Stream.info(conn, stream_name(bucket_name)),
         {:ok, meta} <- info(conn, bucket_name, object_name) do
      receive_chunks(conn, meta, chunk_fun)
    end
  end

  @spec info(Gnat.t(), String.t(), String.t()) :: {:ok, Meta.t()} | {:error, any}
  def info(conn, bucket_name, object_name) do
    with {:ok, _stream_info} <- Stream.info(conn, stream_name(bucket_name)) do
      Stream.get_message(conn, stream_name(bucket_name), %{
        last_by_subj: meta_stream_topic(bucket_name, object_name)
      })
      |> case do
        {:ok, message} ->
          meta = json_to_meta(message.data)
          {:ok, meta}

        error ->
          error
      end
    end
  end

  @type list_option :: {:show_deleted, boolean()}
  @spec list(Gnat.t(), String.t(), list(list_option())) :: {:error, any} | {:ok, list(Meta.t())}
  def list(conn, bucket_name, options \\ []) do
    with {:ok, %{config: stream}} <- Stream.info(conn, stream_name(bucket_name)),
         topic <- Util.reply_inbox(),
         {:ok, sub} <- Gnat.sub(conn, self(), topic),
         {:ok, consumer} <-
           Consumer.create(conn, %Consumer{
             stream_name: stream.name,
             deliver_subject: topic,
             deliver_policy: :last_per_subject,
             filter_subject: meta_stream_subject(bucket_name),
             ack_policy: :none,
             max_ack_pending: nil,
             replay_policy: :instant,
             max_deliver: 1
           }),
         {:ok, messages} <- receive_all_metas(sub, consumer.num_pending) do
      :ok = Gnat.unsub(conn, sub)
      :ok = Consumer.delete(conn, stream.name, consumer.name)

      show_deleted = Keyword.get(options, :show_deleted, false)

      if show_deleted do
        {:ok, messages}
      else
        {:ok, Enum.reject(messages, &(&1.deleted == true))}
      end
    end
  end

  @spec put(Gnat.t(), String.t(), String.t(), File.io_device()) ::
          {:ok, Meta.t()} | {:error, any()}
  def put(conn, bucket_name, object_name, io) do
    nuid = Util.nuid()
    chunk_topic = chunk_stream_topic(bucket_name, nuid)

    with {:ok, %{config: _}} <- Stream.info(conn, stream_name(bucket_name)),
         :ok <- purge_prior_chunks(conn, bucket_name, object_name),
         {:ok, chunks, size, digest} <- send_chunks(conn, io, chunk_topic) do
      object_meta = %Meta{
        name: object_name,
        bucket: bucket_name,
        nuid: nuid,
        size: size,
        chunks: chunks,
        digest: "SHA-256=#{Base.url_encode64(digest)}"
      }

      topic = meta_stream_topic(bucket_name, object_name)
      body = Jason.encode!(object_meta)

      case Gnat.request(conn, topic, body, headers: [{"Nats-Rollup", "sub"}]) do
        {:ok, _} ->
          {:ok, object_meta}

        error ->
          error
      end
    end
  end

  @doc """
  Returns true if the provided stream is an Object bucket, false otherwise
  ## Parameters
  * `stream_name` - the stream name to test
  """
  @spec is_object_bucket_stream?(stream_name :: binary()) :: boolean()
  def is_object_bucket_stream?(stream_name) do
    String.starts_with?(stream_name, "OBJ_")
  end

  @doc """
  Returns a list of all Object buckets
  """
  @spec list_buckets(conn :: Gnat.t()) :: {:error, term()} | {:ok, list(String.t())}
  def list_buckets(conn) do
    with {:ok, %{streams: streams}} <- Stream.list(conn) do
      stream_names =
        streams
        |> Enum.flat_map(fn bucket ->
          if is_object_bucket_stream?(bucket) do
            [bucket |> String.trim_leading(@stream_prefix)]
          else
            []
          end
        end)

      {:ok, stream_names}
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  defp stream_name(bucket_name) do
    "#{@stream_prefix}#{bucket_name}"
  end

  defp stream_subjects(bucket_name) do
    [
      chunk_stream_subject(bucket_name),
      meta_stream_subject(bucket_name)
    ]
  end

  defp chunk_stream_subject(bucket_name) do
    "#{@subject_prefix}#{bucket_name}.C.>"
  end

  defp chunk_stream_topic(bucket_name, nuid) do
    "#{@subject_prefix}#{bucket_name}.C.#{nuid}"
  end

  defp chunk_stream_topic(%Meta{bucket: bucket, nuid: nuid}) do
    "#{@subject_prefix}#{bucket}.C.#{nuid}"
  end

  defp meta_stream_subject(bucket_name) do
    "#{@subject_prefix}#{bucket_name}.M.>"
  end

  defp meta_stream_topic(bucket_name, object_name) do
    key = Base.url_encode64(object_name)
    "#{@subject_prefix}#{bucket_name}.M.#{key}"
  end

  @two_minutes_in_nanoseconds 120_000_000_000
  # The `duplicate_window` can't be greater than the `max_age`. The default `duplicate_window`
  # is 2 minutes. We'll keep the 2 minute window UNLESS the ttl is less than 2 minutes
  defp adjust_duplicate_window(ttl) when ttl > 0 and ttl < @two_minutes_in_nanoseconds, do: ttl
  defp adjust_duplicate_window(_ttl), do: @two_minutes_in_nanoseconds

  defp json_to_meta(json) do
    raw = Jason.decode!(json)

    %{
      "bucket" => bucket,
      "chunks" => chunks,
      "digest" => digest,
      "name" => name,
      "nuid" => nuid,
      "size" => size
    } = raw

    %Meta{
      bucket: bucket,
      chunks: chunks,
      digest: digest,
      deleted: Map.get(raw, "deleted", false),
      name: name,
      nuid: nuid,
      size: size
    }
  end

  defp purge_prior_chunks(conn, bucket, name) do
    case info(conn, bucket, name) do
      {:ok, meta} ->
        Stream.purge(conn, stream_name(bucket), nil, %{filter: chunk_stream_topic(meta)})

      {:error, %{"code" => 404}} ->
        :ok

      {:error, other} ->
        {:error, other}
    end
  end

  defp receive_all_metas(sid, num_pending, messages \\ [])

  defp receive_all_metas(_sid, 0, messages) do
    {:ok, messages}
  end

  defp receive_all_metas(sid, remaining, messages) do
    receive do
      {:msg, %{sid: ^sid, body: body}} ->
        meta = json_to_meta(body)
        receive_all_metas(sid, remaining - 1, [meta | messages])
    after
      10_000 ->
        {:error, :timeout_waiting_for_messages}
    end
  end

  defp receive_chunks(conn, %Meta{} = meta, chunk_fun) do
    topic = chunk_stream_topic(meta)
    stream = stream_name(meta.bucket)
    inbox = Util.reply_inbox()
    {:ok, sub} = Gnat.sub(conn, self(), inbox)

    {:ok, consumer} =
      Consumer.create(conn, %Consumer{
        stream_name: stream,
        deliver_subject: inbox,
        deliver_policy: :all,
        filter_subject: topic,
        ack_policy: :none,
        max_ack_pending: nil,
        replay_policy: :instant,
        max_deliver: 1,
        flow_control: true,
        idle_heartbeat: 5_000_000_000
      })

    :ok = receive_chunks(conn, sub, meta.chunks, chunk_fun)

    :ok = Gnat.unsub(conn, sub)
    :ok = Consumer.delete(conn, stream, consumer.name)
  end

  defp receive_chunks(_conn, _sub, 0, _chunk_fun) do
    :ok
  end

  defp receive_chunks(conn, sub, remaining, chunk_fun) do
    receive do
      # Flow control message with reply - respond to it
      {:msg, %{sid: ^sub, status: "100", description: "FlowControl Request", reply_to: reply}}
      when not is_nil(reply) ->
        Gnat.pub(conn, reply, "")
        receive_chunks(conn, sub, remaining, chunk_fun)

      # Flow control or heartbeat message without reply - get next message
      {:msg, %{sid: ^sub, body: "", status: "100"}} ->
        receive_chunks(conn, sub, remaining, chunk_fun)

      # Regular data message
      {:msg, %{sid: ^sub, body: body}} ->
        chunk_fun.(body)
        receive_chunks(conn, sub, remaining - 1, chunk_fun)
    after
      10_000 ->
        {:error, :timeout_waiting_for_messages}
    end
  end

  @chunk_size 128 * 1024
  defp send_chunks(conn, io, topic) do
    sha = :crypto.hash_init(:sha256)
    size = 0
    chunks = 0
    send_chunks(conn, io, topic, sha, size, chunks)
  end

  defp send_chunks(conn, io, topic, sha, size, chunks) do
    case IO.binread(io, @chunk_size) do
      :eof ->
        sha = :crypto.hash_final(sha)
        {:ok, chunks, size, sha}

      {:error, err} ->
        {:error, err}

      bytes ->
        sha = :crypto.hash_update(sha, bytes)
        size = size + byte_size(bytes)
        chunks = chunks + 1

        case Gnat.request(conn, topic, bytes) do
          {:ok, _} ->
            send_chunks(conn, io, topic, sha, size, chunks)

          error ->
            error
        end
    end
  end

  defp validate_bucket_name(name) do
    case Regex.match?(~r/^[a-zA-Z0-9_-]+$/, name) do
      true -> :ok
      false -> {:error, "invalid bucket name"}
    end
  end
end


================================================
FILE: lib/gnat/jetstream/api/stream.ex
================================================
defmodule Gnat.Jetstream.API.Stream do
  @moduledoc """
  A module representing a NATS JetStream Stream.

  Learn more about Streams: https://docs.nats.io/nats-concepts/jetstream/streams

  ## The Jetstream.API.Stream struct

  The struct's mandatory fields are `:name` and `:subjects`. The rest will have the NATS
  default values set.

  Stream struct fields explanation:

  * `:allow_direct` - Allow higher performance, direct access to get individual messages. E.g. KeyValue
  * `:allow_msg_ttl` - Allow header initiated per-message TTLs.
  * `:allow_rollup_hdrs` - allows the use of the Nats-Rollup header to replace all contents of a stream,
    or subject in a stream, with a single new message.
  * `:deny_delete` - restricts the ability to delete messages from a stream via the API. Cannot be changed
    once set to true.
  * `:deny_purge` - restricts the ability to purge messages from a stream via the API. Cannot be change
    once set to true.
  * `:description` - a short description of the purpose of this stream.
  * `:discard` - determines what happens when a Stream reaches its limits. It has the following options:
     - `:old` - the default option. Old messages are deleted.
     - `:new` - refuses new messages.
  * `:discard_new_per_subject` - - allows to enable discarding new messages per subject when limits are reached.
    Requires `discard: :new` and the `:max_msgs_per_subject` to be configured.
  * `:domain` - JetStream domain, mainly used for leaf nodes.
     See [JetStream on Leaf Nodes](https://docs.nats.io/running-a-nats-service/configuration/leafnodes/jetstream_leafnodes).
  * `:duplicate_window` - the window within which to track duplicate messages, expressed in nanoseconds.
  * `:max_age` - maximum age of any message in the Stream, expressed in nanoseconds.
  * `:max_bytes` - how many bytes the Stream may contain. Adheres to `:discard`, removing oldest or
    refusing new messages if the Stream exceeds this size.
  * `:max_consumers` - how many Consumers can be defined for a given Stream, -1 for unlimited.
  * `:max_msg_size` - the largest message that will be accepted by the Stream.
  * `:max_msgs_per_subject` - For wildcard streams ensure that for every unique subject this many messages are kept - a per subject retention limit.
    Only available on nats-server versions greater than 2.3.0
  * `:max_msgs` - how many messages may be in a Stream. Adheres to `:discard`, removing oldest or refusing
    new messages if the Stream exceeds this number of messages
  * `:mirror` - maintains a 1:1 mirror of another stream with name matching this property.  When a mirror
    is configured subjects and sources must be empty.
  * `:mirror_direct` - Allow higher performance and unified direct access for mirrors as well.
  * `:name` - a name for the Stream.
    See [naming](https://docs.nats.io/running-a-nats-service/nats_admin/jetstream_admin/naming).
  * `:no_ack` - disables acknowledging messages that are received by the Stream.
  * `:num_replicas` - how many replicas to keep for each message.
  * `:placement` - placement directives to consider when placing replicas of this stream, random placement
    when unset. It has the following properties:
     - `:cluster` - the desired cluster name to place the stream.
     - `:tags` - tags required on servers hosting this stream.
  * `:retention` - how messages are retained in the Stream. Once this is exceeded, old messages are removed.
    It has the following options:
     - `:limits` - the default policy.
     - `:interest`
     - `:workqueue`
  * `:sealed` - sealed streams do not allow messages to be deleted via limits or API, sealed streams can not
    be unsealed via configuration update. Can only be set on already created streams via the Update API.
  * `:sources` - list of stream names to replicate into this stream.
  * `:storage` - the type of storage backend. Available options:
     - `:file`
     - `:memory`
  * `:compression` - If file-based and a compression algorithm is specified, the stream data will be compressed on disk.
    Valid options are "none" for no compression or "s2" for Snappy compression.
  * `:subjects` - a list of subjects to consume, supports wildcards.
  * `:subject_delete_marker_ttl` - Enables and sets a duration expressed in nanoseconds. for adding server markers for
    delete, purge and max age limits.
  * `:template_owner` - when the Stream is managed by a Stream Template this identifies the template that
    manages the Stream.
  """

  import Gnat.Jetstream.API.Util

  @enforce_keys [:name, :subjects]
  @derive {Jason.Encoder, except: [:domain]}
  defstruct [
    :description,
    :mirror,
    :name,
    :domain,
    :no_ack,
    :placement,
    :sources,
    :subjects,
    :template_owner,
    allow_direct: false,
    allow_msg_ttl: false,
    allow_rollup_hdrs: false,
    deny_delete: false,
    deny_purge: false,
    discard: :old,
    duplicate_window: 120_000_000_000,
    max_age: 0,
    max_bytes: -1,
    max_consumers: -1,
    max_msg_size: -1,
    max_msgs_per_subject: -1,
    max_msgs: -1,
    mirror_direct: false,
    num_replicas: 1,
    retention: :limits,
    sealed: false,
    storage: :file,
    subject_delete_marker_ttl: 0,
    discard_new_per_subject: false,
    compression: "none"
  ]

  @type nanoseconds :: non_neg_integer()

  @type placement :: %{
          :cluster => binary(),
          optional(:tags) => list(binary())
        }

  @type t :: %__MODULE__{
          allow_direct: boolean(),
          allow_msg_ttl: boolean(),
          allow_rollup_hdrs: boolean(),
          deny_delete: boolean(),
          deny_purge: boolean(),
          description: nil | binary(),
          discard: :old | :new,
          domain: nil | binary(),
          duplicate_window: nil | nanoseconds(),
          max_age: nanoseconds(),
          max_bytes: integer(),
          max_consumers: integer(),
          max_msg_size: nil | integer(),
          max_msgs: integer(),
          max_msgs_per_subject: integer(),
          mirror: nil | source(),
          mirror_direct: boolean(),
          name: binary(),
          no_ack: nil | boolean(),
          num_replicas: pos_integer(),
          placement: nil | placement(),
          retention: :limits | :workqueue | :interest,
          sealed: boolean(),
          sources: nil | list(source()),
          storage: :file | :memory,
          subjects: nil | list(binary()),
          subject_delete_marker_ttl: nanoseconds(),
          template_owner: nil | binary(),
          discard_new_per_subject: boolean(),
          compression: binary()
        }

  @typedoc """
  Stream source fields explained:

  * `:name` - stream name.
  * `:opt_start_seq` - sequence to start replicating from.
  * `:opt_start_time` - timestamp to start replicating from.
  * `:filter_subject` - replicate only a subset of messages based on filter.
  * `:external` - configuration referencing a stream source in another account or JetStream domain.
    It has the following parameters:
     - `:api` - the subject prefix that imports other account/domain `$JS.API.CONSUMER.>` subjects
     - `:deliver` - the delivery subject to use for push consumer
  """
  @type source :: %{
          :name => binary(),
          optional(:opt_start_seq) => integer(),
          optional(:opt_start_time) => DateTime.t(),
          optional(:filter_subject) => binary(),
          optional(:external) => %{
            api: binary(),
            deliver: binary()
          }
        }

  @type info :: %{
          cluster:
            nil
            | %{
                optional(:name) => binary(),
                optional(:leader) => binary(),
                optional(:replicas) =>
                  list(%{
                    :active => nanoseconds(),
                    :name => binary(),
                    :current => boolean(),
                    optional(:offline) => boolean(),
                    optional(:lag) => non_neg_integer()
                  })
              },
          config: t(),
          created: DateTime.t(),
          mirror: nil | source_info(),
          sources: nil | list(source_info()),
          state: state()
        }

  @type state :: %{
          bytes: non_neg_integer(),
          consumer_count: non_neg_integer(),
          deleted: nil | [non_neg_integer()],
          first_seq: non_neg_integer(),
          first_ts: DateTime.t(),
          last_seq: non_neg_integer(),
          last_ts: DateTime.t(),
          lost: nil | list(%{msgs: [non_neg_integer()], bytes: non_neg_integer()}),
          messages: non_neg_integer(),
          num_deleted: nil | integer(),
          num_subjects: nil | integer(),
          subjects:
            nil
            | %{}
            | %{
                binary() => non_neg_integer()
              }
        }

  @typedoc """
  * `code` - HTTP like error code in the 300 to 500 range
  * `description` - A human friendly description of the error
  * `err_code` - The NATS error code unique to each kind of error
  """
  @type response_error :: %{
          :code => non_neg_integer(),
          optional(:description) => binary(),
          optional(:err_code) => non_neg_integer()
        }

  @type source_info :: %{
          :active => nanoseconds(),
          :lag => non_neg_integer(),
          :name => binary(),
          optional(:external) => %{
            api: binary(),
            deliver: binary()
          },
          optional(:error) => response_error()
        }

  @type streams :: %{
          limit: non_neg_integer(),
          offset: non_neg_integer(),
          streams: list(binary()),
          total: non_neg_integer()
        }

  @typedoc """
  * `seq` - Stream sequence number of the message to retrieve, cannot be combined with `last_by_subj`
  * `last_by_subj` - Retrieves the last message for a given subject, cannot be combined with `seq`
  """
  @type message_access_method :: %{
          optional(:seq) => non_neg_integer(),
          optional(:last_by_subj) => binary()
        }

  @typedoc """
  * `data` - The decoded message payload
  * `subject` - The subject the message was originally received on
  * `time` - The time the message was received
  * `seq` - The sequence number of the message in the Stream
  * `hdrs` - The decoded headers for the message
  """
  @type message_response :: %{
          :data => any(),
          :seq => non_neg_integer(),
          :subject => binary(),
          :time => DateTime.t(),
          :hdrs => nil | binary()
        }

  # @doc """
  # Initialize a Stream struct

  # ## Examples

  #     iex> %Stream{} = Gnat.Jetstream.API.Stream.new(:gnat, name: "NEW_STREAM", subjects: ["NEW_STREAM.subjects"])
  # """
  # @spec new(conn :: Gnat.t(), fields :: keyword()) :: t()
  # def new(conn, fields \\ []) do
  #   %__MODULE__{}
  # end

  @doc """
  Creates a new Stream.

  ## Examples

      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "anewstream", subjects: ["anewsubject"]})

  """
  @spec create(conn :: Gnat.t(), stream :: t()) :: {:ok, info()} | {:error, any()}
  def create(conn, %__MODULE__{} = stream) do
    with :ok <- validate(stream),
         {:ok, stream} <-
           request(
             conn,
             "#{js_api(stream.domain)}.STREAM.CREATE.#{stream.name}",
             Jason.encode!(stream)
           ) do
      {:ok, to_info(stream)}
    end
  end

  @doc """
  Updates a Stream.

  ## Examples

      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "update_test_stream", subjects: ["update_subject"]})
      iex> {:ok, _} = Gnat.Jetstream.API.Stream.update(:gnat, %Gnat.Jetstream.API.Stream{name: "update_test_stream", subjects: ["update_subject", "new.update_subject"]})

  """
  @spec update(conn :: Gnat.t(), stream :: t()) :: {:ok, info()} | {:error, any()}
  def update(conn, %__MODULE__{} = stream) do
    with :ok <- validate(stream),
         {:ok, stream} <-
           request(
             conn,
             "#{js_api(stream.domain)}.STREAM.UPDATE.#{stream.name}",
             Jason.encode!(stream)
           ) do
      {:ok, to_info(stream)}
    end
  end

  @doc """
  Deletes a Stream and all its data.

  ## Examples

      iex> Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "delstream", subjects: ["delsubject"]})
      iex> Gnat.Jetstream.API.Stream.delete(:gnat, "delstream")
      :ok

      iex> {:error, %{"code" => 404, "description" => "stream not found"}} = Gnat.Jetstream.API.Stream.delete(:gnat, "wrong_stream")

  """
  @spec delete(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary()) ::
          :ok | {:error, any()}
  def delete(conn, stream_name, domain \\ nil) when is_binary(stream_name) do
    with {:ok, _response} <- request(conn, "#{js_api(domain)}.STREAM.DELETE.#{stream_name}", "") do
      :ok
    end
  end

  @doc """
  Purges all of data in the stream but doesn't delete the stream.

  ## Examples

      iex> Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "purgestream", subjects: ["purgesubject"]})
      iex> Gnat.Jetstream.API.Stream.purge(:gnat, "purgestream")
      :ok

      iex> {:error, %{"code" => 404, "description" => "stream not found"}} = Gnat.Jetstream.API.Stream.purge(:gnat, "wrong_stream")

  """
  @spec purge(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary()) ::
          :ok | {:error, any()}
  def purge(conn, stream_name, domain \\ nil) when is_binary(stream_name) do
    with {:ok, _response} <- request(conn, "#{js_api(domain)}.STREAM.PURGE.#{stream_name}", "") do
      :ok
    end
  end

  @doc """
  Purges some of the messages in a stream according to the supplied filter

  ## Examples

      iex> Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "pstream", subjects: ["psub1", "psub2"]})
      iex> Gnat.Jetstream.API.Stream.purge(:gnat, "pstream", nil, %{filter: "psub1"})
      :ok

  """
  @type method :: %{filter: String.t()}
  @spec purge(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary(), method) ::
          :ok | {:error, any()}
  def purge(conn, stream_name, domain, method) when is_binary(stream_name) do
    with :ok <- validate_purge_method(method),
         body <- Jason.encode!(method),
         {:ok, _response} <- request(conn, "#{js_api(domain)}.STREAM.PURGE.#{stream_name}", body) do
      :ok
    end
  end

  @doc """
  Information about config and state of a Stream.

  ## Examples

      iex> {:ok, _} = Gnat.Jetstream.API.Stream.create(:gnat, %Gnat.Jetstream.API.Stream{name: "infostream", subjects: ["infosubject"]})
      iex> {:ok, %{created: _}} = Gnat.Jetstream.API.Stream.info(:gnat, "infostream")

      iex> {:error, %{"code" => 404, "description" => "stream not found"}} = Gnat.Jetstream.API.Stream.info(:gnat, "wrong_stream")

  """
  @spec info(conn :: Gnat.t(), stream_name :: binary(), domain :: nil | binary()) ::
          {:ok, info()} | {:error, any()}
  def info(conn, stream_name, domain \\ nil) when is_binary(stream_name) do
    with {:ok, decoded} <- request(conn, "#{js_api(domain)}.STREAM.INFO.#{stream_name}", "") do
      {:ok, to_info(decoded)}
    end
  end

  @doc """
  Paged list of known Streams including all their current information.

  ## Options

  * `:offset` - Number of records to skip
  * `:subject` - A subject the `Stream` must collect to appear in the list.

  ## Examples

      iex> {:ok, %{total: _, offset: 0, limit: 1024, streams: _}} = Gnat.Jetstream.API.Stream.list(:gnat)

  """
  @spec list(
          conn :: Gnat.t(),
          params :: [offset: non_neg_integer(), subject: binary(), domain: nil | binary()]
        ) ::
          {:ok, streams()} | {:error, term()}
  def list(conn, params \\ []) do
    domain = Keyword.get(params, :domain)

    payload =
      Jason.encode!(%{
        offset: Keyword.get(params, :offset, 0),
        subject: Keyword.get(params, :subject)
      })

    with {:ok, decoded} <- request(conn, "#{js_api(domain)}.STREAM.NAMES", payload) do
      # Recent versions of NATS sometimes return `"streams": null` in their JSON payload to indicate
      # that no streams are defined. But, that would mean callers have to handle both `nil` and a list, so
      # we coerce that to an empty list to represent no streams being defined.
      streams =
        case Map.get(decoded, "streams") do
          nil -> []
          names when is_list(names) -> names
        end

      result = %{
        limit: Map.get(decoded, "limit"),
        offset: Map.get(decoded, "offset"),
        streams: streams,
        total: Map.get(decoded, "total")
      }

      {:ok, result}
    end
  end

  @doc """
  Get a message from the stream either by "stream sequence number" or the "last message for a given subject"
  """
  @spec get_message(
          conn :: Gnat.t(),
          stream_name :: binary(),
          method :: message_access_method(),
          domain :: nil | binary()
        ) ::
          {:ok, message_response()} | {:error, response_error()}
  def get_message(conn, stream_name, method, domain \\ nil) when is_map(method) do
    with :ok <- validate_message_access_method(method),
         {:ok, %{"message" => message}} <-
           request(conn, "#{js_api(domain)}.STREAM.MSG.GET.#{stream_name}", Jason.encode!(method)) do
      {:ok,
       %{
         data: decode_base64(message["data"]),
         seq: message["seq"],
         subject: message["subject"],
         time: to_datetime(message["time"]),
         hdrs: decode_base64(message["hdrs"])
       }}
    end
  end

  # https://docs.nats.io/running-a-nats-service/configuration/leafnodes/jetstream_leafnodes
  defp js_api(nil), do: "$JS.API"
  defp js_api(""), do: "$JS.API"
  defp js_api(domain), do: "$JS.#{domain}.API"

  defp to_state(state) do
    %{
      bytes: Map.fetch!(state, "bytes"),
      consumer_count: Map.fetch!(state, "consumer_count"),
      deleted: Map.get(state, "deleted"),
      first_seq: Map.fetch!(state, "first_seq"),
      first_ts: Map.fetch!(state, "first_ts") |> to_datetime(),
      last_seq: Map.fetch!(state, "last_seq"),
      last_ts: Map.fetch!(state, "last_ts") |> to_datetime(),
      lost: Map.get(state, "lost"),
      messages: Map.fetch!(state, "messages"),
      num_deleted: Map.get(state, "num_deleted"),
      num_subjects: Map.get(state, "num_subjects"),
      subjects: Map.get(state, "subjects")
    }
  end

  defp to_stream(stream) do
    %__MODULE__{
      description: Map.get(stream, "description"),
      discard: Map.fetch!(stream, "discard") |> to_sym(),
      duplicate_window: Map.get(stream, "duplicate_window"),
      max_age: Map.fetch!(stream, "max_age"),
      max_bytes: Map.fetch!(stream, "max_bytes"),
      max_consumers: Map.fetch!(stream, "max_consumers"),
      max_msg_size: Map.get(stream, "max_msg_size"),
      max_msgs_per_subject: Map.get(stream, "max_msgs_per_subject", -1),
      max_msgs: Map.fetch!(stream, "max_msgs"),
      mirror: Map.get(stream, "mirror"),
      name: Map.fetch!(stream, "name"),
      no_ack: Map.get(stream, "no_ack"),
      num_replicas: Map.fetch!(stream, "num_replicas"),
      placement: Map.get(stream, "placement"),
      retention: Map.fetch!(stream, "retention") |> to_sym(),
      sources: Map.get(stream, "sources"),
      storage: Map.fetch!(stream, "storage") |> to_sym(),
      subjects: Map.get(stream, "subjects"),
      template_owner: Map.get(stream, "template_owner")
    }
    # Check for fields added in NATS versions higher than 2.2.0
    |> put_if_exist(:allow_direct, stream, "allow_direct")
    |> put_if_exist(:allow_msg_ttl, stream, "allow_msg_ttl")
    |> put_if_exist(:allow_rollup_hdrs, stream, "allow_rollup_hdrs")
    |> put_if_exist(:deny_delete, stream, "deny_delete")
    |> put_if_exist(:deny_purge, stream, "deny_purge")
    |> put_if_exist(:discard_new_per_subject, stream, "discard_new_per_subject")
    |> put_if_exist(:mirror_direct, stream, "mirror_direct")
    |> put_if_exist(:sealed, stream, "sealed")
    |> put_if_exist(:subject_delete_marker_ttl, stream, "subject_delete_marker_ttl")
    |> put_if_exist(:compression, stream, "compression")
  end

  defp to_info(%{"config" => config, "state" => state, "created" => created} = response) do
    with {:ok, created, _} <- DateTime.from_iso8601(created) do
      %{
        cluster: Map.get(response, "cluster"),
        config: to_stream(config),
        created: created,
        mirror: Map.get(response, "mirror"),
        sources: Map.get(response, "sources"),
        state: to_state(state)
      }
    end
  end

  defp validate(stream_settings) do
    cond do
      Map.has_key?(stream_settings, :name) == false ->
        {:error, "Must have a :name set"}

      is_binary(Map.get(stream_settings, :name)) == false ->
        {:error, "name must be a string"}

      valid_name?(stream_settings.name) == false ->
        {:error, "invalid name: " <> invalid_name_message()}

      Map.has_key?(stream_settings, :subjects) == false ->
        {:error, "You must specify a :subjects key"}

      is_list(Map.get(stream_settings, :subjects)) == false ->
        {:error, ":subjects must be a list of strings"}

      Enum.all?(Map.get(stream_settings, :subjects), &is_binary/1) == false ->
        {:error, ":subjects must be a list of strings"}

      true ->
        :ok
    end
  end

  defp validate_message_access_method(method) do
    if map_size(method) == 1 do
      :ok
    else
      {:error, "To get a message you must use only one of `seq` or `last_by_subj`"}
    end
  end

  defp validate_purge_method(%{filter: subject}) when is_binary(subject) do
    :ok
  end

  defp validate_purge_method(_) do
    {:error, "When purging, you must pass a %{filter: subject}"}
  end
end


================================================
FILE: lib/gnat/jetstream/api/util.ex
================================================
defmodule Gnat.Jetstream.API.Util do
  @moduledoc false

  @default_inbox_prefix "_INBOX."

  def request(conn, topic, payload) do
    with {:ok, %{body: body}} <- Gnat.request(conn, topic, payload),
         {:ok, decoded} <- Jason.decode(body) do
      case decoded do
        %{"error" => err} ->
          {:error, err}

        other ->
          {:ok, other}
      end
    end
  end

  def to_datetime(nil), do: nil

  def to_datetime(str) do
    {:ok, datetime, _} = DateTime.from_iso8601(str)
    datetime
  end

  def to_sym(nil), do: nil

  def to_sym(str) when is_binary(str) do
    String.to_existing_atom(str)
  end

  def put_if_exist(target_map, target_key, source_map, source_key) do
    case Map.fetch(source_map, source_key) do
      {:ok, value} -> Map.put(target_map, target_key, value)
      _ -> target_map
    end
  end

  def valid_name?(name) do
    !String.contains?(name, [".", "*", ">", " ", "\t"])
  end

  def invalid_name_message do
    "cannot contain '.', '>', '*', spaces or tabs"
  end

  def decode_base64(nil), do: nil
  def decode_base64(data), do: Base.decode64!(data)

  def reply_inbox(prefix \\ @default_inbox_prefix)
  def reply_inbox(nil), do: reply_inbox()
  def reply_inbox(prefix), do: prefix <> nuid()

  def nuid() do
    :crypto.strong_rand_bytes(12) |> Base.url_encode64()
  end
end


================================================
FILE: lib/gnat/jetstream/jetstream.ex
================================================
defmodule Gnat.Jetstream do
  @moduledoc """
  Provides functions for interacting with a [NATS Jetstream](https://github.com/nats-io/jetstream)
  server.
  """

  @doc """
  Sends `AckAck` acknowledgement to the server.

  Acknowledges a message was completely handled.
  """
  @spec ack(message :: Gnat.message()) :: :ok
  def ack(%{reply_to: nil}) do
    {:error, "Cannot ack message with no reply-to"}
  end

  def ack(%{gnat: gnat, reply_to: reply_to}) do
    Gnat.pub(gnat, reply_to, "")
  end

  @doc """
  Sends `AckNext` acknowledgement to the server.

  Acknowledges the message was handled and requests delivery of the next message to the reply
  subject. Only applies to Pull-mode.
  """
  @spec ack_next(message :: Gnat.message(), consumer_subject :: binary()) :: :ok
  def ack_next(%{reply_to: nil}, _consumer_subject) do
    {:error, "Cannot ack message with no reply-to"}
  end

  def ack_next(%{gnat: gnat, reply_to: reply_to}, consumer_subject) do
    Gnat.pub(gnat, reply_to, "+NXT", reply_to: consumer_subject)
  end

  @doc """
  Sends `AckNak` acknowledgement to the server.

  Signals that the message will not be processed now and processing can move onto the next message.
  NAK'd message will be retried.
  """
  @spec nack(message :: Gnat.message()) :: :ok
  def nack(%{reply_to: nil}) do
    {:error, "Cannot ack message with no reply-to"}
  end

  def nack(%{gnat: gnat, reply_to: reply_to}) do
    Gnat.pub(gnat, reply_to, "-NAK")
  end

  @doc """
  Sends `AckProgress` acknowledgement to the server.

  When sent before the `AckWait` period indicates that work is ongoing and the period should be
  extended by another equal to `AckWait`.
  """
  @spec ack_progress(message :: Gnat.message()) :: :ok
  def ack_progress(%{reply_to: nil}) do
    {:error, "Cannot ack message with no reply-to"}
  end

  def ack_progress(%{gnat: gnat, reply_to: reply_to}) do
    Gnat.pub(gnat, reply_to, "+WPI")
  end

  @doc """
  Sends `AckTerm` acknowledgement to the server.

  Instructs the server to stop redelivery of a message without acknowledging it as successfully
  processed.
  """
  @spec ack_term(message :: Gnat.message()) :: :ok
  def ack_term(%{reply_to: nil}) do
    {:error, "Cannot ack message with no reply-to"}
  end

  def ack_term(%{gnat: gnat, reply_to: reply_to}) do
    Gnat.pub(gnat, reply_to, "+TERM")
  end
end


================================================
FILE: lib/gnat/jetstream/pager.ex
================================================
defmodule Gnat.Jetstream.Pager do
  @moduledoc """
  Page through all the messages in a stream

  This module provides a synchronous API to inspect the messages in a stream.
  You can use the reduce module to write a simple function that works like `Enum.reduce` across each message individually.
  If you want to handle messages in batches, you can use the `init` + `page` functions to accomplish that.
  """

  alias Gnat.Jetstream
  alias Gnat.Jetstream.API.{Consumer, Util}

  @opaque pager :: map()
  @type message :: Gnat.message()
  @type opts :: list(opt())

  @typedoc """
  Options you can pass to the pager

  * `batch` controls the maximum number of messages we'll pull in each page/batch (default 10)
  * `domain` You can specify a jetstream domain if needed
  * `from_datetime` Only page through messages recorded on or after this datetime
  * `from_seq` Only page through messages with a sequence number equal or above this option
  * `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.

  """
  @type opt ::
          {:batch, non_neg_integer()}
          | {:domain, String.t()}
          | {:from_datetime, DateTime.t()}
          | {:from_seq, non_neg_integer}
          | {:headers_only, boolean()}

  @spec init(Gnat.t(), String.t(), opts()) :: {:ok, pager()} | {:error, term()}
  def init(conn, stream_name, opts) do
    domain = Keyword.get(opts, :domain)

    consumer = %Consumer{
      stream_name: stream_name,
      domain: domain,
      ack_policy: :all,
      ack_wait: 30_000_000_000,
      deliver_policy: :all,
      description: "ephemeral consumer",
      replay_policy: :instant,
      inactive_threshold: 30_000_000_000,
      headers_only: Keyword.get(opts, :headers_only)
    }

    consumer = apply_opts_to_consumer(consumer, opts)

    inbox = Util.reply_inbox()

    with {:ok, consumer_info} <- Consumer.create(conn, consumer),
         {:ok, sub} <- Gnat.sub(conn, self(), inbox) do
      state = %{
        conn: conn,
        stream_name: stream_name,
        consumer_name: consumer_info.name,
        domain: domain,
        inbox: inbox,
        batch: Keyword.get(opts, :batch, 10),
        sub: sub
      }

      {:ok, state}
    end
  end

  @spec page(pager()) :: {:page, list(message())} | {:done, list(message())} | {:error, term()}
  def page(state) do
    with :ok <- request_next_message(state) do
      receive_messages(state, [])
    end
  end

  def cleanup(%{conn: conn} = state) do
    :ok = Gnat.unsub(conn, state.sub)
    Consumer.delete(conn, state.stream_name, state.consumer_name, state.domain)
  end

  @doc """
  Similar to Enum.reduce but you can iterate through all messages in a stream

  ```
  # Assume we have a stream with messages like "1", "2", ... "10"
  Gnat.Jetstream.Pager.reduce(:gnat, "NUMBERS_STREAM", [batch_size: 5], 0, fn(message, total) ->
    num = String.to_integer(message.body)
    total + num
  end)

  # => {:ok, 55}
  ```
  """
  @spec reduce(
          Gnat.t(),
          String.t(),
          opts(),
          Enum.acc(),
          (Gnat.message(), Enum.acc() -> Enum.acc())
        ) :: {:ok, Enum.acc()} | {:error, term()}
  def reduce(conn, stream_name, opts, initial_state, fun) do
    with {:ok, pager} <- init(conn, stream_name, opts) do
      page_through(pager, initial_state, fun)
    end
  end

  defp page_through(pager, state, fun) do
    case page(pager) do
      {:page, messages} ->
        new_state = Enum.reduce(messages, state, fun)
        page_through(pager, new_state, fun)

      {:done, messages} ->
        new_state = Enum.reduce(messages, state, fun)
        cleanup(pager)
        {:ok, new_state}
    end
  end

  defp request_next_message(state) do
    opts = [batch: state.batch, no_wait: true]

    Consumer.request_next_message(
      state.conn,
      state.stream_name,
      state.consumer_name,
      state.inbox,
      state.domain,
      opts
    )
  end

  defp receive_messages(%{batch: batch}, messages) when length(messages) == batch do
    last = hd(messages)
    :ok = Jetstream.ack(last)
    {:page, Enum.reverse(messages)}
  end

  @terminals ["404", "408"]
  defp receive_messages(%{sub: sid} = state, messages) do
    receive do
      {:msg, %{sid: ^sid, status: status}} when status in @terminals ->
        {:done, Enum.reverse(messages)}

      {:msg, %{sid: ^sid, reply_to: nil}} ->
        {:done, Enum.reverse(messages)}

      {:msg, %{sid: ^sid} = message} ->
        receive_messages(state, [message | messages])
    end
  end

  ## Helpers for accepting user options
  defp apply_opts_to_consumer(consumer = %Consumer{}, opts) do
    from = {Keyword.get(opts, :from_seq), Keyword.get(opts, :from_datetime)}

    case from do
      {nil, nil} ->
        consumer

      {seq, _} when is_integer(seq) ->
        %Consumer{consumer | deliver_policy: :by_start_sequence, opt_start_seq: seq}

      {_, %DateTime{} = dt} ->
        %Consumer{consumer | deliver_policy: :by_start_time, opt_start_time: dt}
    end
  end
end


================================================
FILE: lib/gnat/jetstream/pull_consumer/connection_options.ex
================================================
defmodule Gnat.Jetstream.PullConsumer.ConnectionOptions do
  @moduledoc false

  @default_retry_timeout 1000
  @default_retries 10

  @enforce_keys [
    :connection_name,
    :connection_retry_timeout,
    :connection_retries,
    :inbox_prefix,
    :domain
  ]

  # 5 Seconds in nanoseconds
  @default_request_expires 5_000_000_000

  defstruct @enforce_keys ++
              [
                :stream_name,
                :consumer_name,
                :consumer,
                batch_size: 1,
                request_expires: @default_request_expires
              ]

  def validate!(connection_options) do
    validated_opts =
      Keyword.validate!(connection_options, [
        :connection_name,
        :stream_name,
        :consumer_name,
        :consumer,
        connection_retry_timeout: @default_retry_timeout,
        connection_retries: @default_retries,
        inbox_prefix: nil,
        domain: nil,
        batch_size: 1,
        request_expires: @default_request_expires
      ])

    stream_name = validated_opts[:stream_name]
    consumer_name = validated_opts[:consumer_name]
    consumer = validated_opts[:consumer]

    cond do
      consumer && (stream_name || consumer_name) ->
        raise ArgumentError,
              "cannot specify :consumer with :stream_name or :consumer_name - use consumer struct's stream_name instead"

      consumer && !is_struct(consumer, Gnat.Jetstream.API.Consumer) ->
        raise ArgumentError, ":consumer must be a Consumer struct"

      consumer && consumer.durable_name != nil && consumer.inactive_threshold == nil ->
        raise ArgumentError,
              "durable consumers specified via :consumer must have inactive_threshold set for auto-cleanup"

      consumer && validated_opts[:batch_size] > 1 && consumer.ack_policy != :all ->
        raise ArgumentError,
              "batch_size > 1 requires ack_policy: :all on the consumer, " <>
                "got: #{inspect(consumer.ack_policy)}. With ack_policy: :explicit, " <>
                "only the last message in each batch would be acknowledged and the " <>
                "server would redeliver the rest"

      consumer ->
        # For ephemeral/auto-cleanup consumer case, extract stream_name from consumer struct
        validated_opts = Keyword.put(validated_opts, :stream_name, consumer.stream_name)
        struct!(__MODULE__, validated_opts)

      stream_name && consumer_name ->
        # For traditional durable consumer case
        struct!(__MODULE__, validated_opts)

      true ->
        raise ArgumentError,
              "must specify either :consumer (ephemeral/auto-cleanup) or both :stream_name and :consumer_name (durable)"
    end
  end
end


================================================
FILE: lib/gnat/jetstream/pull_consumer/server.ex
================================================
defmodule Gnat.Jetstream.PullConsumer.Server do
  @moduledoc false

  require Logger

  use Connection

  alias Gnat.Jetstream.PullConsumer.ConnectionOptions
  alias Gnat.Jetstream.API.Util

  defstruct [
    :connection_options,
    :state,
    :listening_topic,
    :module,
    :subscription_id,
    :connection_monitor_ref,
    :consumer_name,
    current_retry: 0,
    buffer: []
  ]

  def init(%{module: module, init_arg: init_arg}) do
    _ = Process.put(:"$initial_call", {module, :init, 1})

    case module.init(init_arg) do
      {:ok, state, connection_options} when is_list(connection_options) ->
        Process.flag(:trap_exit, true)

        connection_options = ConnectionOptions.validate!(connection_options)

        gen_state = %__MODULE__{
          connection_options: connection_options,
          state: state,
          listening_topic: Util.reply_inbox(connection_options.inbox_prefix),
          module: module,
          consumer_name: connection_options.consumer_name
        }

        {:connect, :init, gen_state}

      :ignore ->
        :ignore

      {:stop, _} = stop ->
        stop
    end
  end

  def connect(
        _,
        %__MODULE__{
          connection_options: %ConnectionOptions{
            stream_name: stream_name,
            consumer_name: consumer_name,
            consumer: consumer,
            connection_name: connection_name,
            connection_retry_timeout: connection_retry_timeout,
            connection_retries: connection_retries,
            domain: domain
          },
          listening_topic: listening_topic,
          module: module
        } = gen_state
      ) do
    Logger.debug(
      "#{__MODULE__} for #{stream_name}.#{gen_state.consumer_name} is connecting to Gnat.",
      module: module,
      listening_topic: listening_topic,
      connection_name: connection_name
    )

    with {:ok, conn} <- connection_pid(connection_name),
         monitor_ref = Process.monitor(conn),
         {:ok, consumer_info} <-
           ensure_consumer_exists(
             conn,
             stream_name,
             consumer_name,
             consumer,
             domain
           ),
         :ok <- validate_batch_ack_policy(gen_state.connection_options, consumer_info),
         final_consumer_name = consumer_info.name,
         state = maybe_handle_connected(module, consumer_info, gen_state.state),
         {:ok, sid} <- Gnat.sub(conn, self(), listening_topic),
         gen_state = %{
           gen_state
           | subscription_id: sid,
             connection_monitor_ref: monitor_ref,
             consumer_name: final_consumer_name,
             state: state
         },
         :ok <-
           initial_fetch(
             gen_state,
             conn,
             stream_name,
             final_consumer_name,
             domain,
             listening_topic
           ),
         gen_state = %{gen_state | current_retry: 0} do
      {:ok, gen_state}
    else
      {:error, reason} ->
        if gen_state.current_retry >= connection_retries do
          Logger.error(
            """
            #{__MODULE__} for #{stream_name}.#{gen_state.consumer_name} failed to connect to NATS and \
            retries limit has been exhausted. Stopping.
            """,
            module: module,
            listening_topic: listening_topic,
            connection_name: connection_name
          )

          {:stop, :timeout, %{gen_state | current_retry: 0}}
        else
          Logger.debug(
            """
            #{__MODULE__} for #{stream_name}.#{gen_state.consumer_name} failed to connect to Gnat \
            and will retry. Reason: #{inspect(reason)}
            """,
            module: module,
            listening_topic: listening_topic,
            connection_name: connection_name
          )

          gen_state = Map.update!(gen_state, :current_retry, &(&1 + 1))
          {:backoff, connection_retry_timeout, gen_state}
        end
    end
  end

  def disconnect(
        {:close, from},
        %__MODULE__{
          connection_options: %ConnectionOptions{
            stream_name: stream_name,
            connection_name: connection_name
          },
          listening_topic: listening_topic,
          subscription_id: subscription_id,
          module: module,
          consumer_name: consumer_name
        } = gen_state
      ) do
    Logger.debug(
      "#{__MODULE__} for #{stream_name}.#{consumer_name} is disconnecting from Gnat.",
      module: module,
      listening_topic: listening_topic,
      subscription_id: subscription_id,
      connection_name: connection_name
    )

    with {:ok, conn} <- connection_pid(connection_name),
         true <- Process.demonitor(gen_state.connection_monitor_ref, [:flush]),
         :ok <- Gnat.unsub(conn, subscription_id) do
      Logger.debug(
        "#{__MODULE__} for #{stream_name}.#{consumer_name} is shutting down.",
        module: module,
        listening_topic: listening_topic,
        subscription_id: subscription_id,
        connection_name: connection_name
      )

      Connection.reply(from, :ok)
      {:stop, :shutdown, gen_state}
    end
  end

  defp ensure_consumer_exists(gnat, stream_name, consumer_name, nil, domain) do
    # Durable consumer case - just check it exists
    try do
      case Gnat.Jetstream.API.Consumer.info(gnat, stream_name, consumer_name, domain) do
        {:ok, consumer_info} -> {:ok, consumer_info}
        {:error, reason} -> {:error, reason}
      end
    catch
      :exit, reason -> {:error, {:process_exit, reason}}
      kind, reason -> {:error, {kind, reason}}
    end
  end

  defp ensure_consumer_exists(gnat, _stream_name, nil, consumer_struct, _domain) do
    # Ephemeral or auto-cleanup durable consumer case - create it
    try do
      with {:ok, consumer_definition} <- validate_consumer_for_creation(consumer_struct),
           {:ok, consumer_info} <- Gnat.Jetstream.API.Consumer.create(gnat, consumer_definition) do
        {:ok, consumer_info}
      end
    catch
      :exit, reason -> {:error, {:process_exit, reason}}
      kind, reason -> {:error, {kind, reason}}
    end
  end

  defp validate_consumer_for_creation(consumer_definition) do
    cond do
      consumer_definition.durable_name == nil && consumer_definition.inactive_threshold != nil ->
        {:error, "ephemeral consumers (durable_name: nil) cannot have inactive_threshold set"}

      consumer_definition.durable_name != nil && consumer_definition.inactive_threshold == nil ->
        {:error,
         "durable consumers specified via :consumer must have inactive_threshold set for auto-cleanup"}

      true ->
        {:ok, consumer_definition}
    end
  end

  defp validate_batch_ack_policy(%ConnectionOptions{batch_size: batch_size}, consumer_info)
       when batch_size > 1 do
    case consumer_info.config.ack_policy do
      :all ->
        :ok

      other ->
        {:error,
         "batch_size > 1 requires ack_policy: :all on the consumer, " <>
           "got: #{inspect(other)}. With ack_policy: :explicit, " <>
           "only the last message in each batch would be acknowledged and the " <>
           "server would redeliver the rest"}
    end
  end

  defp validate_batch_ack_policy(_connection_options, _consumer_info), do: :ok

  defp maybe_handle_connected(module, consumer_info, state) do
    if function_exported?(module, :handle_connected, 2) do
      {:ok, state} = module.handle_connected(consumer_info, state)
      state
    else
      state
    end
  end

  defp maybe_handle_status(message, %__MODULE__{module: module, state: state} = gen_state) do
    if function_exported?(module, :handle_status, 2) do
      {:ok, new_state} = module.handle_status(message, state)
      %{gen_state | state: new_state}
    else
      gen_state
    end
  end

  defp connection_pid(connection_name) when is_pid(connection_name) do
    if Process.alive?(connection_name) do
      {:ok, connection_name}
    else
      {:error, :not_alive}
    end
  end

  defp connection_pid(connection_name) do
    case Process.whereis(connection_name) do
      nil -> {:error, :not_found}
      pid -> {:ok, pid}
    end
  end

  # -- Batch mode: 100 is an idle heartbeat — the pull is still alive, do
  # nothing but invoke the user callback. --
  def handle_info(
        {:msg, %{status: "100"} = message},
        %__MODULE__{connection_options: %ConnectionOptions{batch_size: batch_size}} = gen_state
      )
      when batch_size > 1 do
    gen_state = maybe_handle_status(message, gen_state)
    {:noreply, gen_state}
  end

  # -- Batch mode: any other status (404/408 terminators, 409 leadership
  # change / max_ack_pending / max_waiting / consumer-deleted, etc.) ends
  # the outstanding pull request. Process any partial buffer and issue a
  # new pull so the consumer doesn't stall. --
  def handle_info(
        {:msg, %{status: status, gnat: gnat} = message},
        %__MODULE__{
          connection_options: %ConnectionOptions{batch_size: batch_size},
          buffer: buffer
        } = gen_state
      )
      when batch_size > 1 and is_binary(status) and status != "" do
    gen_state = maybe_handle_status(message, gen_state)

    case buffer do
      [] ->
        # Nothing buffered — long-poll for new messages.
        request_batch(gnat, gen_state, :tailing)
        {:noreply, gen_state}

      _messages ->
        # Partial batch — process what we have, then try for more.
        gen_state = process_and_ack_batch(gen_state)
        request_batch(gnat, gen_state, :catching_up)
        {:noreply, gen_state}
    end
  end

  # -- Single-message mode: informational status. Drop + re-pull so the
  # consumer doesn't stall. Matches the nats.go convention of never exposing
  # status messages to the user's message handler. --
  def handle_info(
        {:msg, %{status: status} = message},
        %__MODULE__{
          connection_options: %ConnectionOptions{
            stream_name: stream_name,
            domain: domain
          },
          listening_topic: listening_topic,
          consumer_name: consumer_name
        } = gen_state
      )
      when is_binary(status) and status != "" do
    gen_state = maybe_handle_status(message, gen_state)

    next_message(
      message.gnat,
      stream_name,
      consumer_name,
      domain,
      listening_topic
    )

    {:noreply, gen_state}
  end

  # -- Batch mode: data message — buffer until batch is full --
  def handle_info(
        {:msg, message},
        %__MODULE__{
          connection_options: %ConnectionOptions{batch_size: batch_size},
          buffer: buffer
        } = gen_state
      )
      when batch_size > 1 do
    buffer = [message | buffer]
    gen_state = %{gen_state | buffer: buffer}

    if length(buffer) >= batch_size do
      gen_state = process_and_ack_batch(gen_state)
      request_batch(message.gnat, gen_state, :catching_up)
      {:noreply, gen_state}
    else
      {:noreply, gen_state}
    end
  end

  # -- Single-message mode (batch_size == 1, the default) --
  def handle_info(
        {:msg, message},
        %__MODULE__{
          connection_options: %ConnectionOptions{
            stream_name: stream_name,
            connection_name: connection_name,
            domain: domain
          },
          listening_topic: listening_topic,
          subscription_id: subscription_id,
          state: state,
          module: module,
          consumer_name: consumer_name
        } = gen_state
      ) do
    Logger.debug(
      """
      #{__MODULE__} for #{stream_name}.#{consumer_name} received a message: \
      #{inspect(message, pretty: true)}
      """,
      module: module,
      listening_topic: listening_topic,
      subscription_id: subscription_id,
      connection_name: connection_name
    )

    case module.handle_message(message, state) do
      {:ack, state} ->
        Gnat.Jetstream.ack_next(message, listening_topic)

        gen_state = %{gen_state | state: state}
        {:noreply, gen_state}

      {:nack, state} ->
        Gnat.Jetstream.nack(message)

        next_message(
          message.gnat,
          stream_name,
          consumer_name,
          domain,
          listening_topic
        )

        gen_state = %{gen_state | state: state}
        {:noreply, gen_state}

      {:term, state} ->
        Gnat.Jetstream.ack_term(message)

        next_message(
          message.gnat,
          stream_name,
          consumer_name,
          domain,
          listening_topic
        )

        gen_state = %{gen_state | state: state}
        {:noreply, gen_state}

      {:noreply, state} ->
        next_message(
          message.gnat,
          stream_name,
          consumer_name,
          domain,
          listening_topic
        )

        gen_state = %{gen_state | state: state}
        {:noreply, gen_state}
    end
  end

  def handle_info(
        {:DOWN, ref, :process, _pid, _reason},
        %__MODULE__{
          connection_options: %ConnectionOptions{
            connection_name: connection_name,
            stream_name: stream_name
          },
          subscription_id: subscription_id,
          listening_topic: listening_topic,
          module: module,
          connection_monitor_ref: monitor_ref,
          consumer_name: consumer_name
        } = gen_state
      )
      when ref == monitor_ref do
    Logger.debug(
      """
      #{__MODULE__} for #{stream_name}.#{consumer_name}:
      NATS connection has died. PullConsumer is reconnecting.
      """,
      module: module,
      listening_topic: listening_topic,
      subscription_id: subscription_id,
      connection_name: connection_name
    )

    # Clear consumer name on reconnect so it gets recreated (for ephemeral and auto-cleanup consumers)
    # Clear buffer to avoid processing stale messages from the dead connection
    gen_state = %{
      gen_state
      | consumer_name: nil,
        subscription_id: nil,
        connection_monitor_ref: nil,
        buffer: []
    }

    {:connect, :reconnect, gen_state}
  end

  def handle_info(
        other,
        %__MODULE__{
          connection_options: %ConnectionOptions{
            connection_name: connection_name,
            stream_name: stream_name
          },
          subscription_id: subscription_id,
          listening_topic: listening_topic,
          module: module,
          consumer_name: consumer_name
        } = gen_state
      ) do
    Logger.debug(
      """
      #{__MODULE__} for #{stream_name}.#{consumer_name} received
      unexpected message: #{inspect(other, pretty: true)}
      """,
      module: module,
      listening_topic: listening_topic,
      subscription_id: subscription_id,
      connection_name: connection_name
    )

    {:noreply, gen_state}
  end

  def handle_call(
        :close,
        from,
        %__MODULE__{
          connection_options: %ConnectionOptions{
            connection_name: connection_name,
            stream_name: stream_name
          },
          subscription_id: subscription_id,
          listening_topic: listening_topic,
          module: module,
          consumer_name: consumer_name
        } = gen_state
      ) do
    Logger.debug("#{__MODULE__} for #{stream_name}.#{consumer_name} received :close call.",
      module: module,
      listening_topic: listening_topic,
      subscription_id: subscription_id,
      connection_name: connection_name
    )

    {:disconnect, {:close, from}, gen_state}
  end

  defp next_message(conn, stream_name, consumer_name, domain, listening_topic) do
    Gnat.Jetstream.API.Consumer.request_next_message(
      conn,
      stream_name,
      consumer_name,
      listening_topic,
      domain
    )
  end

  defp initial_fetch(gen_state, conn, stream_name, consumer_name, domain, listening_topic) do
    if gen_state.connection_options.batch_size > 1 do
      request_batch(conn, gen_state, :catching_up)
    else
      next_message(conn, stream_name, consumer_name, domain, listening_topic)
    end
  end

  defp request_batch(conn, gen_state, mode) do
    %{
      connection_options: %ConnectionOptions{
        stream_name: stream_name,
        batch_size: batch_size,
        domain: domain,
        request_expires: expires
      },
      consumer_name: consumer_name,
      listening_topic: listening_topic
    } = gen_state

    opts =
      case mode do
        :catching_up -> [batch: batch_size, no_wait: true]
        :tailing -> [batch: batch_size, expires: expires]
      end

    Gnat.Jetstream.API.Consumer.request_next_message(
      conn,
      stream_name,
      consumer_name,
      listening_topic,
      domain,
      opts
    )
  end

  defp process_and_ack_batch(%{buffer: buffer, module: module, state: state} = gen_state) do
    messages = Enum.reverse(buffer)

    new_state =
      Enum.reduce(messages, state, fn message, acc_state ->
        case module.handle_message(message, acc_state) do
          {:ack, updated_state} ->
            updated_state

          {action, updated_state} ->
            Logger.warning(
              "PullConsumer batch mode does not support #{inspect(action)}, treating as :ack"
            )

            updated_state
        end
      end)

    # With ack_policy: :all, acking the last message covers the entire batch
    last = List.last(messages)
    Gnat.Jetstream.ack(last)

    %{gen_state | state: new_state, buffer: []}
  end
end


================================================
FILE: lib/gnat/jetstream/pull_consumer.ex
================================================
defmodule Gnat.Jetstream.PullConsumer do
  @moduledoc """
  A behaviour which provides the NATS JetStream Pull Consumer functionalities.

  When a Consumer is pull-based, it means that the messages will be delivered when the server
  is asked for them.

  ## Example

  Declare a module which uses `Gnat.Jetstream.PullConsumer` and implements `c:init/1` and
  `c:handle_message/2` callbacks.

      defmodule MyApp.PullConsumer do
        use Gnat.Jetstream.PullConsumer

        def start_link(arg) do
          Jetstream.PullConsumer.start_link(__MODULE__, arg)
        end

        @impl true
        def init(_arg) do
          {:ok, nil,
            connection_name: :gnat,
            stream_name: "TEST_STREAM",
            consumer_name: "TEST_CONSUMER"}
        end

        @impl true
        def handle_message(message, state) do
          # Do some processing with the message.
          {:ack, state}
        end
      end

  You can then place your Pull Consumer in a supervision tree. Remember that you need to have the
  `Gnat.ConnectionSupervisor` set up.

      defmodule MyApp.Application do
        use Application

        @impl true
        def start(_type, _args) do
          children = [
            # Create NATS connection
            {Gnat.ConnectionSupervisor, ...},
            # Start NATS Jetstream Pull Consumer
            MyApp.PullConsumer,
          ]
          opts = [strategy: :one_for_one]
          Supervisor.start_link(children, opts)
        end
      end

  ## Connection Options

  In order to establish consumer connection with NATS, you need to pass several connection options
  via keyword list in third element of a tuple returned from `c:init/1` callback.

  Following options **must** be provided. Omitting this options will cause the process to raise
  errors upon initialization:

  * `:connection_name` - Gnat connection or `Gnat.ConnectionSupervisor` name/PID.

  For **durable consumers**, provide:
  * `:stream_name` - name of an existing stream the consumer will consume messages from.
  * `:consumer_name` - name of an existing consumer pointing at the stream.

  For **ephemeral consumers**, provide:
  * `:consumer` - a `Gnat.Jetstream.API.Consumer` struct for creating an ephemeral consumer.
    The consumer struct must have `durable_name: nil` OR `inactive_threshold` set to ensure
    that the server will clean it up. The `stream_name` field must also be set.

  You can also pass the optional ones:
  * `:connection_retry_timeout` - a duration in milliseconds after which the PullConsumer which
    failed to establish NATS connection retries, defaults to `1000`
  * `:connection_retries` - a number of attempts the PullConsumer will make to establish the NATS
    connection. When this value is exceeded, the pull consumer stops with the `:timeout` reason,
    defaults to `10`
  * `:inbox_prefix` - allows the default `_INBOX.` prefix to be customized. Should end with a dot.
  * `:domain` - use a JetStream domain, this is mostly used on leaf nodes.
  * `:batch_size` - when set to a value greater than 1, enables batch mode. Messages are
    buffered and delivered to `c:handle_message/2` in batches. Only the last message per
    batch is acknowledged, so the underlying consumer should use `ack_policy: :all` for
    correctness. This dramatically improves throughput for consumers that need to catch up
    on large backlogs. In batch mode, `:nack` and `:term` returns from `c:handle_message/2`
    are treated as `:ack` since `ack_policy: :all` cannot selectively reject messages.
    Defaults to `1` (single-message mode).
  * `:request_expires` - duration in nanoseconds a batch-mode pull request will linger on
    the server while tailing (no backlog) before the server replies with a `408` terminator
    and the consumer issues a fresh pull. Only used when `:batch_size` is greater than 1.
    Defaults to `5_000_000_000` (5 seconds).

  ## Dynamic Connection Options

  It is possible that you have to determine some of the options dynamically depending on pull
  consumer's init argument. To do so, it is recommended to derive these options values from some
  init argument:

      defmodule MyApp.PullConsumer do
        use Gnat.Jetstream.PullConsumer

        def start_link() do
          Gnat.Jetstream.PullConsumer.start_link(__MODULE__, %{counter: counter})
        end

        @impl true
        def init(%{counter: counter}) do
          {:ok, nil,
            connection_name: :gnat,
            stream_name: "TEST_STREAM_#\{counter}",
            consumer_name: "TEST_CONSUMER_#\{counter}"}
        end

        ...
      end

  ## Ephemeral Consumer Example

  You can create ephemeral consumers by providing a `:consumer` struct with `durable_name: nil`.
  These are automatically created and cleaned up with the connection lifecycle:

      defmodule MyApp.EphemeralPullConsumer do
        use Gnat.Jetstream.PullConsumer

        def start_link(arg) do
          Jetstream.PullConsumer.start_link(__MODULE__, arg)
        end

        @impl true
        def init(_arg) do
          consumer = %Gnat.Jetstream.API.Consumer{
            stream_name: "TEST_STREAM",
            durable_name: nil,  # Must be nil for ephemeral consumers
            filter_subject: "orders.*"
          }

          {:ok, nil,
            connection_name: :gnat,
            consumer: consumer}
        end

        @impl true
        def handle_message(message, state) do
          # Do some processing with the message.
          {:ack, state}
        end
      end

  ## Auto-Cleanup Durable Consumer Example

  For scenarios where you want persistence across reconnections but automatic cleanup
  (e.g., Kubernetes pods), you can create durable consumers with `inactive_threshold`:

      defmodule MyApp.AutoCleanupPullConsumer do
        use Gnat.Jetstream.PullConsumer

        def start_link(pod_name) do
          Jetstream.PullConsumer.start_link(__MODULE__, pod_name)
        end

        @impl true
        def init(pod_name) do
          consumer = %Gnat.Jetstream.API.Consumer{
            stream_name: "ORDERS_STREAM",
            durable_name: "orders-consumer-#{System.get_env("POD_NAME")}",  # Named after pod
            inactive_threshold: 300_000_000_000,  # 5 minutes in nanoseconds
            filter_subject: "orders.*"
          }

          {:ok, nil,
            connection_name: :gnat,
            consumer: consumer}
        end

        @impl true
        def handle_message(message, state) do
          # Process order message with state persistence
          {:ack, state}
        end
      end

  ## How to supervise

  A `PullConsumer` is most commonly started under a supervision tree. When we invoke
  `use Gnat.Jetstream.PullConsumer`, it automatically defines a `child_spec/1` function that allows us
  to start the pull consumer directly under a supervisor. To start a pull consumer under
  a supervisor with an initial argument of :example, one may do:

      children = [
        {MyPullConsumer, :example}
      ]
      Supervisor.start_link(children, strategy: :one_for_all)

  While one could also simply pass the `MyPullConsumer` as a child to the supervisor, such as:

      children = [
        MyPullConsumer # Same as {MyPullConsumer, []}
      ]
      Supervisor.start_link(children, strategy: :one_for_all)

  A common approach is to use a keyword list, which allows setting init argument and server options,
  for example:

      def start_link(opts) do
        {initial_state, opts} = Keyword.pop(opts, :initial_state, nil)
        Gnat.Jetstream.PullConsumer.start_link(__MODULE__, initial_state, opts)
      end

  and then you can use `MyPullConsumer`, `{MyPullConsumer, name: :my_consumer}` or even
  `{MyPullConsumer, initial_state: :example, name: :my_consumer}` as a child specification.

  `use Gnat.Jetstream.PullConsumer` also accepts a list of options which configures the child
  specification and therefore how it runs under a supervisor. The generated `child_spec/1` can be
  customized with the following options:

    * `:id` - the child specification identifier, defaults to the current module
    * `:restart` - when the child should be restarted, defaults to `:permanent`
    * `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down

  For example:

      use Gnat.Jetstream.PullConsumer, restart: :transient, shutdown: 10_000

  See the "Child specification" section in the `Supervisor` module for more detailed information.
  The `@doc` annotation immediately preceding `use Jetstream.PullConsumer` will be attached to
  the generated `child_spec/1` function.

  ## Name registration

  A pull consumer is bound to the same name registration rules as GenServers.
  Read more about it in the `GenServer` documentation.
  """

  @doc """
  Invoked when the server is started. `start_link/3` or `start/3` will block until it returns.

  `init_arg` is the argument term (second argument) passed to `start_link/3`.

  See `c:Connection.init/1` for more details.
  """
  @callback init(init_arg :: term) ::
              {:ok, state :: term(), connection_options()}
              | :ignore
              | {:stop, reason :: any}

  @doc """
  Invoked to synchronously process a message pulled by the consumer.
  Depending on the value it returns, the acknowledgement is or is not sent.

  Only real stream messages reach this callback. JetStream informational
  status messages (e.g. `100` heartbeat, `404`/`408` pull terminator, `409`
  leadership change) are intercepted by the consumer and never passed here.
  See `c:handle_status/2` if you want to observe them.

  ## ACK actions

  Possible ACK actions values explained:

  * `:ack` - acknowledges the message was handled and requests delivery of the next message to
    the reply subject.
  * `:nack` - signals that the message will not be processed now and processing can move onto
    the next message, NAK'd message will be retried.
  * `:term` - instructs the server to stop redelivery of a message without acknowledging it as
    successfully processed.
  * `:noreply` - nothing is sent. You may send later asynchronously an ACK or NACK message using
    the `Jetstream.ack/1` or `Jetstream.nack/1` and similar functions from `Jetstream` module.

  ## Example

      def handle_message(message, state) do
        IO.inspect(message)
        {:ack, state}
      end

  """
  @callback handle_message(message :: Gnat.message(), state :: term()) ::
              {ack_action, new_state}
            when ack_action: :ack | :nack | :term | :noreply, new_state: term()

  @doc """
  Invoked after the consumer has been created or verified on the NATS server.

  This callback is called during connection (and reconnection) after the JetStream
  consumer has been successfully created or confirmed to exist. It receives the full
  consumer info map returned by the server, which includes fields like `num_pending`
  (the number of messages waiting to be delivered).

  This is useful for detecting the initial state of the consumer. For example, if
  `num_pending` is `0`, you know there are no existing messages to replay and can
  mark the consumer as caught up immediately.

  Returning `{:ok, state}` allows you to update the consumer's state based on the
  consumer info.

  This callback is optional. If not implemented, the state is passed through unchanged.

  ## Example

      @impl true
      def handle_connected(consumer_info, state) do
        if consumer_info.num_pending == 0 do
          {:ok, mark_as_loaded(state)}
        else
          {:ok, state}
        end
      end

  """
  @callback handle_connected(
              consumer_info :: Gnat.Jetstream.API.Consumer.info(),
              state :: term()
            ) :: {:ok, new_state :: term()}

  @doc """
  Invoked when the consumer receives an informational JetStream status message
  instead of a real stream message.

  JetStream delivers status messages on the same subscription as regular
  messages — for example a `100` idle heartbeat, a `404`/`408` pull request
  terminator, or a `409` leadership change. These are not stream records and
  cannot be acked, so the PullConsumer never forwards them to `c:handle_message/2`.

  By default they are silently dropped and the consumer continues fetching
  the next message. Implement this callback if you want to observe them — for
  example to log a warning on leadership changes, or to track heartbeat
  arrival.

  The callback receives the raw `Gnat.message()` (which includes `:status`
  and optionally `:description`) and the current state. Returning
  `{:ok, new_state}` updates the state; the consumer then proceeds the same
  way it would have if the callback had not been defined.

  This callback is optional.

  ## Example

      @impl true
      def handle_status(%{status: "409", description: description}, state) do
        Logger.warning("JetStream 409 from consumer: #\{description}")
        {:ok, state}
      end

      def handle_status(_message, state), do: {:ok, state}

  """
  @callback handle_status(message :: Gnat.message(), state :: term()) ::
              {:ok, new_state :: term()}

  @optional_callbacks [handle_connected: 2, handle_status: 2]

  @typedoc """
  The pull consumer reference.
  """
  @type consumer :: GenServer.server()

  @typedoc """
  Connection option values used to connect the consumer to NATS server.
  """
  @type connection_option ::
          {:connection_name, GenServer.server()}
          | {:stream_name, String.t()}
          | {:consumer_name, String.t()}
          | {:consumer, Gnat.Jetstream.API.Consumer.t()}
          | {:connection_retry_timeout, non_neg_integer()}
          | {:connection_retries, non_neg_integer()}
          | {:domain, String.t()}
          | {:batch_size, pos_integer()}
          | {:request_expires, non_neg_integer()}

  @typedoc """
  Connection options used to connect the consumer to NATS server.
  """
  @type connection_options :: [connection_option()]

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @behaviour Gnat.Jetstream.PullConsumer

      unless Module.has_attribute?(__MODULE__, :doc) do
        @doc """
        Returns a specification to start this module under a supervisor.

        See the "Child specification" section in the `Supervisor` module for more detailed
        information.
        """
      end

      @spec child_spec(arg :: GenServer.options()) :: Supervisor.child_spec()
      def child_spec(arg) do
        default = %{
          id: __MODULE__,
          start: {__MODULE__, :start_link, [arg]}
        }

        Supervisor.child_spec(default, unquote(Macro.escape(opts)))
      end

      defoverridable child_spec: 1
    end
  end

  @doc """
  Starts a pull consumer linked to the current process with the given function.

  This is often used to start the pull consumer as part of a supervision tree.

  Once the server is started, the `c:init/1` function of the given `module` is called with
  `init_arg` as its argument to initialize the server. To ensure a synchronized start-up procedure,
  this function does not return until `c:init/1` has returned.

  See `GenServer.start_link/3` for more details.
  """
  @spec start_link(module(), init_arg :: term(), option
Download .txt
gitextract_y72cyuck/

├── .dialyzer_ignore.exs
├── .formatter.exs
├── .github/
│   └── workflows/
│       └── CI.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── MAINTAINERS.md
├── README.md
├── bench/
│   ├── client.exs
│   ├── kv_consume.exs
│   ├── parse.exs
│   ├── publish.exs
│   ├── request.exs
│   ├── request_multi.exs
│   ├── server.exs
│   └── service_bench.exs
├── dependencies.md
├── docs/
│   └── js/
│       ├── guides/
│       │   ├── broadway.md
│       │   ├── managing.md
│       │   └── push_based_consumer.md
│       └── introduction/
│           ├── getting_started.md
│           └── overview.md
├── lib/
│   ├── gnat/
│   │   ├── command.ex
│   │   ├── connection_supervisor.ex
│   │   ├── consumer_supervisor.ex
│   │   ├── handshake.ex
│   │   ├── jetstream/
│   │   │   ├── api/
│   │   │   │   ├── consumer.ex
│   │   │   │   ├── kv/
│   │   │   │   │   ├── entry.ex
│   │   │   │   │   └── watcher.ex
│   │   │   │   ├── kv.ex
│   │   │   │   ├── message.ex
│   │   │   │   ├── object/
│   │   │   │   │   └── meta.ex
│   │   │   │   ├── object.ex
│   │   │   │   ├── stream.ex
│   │   │   │   └── util.ex
│   │   │   ├── jetstream.ex
│   │   │   ├── pager.ex
│   │   │   ├── pull_consumer/
│   │   │   │   ├── connection_options.ex
│   │   │   │   └── server.ex
│   │   │   └── pull_consumer.ex
│   │   ├── parsec.ex
│   │   ├── server.ex
│   │   └── services/
│   │       ├── server.ex
│   │       ├── service.ex
│   │       ├── service_responder.ex
│   │       └── wire_protocol.ex
│   └── gnat.ex
├── mix.exs
├── scripts/
│   └── cluster/
│       ├── cluster.sh
│       ├── driver.exs
│       ├── n1.conf
│       ├── n2.conf
│       └── n3.conf
└── test/
    ├── command_test.exs
    ├── fixtures/
    │   ├── ca.pem
    │   ├── client-cert.pem
    │   ├── client-key.pem
    │   ├── nkey_config
    │   ├── nkey_seed
    │   ├── server-cert.pem
    │   └── server-key.pem
    ├── gnat/
    │   ├── consumer_supervisor_test.exs
    │   ├── handshake_test.exs
    │   ├── parsec_property_test.exs
    │   └── parsec_test.exs
    ├── gnat_property_test.exs
    ├── gnat_test.exs
    ├── jetstream/
    │   ├── api/
    │   │   ├── consumer_doc_test.exs
    │   │   ├── consumer_test.exs
    │   │   ├── kv/
    │   │   │   ├── entry_test.exs
    │   │   │   └── watcher_test.exs
    │   │   ├── kv_test.exs
    │   │   ├── object_test.exs
    │   │   ├── stream_doc_test.exs
    │   │   └── stream_test.exs
    │   ├── message_test.exs
    │   └── pager_test.exs
    ├── pull_consumer/
    │   ├── batch_test.exs
    │   ├── connectivity_test.exs
    │   ├── ephemeral_test.exs
    │   ├── status_messages_test.exs
    │   └── using_macro_test.exs
    ├── support/
    │   ├── conn_case.ex
    │   └── generators.ex
    └── test_helper.exs
Download .txt
SYMBOL INDEX (493 symbols across 58 files)

FILE: bench/client.exs
  class Client (line 5) | defmodule Client
    method setup (line 8) | def setup(id) do
    method send_request (line 14) | def send_request(gnat, request) do
    method send_requests (line 18) | def send_requests(gnat, how_many, request) do
  class Benchmark (line 27) | defmodule Benchmark
    method benchmark (line 28) | def benchmark(num_actors, requests_per_actor, request) do
    method record_rpc_time (line 37) | def record_rpc_time(micro_seconds) do
    method print_statistics (line 41) | def print_statistics(total_requests, total_bytes, total_micros) do
    method time_benchmark (line 66) | def time_benchmark(num_actors, requests_per_actor, request) do
    method wait_for_times (line 82) | def wait_for_times(0), do: :done
    method wait_for_times (line 83) | def wait_for_times(n) do

FILE: bench/kv_consume.exs
  class BenchBatchPullConsumer (line 28) | defmodule BenchBatchPullConsumer
    method start (line 31) | def start(args) do
    method init (line 36) | def init(%{tab: tab, notify: pid, expected: expected, batch_size: batc...
    method handle_message (line 50) | def handle_message(message, state) do
  class KVConsumeBench (line 62) | defmodule KVConsumeBench
    method setup (line 67) | def setup(conn, count) do
    method pager_consume (line 96) | def pager_consume(conn, batch_size) do
    method pull_ack_next_consume (line 113) | def pull_ack_next_consume(conn, batch_size) do
    method receive_with_ack_next (line 152) | defp receive_with_ack_next(_sub, _inbox, _tab, total, total), do: :ok
    method receive_with_ack_next (line 154) | defp receive_with_ack_next(sub, inbox, tab, count, total) do
    method batch_pull_consumer_consume (line 185) | def batch_pull_consumer_consume(expected, batch_size) do

FILE: bench/request.exs
  class EchoServer (line 1) | defmodule EchoServer
    method run (line 2) | def run(gnat) do
    method init (line 6) | def init(gnat) do
    method loop (line 11) | def loop(gnat) do

FILE: bench/request_multi.exs
  class EchoServer (line 1) | defmodule EchoServer
    method run (line 2) | def run(gnat) do
    method init (line 6) | def init(gnat) do
    method loop (line 11) | def loop(gnat) do

FILE: bench/server.exs
  class EchoServer (line 16) | defmodule EchoServer
    method handle (line 17) | def handle(%{body: body, reply_to: reply_to, gnat: gnat_pid}) do
    method wait_loop (line 21) | def wait_loop do

FILE: bench/service_bench.exs
  class EchoService (line 1) | defmodule EchoService
    method request (line 4) | def request(%{body: body}, "echo", _group) do
    method definition (line 8) | def definition do

FILE: lib/gnat.ex
  class Gnat (line 5) | defmodule Gnat
    method start_link (line 153) | def start_link(connection_settings \\ %{}, opts \\ []) do
    method stop (line 166) | def stop(pid), do: GenServer.call(pid, :stop)
    method sub (line 191) | def sub(pid, subscriber, topic, opts \\ []) do
    method pub (line 228) | def pub(pid, topic, message, opts \\ []) do
    method request (line 264) | def request(pid, topic, body, opts \\ []) do
    method request_multi (line 303) | def request_multi(pid, topic, body, opts \\ []) do
    method unsub (line 352) | def unsub(pid, sid, opts \\ []) do
    method ping (line 363) | def ping(_pid) do
    method active_subscriptions (line 369) | def active_subscriptions(pid) do
    method server_info (line 377) | def server_info(name) do
    method init (line 382) | def init(connection_settings) do
    method handle_info (line 414) | def handle_info(:ping_check, %{waiting_on_pong: true} = state) do
    method handle_info (line 422) | def handle_info(:ping_check, %{waiting_on_pong: false} = state) do
    method handle_info (line 428) | def handle_info({:tcp, socket, data}, %{socket: socket} = state) do
    method handle_info (line 441) | def handle_info({:ssl, socket, data}, state) do
    method handle_info (line 445) | def handle_info({:tcp_closed, _}, state) do
    method handle_info (line 449) | def handle_info({:ssl_closed, _}, state) do
    method handle_info (line 453) | def handle_info({:tcp_error, _, reason}, state) do
    method handle_info (line 457) | def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    method handle_info (line 466) | def handle_info(other, state) do
    method handle_call (line 472) | def handle_call(:stop, _from, state) do
    method handle_call (line 477) | def handle_call({:sub, receiver, topic, opts}, _from, %{next_sid: sid}...
    method handle_call (line 488) | def handle_call({:pub, topic, message, opts}, from, state) do
    method handle_call (line 497) | def handle_call({:request, request}, _from, state) do
    method handle_call (line 529) | def handle_call({:unsub, sid, opts}, _from, state) do
    method handle_call (line 534) | def handle_call({:ping, pinger}, _from, state) do
    method handle_call (line 539) | def handle_call(:active_subscriptions, _from, state) do
    method handle_call (line 544) | def handle_call(:server_info, _from, state) do
    method unsub_sid (line 548) | defp unsub_sid(sid, opts, state) do
    method create_request_subscription (line 562) | defp create_request_subscription(%{request_inbox_prefix: request_inbox...
    method make_new_inbox (line 570) | defp make_new_inbox(%{request_inbox_prefix: prefix}), do: prefix <> nu...
    method nuid (line 572) | defp nuid(), do: :crypto.strong_rand_bytes(12) |> Base.encode64()
    method prepare_headers (line 574) | defp prepare_headers(opts) do
    method socket_close (line 583) | defp socket_close(%{socket: socket, connection_settings: %{tls: true}}...
    method socket_close (line 584) | defp socket_close(%{socket: socket}), do: :gen_tcp.close(socket)
    method socket_write (line 586) | defp socket_write(%{socket: socket, connection_settings: %{tls: true}}...
    method socket_write (line 590) | defp socket_write(%{socket: socket}, iodata), do: :gen_tcp.send(socket...
    method add_subscription_to_state (line 592) | defp add_subscription_to_state(%{receivers: receivers} = state, sid, p...
    method cleanup_subscription_from_state (line 599) | defp cleanup_subscription_from_state(%{receivers: receivers} = state, ...
    method cleanup_subscription_from_state (line 604) | defp cleanup_subscription_from_state(%{receivers: receivers} = state, ...
    method process_message (line 609) | defp process_message({:info, server_info}, state) do
    method process_message (line 613) | defp process_message({:msg, topic, @request_sid, reply_to, body}, stat...
    method process_message (line 627) | defp process_message({:msg, topic, sid, reply_to, body}, state) do
    method process_message (line 643) | defp process_message(
    method process_message (line 666) | defp process_message({:hmsg, topic, sid, reply_to, status, description...
    method process_message (line 689) | defp process_message(:ping, state) do
    method process_message (line 694) | defp process_message(:pong, state) do
    method process_message (line 698) | defp process_message({:error, message}, state) do
    method receive_additional_pubs (line 707) | defp receive_additional_pubs(commands, froms, 0), do: {commands, froms}
    method receive_additional_pubs (line 709) | defp receive_additional_pubs(commands, froms, how_many_more) do
    method receive_additional_tcp_data (line 720) | defp receive_additional_tcp_data(_socket, packets, 0), do: Enum.revers...
    method receive_additional_tcp_data (line 722) | defp receive_additional_tcp_data(socket, packets, n) do
    method update_subscriptions_after_delivering_message (line 731) | defp update_subscriptions_after_delivering_message(%{receivers: receiv...
    method receive_multi_request_responses (line 742) | defp receive_multi_request_responses(_sub, _exp, 0), do: []
    method receive_multi_request_responses (line 744) | defp receive_multi_request_responses(subscription, expiration, max_mes...
    method receive_request_response (line 765) | defp receive_request_response(subscription, timeout) do
    method schedule_ping_check (line 778) | defp schedule_ping_check(connection_settings) do

FILE: lib/gnat/command.ex
  class Gnat.Command (line 1) | defmodule Gnat.Command
    method build (line 10) | def build(:pub, topic, payload, []),
    method build (line 13) | def build(:pub, topic, payload, reply_to: reply),
    method build (line 26) | def build(:pub, topic, payload, headers: headers) do
    method build (line 48) | def build(:pub, topic, payload, headers: headers, reply_to: reply) do
    method build (line 72) | def build(:sub, topic, sid, []), do: [@sub, " ", topic, " ", Integer.t...
    method build (line 74) | def build(:sub, topic, sid, queue_group: qg),
    method build (line 77) | def build(:unsub, sid, []), do: [@unsub, " #{sid}", @newline]
    method build (line 78) | def build(:unsub, sid, max_messages: max), do: [@unsub, " #{sid}", " #...

FILE: lib/gnat/connection_supervisor.ex
  class Gnat.ConnectionSupervisor (line 1) | defmodule Gnat.ConnectionSupervisor
    method start_link (line 40) | def start_link(settings, options \\ []) do
    method init (line 45) | def init(options) do
    method handle_info (line 59) | def handle_info(:attempt_connection, state) do
    method handle_info (line 77) | def handle_info({:EXIT, _pid, _reason}, %{gnat: nil} = state) do
    method handle_info (line 81) | def handle_info({:EXIT, _pid, reason}, state) do
    method handle_info (line 87) | def handle_info(msg, state) do
    method random_connection_config (line 92) | defp random_connection_config(%{connection_settings: connection_settin...

FILE: lib/gnat/consumer_supervisor.ex
  class Gnat.ConsumerSupervisor (line 1) | defmodule Gnat.ConsumerSupervisor
    method start_link (line 63) | def start_link(settings, options \\ []) do
    method init (line 68) | def init(settings) do
    method handle_info (line 94) | def handle_info(:connect, %{connection_name: name} = state) do
    method handle_info (line 114) | def handle_info(
    method handle_info (line 123) | def handle_info({:DOWN, _ref, :process, _task_pid, _reason}, state), d...
    method handle_info (line 126) | def handle_info(
    method handle_info (line 134) | def handle_info({:msg, gnat_message}, %{service: service, module: modu...
    method handle_info (line 144) | def handle_info({:msg, gnat_message}, %{module: module} = state) do
    method handle_info (line 153) | def handle_info({:msg, gnat_message}, %{consuming_function: {mod, fun}...
    method handle_info (line 158) | def handle_info(other, state) do
    method terminate (line 164) | def terminate(:shutdown, state) do
    method terminate (line 178) | def terminate(reason, _state) do
    method receive_final_broker_messages (line 182) | defp receive_final_broker_messages(state) do
    method wait_for_empty_task_supervisor (line 193) | defp wait_for_empty_task_supervisor(%{task_supervisor_pid: pid} = stat...
    method subscribe_to_topics (line 205) | defp subscribe_to_topics(%{service: service}, connection_pid) do
    method subscribe_to_topics (line 218) | defp subscribe_to_topics(state, connection_pid) do
    method maybe_append_service (line 232) | defp maybe_append_service(state, %{service_definition: config}) do
    method maybe_append_service (line 242) | defp maybe_append_service(state, _), do: {:ok, state}
    method maybe_append_module (line 244) | defp maybe_append_module(state, %{module: module}) do
    method maybe_append_module (line 248) | defp maybe_append_module(state, _), do: {:ok, state}
    method maybe_append_consuming_function (line 250) | defp maybe_append_consuming_function(state, %{consuming_function: cons...
    method maybe_append_consuming_function (line 254) | defp maybe_append_consuming_function(state, _), do: {:ok, state}
    method validate_state (line 256) | defp validate_state(state) do

FILE: lib/gnat/handshake.ex
  class Gnat.Handshake (line 1) | defmodule Gnat.Handshake
    method connect (line 9) | def connect(settings) do
    method negotiate_settings (line 18) | def negotiate_settings(server_settings, user_settings) do
    method perform_handshake (line 27) | defp perform_handshake(tcp, user_settings) do
    method send_connect (line 41) | defp send_connect(%{tls: true}, settings, socket) do
    method send_connect (line 45) | defp send_connect(_, settings, socket) do
    method negotiate_auth (line 49) | defp negotiate_auth(
    method negotiate_auth (line 58) | defp negotiate_auth(settings, _server, %{token: token} = _user, true =...
    method negotiate_auth (line 62) | defp negotiate_auth(
    method negotiate_auth (line 74) | defp negotiate_auth(
    method negotiate_auth (line 87) | defp negotiate_auth(settings, _server, _user, _auth_required) do
    method negotiate_headers (line 91) | defp negotiate_headers(settings, %{headers: true} = _server, user_sett...
    method negotiate_headers (line 99) | defp negotiate_headers(_settings, _server, %{headers: true} = _user) do
    method negotiate_headers (line 103) | defp negotiate_headers(settings, _server, _user) do
    method negotiate_no_responders (line 107) | defp negotiate_no_responders(%{headers: true} = settings, _server_sett...
    method negotiate_no_responders (line 113) | defp negotiate_no_responders(settings, _server_settings, _user_setting...
    method upgrade_connection (line 117) | defp upgrade_connection(tcp, %{tls: true, ssl_opts: opts}) do
    method upgrade_connection (line 122) | defp upgrade_connection(tcp, _settings), do: {:ok, tcp}

FILE: lib/gnat/jetstream/api/consumer.ex
  class Gnat.Jetstream.API.Consumer (line 1) | defmodule Gnat.Jetstream.API.Consumer
    method create (line 241) | def create(conn, %__MODULE__{} = consumer) do
    method delete (line 270) | def delete(conn, stream_name, consumer_name, domain \\ nil) do
    method info (line 297) | def info(conn, stream_name, consumer_name, domain \\ nil) do
    method list (line 322) | def list(conn, stream_name, params \\ []) do
    method request_next_message (line 377) | def request_next_message(
    method js_api (line 411) | defp js_api(nil), do: "$JS.API"
    method js_api (line 412) | defp js_api(""), do: "$JS.API"
    method js_api (line 413) | defp js_api(domain), do: "$JS.#{domain}.API"
    method create_payload (line 415) | defp create_payload(%__MODULE__{} = cons) do
    method to_config (line 447) | defp to_config(raw) do
    method to_info (line 475) | defp to_info(raw) do
    method validate (line 498) | defp validate(consumer) do
    method validate_durable (line 527) | defp validate_durable(consumer) do

FILE: lib/gnat/jetstream/api/kv.ex
  class Gnat.Jetstream.API.KV (line 1) | defmodule Gnat.Jetstream.API.KV
    method create_bucket (line 52) | def create_bucket(conn, bucket_name, params \\ []) do
    method adjust_duplicate_window (line 83) | defp adjust_duplicate_window(_ttl), do: @two_minutes_in_nanoseconds
    method delete_bucket (line 93) | def delete_bucket(conn, bucket_name) do
    method create_key (line 116) | def create_key(conn, bucket_name, key, value, opts \\ []) do
    method delete_key (line 141) | def delete_key(conn, bucket_name, key, opts \\ []) do
    method purge_key (line 170) | def purge_key(conn, bucket_name, key, opts \\ []) do
    method put_value (line 200) | def put_value(conn, bucket_name, key, value, opts \\ []) do
    method get_value (line 220) | def get_value(conn, bucket_name, key) do
    method contents (line 244) | def contents(conn, bucket_name, opts \\ []) do
    method keys (line 282) | def keys(conn, bucket_name, opts \\ []) do
    method info (line 324) | def info(conn, bucket_name, opts \\ []) do
    method watch (line 338) | def watch(conn, bucket_name, handler) do
    method unwatch (line 354) | def unwatch(pid) do
    method is_kv_bucket_stream? (line 360) | def is_kv_bucket_stream?(stream_name) do
    method kv_bucket_stream? (line 371) | def kv_bucket_stream?(stream_name) do
    method list_buckets (line 379) | def list_buckets(conn) do
    method stream_name (line 394) | def stream_name(bucket_name) do
    method stream_subjects (line 398) | defp stream_subjects(bucket_name) do
    method key_name (line 402) | defp key_name(bucket_name, key) do
    method subject_to_key (line 407) | def subject_to_key(subject, bucket_name) do

FILE: lib/gnat/jetstream/api/kv/entry.ex
  class Gnat.Jetstream.API.KV.Entry (line 1) | defmodule Gnat.Jetstream.API.KV.Entry
    method from_message (line 95) | def from_message(message, bucket_name) do
    method status_message? (line 112) | defp status_message?(_), do: false
    method extract_key (line 114) | defp extract_key(%{topic: topic}, bucket_name) do
    method operation (line 133) | defp operation(_message), do: :put
    method apply_metadata (line 135) | defp apply_metadata(entry, message) do

FILE: lib/gnat/jetstream/api/kv/watcher.ex
  class Gnat.Jetstream.API.KV.Watcher (line 1) | defmodule Gnat.Jetstream.API.KV.Watcher
    method start_link (line 29) | def start_link(opts) do
    method stop (line 33) | def stop(pid) do
    method init (line 37) | def init(opts) do
    method terminate (line 51) | def terminate(_reason, state) do
    method handle_info (line 74) | def handle_info({:msg, message}, state) do
    method action (line 86) | defp action(:put), do: :key_added
    method action (line 87) | defp action(:delete), do: :key_deleted
    method action (line 88) | defp action(:purge), do: :key_purged
    method subscribe (line 90) | defp subscribe(conn, bucket_name) do

FILE: lib/gnat/jetstream/api/message.ex
  class Gnat.Jetstream.API.Message (line 1) | defmodule Gnat.Jetstream.API.Message
    method metadata (line 35) | def metadata(%{reply_to: "$JS.ACK." <> ack_topic}),
    method metadata (line 38) | def metadata(_), do: {:error, :no_jetstream_message}
    method decode_reply_to (line 42) | defp decode_reply_to([
    method decode_reply_to (line 76) | defp decode_reply_to([
    method decode_reply_to (line 106) | defp decode_reply_to(_), do: {:error, :invalid_ack_reply_to}
  class Metadata (line 10) | defmodule Metadata

FILE: lib/gnat/jetstream/api/object.ex
  class Gnat.Jetstream.API.Object (line 1) | defmodule Gnat.Jetstream.API.Object
    method create_bucket (line 23) | def create_bucket(conn, bucket_name, params \\ []) do
    method delete_bucket (line 45) | def delete_bucket(conn, bucket_name) do
    method delete (line 50) | def delete(conn, bucket_name, object_name) do
    method get (line 62) | def get(conn, bucket_name, object_name, chunk_fun) do
    method info (line 70) | def info(conn, bucket_name, object_name) do
    method list (line 88) | def list(conn, bucket_name, options \\ []) do
    method put (line 119) | def put(conn, bucket_name, object_name, io) do
    method is_object_bucket_stream? (line 154) | def is_object_bucket_stream?(stream_name) do
    method list_buckets (line 162) | def list_buckets(conn) do
    method stream_name (line 181) | defp stream_name(bucket_name) do
    method stream_subjects (line 185) | defp stream_subjects(bucket_name) do
    method chunk_stream_subject (line 192) | defp chunk_stream_subject(bucket_name) do
    method chunk_stream_topic (line 196) | defp chunk_stream_topic(bucket_name, nuid) do
    method chunk_stream_topic (line 200) | defp chunk_stream_topic(%Meta{bucket: bucket, nuid: nuid}) do
    method meta_stream_subject (line 204) | defp meta_stream_subject(bucket_name) do
    method meta_stream_topic (line 208) | defp meta_stream_topic(bucket_name, object_name) do
    method adjust_duplicate_window (line 217) | defp adjust_duplicate_window(_ttl), do: @two_minutes_in_nanoseconds
    method json_to_meta (line 219) | defp json_to_meta(json) do
    method purge_prior_chunks (line 242) | defp purge_prior_chunks(conn, bucket, name) do
    method receive_all_metas (line 255) | defp receive_all_metas(sid, num_pending, messages \\ [])
    method receive_all_metas (line 257) | defp receive_all_metas(_sid, 0, messages) do
    method receive_all_metas (line 261) | defp receive_all_metas(sid, remaining, messages) do
    method receive_chunks (line 272) | defp receive_chunks(conn, %Meta{} = meta, chunk_fun) do
    method receive_chunks (line 298) | defp receive_chunks(_conn, _sub, 0, _chunk_fun) do
    method receive_chunks (line 302) | defp receive_chunks(conn, sub, remaining, chunk_fun) do
    method send_chunks (line 325) | defp send_chunks(conn, io, topic) do
    method send_chunks (line 332) | defp send_chunks(conn, io, topic, sha, size, chunks) do
    method validate_bucket_name (line 356) | defp validate_bucket_name(name) do

FILE: lib/gnat/jetstream/api/object/meta.ex
  class Gnat.Jetstream.API.Object.Meta (line 1) | defmodule Gnat.Jetstream.API.Object.Meta

FILE: lib/gnat/jetstream/api/stream.ex
  class Gnat.Jetstream.API.Stream (line 1) | defmodule Gnat.Jetstream.API.Stream
    method create (line 287) | def create(conn, %__MODULE__{} = stream) do
    method update (line 309) | def update(conn, %__MODULE__{} = stream) do
    method list (line 419) | def list(conn, params \\ []) do
    method js_api (line 475) | defp js_api(nil), do: "$JS.API"
    method js_api (line 476) | defp js_api(""), do: "$JS.API"
    method js_api (line 477) | defp js_api(domain), do: "$JS.#{domain}.API"
    method to_state (line 479) | defp to_state(state) do
    method to_stream (line 496) | defp to_stream(stream) do
    method to_info (line 531) | defp to_info(%{"config" => config, "state" => state, "created" => crea...
    method validate (line 544) | defp validate(stream_settings) do
    method validate_message_access_method (line 569) | defp validate_message_access_method(method) do
    method validate_purge_method (line 581) | defp validate_purge_method(_) do

FILE: lib/gnat/jetstream/api/util.ex
  class Gnat.Jetstream.API.Util (line 1) | defmodule Gnat.Jetstream.API.Util
    method request (line 6) | def request(conn, topic, payload) do
    method to_datetime (line 19) | def to_datetime(nil), do: nil
    method to_datetime (line 21) | def to_datetime(str) do
    method to_sym (line 26) | def to_sym(nil), do: nil
    method put_if_exist (line 32) | def put_if_exist(target_map, target_key, source_map, source_key) do
    method valid_name? (line 39) | def valid_name?(name) do
    method invalid_name_message (line 43) | def invalid_name_message do
    method decode_base64 (line 47) | def decode_base64(nil), do: nil
    method decode_base64 (line 48) | def decode_base64(data), do: Base.decode64!(data)
    method reply_inbox (line 50) | def reply_inbox(prefix \\ @default_inbox_prefix)
    method reply_inbox (line 51) | def reply_inbox(nil), do: reply_inbox()
    method reply_inbox (line 52) | def reply_inbox(prefix), do: prefix <> nuid()
    method nuid (line 54) | def nuid() do

FILE: lib/gnat/jetstream/jetstream.ex
  class Gnat.Jetstream (line 1) | defmodule Gnat.Jetstream
    method ack (line 13) | def ack(%{reply_to: nil}) do
    method ack (line 17) | def ack(%{gnat: gnat, reply_to: reply_to}) do
    method ack_next (line 28) | def ack_next(%{reply_to: nil}, _consumer_subject) do
    method ack_next (line 32) | def ack_next(%{gnat: gnat, reply_to: reply_to}, consumer_subject) do
    method nack (line 43) | def nack(%{reply_to: nil}) do
    method nack (line 47) | def nack(%{gnat: gnat, reply_to: reply_to}) do
    method ack_progress (line 58) | def ack_progress(%{reply_to: nil}) do
    method ack_progress (line 62) | def ack_progress(%{gnat: gnat, reply_to: reply_to}) do
    method ack_term (line 73) | def ack_term(%{reply_to: nil}) do
    method ack_term (line 77) | def ack_term(%{gnat: gnat, reply_to: reply_to}) do

FILE: lib/gnat/jetstream/pager.ex
  class Gnat.Jetstream.Pager (line 1) | defmodule Gnat.Jetstream.Pager
    method init (line 35) | def init(conn, stream_name, opts) do
    method page (line 71) | def page(state) do
    method cleanup (line 77) | def cleanup(%{conn: conn} = state) do
    method reduce (line 102) | def reduce(conn, stream_name, opts, initial_state, fun) do
    method page_through (line 108) | defp page_through(pager, state, fun) do
    method request_next_message (line 121) | defp request_next_message(state) do
    method receive_messages (line 141) | defp receive_messages(%{sub: sid} = state, messages) do
    method apply_opts_to_consumer (line 155) | defp apply_opts_to_consumer(consumer = %Consumer{}, opts) do

FILE: lib/gnat/jetstream/pull_consumer.ex
  class Gnat.Jetstream.PullConsumer (line 1) | defmodule Gnat.Jetstream.PullConsumer
    method close (line 454) | def close(consumer) do

FILE: lib/gnat/jetstream/pull_consumer/connection_options.ex
  class Gnat.Jetstream.PullConsumer.ConnectionOptions (line 1) | defmodule Gnat.Jetstream.PullConsumer.ConnectionOptions
    method validate! (line 27) | def validate!(connection_options) do

FILE: lib/gnat/jetstream/pull_consumer/server.ex
  class Gnat.Jetstream.PullConsumer.Server (line 1) | defmodule Gnat.Jetstream.PullConsumer.Server
    method init (line 23) | def init(%{module: module, init_arg: init_arg}) do
    method connect (line 50) | def connect(
    method disconnect (line 136) | def disconnect(
    method ensure_consumer_exists (line 173) | defp ensure_consumer_exists(gnat, stream_name, consumer_name, nil, dom...
    method ensure_consumer_exists (line 186) | defp ensure_consumer_exists(gnat, _stream_name, nil, consumer_struct, ...
    method validate_consumer_for_creation (line 199) | defp validate_consumer_for_creation(consumer_definition) do
    method validate_batch_ack_policy (line 228) | defp validate_batch_ack_policy(_connection_options, _consumer_info), d...
    method maybe_handle_connected (line 230) | defp maybe_handle_connected(module, consumer_info, state) do
    method maybe_handle_status (line 239) | defp maybe_handle_status(message, %__MODULE__{module: module, state: s...
    method connection_pid (line 256) | defp connection_pid(connection_name) do
    method handle_info (line 352) | def handle_info(
    method handle_info (line 466) | def handle_info(
    method handle_call (line 493) | def handle_call(
    method next_message (line 517) | defp next_message(conn, stream_name, consumer_name, domain, listening_...
    method initial_fetch (line 527) | defp initial_fetch(gen_state, conn, stream_name, consumer_name, domain...
    method request_batch (line 535) | defp request_batch(conn, gen_state, mode) do
    method process_and_ack_batch (line 563) | defp process_and_ack_batch(%{buffer: buffer, module: module, state: st...

FILE: lib/gnat/parsec.ex
  class Gnat.Parsec (line 1) | defmodule Gnat.Parsec
    method new (line 100) | def new, do: %__MODULE__{}
    method parse (line 102) | def parse(%__MODULE__{partial: nil} = state, string) do
    method parse (line 107) | def parse(%__MODULE__{partial: partial} = state, string) do
    method parse_commands (line 112) | def parse_commands("", list), do: {nil, Enum.reverse(list)}
    method parse_commands (line 114) | def parse_commands(str, list) do
    method parse_command (line 122) | def parse_command(string) do
    method parse_headers (line 150) | def parse_headers("NATS/1.0" <> rest) do
    method parse_headers (line 178) | def parse_headers(_other) do
    method finish_msg (line 182) | def finish_msg(subject, sid, reply_to, length, rest, string) do
    method finish_hmsg (line 192) | def finish_hmsg(subject, sid, reply_to, header_length, total_length, r...

FILE: lib/gnat/server.ex
  class Gnat.Server (line 1) | defmodule Gnat.Server
    method execute (line 61) | def execute(module, message) do
    method execute_error (line 76) | defp execute_error(module, message, error) do
    method send_reply (line 106) | def send_reply(_other, _iodata) do

FILE: lib/gnat/services/server.ex
  class Gnat.Services.Server (line 1) | defmodule Gnat.Services.Server
    method execute (line 113) | def execute(_module, %{topic: "$SRV" <> _} = message, service) do
    method execute (line 117) | def execute(module, message, service) do
    method execute_error (line 153) | defp execute_error(module, message, error) do
    method send_reply (line 183) | def send_reply(_other, _iodata) do

FILE: lib/gnat/services/service.ex
  class Gnat.Services.Service (line 1) | defmodule Gnat.Services.Service
    method init (line 17) | def init(configuration) do
    method info (line 33) | def info(service) do
    method ping (line 49) | def ping(service) do
    method record_request (line 58) | def record_request(%{counters: counters} = _endpoint, elapsed_micros) do
    method record_error (line 63) | def record_error(%{counters: counters} = _endpoint, elapsed_micros) do
    method subscription_topics_with_queue_group (line 68) | def subscription_topics_with_queue_group(service) do
    method stats (line 79) | def stats(service) do
    method build_subject_map (line 94) | defp build_subject_map(endpoints) do
    method derive_subscription_subject (line 112) | defp derive_subscription_subject(endpoint) do
    method endpoint_info (line 128) | defp endpoint_info(endpoint) do
    method endpoint_stats (line 137) | defp endpoint_stats(%{counters: counters} = endpoint) do
    method validate_configuration (line 168) | defp validate_configuration(configuration) do
    method valid_version? (line 196) | defp valid_version?(service_definition) do
    method valid_name? (line 206) | defp valid_name?(service_definition) do
    method valid_metadata? (line 216) | defp valid_metadata?(nil), do: :ok
    method valid_metadata? (line 218) | defp valid_metadata?(md) do
    method valid_endpoint? (line 232) | defp valid_endpoint?(endpoint_definition) do

FILE: lib/gnat/services/service_responder.ex
  class Gnat.Services.ServiceResponder (line 1) | defmodule Gnat.Services.ServiceResponder
    method maybe_respond (line 11) | def maybe_respond(%{topic: topic} = message, service) do
    method handle_ping (line 27) | defp handle_ping(tail, service, %{reply_to: rt, gnat: gnat}) do
    method handle_info (line 34) | defp handle_info(tail, service, %{reply_to: rt, gnat: gnat}) do
    method handle_stats (line 41) | defp handle_stats(tail, service, %{reply_to: rt, gnat: gnat}) do
    method should_respond? (line 49) | defp should_respond?(tail, service_name, instance_id) do

FILE: lib/gnat/services/wire_protocol.ex
  class Gnat.Services.WireProtocol (line 1) | defmodule Gnat.Services.WireProtocol
  class InfoResponse (line 4) | defmodule InfoResponse
  class PingResponse (line 38) | defmodule PingResponse
  class StatsResponse (line 60) | defmodule StatsResponse

FILE: mix.exs
  class Gnat.Mixfile (line 1) | defmodule Gnat.Mixfile
    method project (line 7) | def project do
    method application (line 27) | def application do
    method deps (line 31) | defp deps do
    method docs (line 46) | defp docs do
    method elixirc_paths (line 68) | defp elixirc_paths(:test), do: ["lib", "test/support"]
    method elixirc_paths (line 69) | defp elixirc_paths(_), do: ["lib"]
    method package (line 71) | defp package do

FILE: scripts/cluster/driver.exs
  class FailoverConsumer (line 88) | defmodule FailoverConsumer
    method start_link (line 92) | def start_link(opts), do: Gnat.Jetstream.PullConsumer.start_link(__MOD...
    method init (line 95) | def init(opts) do
    method handle_message (line 100) | def handle_message(%{body: body, reply_to: reply_to}, state) do
    method handle_status (line 113) | def handle_status(%{status: status, description: desc}, state) do
    method handle_status (line 118) | def handle_status(%{status: status}, state) do
    method handle_connected (line 124) | def handle_connected(consumer_info, state) do
  class Publisher (line 143) | defmodule Publisher
    method loop (line 146) | def loop(subject, attempt \\ 1, accepted \\ 0) do

FILE: test/command_test.exs
  class Gnat.CommandTest (line 1) | defmodule Gnat.CommandTest

FILE: test/gnat/consumer_supervisor_test.exs
  class Gnat.ConsumerSupervisorTest (line 1) | defmodule Gnat.ConsumerSupervisorTest
    method start_service_supervisor (line 158) | defp start_service_supervisor(service_config) do

FILE: test/gnat/handshake_test.exs
  class Gnat.HandshakeTest (line 1) | defmodule Gnat.HandshakeTest

FILE: test/gnat/parsec_property_test.exs
  class Gnat.ParsecPropertyTest (line 1) | defmodule Gnat.ParsecPropertyTest
    method random_chunk (line 81) | def random_chunk(binary), do: random_chunk(binary, [])
    method random_chunk (line 82) | def random_chunk("", list), do: Enum.reverse(list)
    method random_chunk (line 84) | def random_chunk(binary, list) do

FILE: test/gnat/parsec_test.exs
  class Gnat.ParsecTest (line 1) | defmodule Gnat.ParsecTest

FILE: test/gnat_property_test.exs
  class GnatPropertyTest (line 1) | defmodule GnatPropertyTest

FILE: test/gnat_test.exs
  class GnatTest (line 1) | defmodule GnatTest
    method spin_up_echo_server_on_topic (line 329) | defp spin_up_echo_server_on_topic(ready, gnat, topic) do

FILE: test/jetstream/api/consumer_doc_test.exs
  class Gnat.Jetstream.API.ConsumerDocTest (line 1) | defmodule Gnat.Jetstream.API.ConsumerDocTest

FILE: test/jetstream/api/consumer_test.exs
  class Gnat.Jetstream.API.ConsumerTest (line 1) | defmodule Gnat.Jetstream.API.ConsumerTest

FILE: test/jetstream/api/kv/entry_test.exs
  class Gnat.Jetstream.API.KV.EntryTest (line 1) | defmodule Gnat.Jetstream.API.KV.EntryTest

FILE: test/jetstream/api/kv/watcher_test.exs
  class Gnat.Jetstream.API.KV.WatcherTest (line 1) | defmodule Gnat.Jetstream.API.KV.WatcherTest

FILE: test/jetstream/api/kv_test.exs
  class Gnat.Jetstream.API.KVTest (line 1) | defmodule Gnat.Jetstream.API.KVTest

FILE: test/jetstream/api/object_test.exs
  class Gnat.Jetstream.API.ObjectTest (line 1) | defmodule Gnat.Jetstream.API.ObjectTest
    method generate_big_file (line 240) | defp generate_big_file(tmp_dir) do
    method put_filepath (line 256) | defp put_filepath(path, bucket, name) do
    method stream_byte_size (line 261) | defp stream_byte_size(bucket) do

FILE: test/jetstream/api/stream_doc_test.exs
  class Gnat.Jetstream.API.StreamDocTest (line 1) | defmodule Gnat.Jetstream.API.StreamDocTest

FILE: test/jetstream/api/stream_test.exs
  class Gnat.Jetstream.API.StreamTest (line 1) | defmodule Gnat.Jetstream.API.StreamTest

FILE: test/jetstream/message_test.exs
  class Gnat.Jetstream.MessageTest (line 1) | defmodule Gnat.Jetstream.MessageTest

FILE: test/jetstream/pager_test.exs
  class Gnat.Jetstream.PagerTest (line 1) | defmodule Gnat.Jetstream.PagerTest
    method create_stream (line 57) | defp create_stream(name) do

FILE: test/pull_consumer/batch_test.exs
  class Gnat.Jetstream.PullConsumer.BatchTest (line 1) | defmodule Gnat.Jetstream.PullConsumer.BatchTest
  class BatchPullConsumer (line 9) | defmodule BatchPullConsumer
    method start_link (line 12) | def start_link(opts) do
    method init (line 17) | def init(opts) do
    method maybe_put (line 37) | defp maybe_put(conn_opts, key, opts) do
    method handle_connected (line 45) | def handle_connected(consumer_info, state) do
    method handle_message (line 51) | def handle_message(message, state) do
  class NonAckBatchConsumer (line 60) | defmodule NonAckBatchConsumer
    method start_link (line 63) | def start_link(opts) do
    method init (line 68) | def init(opts) do
    method handle_message (line 83) | def handle_message(%{body: "nack_me"}, state) do
    method handle_message (line 88) | def handle_message(%{body: "term_me"}, state) do
    method handle_message (line 93) | def handle_message(message, state) do

FILE: test/pull_consumer/connectivity_test.exs
  class Gnat.Jetstream.PullConsumer.ConnectivityTest (line 1) | defmodule Gnat.Jetstream.PullConsumer.ConnectivityTest
    method cleanup (line 168) | defp cleanup do
  class ExamplePullConsumer (line 6) | defmodule ExamplePullConsumer
    method start_link (line 9) | def start_link(opts) do
    method init (line 14) | def init(opts) do
    method handle_message (line 19) | def handle_message(%{topic: "ackable"}, state) do
    method handle_message (line 23) | def handle_message(%{topic: "non-ackable", reply_to: reply_to}, state) do
    method handle_message (line 34) | def handle_message(%{topic: "terminatable"}, state) do
    method handle_message (line 38) | def handle_message(%{topic: "skippable"}, state) do

FILE: test/pull_consumer/ephemeral_test.exs
  class Gnat.Jetstream.PullConsumer.EphemeralTest (line 1) | defmodule Gnat.Jetstream.PullConsumer.EphemeralTest
    method cleanup (line 80) | defp cleanup do
  class ExamplePullConsumer (line 6) | defmodule ExamplePullConsumer
    method start_link (line 9) | def start_link(opts) do
    method init (line 14) | def init(opts) do
    method handle_connected (line 27) | def handle_connected(consumer_info, state) do
    method handle_message (line 33) | def handle_message(message, state) do

FILE: test/pull_consumer/status_messages_test.exs
  class Gnat.Jetstream.PullConsumer.StatusMessagesTest (line 1) | defmodule Gnat.Jetstream.PullConsumer.StatusMessagesTest
    method drain_pulls (line 271) | defp drain_pulls(timeout) do
  class ObservingConsumer (line 6) | defmodule ObservingConsumer
    method start_link (line 9) | def start_link(opts) do
    method init (line 14) | def init(opts) do
    method handle_message (line 22) | def handle_message(message, state) do
    method handle_status (line 28) | def handle_status(message, state) do
  class SilentConsumer (line 34) | defmodule SilentConsumer
    method start_link (line 37) | def start_link(opts) do
    method init (line 42) | def init(opts) do
    method handle_message (line 49) | def handle_message(message, test_pid) do

FILE: test/pull_consumer/using_macro_test.exs
  class Gnat.Jetstream.PullConsumer.UsingMacroTest (line 1) | defmodule Gnat.Jetstream.PullConsumer.UsingMacroTest
  class ExamplePullConsumer (line 4) | defmodule ExamplePullConsumer
    method start_link (line 7) | def start_link(_) do
    method init (line 12) | def init([]) do
    method handle_message (line 17) | def handle_message(%{}, state) do

FILE: test/support/conn_case.ex
  class Gnat.Jetstream.ConnCase (line 1) | defmodule Gnat.Jetstream.ConnCase
    method module_tags (line 18) | defp module_tags(opts) do
    method add_module_tag (line 24) | defp add_module_tag(tags, {:min_server_version, min_version}) do
    method add_module_tag (line 32) | defp add_module_tag(tags, _opt), do: tags
    method get_server_version (line 34) | defp get_server_version(conn) do
    method server_version_incompatible? (line 38) | defp server_version_incompatible?(min_version) do

FILE: test/support/generators.ex
  class Gnat.Generators (line 1) | defmodule Gnat.Generators
    method alphanumeric_char (line 5) | def alphanumeric_char do
    method alphanumeric_space_char (line 9) | def alphanumeric_space_char do
    method numeric_char (line 13) | def numeric_char do
    method delimiter (line 18) | def delimiter, do: let(chunks <- non_empty(list(delimiter_char())), do...
    method delimiter_char (line 20) | def delimiter_char, do: union([" ", "\t"])
    method error (line 22) | def error do
    method host_port (line 28) | def host_port do
    method info (line 35) | def info do
    method info_options (line 41) | def info_options do
    method ok (line 62) | def ok, do: %{binary: "+OK\r\n"}
    method ping (line 64) | def ping, do: %{binary: "PING\r\n"}
    method pong (line 66) | def pong, do: %{binary: "PONG\r\n"}
    method message (line 70) | def message, do: sized(size, message(size))
    method message (line 71) | def message(size), do: union([message_without_reply(size), message_wit...
    method message_with_reply (line 73) | def message_with_reply(size) do
    method message_without_reply (line 91) | def message_without_reply(size) do
    method payload (line 108) | def payload(size), do: binary(size)
    method protocol_message (line 110) | def protocol_message do
    method sid (line 116) | def sid, do: non_neg_integer()
    method subject (line 118) | def subject do
    method subject_chunks (line 122) | def subject_chunks do
    method reply_to (line 129) | def reply_to, do: subject()

FILE: test/test_helper.exs
  class RpcEndpoint (line 35) | defmodule RpcEndpoint
    method init (line 36) | def init do
    method loop (line 42) | def loop(pid) do
  class ExampleService (line 53) | defmodule ExampleService
    method request (line 68) | def request(_, _, _) do
    method error (line 72) | def error(_msg, "oops") do
  class ExampleServer (line 77) | defmodule ExampleServer
    method request (line 80) | def request(%{topic: "example.good", body: body}) do
    method request (line 84) | def request(%{topic: "example.error"}) do
    method request (line 88) | def request(%{topic: "example.raise"}) do
    method error (line 92) | def error(_msg, "oops") do
    method error (line 96) | def error(_msg, %RuntimeError{message: "oops"}) do
    method error (line 100) | def error(msg, other) do
  class CheckForExpectedNatsServers (line 136) | defmodule CheckForExpectedNatsServers
    method check (line 137) | def check(tags) do
    method check_for_default (line 142) | def check_for_default do
    method check_for_tag (line 157) | def check_for_tag(:multi_server) do
    method check_for_tag (line 242) | def check_for_tag(_), do: :ok
Condensed preview — 85 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (416K chars).
[
  {
    "path": ".dialyzer_ignore.exs",
    "chars": 4,
    "preview": "[\n]\n"
  },
  {
    "path": ".formatter.exs",
    "chars": 67,
    "preview": "[\n  inputs: [\"{.formatter,mix}.exs\", \"{lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": ".github/workflows/CI.yml",
    "chars": 2503,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  schedule:\n    - cron: \"0 0 1 */1 *\"\n\np"
  },
  {
    "path": ".gitignore",
    "chars": 824,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build\n\n# If you run \"mix test --cover\", coverage assets end up h"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 6313,
    "preview": "# Changelog\n\n## 1.14\n\n* Add `PullConsumer.handle_connected/2` optional callback to get consumer info\n* Add `PullConsumer"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1052,
    "preview": "Copyright 2017 Michael Ries\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this softwa"
  },
  {
    "path": "MAINTAINERS.md",
    "chars": 327,
    "preview": "# Maintainers\n\nMaintainership is on a per project basis.\n\n### Maintainers\n  - Colin Sullivan <colin@nats.io> [@ColinSull"
  },
  {
    "path": "README.md",
    "chars": 7480,
    "preview": "[![hex.pm](https://img.shields.io/hexpm/v/gnat.svg)](https://hex.pm/packages/gnat)\n[![hex.pm](https://img.shields.io/hex"
  },
  {
    "path": "bench/client.exs",
    "chars": 3267,
    "preview": "Application.put_env(:client, :num_connections, 4)\nnum_requesters = 16\nrequests_per_requester = 500\n\ndefmodule Client do\n"
  },
  {
    "path": "bench/kv_consume.exs",
    "chars": 8420,
    "preview": "# bench/kv_consume.exs\n#\n# Compares three approaches for consuming all messages from a KV bucket into ETS:\n#\n#   1. Page"
  },
  {
    "path": "bench/parse.exs",
    "chars": 666,
    "preview": "msg1024 = :crypto.strong_rand_bytes(1024)\nmsg128  = :crypto.strong_rand_bytes(128)\nmsg16   = :crypto.strong_rand_bytes(1"
  },
  {
    "path": "bench/publish.exs",
    "chars": 418,
    "preview": "inputs = %{\n  \"16 byte\" => :crypto.strong_rand_bytes(16),\n  \"128 byte\" => :crypto.strong_rand_bytes(128),\n  \"1024 byte\" "
  },
  {
    "path": "bench/request.exs",
    "chars": 937,
    "preview": "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,"
  },
  {
    "path": "bench/request_multi.exs",
    "chars": 1067,
    "preview": "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,"
  },
  {
    "path": "bench/server.exs",
    "chars": 745,
    "preview": "num_connections = 4\nnum_subscribers = 4\n\nEnum.each(0..(num_connections - 1), fn(i) ->\n  name = :\"gnat#{i}\"\n  {:ok, _pid}"
  },
  {
    "path": "bench/service_bench.exs",
    "chars": 1599,
    "preview": "defmodule EchoService do\n  use Gnat.Services.Server\n\n  def request(%{body: body}, \"echo\", _group) do\n    {:reply, body}\n"
  },
  {
    "path": "dependencies.md",
    "chars": 1020,
    "preview": "# Project Dependencies\n\nThis is a list of dependencies that will be pulled into your project when you use this library. "
  },
  {
    "path": "docs/js/guides/broadway.md",
    "chars": 4856,
    "preview": "# Using Broadway with Jetstream\n\nBroadway is a library which allows building concurrent and multi-stage data ingestion a"
  },
  {
    "path": "docs/js/guides/managing.md",
    "chars": 492,
    "preview": "# Managing Streams and Consumers\n\nJetstream provides a JSON API for managing streams and consumers.\nThis library exposes"
  },
  {
    "path": "docs/js/guides/push_based_consumer.md",
    "chars": 2053,
    "preview": "# Push based consumer\n\n```elixir\n# Start a nats server with jetstream enabled and default configs\n# Now run the followin"
  },
  {
    "path": "docs/js/introduction/getting_started.md",
    "chars": 5964,
    "preview": "# Getting Started\n\nIn this guide, we're going to learn how to install Jetstream in your project and start consuming\nmess"
  },
  {
    "path": "docs/js/introduction/overview.md",
    "chars": 802,
    "preview": "# Overview\n\n[Jetstream](https://docs.nats.io/nats-concepts/jetstream) is a distributed persistence system\nbuilt-in to [N"
  },
  {
    "path": "lib/gnat/command.ex",
    "chars": 1923,
    "preview": "defmodule Gnat.Command do\n  @moduledoc false\n\n  @newline \"\\r\\n\"\n  @hpub \"HPUB\"\n  @pub \"PUB\"\n  @sub \"SUB\"\n  @unsub \"UNSUB"
  },
  {
    "path": "lib/gnat/connection_supervisor.ex",
    "chars": 3560,
    "preview": "defmodule Gnat.ConnectionSupervisor do\n  use GenServer\n  require Logger\n\n  @moduledoc \"\"\"\n  A process that can supervise"
  },
  {
    "path": "lib/gnat/consumer_supervisor.ex",
    "chars": 9538,
    "preview": "defmodule Gnat.ConsumerSupervisor do\n  use GenServer\n  require Logger\n  alias Gnat.Services.Service\n\n  @moduledoc \"\"\"\n  "
  },
  {
    "path": "lib/gnat/handshake.ex",
    "chars": 3854,
    "preview": "defmodule Gnat.Handshake do\n  @moduledoc false\n  alias Gnat.Parsec\n\n  @doc \"\"\"\n  This function handles all of the variat"
  },
  {
    "path": "lib/gnat/jetstream/api/consumer.ex",
    "chars": 22467,
    "preview": "defmodule Gnat.Jetstream.API.Consumer do\n  @moduledoc \"\"\"\n  A module representing a NATS JetStream Consumer.\n\n  Learn mo"
  },
  {
    "path": "lib/gnat/jetstream/api/kv/entry.ex",
    "chars": 5081,
    "preview": "defmodule Gnat.Jetstream.API.KV.Entry do\n  @moduledoc \"\"\"\n  A parsed view of a single message from a Key/Value bucket's "
  },
  {
    "path": "lib/gnat/jetstream/api/kv/watcher.ex",
    "chars": 3666,
    "preview": "defmodule Gnat.Jetstream.API.KV.Watcher do\n  @moduledoc \"\"\"\n  The watcher server establishes a subscription to the chang"
  },
  {
    "path": "lib/gnat/jetstream/api/kv.ex",
    "chars": 12786,
    "preview": "defmodule Gnat.Jetstream.API.KV do\n  @moduledoc \"\"\"\n  API for interacting with the Key/Value store functionality in Nats"
  },
  {
    "path": "lib/gnat/jetstream/api/message.ex",
    "chars": 2927,
    "preview": "defmodule Gnat.Jetstream.API.Message do\n  @moduledoc \"\"\"\n  This module provides a way to parse the `reply_to` received b"
  },
  {
    "path": "lib/gnat/jetstream/api/object/meta.ex",
    "chars": 945,
    "preview": "defmodule Gnat.Jetstream.API.Object.Meta do\n  @enforce_keys [:bucket, :chunks, :digest, :name, :nuid, :size]\n  defstruct"
  },
  {
    "path": "lib/gnat/jetstream/api/object.ex",
    "chars": 11015,
    "preview": "defmodule Gnat.Jetstream.API.Object do\n  @moduledoc \"\"\"\n  API for interacting with the JetStream Object Store\n\n  Learn m"
  },
  {
    "path": "lib/gnat/jetstream/api/stream.ex",
    "chars": 21841,
    "preview": "defmodule Gnat.Jetstream.API.Stream do\n  @moduledoc \"\"\"\n  A module representing a NATS JetStream Stream.\n\n  Learn more a"
  },
  {
    "path": "lib/gnat/jetstream/api/util.ex",
    "chars": 1334,
    "preview": "defmodule Gnat.Jetstream.API.Util do\n  @moduledoc false\n\n  @default_inbox_prefix \"_INBOX.\"\n\n  def request(conn, topic, p"
  },
  {
    "path": "lib/gnat/jetstream/jetstream.ex",
    "chars": 2354,
    "preview": "defmodule Gnat.Jetstream do\n  @moduledoc \"\"\"\n  Provides functions for interacting with a [NATS Jetstream](https://github"
  },
  {
    "path": "lib/gnat/jetstream/pager.ex",
    "chars": 5102,
    "preview": "defmodule Gnat.Jetstream.Pager do\n  @moduledoc \"\"\"\n  Page through all the messages in a stream\n\n  This module provides a"
  },
  {
    "path": "lib/gnat/jetstream/pull_consumer/connection_options.ex",
    "chars": 2703,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.ConnectionOptions do\n  @moduledoc false\n\n  @default_retry_timeout 1000\n  @default_"
  },
  {
    "path": "lib/gnat/jetstream/pull_consumer/server.ex",
    "chars": 17400,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.Server do\n  @moduledoc false\n\n  require Logger\n\n  use Connection\n\n  alias Gnat.Jet"
  },
  {
    "path": "lib/gnat/jetstream/pull_consumer.ex",
    "chars": 16559,
    "preview": "defmodule Gnat.Jetstream.PullConsumer do\n  @moduledoc \"\"\"\n  A behaviour which provides the NATS JetStream Pull Consumer "
  },
  {
    "path": "lib/gnat/parsec.ex",
    "chars": 5537,
    "preview": "defmodule Gnat.Parsec do\n  @moduledoc false\n  defstruct partial: nil\n\n  import NimbleParsec\n\n  subject = ascii_string([?"
  },
  {
    "path": "lib/gnat/server.ex",
    "chars": 3234,
    "preview": "defmodule Gnat.Server do\n  require Logger\n\n  @moduledoc \"\"\"\n  A behavior for acting as a server for nats messages.\n\n  Yo"
  },
  {
    "path": "lib/gnat/services/server.ex",
    "chars": 6837,
    "preview": "defmodule Gnat.Services.Server do\n  require Logger\n  alias Gnat.Services.{Service, ServiceResponder}\n\n  @moduledoc \"\"\"\n "
  },
  {
    "path": "lib/gnat/services/service.ex",
    "chars": 6539,
    "preview": "defmodule Gnat.Services.Service do\n  @moduledoc false\n\n  @subscription_subject \"$SRV.>\"\n  # required default, see https:"
  },
  {
    "path": "lib/gnat/services/service_responder.ex",
    "chars": 1586,
    "preview": "defmodule Gnat.Services.ServiceResponder do\n  @moduledoc false\n\n  require Logger\n  alias Gnat.Services.Service\n\n  @op_pi"
  },
  {
    "path": "lib/gnat/services/wire_protocol.ex",
    "chars": 2208,
    "preview": "defmodule Gnat.Services.WireProtocol do\n  @moduledoc false\n\n  defmodule InfoResponse do\n    @moduledoc false\n    @info_r"
  },
  {
    "path": "lib/gnat.ex",
    "chars": 27470,
    "preview": "# State transitions:\n#  :waiting_for_message => receive PING, send PONG => :waiting_for_message\n#  :waiting_for_message "
  },
  {
    "path": "mix.exs",
    "chars": 2411,
    "preview": "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  d"
  },
  {
    "path": "scripts/cluster/cluster.sh",
    "chars": 3043,
    "preview": "#!/usr/bin/env bash\n# Minimal control script for a 3-node local nats cluster used for\n# manually exercising PullConsumer"
  },
  {
    "path": "scripts/cluster/driver.exs",
    "chars": 5490,
    "preview": "# Manual failover driver for the 3-node cluster under scripts/cluster/.\n#\n# Usage (from repo root, after `scripts/cluste"
  },
  {
    "path": "scripts/cluster/n1.conf",
    "chars": 239,
    "preview": "port: 4223\nhttp_port: 8223\nserver_name: n1\n\njetstream {\n  store_dir: \"./scripts/cluster/data/n1\"\n}\n\ncluster {\n  name: fa"
  },
  {
    "path": "scripts/cluster/n2.conf",
    "chars": 239,
    "preview": "port: 4224\nhttp_port: 8224\nserver_name: n2\n\njetstream {\n  store_dir: \"./scripts/cluster/data/n2\"\n}\n\ncluster {\n  name: fa"
  },
  {
    "path": "scripts/cluster/n3.conf",
    "chars": 239,
    "preview": "port: 4225\nhttp_port: 8225\nserver_name: n3\n\njetstream {\n  store_dir: \"./scripts/cluster/data/n3\"\n}\n\ncluster {\n  name: fa"
  },
  {
    "path": "test/command_test.exs",
    "chars": 1182,
    "preview": "defmodule Gnat.CommandTest do\n  use ExUnit.Case, async: true\n  alias Gnat.Command\n\n  test \"formatting a simple pub messa"
  },
  {
    "path": "test/fixtures/ca.pem",
    "chars": 1223,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIDXDCCAkQCCQDI2Vsry8+BDDANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCQ0ExEDAOBgN"
  },
  {
    "path": "test/fixtures/client-cert.pem",
    "chars": 1191,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIDQjCCAiqgAwIBAgIJAJCSLX9jr5WzMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTE"
  },
  {
    "path": "test/fixtures/client-key.pem",
    "chars": 1707,
    "preview": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCsnD6dO3oSVoV4\nyt+c/Ax+XvJPIjNGgThT16clj9f"
  },
  {
    "path": "test/fixtures/nkey_config",
    "chars": 104,
    "preview": "authorization: {\n  users: [\n    { nkey: UBSUDO5PNPFR72YUCWWSN4ADPIEU3WESNZ35S3VZAWERPXCZSQTDH7SS }\n  ]\n}"
  },
  {
    "path": "test/fixtures/nkey_seed",
    "chars": 58,
    "preview": "SUAMH3IDGSDQ2AVZFOWYAKNA7R2FXIZZSQ3BQMA5QNJRYP3ABIKDDP5DBA"
  },
  {
    "path": "test/fixtures/server-cert.pem",
    "chars": 1183,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIDPTCCAiWgAwIBAgIJAJCSLX9jr5W7MA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTE"
  },
  {
    "path": "test/fixtures/server-key.pem",
    "chars": 1699,
    "preview": "-----BEGIN PRIVATE KEY-----\nMIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDm+0dlzcmiLa+L\nzdVqeVQ8B1/rWnErK+VvvjH7FmV"
  },
  {
    "path": "test/gnat/consumer_supervisor_test.exs",
    "chars": 5339,
    "preview": "defmodule Gnat.ConsumerSupervisorTest do\n  alias Gnat.ConsumerSupervisor\n  use ExUnit.Case, async: true\n\n  # these reque"
  },
  {
    "path": "test/gnat/handshake_test.exs",
    "chars": 3384,
    "preview": "defmodule Gnat.HandshakeTest do\n  use ExUnit.Case, async: true\n  alias Gnat.Handshake\n\n  describe \"negotiate_settings/2\""
  },
  {
    "path": "test/gnat/parsec_property_test.exs",
    "chars": 2911,
    "preview": "defmodule Gnat.ParsecPropertyTest do\n  use ExUnit.Case, async: true\n  use PropCheck\n  import Gnat.Generators, only: [mes"
  },
  {
    "path": "test/gnat/parsec_test.exs",
    "chars": 6385,
    "preview": "defmodule Gnat.ParsecTest do\n  use ExUnit.Case, async: true\n  alias Gnat.Parsec\n\n  test \"parsing a complete message\" do\n"
  },
  {
    "path": "test/gnat_property_test.exs",
    "chars": 1223,
    "preview": "defmodule GnatPropertyTest do\n  use ExUnit.Case, async: true\n  use PropCheck\n  import Gnat.Generators, only: [message: 0"
  },
  {
    "path": "test/gnat_test.exs",
    "chars": 12292,
    "preview": "defmodule GnatTest do\n  use ExUnit.Case, async: true\n  doctest Gnat\n\n  setup context do\n    CheckForExpectedNatsServers."
  },
  {
    "path": "test/jetstream/api/consumer_doc_test.exs",
    "chars": 150,
    "preview": "defmodule Gnat.Jetstream.API.ConsumerDocTest do\n  use Gnat.Jetstream.ConnCase\n  @moduletag with_gnat: :gnat\n  doctest Gn"
  },
  {
    "path": "test/jetstream/api/consumer_test.exs",
    "chars": 8942,
    "preview": "defmodule Gnat.Jetstream.API.ConsumerTest do\n  use Gnat.Jetstream.ConnCase\n  alias Gnat.Jetstream.API.{Consumer, Stream}"
  },
  {
    "path": "test/jetstream/api/kv/entry_test.exs",
    "chars": 3155,
    "preview": "defmodule Gnat.Jetstream.API.KV.EntryTest do\n  use ExUnit.Case, async: true\n\n  alias Gnat.Jetstream.API.KV.Entry\n\n  @buc"
  },
  {
    "path": "test/jetstream/api/kv/watcher_test.exs",
    "chars": 1850,
    "preview": "defmodule Gnat.Jetstream.API.KV.WatcherTest do\n  use Gnat.Jetstream.ConnCase, min_server_version: \"2.6.2\"\n\n  alias Gnat."
  },
  {
    "path": "test/jetstream/api/kv_test.exs",
    "chars": 10858,
    "preview": "defmodule Gnat.Jetstream.API.KVTest do\n  use Gnat.Jetstream.ConnCase, min_server_version: \"2.6.2\"\n  alias Gnat.Jetstream"
  },
  {
    "path": "test/jetstream/api/object_test.exs",
    "chars": 9437,
    "preview": "defmodule Gnat.Jetstream.API.ObjectTest do\n  use Gnat.Jetstream.ConnCase, min_server_version: \"2.6.2\"\n  alias Gnat.Jetst"
  },
  {
    "path": "test/jetstream/api/stream_doc_test.exs",
    "chars": 146,
    "preview": "defmodule Gnat.Jetstream.API.StreamDocTest do\n  use Gnat.Jetstream.ConnCase\n  @moduletag with_gnat: :gnat\n  doctest Gnat"
  },
  {
    "path": "test/jetstream/api/stream_test.exs",
    "chars": 11767,
    "preview": "defmodule Gnat.Jetstream.API.StreamTest do\n  use Gnat.Jetstream.ConnCase\n  alias Gnat.Jetstream.API.Stream\n\n  @moduletag"
  },
  {
    "path": "test/jetstream/message_test.exs",
    "chars": 1266,
    "preview": "defmodule Gnat.Jetstream.MessageTest do\n  use ExUnit.Case, async: true\n\n  test \"message metadata without domain\" do\n    "
  },
  {
    "path": "test/jetstream/pager_test.exs",
    "chars": 1585,
    "preview": "defmodule Gnat.Jetstream.PagerTest do\n  use Gnat.Jetstream.ConnCase\n  alias Gnat.Jetstream.Pager\n  alias Gnat.Jetstream."
  },
  {
    "path": "test/pull_consumer/batch_test.exs",
    "chars": 14548,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.BatchTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Consumer, "
  },
  {
    "path": "test/pull_consumer/connectivity_test.exs",
    "chars": 5188,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.ConnectivityTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Con"
  },
  {
    "path": "test/pull_consumer/ephemeral_test.exs",
    "chars": 2216,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.EphemeralTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{Consum"
  },
  {
    "path": "test/pull_consumer/status_messages_test.exs",
    "chars": 8982,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.StatusMessagesTest do\n  use Gnat.Jetstream.ConnCase\n\n  alias Gnat.Jetstream.API.{C"
  },
  {
    "path": "test/pull_consumer/using_macro_test.exs",
    "chars": 822,
    "preview": "defmodule Gnat.Jetstream.PullConsumer.UsingMacroTest do\n  use Gnat.Jetstream.ConnCase\n\n  defmodule ExamplePullConsumer d"
  },
  {
    "path": "test/support/conn_case.ex",
    "chars": 1470,
    "preview": "defmodule Gnat.Jetstream.ConnCase do\n  @moduledoc \"\"\"\n  This module defines the test case to be used by tests that requi"
  },
  {
    "path": "test/support/generators.ex",
    "chars": 3762,
    "preview": "defmodule Gnat.Generators do\n  use PropCheck\n\n  # Character classes useful for generating text\n  def alphanumeric_char d"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 6384,
    "preview": "ExUnit.configure(exclude: [:pending, :property, :multi_server, :message_ttl])\n\nExUnit.start()\n\n# set assert_receive defa"
  }
]

About this extraction

This page contains the full source code of the nats-io/nats.ex GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 85 files (384.2 KB), approximately 109.9k tokens, and a symbol index with 493 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!