Repository: nats-io/nats.zig
Branch: main
Commit: 35e7358b3480
Files: 161
Total size: 1.8 MB
Directory structure:
gitextract_52v3q3d0/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── claude.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── build.zig
├── build.zig.zon
├── doc/
│ ├── JetStream.md
│ └── nats-by-example/
│ ├── README.md
│ ├── auth/
│ │ ├── NKeys-JWTs.md
│ │ └── nkeys-jwts.zig
│ └── messaging/
│ ├── Concurrent.md
│ ├── Iterating-Multiple-Subscriptions.md
│ ├── Json.md
│ ├── Pub-Sub.md
│ ├── README.md
│ ├── Request-Reply.md
│ ├── concurrent.zig
│ ├── iterating-multiple-subscriptions.zig
│ ├── json.zig
│ ├── pub-sub.zig
│ └── request-reply.zig
└── src/
├── Client.zig
├── auth/
│ ├── base32.zig
│ ├── crc16.zig
│ ├── creds.zig
│ ├── jwt.zig
│ └── nkey.zig
├── auth.zig
├── connection/
│ ├── errors.zig
│ ├── events.zig
│ ├── io_task.zig
│ ├── reconnect_test.zig
│ ├── server_pool.zig
│ ├── server_pool_test.zig
│ └── state.zig
├── connection.zig
├── dbg.zig
├── defaults.zig
├── events.zig
├── examples/
│ ├── README.md
│ ├── batch_receiving.zig
│ ├── callback.zig
│ ├── events.zig
│ ├── graceful_shutdown.zig
│ ├── headers.zig
│ ├── jetstream_async_publish.zig
│ ├── jetstream_consume.zig
│ ├── jetstream_publish.zig
│ ├── jetstream_push.zig
│ ├── kv.zig
│ ├── kv_watch.zig
│ ├── micro_echo.zig
│ ├── polling_loop.zig
│ ├── queue_groups.zig
│ ├── reconnection.zig
│ ├── request_reply.zig
│ ├── request_reply_callback.zig
│ ├── select.zig
│ └── simple.zig
├── io_backend.zig
├── jetstream/
│ ├── JetStream.zig
│ ├── async_publish.zig
│ ├── consumer.zig
│ ├── errors.zig
│ ├── kv.zig
│ ├── message.zig
│ ├── ordered.zig
│ ├── publish_headers.zig
│ ├── pull.zig
│ ├── push.zig
│ └── types.zig
├── jetstream.zig
├── memory/
│ ├── sidmap.zig
│ ├── sidmap_test.zig
│ └── slab.zig
├── memory.zig
├── micro/
│ ├── Service.zig
│ ├── endpoint.zig
│ ├── json_util.zig
│ ├── protocol.zig
│ ├── request.zig
│ ├── stats.zig
│ ├── timeutil.zig
│ └── validation.zig
├── micro.zig
├── nats.zig
├── protocol/
│ ├── commands.zig
│ ├── encoder.zig
│ ├── encoder_test.zig
│ ├── errors.zig
│ ├── header_map.zig
│ ├── headers.zig
│ ├── parser.zig
│ └── parser_test.zig
├── protocol.zig
├── pubsub/
│ ├── inbox.zig
│ ├── subject.zig
│ ├── subject_test.zig
│ ├── subscription.zig
│ └── subscription_test.zig
├── pubsub.zig
├── sync/
│ ├── byte_ring.zig
│ ├── spin_lock.zig
│ └── spsc_queue.zig
└── testing/
├── README.md
├── certs/
│ ├── client-all.pem
│ ├── client-cert.pem
│ ├── client-key.pem
│ ├── ip-ca.pem
│ ├── ip-cert.pem
│ ├── ip-key.pem
│ ├── rootCA-key.pem
│ ├── rootCA.pem
│ ├── server-cert.pem
│ └── server-key.pem
├── client/
│ ├── advanced.zig
│ ├── async_patterns.zig
│ ├── auth.zig
│ ├── autoflush.zig
│ ├── basic.zig
│ ├── callback.zig
│ ├── concurrency.zig
│ ├── connection.zig
│ ├── drain.zig
│ ├── dynamic_jwt.zig
│ ├── edge_cases.zig
│ ├── error_handling.zig
│ ├── flush_confirmed.zig
│ ├── getters.zig
│ ├── headers.zig
│ ├── jetstream.zig
│ ├── jwt.zig
│ ├── micro.zig
│ ├── multi_client.zig
│ ├── multithread.zig
│ ├── nkey.zig
│ ├── protocol.zig
│ ├── publish.zig
│ ├── queue.zig
│ ├── reconnect.zig
│ ├── request_reply.zig
│ ├── server.zig
│ ├── state_notifications.zig
│ ├── stats.zig
│ ├── stress.zig
│ ├── stress_subs.zig
│ ├── subscribe.zig
│ ├── tests.zig
│ ├── tls.zig
│ └── wildcard.zig
├── configs/
│ ├── TestUser.creds
│ ├── jwt.conf
│ └── tls.conf
├── integration_test.zig
├── micro_integration_test.zig
├── server_manager.zig
├── test_utils.zig
└── tls_integration_test.zig
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
ZIG_VERSION: 0.16.0
NATS_SERVER_VERSION: v2.12.7
NATS_CLI_VERSION: v0.3.1
jobs:
build:
name: Build and unit tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Zig
uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- name: Show Zig version
run: zig version
- name: Check formatting
run: zig build fmt-check
- name: Build examples and package
run: zig build
- name: Run unit tests
run: zig build test
integration:
name: Integration tests (${{ matrix.mode }})
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- mode: Debug
args: ""
- mode: ReleaseFast
args: -Doptimize=ReleaseFast
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Zig
uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
cache-key: integration-${{ matrix.mode }}
- name: Install NATS tools
shell: bash
run: |
set -euo pipefail
nats_server_archive="nats-server-${NATS_SERVER_VERSION}-linux-amd64.tar.gz"
nats_server_url="https://github.com/nats-io/nats-server/releases/download/${NATS_SERVER_VERSION}/${nats_server_archive}"
nats_cli_version="${NATS_CLI_VERSION#v}"
nats_cli_archive="nats-${nats_cli_version}-linux-amd64.zip"
nats_cli_url="https://github.com/nats-io/natscli/releases/download/${NATS_CLI_VERSION}/${nats_cli_archive}"
mkdir -p "$HOME/.local/bin" "$RUNNER_TEMP/nats-server" "$RUNNER_TEMP/nats-cli"
curl --fail --location --show-error --silent "$nats_server_url" --output "$RUNNER_TEMP/$nats_server_archive"
tar -xzf "$RUNNER_TEMP/$nats_server_archive" -C "$RUNNER_TEMP/nats-server" --strip-components=1
install -m 0755 "$RUNNER_TEMP/nats-server/nats-server" "$HOME/.local/bin/nats-server"
curl --fail --location --show-error --silent "$nats_cli_url" --output "$RUNNER_TEMP/$nats_cli_archive"
unzip -q "$RUNNER_TEMP/$nats_cli_archive" -d "$RUNNER_TEMP/nats-cli"
nats_cli_bin="$(find "$RUNNER_TEMP/nats-cli" -type f -name nats -print -quit)"
test -n "$nats_cli_bin"
install -m 0755 "$nats_cli_bin" "$HOME/.local/bin/nats"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Show tool versions
run: |
zig version
nats-server --version
nats --version
- name: Run integration tests
run: zig build test-integration ${{ matrix.args }}
================================================
FILE: .github/workflows/claude.yml
================================================
name: Claude Code
# GITHUB_TOKEN is neutered — all GitHub API access uses the App token instead.
permissions: {}
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_target:
types: [opened, reopened]
jobs:
claude:
uses: synadia-io/ai-workflows/.github/workflows/claude.yml@v2
with:
gh_app_id: ${{ vars.CLAUDE_GH_APP_ID }}
checkout_mode: base
review_focus: |
Additionally focus on:
- Zig best practices: proper error handling, comptime usage, memory management
- Correct use of allocators and avoiding memory leaks
- Adherence to Zig style conventions and idiomatic patterns
- NATS protocol correctness and client implementation details
secrets:
claude_oauth_token: ${{ secrets.CLAUDE_OAUTH_TOKEN }}
gh_app_private_key: ${{ secrets.CLAUDE_GH_APP_PRIVATE_KEY }}
================================================
FILE: .gitignore
================================================
# The build cache
zig-cache
.zig-cache
zig-out/
tmp/
.mcp.json
.claude/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thanks for helping improve `nats.zig`.
## Development Setup
Required tools:
- Zig 0.16.0 or later
- `nats-server` on `PATH` for integration tests
- `nats` CLI on `PATH` for JetStream/KV cross-verification tests
Common commands:
```sh
zig build
zig build test
zig build fmt
zig build fmt-check
zig build test-integration
```
Focused integration targets are also available:
```sh
zig build test-integration-tls
zig build test-integration-micro
```
See `src/testing/README.md` for integration test layout and fixtures.
## Before Opening a Pull Request
- Run `zig build`.
- Run `zig build test`.
- Run the relevant integration target when changing connection,
authentication, TLS, JetStream, KV, or service behavior.
- Keep examples compiling when public APIs change.
- Update `README.md` or `src/examples/README.md` when adding or
changing user-facing examples.
## Style
- Prefer existing module patterns over new abstractions.
- Keep ownership rules explicit in public APIs and examples.
- Avoid unrelated refactors in bug-fix changes.
- Format changed Zig files with `zig build fmt`.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 The nats.zig Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
[](https://github.com/nats-io/nats.zig/actions/workflows/ci.yml)
[](LICENSE)

A Zig client for the NATS messaging system.
# nats.zig
A [Zig](https://ziglang.org/) client for the [NATS messaging system](https://nats.io).
Built on `std.Io`.
> **Pre-1.0** - This library is under active development.
> Core pub/sub, server-authenticated TLS, JetStream (pull + push
> consumers), Key-Value Store, and the Micro Services API are
> supported and covered by integration tests. Object Store and
> mTLS are not yet implemented. The API may change before 1.0.
Check out [NATS by Example](https://natsbyexample.com) for
runnable, cross-client NATS examples. This repository includes
Zig ports in [doc/nats-by-example](doc/nats-by-example/README.md).
## Contents
- [Requirements](#requirements)
- [Documentation](#documentation)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Examples](#examples)
- [Memory Ownership](#memory-ownership)
- [Publishing](#publishing)
- [Subscribing](#subscribing)
- [Request/Reply](#requestreply)
- [Headers](#headers)
- [JetStream](#jetstream)
- [Micro Services](#micro-services)
- [Async Patterns with std.Io](#async-patterns-with-stdio)
- [Connections](#connections)
- [Authentication](#authentication)
- [Error Handling](#error-handling)
- [Server Compatibility](#server-compatibility)
- [Building](#building)
- [Status](#status)
## Documentation
- [Examples](src/examples/README.md) - runnable examples built by `zig build`
- [JetStream guide](doc/JetStream.md) - stream, consumer, publish,
pull-consume, ack, and error-handling details
- [NATS by Example ports](doc/nats-by-example/README.md) - Zig ports of
selected cross-client examples from natsbyexample.com
- [Integration tests](src/testing/README.md) - local test layout,
fixtures, and focused test targets
## Requirements
- Zig 0.16.0 or later
- NATS server (for running examples and tests)
## Installation
```bash
zig fetch --save https://github.com/nats-io/nats.zig/archive/refs/tags/v0.1.0.tar.gz
```
Then in `build.zig`:
```zig
const nats_dep = b.dependency("nats", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats_dep.module("nats") },
},
}),
});
b.installArtifact(exe);
```
## Quick Start
Subscriptions use callbacks - messages are dispatched automatically,
no manual receive loop needed. There are three ways to subscribe:
**`subscribe()` with a MsgHandler** - captures state, like a closure:
```zig
const std = @import("std");
const nats = @import("nats");
// Handler struct captures external state via pointer
const Handler = struct {
counter: *u32,
pub fn onMessage(self: *@This(), msg: *const nats.Message) void {
// Modify captured state from within the callback
self.counter.* += 1;
std.debug.print("[{d}] {s}\n", .{ self.counter.*, msg.data });
}
};
pub fn main(init: std.process.Init) !void {
const client = try nats.Client.connect(
init.gpa,
init.io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
// State lives in main - handler captures a pointer to it
var count: u32 = 0;
var handler = Handler{ .counter = &count };
const sub = try client.subscribe(
"greet.*",
nats.MsgHandler.init(Handler, &handler),
);
defer sub.deinit();
try client.publish("greet.hello", "Hello, NATS!");
init.io.sleep(.fromSeconds(1), .awake) catch {};
// Main sees the mutations made by the callback
std.debug.print("Total: {d}\n", .{count});
}
```
**`subscribeFn()` with a plain function** - when no state is needed:
```zig
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const client = try nats.Client.connect(
init.gpa,
init.io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
const sub = try client.subscribeFn("greet.*", onMessage);
defer sub.deinit();
try client.publish("greet.hello", "Hello, NATS!");
init.io.sleep(.fromSeconds(1), .awake) catch {};
}
fn onMessage(msg: *const nats.Message) void {
std.debug.print("Received: {s}\n", .{msg.data});
}
```
> **Note:** Callback messages are freed automatically after your handler
> returns. No `msg.deinit()` needed.
**`subscribeSync()` for manual receive** - you control the receive loop:
```zig
const sub = try client.subscribeSync("greet.*");
defer sub.deinit();
try client.publish("greet.hello", "Hello, NATS!");
if (try sub.nextMsgTimeout(5000)) |msg| {
defer msg.deinit();
std.debug.print("Received: {s}\n", .{msg.data});
}
```
See [Examples](#examples) below for more patterns including
request/reply, queue groups, headers, and async I/O.
## Examples
Run with `zig build run-` (requires `nats-server` on localhost:4222).
| Example | Run | Description |
|---------|-----|-------------|
| simple | `run-simple` | Basic pub/sub - connect, `subscribeSync`, publish, receive |
| request_reply | `run-request-reply` | RPC pattern with automatic inbox handling |
| headers | `run-headers` | Publish, receive, and parse NATS headers |
| queue_groups | `run-queue-groups` | Load-balanced workers with `io.concurrent()` |
| polling_loop | `run-polling-loop` | Non-blocking `tryNextMsg()` with priority scheduling |
| select | `run-select` | Race subscription against timeout with `Io.Select` |
| batch_receiving | `run-batch-receiving` | `nextMsgBatch()` for bulk receives, stats monitoring |
| reconnection | `run-reconnection` | Auto-reconnect, backoff, buffer during disconnect |
| events | `run-events` | EventHandler callbacks with external state |
| callback | `run-callback` | `subscribe()` and `subscribeFn()` callback subscriptions |
| request_reply_callback | `run-request-reply-callback` | Service responder via callback subscription |
| graceful_shutdown | `run-graceful-shutdown` | `drain()` lifecycle, pre-shutdown health checks |
| jetstream_publish | `run-jetstream-publish` | Create a stream and publish with ack confirmation |
| jetstream_consume | `run-jetstream-consume` | Pull consumer fetch and acknowledgement |
| jetstream_push | `run-jetstream-push` | Push consumer callback delivery |
| jetstream_async_publish | `run-jetstream-async-publish` | Async JetStream publishing |
| kv | `run-kv` | Key-Value bucket operations |
| kv_watch | `run-kv-watch` | Watch Key-Value updates |
| micro_echo | `run-micro-echo` | NATS service API echo service |
Source: `src/examples/`
### NATS by Example
Ports of [natsbyexample.com](https://natsbyexample.com) examples.
| Example | Run |
|---------|-----|
| [Pub-Sub](doc/nats-by-example/messaging/pub-sub.zig) | `run-nbe-messaging-pub-sub` |
| [Request-Reply](doc/nats-by-example/messaging/request-reply.zig) | `run-nbe-messaging-request-reply` |
| [JSON](doc/nats-by-example/messaging/json.zig) | `run-nbe-messaging-json` |
| [Concurrent](doc/nats-by-example/messaging/concurrent.zig) | `run-nbe-messaging-concurrent` |
| [Multiple Subscriptions](doc/nats-by-example/messaging/iterating-multiple-subscriptions.zig) | `run-nbe-messaging-iterating-multiple-subscriptions` |
| [NKeys & JWTs](doc/nats-by-example/auth/nkeys-jwts.zig) | `run-nbe-auth-nkeys-jwts` |
---
## Memory Ownership
Messages returned by `nextMsg()`, `tryNextMsg()`, and `nextMsgTimeout()` are **owned**.
You **must** call `deinit()` to free memory:
```zig
const msg = try sub.nextMsg();
defer msg.deinit();
// Access message fields (valid until deinit)
std.debug.print("Subject: {s}\n", .{msg.subject});
std.debug.print("Data: {s}\n", .{msg.data});
```
### Message Structure
```zig
pub const Message = struct {
subject: []const u8, // Message subject
sid: u64, // Subscription ID
reply_to: ?[]const u8, // Reply-to address (for request/reply)
data: []const u8, // Message payload
headers: ?[]const u8, // Raw NATS headers (use headers.parse())
};
```
---
## Publishing
### Auto-Flush Behavior
Messages are buffered and automatically flushed to the network:
```zig
// Write to buffer - auto-flushed by io_task
try client.publish("events.click", "button1");
try client.publish("events.click", "button2");
try client.publish("events.click", "button3");
```
**How it works:**
- `publish()` encodes into a lock-free ring buffer (no mutex)
- The io_task background thread drains the ring to the socket
- Multiple rapid publishes are naturally batched for efficiency
- Works at full speed even in tight loops (100K+ msgs/sec)
- Ring size: 2MB minimum (auto-sized, power-of-2)
### Confirmed Flush
For scenarios where you need confirmation that the server received your messages,
use `flush()`. It sends PING and waits for PONG (matches Go/C client behavior):
```zig
try client.publish("events.important", data);
try client.flush(5_000_000_000); // 5 second timeout
// Server has confirmed receipt of all buffered messages
```
**When to use:**
- Critical messages where delivery confirmation matters
- Before shutting down to ensure all messages were sent
- Synchronization points in your application
### When Does Data Hit the Network?
| Method | Network I/O |
|--------|-------------|
| `publish()` | Auto-flushed |
| `publishRequest()` | Auto-flushed |
| `publishWithHeaders()` | Auto-flushed |
| `publishRequestWithHeaders()` | Auto-flushed |
| `flushBuffer()` | Yes - sends buffer to socket immediately (used internally) |
| `flush()` | Yes - sends buffer + PING, waits for PONG |
| `request()` | Yes - flushes, waits for response |
| `requestWithHeaders()` | Yes - flushes, waits for response |
---
## Subscribing
### Subscribe (Callback)
Messages are dispatched automatically via callback.
**MsgHandler pattern** (handler struct with state):
```zig
const MyHandler = struct {
counter: *u32,
pub fn onMessage(self: *@This(), msg: *const nats.Message) void {
self.counter.* += 1;
std.debug.print("got: {s}\n", .{msg.data});
}
};
var count: u32 = 0;
var handler = MyHandler{ .counter = &count };
const sub = try client.subscribe(
"events.>",
nats.MsgHandler.init(MyHandler, &handler),
);
defer sub.deinit();
```
**Plain function** (no state needed):
```zig
fn onAlert(msg: *const nats.Message) void {
std.debug.print("alert: {s}\n", .{msg.data});
}
const sub = try client.subscribeFn(
"alerts.>",
onAlert,
);
defer sub.deinit();
```
**Queue group** (load balancing - only one subscriber in the group
receives each message):
```zig
const sub = try client.queueSubscribe(
"tasks.*",
"workers",
handler,
);
```
| Method | Handler | Queue Group |
|--------|---------|-------------|
| `subscribe` | MsgHandler | No |
| `queueSubscribe` | MsgHandler | Yes |
| `subscribeFn` | plain fn | No |
| `queueSubscribeFn` | plain fn | Yes |
> **Warning:** Do not call `nextMsg()`, `tryNextMsg()`, or other receive methods on
> a callback subscription. They assert `mode == .manual` and will trap.
### Subscribe Sync (Manual Receive)
For manual control over message receiving, use `subscribeSync()`. You call
`nextMsg()`, `tryNextMsg()`, or `nextMsgBatch()` yourself:
```zig
const sub = try client.subscribeSync("events.>");
defer sub.deinit();
// Wildcards:
// * matches single token: "events.*" matches "events.click" but not "events.user.login"
// > matches remainder: "events.>" matches "events.click" and "events.user.login"
while (true) {
const msg = try sub.nextMsg();
defer msg.deinit();
std.debug.print("{s}: {s}\n", .{ msg.subject, msg.data });
}
```
**Queue group** variant:
```zig
const sub1 = try client.queueSubscribeSync("tasks.*", "workers");
const sub2 = try client.queueSubscribeSync("tasks.*", "workers");
// Message goes to either sub1 OR sub2, not both
```
### Subscription Registration
When subscribing, the SUB command is buffered and sent to the server asynchronously.
If you need to ensure the subscription is fully registered before publishing (especially
with separate publisher/subscriber clients), call `flush()` after subscribing:
```zig
const sub = try client.subscribeSync("events.>");
defer sub.deinit();
// Ensure subscription is registered on server before publishing
try client.flush(5_000_000_000); // 5 second timeout
// Now safe to publish from another client
```
**When is this needed?**
- Multi-client scenarios where one client publishes and another subscribes
- Tests that need deterministic message delivery
- Any situation requiring subscription to be active before first publish
**When is this NOT needed?**
- Single client publishing to itself (same client does subscribe + publish)
- Using `request()` which handles synchronization internally
### Unsubscribing
**Zig deinit pattern (recommended):** Use `defer sub.deinit()` - it calls `unsubscribe()`
internally and handles errors gracefully:
```zig
const sub = try client.subscribeSync("events.>");
defer sub.deinit(); // Unsubscribes + frees memory
// ... use subscription ...
```
**Explicit unsubscribe:** For users who need to check if the server
received the UNSUB command, call `unsubscribe()` directly:
```zig
const sub = try client.subscribeSync("events.>");
// ... use subscription ...
// Explicit unsubscribe with error checking
sub.unsubscribe() catch |err| {
std.log.warn("Unsubscribe failed: {}", .{err});
};
sub.deinit(); // Still needed to free memory
```
| Method | Returns | Purpose |
|--------|---------|---------|
| `sub.unsubscribe()` | `!void` | Sends UNSUB to server, removes from tracking |
| `sub.deinit()` | `void` | Calls unsubscribe (if needed) + frees memory |
**Note:** `unsubscribe()` is idempotent - calling it multiple times is safe.
`deinit()` always succeeds (errors are logged, not returned) making it safe for
`defer`.
### Receiving Messages
**Blocking:** `nextMsg()` blocks until a message arrives. For use in dedicated receiver loops:
```zig
while (true) {
const msg = try sub.nextMsg();
defer msg.deinit(); // ALWAYS defer deinit
std.debug.print("Subject: {s}\n", .{msg.subject});
std.debug.print("Data: {s}\n", .{msg.data});
if (msg.reply_to) |rt| {
std.debug.print("Reply-to: {s}\n", .{rt});
}
}
```
**Non-Blocking:** `tryNextMsg()` returns immediately. Use for event loops or polling:
```zig
// Process all available messages without waiting
while (sub.tryNextMsg()) |msg| {
defer msg.deinit();
processMessage(msg);
}
// No more messages - continue with other work
```
**With Timeout:** `nextMsgTimeout()` returns `null` on timeout:
```zig
if (try sub.nextMsgTimeout(5000)) |msg| {
defer msg.deinit();
std.debug.print("Got: {s}\n", .{msg.data});
} else {
std.debug.print("No message within 5 seconds\n", .{});
}
```
**Batch:** `nextMsgBatch()` / `tryNextMsgBatch()` receive multiple messages at once:
```zig
var buf: [64]Message = undefined;
// Blocking - waits for at least 1 message, returns up to 64
const count = try sub.nextMsgBatch(io, &buf);
for (buf[0..count]) |*msg| {
defer msg.deinit();
processMessage(msg.*);
}
// Non-blocking - returns immediately with available messages
const available = sub.tryNextMsgBatch(&buf);
for (buf[0..available]) |*msg| {
defer msg.deinit();
processMessage(msg.*);
}
```
### Receive Method Comparison
| Method | Blocks | Returns | Use Case |
|--------|--------|---------|----------|
| `nextMsg()` | Yes | `!Message` | Dedicated receiver loop |
| `tryNextMsg()` | No | `?Message` | Polling, event loops |
| `nextMsgTimeout()` | Yes (bounded) | `!?Message` | Request/reply, timed waits |
| `nextMsgBatch()` | Yes | `!usize` | High-throughput batching |
| `tryNextMsgBatch()` | No | `usize` | Drain queue without blocking |
### Subscription Control
**Auto-Unsubscribe:** Automatically unsubscribe after receiving a specific number of messages:
```zig
const sub = try client.subscribeSync("events.>");
// Auto-unsubscribe after 10 messages
try sub.autoUnsubscribe(10);
// Process messages (subscription closes after 10th)
while (sub.isValid()) {
if (sub.tryNextMsg()) |msg| {
defer msg.deinit();
processMessage(msg);
}
}
```
**Statistics:**
```zig
// Messages waiting in queue
const pending = sub.pending();
// Messages delivered (only tracked if autoUnsubscribe was called)
const delivered = sub.delivered();
// Check if subscription is still valid
if (sub.isValid()) {
// Can still receive messages
}
```
**Per-Subscription Drain:** Drain a single subscription while keeping others active:
```zig
try sub.drain();
// Subscription stops receiving new messages
// Already-queued messages can still be consumed
```
---
## Request/Reply
### Using `request()` (Recommended)
The simplest way - handles inbox creation, subscription, and timeout:
```zig
// Returns null on timeout
if (try client.request("math.double", "21", 5000)) |reply| {
defer reply.deinit();
std.debug.print("Result: {s}\n", .{reply.data}); // "42"
} else {
std.debug.print("Service did not respond\n", .{});
}
```
### Building a Service
Respond to requests by publishing to the `reply_to` subject:
```zig
const service = try client.subscribeSync("math.double");
defer service.deinit();
while (true) {
const req = try service.nextMsg();
defer req.deinit();
// Parse request
const num = std.fmt.parseInt(i32, req.data, 10) catch 0;
// Build response
var buf: [32]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "{d}", .{num * 2}) catch "error";
// Send reply (auto-flushed)
if (req.reply_to) |reply_to| {
try client.publish(reply_to, result);
}
}
```
### Responding with `msg.respond()`
Convenience method for the request/reply pattern:
```zig
const msg = try sub.nextMsg();
defer msg.deinit();
// Respond using the message's reply_to (auto-flushed)
msg.respond(client, "response data") catch |err| {
if (err == error.NoReplyTo) {
// Message had no reply_to address
}
};
```
### Manual Request/Reply Pattern
For more control, manage the inbox yourself:
```zig
// Create inbox subscription
const inbox = try client.newInbox();
defer allocator.free(inbox);
const reply_sub = try client.subscribeSync(inbox);
defer reply_sub.deinit();
// Send request with reply-to (auto-flushed)
try client.publishRequest("service", inbox, "request data");
// Wait for response with timeout
if (try reply_sub.nextMsgTimeout(5000)) |reply| {
defer reply.deinit();
// Process reply
} else {
// Timeout
}
```
### Check No-Responders Status
Detect when a request has no available responders (status 503):
```zig
const reply = try client.request("service.endpoint", "data", 1000);
if (reply) |msg| {
defer msg.deinit();
if (msg.isNoResponders()) {
// No service available to handle request
std.debug.print("No responders for request\n", .{});
} else {
// Normal response - check status code if needed
if (msg.status()) |status| {
std.debug.print("Status: {d}\n", .{status});
}
}
}
```
### Implementation Note: Response Multiplexer
`request()`, `requestMsg()`, and `requestWithHeaders()` use a shared
*response multiplexer* internally - the same pattern as the Go
client's `respMux`. The first call lazily subscribes once to a
wildcard inbox `_INBOX..*` and does a PING/PONG round-trip
to confirm server registration. Every subsequent call reuses that
single subscription and just registers a per-request waiter in a
token-keyed map. The dispatcher routes incoming replies back to the
matching waiter.
Benefits over the naive per-request subscription approach:
- **No SUB/UNSUB protocol churn** - the server (and any clustered
gateways/leaf nodes) sees one wildcard subscription per connection
instead of one SUB+UNSUB pair per request.
- **No per-request allocations** for the subscription struct, queue
buffer, or owned subject string.
- **No latency floor** - the old implementation burned a hardcoded
5ms sleep on every request to give the server time to process the
per-request SUB. The muxer pays one PING/PONG round-trip *once* on
the first request and amortizes it to zero across subsequent calls.
- **Better concurrent throughput** - relevant for JetStream and KV
workloads, which are RPC-heavy internally.
---
## Headers
NATS headers allow attaching metadata to messages (similar to HTTP headers).
Headers are supported with NATS server 2.2+.
### Publishing with Headers
```zig
const nats = @import("nats");
const headers = nats.protocol.headers;
// Single header
const hdrs = [_]headers.Entry{
.{ .key = "X-Request-Id", .value = "req-123" },
};
try client.publishWithHeaders("events.user", &hdrs, "user logged in");
// Multiple headers
const multi_hdrs = [_]headers.Entry{
.{ .key = "Content-Type", .value = "application/json" },
.{ .key = "X-Correlation-Id", .value = "corr-456" },
.{ .key = "X-Timestamp", .value = "2026-01-21T10:30:00Z" },
};
try client.publishWithHeaders("events.order", &multi_hdrs, order_json);
```
### Publish with Headers and Reply-To
```zig
const hdrs = [_]headers.Entry{
.{ .key = "X-Request-Id", .value = "req-789" },
};
try client.publishRequestWithHeaders("service.echo", "my.inbox", &hdrs, "ping");
```
### Request/Reply with Headers
```zig
const hdrs = [_]headers.Entry{
.{ .key = headers.HeaderName.msg_id, .value = "unique-001" },
};
if (try client.requestWithHeaders("service.api", &hdrs, "data", 5000)) |reply| {
defer reply.deinit();
std.debug.print("Response: {s}\n", .{reply.data});
} else {
std.debug.print("Timeout\n", .{});
}
```
### Receiving and Parsing Headers
```zig
const msg = try sub.nextMsg();
defer msg.deinit();
if (msg.headers) |raw_headers| {
var parsed = headers.parse(allocator, raw_headers);
defer parsed.deinit(); // MUST call deinit!
if (parsed.err == null) {
// Iterate all headers
for (parsed.items()) |entry| {
std.debug.print("{s}: {s}\n", .{ entry.key, entry.value });
}
// Lookup by name (case-insensitive)
if (parsed.get("X-Request-Id")) |req_id| {
std.debug.print("Request ID: {s}\n", .{req_id});
}
// Check for no-responders status
if (parsed.isNoResponders()) {
std.debug.print("No responders available\n", .{});
}
}
}
```
**Important**: `ParseResult` owns its data (copies strings to heap). This means
parsed headers remain valid even after `msg.deinit()` is called. Always call
`parsed.deinit()` to free memory.
### Well-Known Header Names
Use constants from `headers.HeaderName` for JetStream and NATS features:
```zig
const hdrs = [_]headers.Entry{
// JetStream message deduplication
.{ .key = headers.HeaderName.msg_id, .value = "unique-msg-001" },
// Expected stream for publish
.{ .key = headers.HeaderName.expected_stream, .value = "ORDERS" },
};
```
| Constant | Header Name | Purpose |
|----------|-------------|---------|
| `msg_id` | `Nats-Msg-Id` | JetStream deduplication |
| `expected_stream` | `Nats-Expected-Stream` | Verify target stream |
| `expected_last_msg_id` | `Nats-Expected-Last-Msg-Id` | Optimistic concurrency |
| `expected_last_seq` | `Nats-Expected-Last-Sequence` | Sequence verification |
### HeaderMap Builder
For programmatic header construction:
```zig
const nats = @import("nats");
var headers = nats.Client.HeaderMap.init(allocator);
defer headers.deinit();
// Set headers (replaces existing)
try headers.set("Content-Type", "application/json");
try headers.set("X-Request-Id", "req-123");
// Add headers (allows multiple values for same key)
try headers.add("X-Tag", "important");
try headers.add("X-Tag", "urgent");
// Get values
if (headers.get("Content-Type")) |ct| {
std.debug.print("Content-Type: {s}\n", .{ct});
}
// Get all values for a key
if (try headers.getAll("X-Tag")) |tags| {
defer allocator.free(tags);
for (tags) |tag| {
std.debug.print("Tag: {s}\n", .{tag});
}
}
// Delete headers
headers.delete("X-Tag");
// Publish with HeaderMap (auto-flushed)
try client.publishWithHeaderMap("subject", &headers, "payload");
```
### Header Notes
- Header values can contain colons (URLs, timestamps work fine)
- Case-insensitive lookup for header names
- Header names must be non-empty and cannot contain whitespace, control
characters, DEL, or `:`. Header values cannot contain control characters
or DEL. Invalid headers return `error.InvalidHeader`.
- On parse error: `items()` returns empty slice, `get()` returns null
---
## JetStream
JetStream is NATS' persistence and streaming layer. It provides
at-least-once delivery, message replay, and durable consumers --
all through a JSON request/reply API on `$JS.API.*` subjects.
For runnable examples, see `src/examples/jetstream_*.zig`,
`src/examples/kv*.zig`, the focused [JetStream guide](doc/JetStream.md),
and the feature coverage summary below.
### JetStream Example
```zig
const nats = @import("nats");
const js_mod = nats.jetstream;
// Create a JetStream context (stack-allocated, no heap)
var js = try js_mod.JetStream.init(client, .{});
// Create a stream
var stream = try js.createStream(.{
.name = "ORDERS",
.subjects = &.{"orders.>"},
.storage = .memory,
});
defer stream.deinit();
// Publish with ack confirmation
var ack = try js.publish("orders.new", "order-1");
defer ack.deinit();
// ack.value.seq, ack.value.stream
// Create a pull consumer and fetch messages
var cons = try js.createConsumer("ORDERS", .{
.name = "processor",
.durable_name = "processor",
.ack_policy = .explicit,
});
defer cons.deinit();
var pull = js_mod.PullSubscription{
.js = &js,
.stream = "ORDERS",
};
try pull.setConsumer("processor");
var result = try pull.fetch(.{
.max_messages = 10,
.timeout_ms = 5000,
});
defer result.deinit();
for (result.messages) |*msg| {
try msg.ack();
}
```
### Key-Value Store Example
```zig
const js_mod = nats.jetstream;
var js = try js_mod.JetStream.init(client, .{});
// Create a KV bucket
var kv = try js.createKeyValue(.{
.bucket = "config",
.storage = .memory,
.history = 5,
});
// Put and get
const rev = try kv.put("db.host", "localhost:5432");
var entry = (try kv.get("db.host")).?;
defer entry.deinit();
// entry.revision == rev, entry.operation == .put
// Optimistic concurrency
const rev2 = try kv.update("db.host", "newhost:5432", rev);
// Create only if key doesn't exist
_ = try kv.create("db.port", "5432");
_ = kv.create("db.port", "9999") catch |err| {
// err == error.ApiError (key exists)
};
// List all keys
const keys = try kv.keys(allocator);
defer {
for (keys) |k| allocator.free(k);
allocator.free(keys);
}
// Watch for real-time updates
var watcher = try kv.watchAll();
defer watcher.deinit();
while (try watcher.next(5000)) |*update| {
defer update.deinit();
// update.key, update.revision, update.operation
}
```
Bucket names and keys are validated client-side before API requests are sent.
Bucket names may not be empty, exceed 64 bytes, or contain wildcards,
separators, whitespace, control characters, or DEL. KV keys must be non-empty
NATS subject tokens without wildcards; watch patterns may use `*` and a
terminal `>`.
### Supported JetStream Features
| Area | Supported APIs | Notes |
|------|----------------|-------|
| Streams | `createStream()`, `updateStream()`, `deleteStream()`, `streamInfo()`, `purgeStream()`, `purgeStreamSubject()` | Includes stream listing and subject-filtered purge. |
| Consumers | `createConsumer()`, `updateConsumer()`, `deleteConsumer()`, `consumerInfo()` | Pull, push, and ordered consumer workflows. |
| Listing | `streamNames()`, `streams()`, `consumerNames()`, `consumers()`, `accountInfo()` | Paginated listing APIs are available for streams and consumers. |
| Publishing | `publish()`, `publishWithOpts()`, `publishMsg()` | Publish acknowledgments, deduplication headers, optimistic concurrency, and publish TTL. |
| Pull Consumers | `fetch()`, `fetchNoWait()`, `fetchBytes()`, `next()`, `messages()`, `consume()` | Batch fetch, single-message fetch, continuous pull iteration, callbacks, heartbeat monitoring, and ordered delivery. |
| Push Consumers | `createPushConsumer()`, `PushSubscription.consume()` | Callback delivery uses `JsMsgHandler`; callback messages are borrowed and valid only during the callback. |
| Acknowledgment | `ack()`, `doubleAck()`, `nak()`, `nakWithDelay()`, `inProgress()`, `term()`, `termWithReason()` | Metadata can be parsed from JetStream reply subjects. |
| Key-Value Store | `createKeyValue()`, `keyValue()`, `deleteKeyValue()`, `put()`, `get()`, `create()`, `update()`, `delete()`, `purge()`, `keys()`, `history()`, `watch()`, `watchAll()` | Bucket management, optimistic concurrency by revision, history, filtered key listing, and live watches. |
| Error Handling | `lastApiError()` | JetStream API errors expose server status, error code, and description. |
| Domains | `try JetStream.init(client, .{ .domain = ... })` | Supports multi-tenant JetStream domains. |
### Current Limitations
| Feature | Status |
|---------|--------|
| Object Store | Not implemented |
---
## Micro Services
The `nats.micro` module implements the NATS service API for
discoverable request/reply services. Services automatically register
monitoring endpoints under `$SRV.PING`, `$SRV.INFO`, and `$SRV.STATS`
including name- and id-specific variants.
```zig
const std = @import("std");
const nats = @import("nats");
const Echo = struct {
pub fn onRequest(_: *@This(), req: *nats.micro.Request) void {
req.respond(req.data()) catch {};
}
};
pub fn main(init: std.process.Init) !void {
const client = try nats.Client.connect(
init.gpa,
init.io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
var echo = Echo{};
const service = try nats.micro.addService(client, .{
.name = "echo",
.version = "1.0.0",
.description = "Echo service",
.endpoint = .{
.subject = "echo",
.handler = nats.micro.Handler.init(Echo, &echo),
},
});
defer service.deinit();
while (true) {
init.io.sleep(.fromSeconds(1), .awake) catch {};
}
}
```
Handlers can be comptime vtable handlers with `Handler.init(T, &value)`
or plain functions with `Handler.fromFn(fn)`. A request handler can
read `req.subject()`, `req.data()`, `req.headers()`, and reply with
`req.respond()`, `req.respondJson()`, or `req.respondError()`.
Services support endpoint groups, queue groups, metadata, stats reset,
and graceful stop/drain:
```zig
var api = try service.addGroup("api");
_ = try api.addEndpoint(.{
.subject = "v1.echo",
.handler = nats.micro.Handler.init(Echo, &echo),
});
try service.stop(null);
try service.waitStopped();
```
Run the complete example with:
```bash
zig build run-micro-echo
```
---
## Async Patterns with std.Io
### Cancellation Pattern
Always defer cancel when using `io.async()`:
```zig
var future = io.async(someFn, .{args});
defer future.cancel(io) catch {}; // defer cancel
const result = try future.await(io);
```
### Racing Operations with `Io.Select`
Wait for the first of multiple operations to complete:
```zig
fn sleepMs(io_ctx: std.Io, ms: i64) void {
io_ctx.sleep(.fromMilliseconds(ms), .awake) catch {};
}
const Sel = std.Io.Select(union(enum) {
message: anyerror!nats.Message,
timeout: void,
});
var buf: [2]Sel.Union = undefined;
var sel = Sel.init(io, &buf);
sel.async(.message, nats.Client.Sub.nextMsg, .{sub});
sel.async(.timeout, sleepMs, .{ io, 5000 });
const result = sel.await() catch |err| {
while (sel.cancel()) |remaining| {
switch (remaining) {
.message => |r| {
if (r) |m| m.deinit() else |_| {}
},
.timeout => {},
}
}
return err;
};
while (sel.cancel()) |remaining| {
switch (remaining) {
.message => |r| {
if (r) |m| m.deinit() else |_| {}
},
.timeout => {},
}
}
switch (result) {
.message => |msg_result| {
const msg = try msg_result;
defer msg.deinit();
std.debug.print("Received: {s}\n", .{msg.data});
},
.timeout => {
std.debug.print("Timeout!\n", .{});
},
}
```
### Async Message Receive with Ownership
When using `io.async()` to receive messages, handle ownership carefully:
```zig
var future = io.async(nats.Client.Sub.nextMsg, .{sub});
defer if (future.cancel(io)) |m| m.deinit() else |_| {};
if (future.await(io)) |msg| {
// Message ownership transferred - use it here
// Do not add defer msg.deinit() - outer defer handles cleanup
std.debug.print("Got: {s}\n", .{msg.data});
return; // outer defer runs, cancel() returns null
} else |err| {
std.debug.print("Error: {}\n", .{err});
}
```
**Key points:**
- After `await()` succeeds, `cancel()` returns null (message already consumed)
- If function exits before `await()`, `cancel()` returns the pending message
- Adding a second `defer msg.deinit()` inside the if-block would cause double-free
### Io.Queue for Cross-Thread Communication
Use `Io.Queue(T)` for producer/consumer patterns across threads:
```zig
const WorkResult = struct {
worker_id: u8,
msg: nats.Message,
fn deinit(self: WorkResult) void {
self.msg.deinit();
}
};
// Fixed-size buffer backing the queue
var queue_buf: [32]WorkResult = undefined;
var queue: Io.Queue(WorkResult) = .init(&queue_buf);
// Worker thread: push results
fn worker(io: Io, sub: *Sub, q: *Io.Queue(WorkResult)) void {
while (true) {
const msg = sub.nextMsg() catch return;
q.putOne(io, .{ .worker_id = 1, .msg = msg }) catch return;
}
}
// Main thread: consume results
while (true) {
const result = queue.getOne(io) catch break;
defer result.deinit();
std.debug.print("Worker {d}: {s}\n", .{ result.worker_id, result.msg.data });
}
```
**Use cases:**
- Load-balanced workers reporting to main thread
- Aggregating results from `io.concurrent()` tasks
- Decoupling message producers from consumers
---
## Connections
### Connection Options
```zig
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
// Identity
.name = "my-app", // Client name (visible in server logs)
// Buffers
.reader_buffer_size = 1024 * 1024 + 8 * 1024, // Read buffer default
.writer_buffer_size = 1024 * 1024 + 8 * 1024, // Write buffer default
.sub_queue_size = 8192, // Per-subscription queue size
.tcp_rcvbuf = 1024 * 1024, // TCP receive buffer hint default
// Timeouts
.connect_timeout_ns = 5_000_000_000, // 5 second connect timeout
// Reconnection
.reconnect = true, // Enable auto-reconnect
.max_reconnect_attempts = 60, // Max attempts (0 = infinite)
.reconnect_wait_ms = 2000, // Initial backoff
// Keepalive
.ping_interval_ms = 120_000, // PING every 2 minutes
.max_pings_outstanding = 2, // Disconnect after 2 missed PONGs
// Inbox prefix (for request/reply)
.inbox_prefix = "_INBOX", // Custom inbox prefix
// Connection behavior
.retry_on_failed_connect = false, // Retry on initial failure
.no_randomize = false, // Don't randomize server order
.ignore_discovered_servers = false, // Only use explicit servers
.drain_timeout_ms = 30_000, // Default drain timeout
.flush_timeout_ms = 10_000, // Default flush timeout
});
```
### Event Callbacks
Handle connection lifecycle events using the `EventHandler` pattern - a type-safe,
Zig-idiomatic approach similar to `std.mem.Allocator`.
```zig
const MyHandler = struct {
pub fn onConnect(self: *@This()) void {
_ = self;
std.log.info("Connected!", .{});
}
pub fn onDisconnect(self: *@This(), err: ?anyerror) void {
_ = self;
std.log.warn("Disconnected: {any}", .{err});
}
pub fn onReconnect(self: *@This()) void {
_ = self;
std.log.info("Reconnected!", .{});
}
};
var handler = MyHandler{};
const client = try nats.Client.connect(allocator, io, url, .{
.event_handler = nats.EventHandler.init(MyHandler, &handler),
});
```
**Accessing External State:** Handlers can reference external application state:
```zig
const AppState = struct {
is_online: bool = false,
reconnect_count: u32 = 0,
last_error: ?anyerror = null,
};
const MyHandler = struct {
app: *AppState,
pub fn onConnect(self: *@This()) void {
self.app.is_online = true;
}
pub fn onDisconnect(self: *@This(), err: ?anyerror) void {
self.app.is_online = false;
self.app.last_error = err;
}
pub fn onReconnect(self: *@This()) void {
self.app.is_online = true;
self.app.reconnect_count += 1;
}
};
var app_state = AppState{};
var handler = MyHandler{ .app = &app_state };
const client = try nats.Client.connect(allocator, io, url, .{
.event_handler = nats.EventHandler.init(MyHandler, &handler),
});
```
| Callback | When Fired |
|----------|------------|
| `onConnect()` | Initial connection established |
| `onDisconnect(?anyerror)` | Connection lost (error or clean close) |
| `onReconnect()` | Reconnection successful |
| `onClose()` | Connection permanently closed |
| `onError(anyerror)` | Async error (slow consumer, etc.) |
| `onLameDuck()` | Server entering shutdown mode |
| `onDiscoveredServers(u8)` | New server discovered in cluster |
| `onDraining()` | Drain process started |
| `onSubscriptionComplete(u64)` | Subscription drain finished (receives SID) |
All callbacks are **optional** - only implement the ones you need.
### Connection State
```zig
const State = @import("nats").connection.State;
const status = client.status();
switch (status) {
.connected => std.debug.print("Connected\n", .{}),
.reconnecting => std.debug.print("Reconnecting...\n", .{}),
.draining => std.debug.print("Draining\n", .{}),
.closed => std.debug.print("Closed\n", .{}),
else => {},
}
// Convenience checks
if (client.isClosed()) { /* permanently closed */ }
if (client.isDraining()) { /* draining subscriptions */ }
if (client.isReconnecting()) { /* attempting reconnect */ }
// Subscription count
const num_subs = client.numSubscriptions();
```
### Connection Information
```zig
// Server details (from INFO response)
if (client.connectedUrl()) |url| {
std.debug.print("Connected to: {s}\n", .{url});
}
if (client.connectedServerId()) |id| {
std.debug.print("Server ID: {s}\n", .{id});
}
if (client.connectedServerName()) |name| {
std.debug.print("Server name: {s}\n", .{name});
}
if (client.connectedServerVersion()) |version| {
std.debug.print("Server version: {s}\n", .{version});
}
// Payload and feature info
const max_payload = client.maxPayload();
const supports_headers = client.headersSupported();
// Server pool (for cluster connections)
const server_count = client.serverCount();
for (0..server_count) |i| {
if (client.serverUrl(@intCast(i))) |url| {
std.debug.print("Known server: {s}\n", .{url});
}
}
// RTT measurement
const rtt_ns = try client.rtt();
const rtt_ms = @as(f64, @floatFromInt(rtt_ns)) / 1_000_000.0;
std.debug.print("RTT: {d:.2}ms\n", .{rtt_ms});
```
### Connection Statistics
Monitor throughput and connection health:
```zig
const stats = client.stats();
std.debug.print("Messages: in={d} out={d}\n", .{stats.msgs_in, stats.msgs_out});
std.debug.print("Bytes: in={d} out={d}\n", .{stats.bytes_in, stats.bytes_out});
std.debug.print("Reconnects: {d}\n", .{stats.reconnects});
```
| Field | Type | Description |
|-------|------|-------------|
| `msgs_in` | `u64` | Total messages received |
| `msgs_out` | `u64` | Total messages sent |
| `bytes_in` | `u64` | Total bytes received |
| `bytes_out` | `u64` | Total bytes sent |
| `reconnects` | `u32` | Number of reconnections |
| `connects` | `u32` | Total successful connections |
### Connection Control
**Flush with Server Confirmation:**
```zig
// Sends PING and waits for PONG (confirms server received messages)
client.flush(5_000_000_000) catch |err| {
if (err == error.Timeout) {
std.debug.print("Flush timed out\n", .{});
}
};
```
**Force Reconnect:**
```zig
try client.forceReconnect();
// Connection closes, io_task starts reconnection process
```
**Drain with Timeout:**
```zig
const result = client.drainTimeout(30_000_000_000) catch |err| {
if (err == error.Timeout) {
std.debug.print("Drain timed out\n", .{});
}
return err;
};
if (!result.isClean()) {
std.debug.print("Drain had failures\n", .{});
}
```
### Handling Slow Consumers
When messages arrive faster than you process them, the queue fills up and messages are dropped:
```zig
while (true) {
const msg = try sub.nextMsg();
defer msg.deinit();
// Check for dropped messages periodically
const dropped = sub.dropped();
if (dropped > 0) {
std.log.warn("Dropped {d} messages - consumer too slow", .{dropped});
}
processMessage(msg);
}
```
**Tuning for High Throughput:**
```zig
const client = try nats.Client.connect(allocator, io, url, .{
.sub_queue_size = 16384, // Larger per-subscription queue
.tcp_rcvbuf = 512 * 1024, // 512KB TCP buffer
.reader_buffer_size = 2 * 1024 * 1024, // 2MB read buffer
.writer_buffer_size = 2 * 1024 * 1024, // 2MB write buffer
});
```
---
## Authentication
### Username/Password
```zig
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.user = "user",
.pass = "pass",
});
```
### Token Authentication
```zig
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.auth_token = "my-secret-token",
});
```
### NKey Authentication
NKey authentication uses Ed25519 signatures for secure, password-less
authentication. NKeys are the recommended authentication method for production
NATS deployments.
**Using NKey Seed (Direct):**
```zig
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.nkey_seed = "SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY",
});
```
**Using NKey Seed File:**
```zig
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.nkey_seed_file = "/path/to/user.nk",
});
```
**Using Signing Callback (HSM/Hardware Keys):**
```zig
fn mySignCallback(nonce: []const u8, sig: *[64]u8) bool {
// Sign nonce using HSM, hardware token, etc.
return hsm.sign(nonce, sig);
}
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.nkey_pubkey = "UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4",
.nkey_sign_fn = &mySignCallback,
});
```
### JWT/Credentials Authentication
For NATS deployments using the account/user JWT model.
**Using Credentials File:**
```zig
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.creds_file = "/path/to/user.creds",
});
```
**Using Credentials Content:**
```zig
// From environment variable
const creds = std.posix.getenv("NATS_CREDS") orelse return error.MissingCreds;
const client = try nats.Client.connect(allocator, io, url, .{
.creds = creds,
});
// Or embed at compile time
const client = try nats.Client.connect(allocator, io, url, .{
.creds = @embedFile("user.creds"),
});
```
### NKey Generation & JWT Encoding
Generate NKey keypairs, encode JWTs, and format credentials files
programmatically. No allocator needed - all operations use
caller-provided stack buffers.
**Generate Keypairs:**
```zig
const nats = @import("nats");
// Generate operator, account, and user keypairs
var op_kp = nats.auth.KeyPair.generate(io, .operator);
defer op_kp.wipe();
var acct_kp = nats.auth.KeyPair.generate(io, .account);
defer acct_kp.wipe();
var user_kp = nats.auth.KeyPair.generate(io, .user);
defer user_kp.wipe();
// Get public key (base32-encoded, 56 chars)
var pk_buf: [56]u8 = undefined;
const pub_key = op_kp.publicKey(&pk_buf); // "O..."
// Encode seed (base32-encoded, 58 chars)
var seed_buf: [58]u8 = undefined;
const seed = op_kp.encodeSeed(&seed_buf); // "SO..."
```
**Encode JWTs:**
```zig
// Account JWT (signed by operator)
var acct_jwt_buf: [2048]u8 = undefined;
const acct_jwt = try nats.auth.jwt.encodeAccountClaims(
&acct_jwt_buf,
acct_pub, // account public key (subject)
"my-account", // account name
op_kp, // operator keypair (signer)
iat, // issued-at (unix seconds)
.{}, // AccountOptions (defaults: unlimited)
);
// User JWT with permissions (signed by account)
var user_jwt_buf: [2048]u8 = undefined;
const user_jwt = try nats.auth.jwt.encodeUserClaims(
&user_jwt_buf,
user_pub, // user public key (subject)
"my-user", // user name
acct_kp, // account keypair (signer)
iat, // issued-at (unix seconds)
.{
.pub_allow = &.{"app.>"},
.sub_allow = &.{ "app.>", "_INBOX.>" },
},
);
```
**Format Credentials File:**
```zig
var creds_buf: [4096]u8 = undefined;
const creds = nats.auth.creds.format(
&creds_buf,
user_jwt, // JWT string
user_seed, // NKey seed string
);
// creds contains the full .creds file content
```
**Account Options (limits):**
| Field | Default | Description |
|-------|---------|-------------|
| `subs` | `-1` | Max subscriptions (-1 = unlimited) |
| `conn` | `-1` | Max connections |
| `data` | `-1` | Max data bytes |
| `payload` | `-1` | Max message payload |
| `imports` | `-1` | Max imports |
| `exports` | `-1` | Max exports |
| `leaf` | `-1` | Max leaf node connections |
| `mem_storage` | `-1` | Max memory storage |
| `disk_storage` | `-1` | Max disk storage |
| `wildcards` | `true` | Allow wildcard subscriptions |
**User Options (permissions):**
| Field | Default | Description |
|-------|---------|-------------|
| `pub_allow` | `&.{}` | Subjects allowed to publish |
| `sub_allow` | `&.{}` | Subjects allowed to subscribe |
| `subs` | `-1` | Max subscriptions (-1 = unlimited) |
| `data` | `-1` | Max data bytes |
| `payload` | `-1` | Max message payload |
See the [NKeys & JWTs example](doc/nats-by-example/auth/nkeys-jwts.zig)
for a complete working example.
### TLS
**Enabling TLS:**
```zig
// 1. URL scheme (recommended)
const client = try nats.Client.connect(allocator, io, "tls://localhost:4443", .{});
// 2. Explicit option
const client = try nats.Client.connect(allocator, io, "nats://localhost:4443", .{
.tls_required = true,
});
// 3. Automatic - if server requires TLS, client upgrades automatically
```
**TLS Options:**
```zig
const client = try nats.Client.connect(allocator, io, "tls://localhost:4443", .{
// Server certificate verification (production)
.tls_ca_file = "/path/to/ca.pem",
// Skip verification (development only!)
.tls_insecure_skip_verify = true,
// TLS-first handshake (for TLS-terminating proxies)
.tls_handshake_first = true,
});
```
**Mutual TLS (mTLS):** client certificates are planned but not
implemented yet. Setting `tls_cert_file` or `tls_key_file` currently
returns `error.MtlsNotImplemented`.
**Checking TLS Status:**
```zig
if (client.isTls()) {
std.debug.print("Connection is encrypted\n", .{});
}
```
| Option | Type | Description |
|--------|------|-------------|
| `tls_required` | `bool` | Force TLS connection |
| `tls_ca_file` | `?[]const u8` | CA certificate file path (PEM) |
| `tls_cert_file` | `?[]const u8` | Reserved for mTLS; currently returns `error.MtlsNotImplemented` |
| `tls_key_file` | `?[]const u8` | Reserved for mTLS; currently returns `error.MtlsNotImplemented` |
| `tls_insecure_skip_verify` | `bool` | Skip server certificate verification |
| `tls_handshake_first` | `bool` | TLS handshake before NATS protocol |
### Authentication Priority
When multiple auth options are set:
1. `creds_file` / `creds` - JWT + NKey from credentials
2. `nkey_seed` / `nkey_seed_file` - NKey only
3. `nkey_sign_fn` + `nkey_pubkey` - Custom signing
4. `user` / `pass` or `auth_token` - Basic auth
### Security Notes
- The library wipes seed data from memory after use (best effort)
---
## Error Handling
```zig
client.publish(subject, data) catch |err| switch (err) {
error.NotConnected => {
// Connection lost - wait for reconnect or handle
},
error.PayloadTooLarge => {
// Message exceeds server max_payload (usually 1MB)
},
error.EncodingFailed => {
// Protocol encoding error
},
else => return err,
};
```
### Common Errors
| Error | Meaning |
|-------|---------|
| `NotConnected` | Not connected to server |
| `ConnectionClosed` | Connection closed unexpectedly |
| `ConnectionTimeout` | Connection attempt timed out |
| `ConnectionRefused` | Server refused connection |
| `AuthenticationFailed` | Authentication failed |
| `PayloadTooLarge` | Message exceeds max_payload |
| `TooManySubscriptions` | Subscription limit reached (16,384) |
| `Closed` | Connection was closed |
| `Canceled` | Operation was cancelled |
| `Timeout` | Operation timed out |
---
## Server Compatibility
Verify the server meets minimum version requirements:
```zig
// Check for NATS 2.10.0 or later (required for some features)
if (client.checkCompatibility(2, 10, 0)) {
// Server supports NATS 2.10+ features
} else {
std.debug.print("Server version too old\n", .{});
}
// Get the actual version string
if (client.connectedServerVersion()) |version| {
std.debug.print("Connected to NATS {s}\n", .{version});
}
```
---
## Building
```bash
# Build library
zig build
# Run unit tests
zig build test
# Run integration tests (requires nats-server and nats CLI)
zig build test-integration
# Format code
zig build fmt
```
See [src/testing/README.md](src/testing/README.md) for integration test
layout, fixtures, and focused test targets.
---
## Status
| Component | Status |
|-----------|--------|
| Core Protocol | Supported |
| Pub/Sub | Supported |
| Request/Reply | Supported |
| Headers | Supported |
| Reconnection | Supported |
| Event Callbacks | Supported |
| NKey Authentication | Supported |
| JWT/Credentials | Supported |
| Server-authenticated TLS | Supported |
| mTLS client certificates | Planned |
| JetStream Core | Supported |
| JetStream Pull Consumers | Supported |
| JetStream Push Consumers | Supported |
| JetStream Ordered Consumer | Supported |
| Key-Value Store | Supported |
| Micro Services API | Supported |
| Object Store | Planned |
| Async Publish | Supported |
## Related Projects
Other Zig-based NATS implementations from the community:
- [NATS C client library, packaged for Zig](https://github.com/allyourcodebase/nats.c)
- [Zig language bindings to the NATS.c library](https://github.com/epicyclic-dev/nats-client)
- [Zig client for NATS Core and JetStream](https://github.com/g41797/nats)
- [A Zig client library for NATS, the cloud-native messaging system](https://github.com/lalinsky/nats.zig)
- [Minimal synchronous NATS Zig client](https://github.com/ianic/nats.zig)
- [Work-in-progress NATS library for Zig](https://github.com/rutgerbrf/zig-nats)
## License
Apache 2.0
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup,
test commands, and contribution guidelines.
================================================
FILE: SECURITY.md
================================================
# Security Policy
Please report suspected security vulnerabilities privately. Do not open a
public issue for a vulnerability report.
If GitHub private vulnerability reporting is enabled for this repository, use
that flow. Otherwise, contact the maintainers through the NATS project security
process before disclosing details publicly.
Public NATS security advisories are published at:
https://advisories.nats.io/
When reporting a vulnerability, include:
- affected version or commit;
- a minimal reproduction when possible;
- expected and observed behavior;
- impact assessment;
- any known workaround.
================================================
FILE: build.zig
================================================
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Debug option for reconnection events (default: false)
const enable_debug = b.option(
bool,
"EnableDebug",
"Enable debug prints for reconnection events (default: false)",
) orelse false;
// Io backend selector.
// 'threaded' = std.Io.Threaded (default, OS threads).
// 'evented' = std.Io.Evented (Linux: Uring, BSD: Kqueue, Apple: Dispatch).
const io_backend_choice = b.option(
[]const u8,
"io_backend",
"Io backend: 'threaded' (default) or 'evented'",
) orelse "threaded";
// Create build options module. Share a single Module instance
// across all consumers (nats, io_backend, ...) — calling
// createModule() twice would generate two distinct Modules
// pointing at the same options.zig file, which Zig rejects
// when both end up in the same compile graph.
const build_options = b.addOptions();
build_options.addOption(bool, "enable_debug", enable_debug);
build_options.addOption([]const u8, "io_backend", io_backend_choice);
const build_options_mod = build_options.createModule();
const nats = b.addModule("nats", .{
.root_source_file = b.path("src/nats.zig"),
.target = target,
.imports = &.{
.{ .name = "build_options", .module = build_options_mod },
},
});
const mod_tests = b.addTest(.{ .root_module = nats });
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
// Backend selector module. Used by entry points (examples,
// integration tests) so they can flip between
// std.Io.Threaded and std.Io.Evented via -Dio_backend=...
// The library module itself does NOT depend on this; only
// application code chooses a backend.
const io_backend_mod = b.createModule(.{
.root_source_file = b.path("src/io_backend.zig"),
.target = target,
.imports = &.{
.{
.name = "build_options",
.module = build_options_mod,
},
},
});
// Standalone test for the io_backend selector module. Ensures
// src/io_backend.zig compiles under -Dio_backend=threaded and
// -Dio_backend=evented.
const io_backend_tests = b.addTest(.{ .root_module = io_backend_mod });
const run_io_backend_tests = b.addRunArtifact(io_backend_tests);
test_step.dependOn(&run_io_backend_tests.step);
// 1. Simple example (hello world - entry point)
const simple_exe = b.addExecutable(.{
.name = "example-simple",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/simple.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(simple_exe);
const run_simple = b.step("run-simple", "Run simple hello world example");
const simple_cmd = b.addRunArtifact(simple_exe);
run_simple.dependOn(&simple_cmd.step);
simple_cmd.step.dependOn(b.getInstallStep());
// 2. Request/Reply example (RPC pattern)
const request_reply_exe = b.addExecutable(.{
.name = "example-request-reply",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/request_reply.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(request_reply_exe);
const run_request_reply = b.step(
"run-request-reply",
"Run request/reply RPC example",
);
const request_reply_cmd = b.addRunArtifact(request_reply_exe);
run_request_reply.dependOn(&request_reply_cmd.step);
request_reply_cmd.step.dependOn(b.getInstallStep());
// Headers example (metadata with HPUB/HMSG)
const headers_exe = b.addExecutable(.{
.name = "example-headers",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/headers.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(headers_exe);
const run_headers = b.step(
"run-headers",
"Run headers example",
);
const headers_cmd = b.addRunArtifact(headers_exe);
run_headers.dependOn(&headers_cmd.step);
headers_cmd.step.dependOn(b.getInstallStep());
// 3. Queue Groups example (load balancing with workers)
const queue_groups_exe = b.addExecutable(.{
.name = "example-queue-groups",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/queue_groups.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(queue_groups_exe);
const run_queue_groups = b.step(
"run-queue-groups",
"Run queue groups (load balancing) example",
);
const queue_groups_cmd = b.addRunArtifact(queue_groups_exe);
run_queue_groups.dependOn(&queue_groups_cmd.step);
queue_groups_cmd.step.dependOn(b.getInstallStep());
// 4. Select example (io.select timeout pattern)
const select_exe = b.addExecutable(.{
.name = "example-select",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/select.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(select_exe);
const run_select = b.step(
"run-select",
"Run io.select() async timeout example",
);
const select_cmd = b.addRunArtifact(select_exe);
run_select.dependOn(&select_cmd.step);
select_cmd.step.dependOn(b.getInstallStep());
// 5. Batch Receiving example (efficient batch message retrieval)
const batch_exe = b.addExecutable(.{
.name = "example-batch-receiving",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/batch_receiving.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(batch_exe);
const run_batch = b.step(
"run-batch-receiving",
"Run batch receiving patterns example",
);
const batch_cmd = b.addRunArtifact(batch_exe);
run_batch.dependOn(&batch_cmd.step);
batch_cmd.step.dependOn(b.getInstallStep());
// 6. Graceful Shutdown example (drain and lifecycle)
const shutdown_exe = b.addExecutable(.{
.name = "example-graceful-shutdown",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/graceful_shutdown.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(shutdown_exe);
const run_shutdown = b.step(
"run-graceful-shutdown",
"Run graceful shutdown (drain) example",
);
const shutdown_cmd = b.addRunArtifact(shutdown_exe);
run_shutdown.dependOn(&shutdown_cmd.step);
shutdown_cmd.step.dependOn(b.getInstallStep());
// 7. Reconnection example (resilience patterns)
const reconnect_exe = b.addExecutable(.{
.name = "example-reconnection",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/reconnection.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(reconnect_exe);
const run_reconnect = b.step(
"run-reconnection",
"Run reconnection resilience example",
);
const reconnect_cmd = b.addRunArtifact(reconnect_exe);
run_reconnect.dependOn(&reconnect_cmd.step);
reconnect_cmd.step.dependOn(b.getInstallStep());
// 8. Polling Loop example (non-blocking patterns)
const polling_exe = b.addExecutable(.{
.name = "example-polling-loop",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/polling_loop.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(polling_exe);
const run_polling = b.step(
"run-polling-loop",
"Run non-blocking polling loop example",
);
const polling_cmd = b.addRunArtifact(polling_exe);
run_polling.dependOn(&polling_cmd.step);
polling_cmd.step.dependOn(b.getInstallStep());
// 9. Event Callbacks example (connection lifecycle)
const events_exe = b.addExecutable(.{
.name = "example-events",
.root_module = b.createModule(.{
.root_source_file = b.path("src/examples/events.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(events_exe);
const run_events = b.step(
"run-events",
"Run event callbacks (connection lifecycle) example",
);
const events_cmd = b.addRunArtifact(events_exe);
run_events.dependOn(&events_cmd.step);
events_cmd.step.dependOn(b.getInstallStep());
// 10. Callback Subscriptions example
const callback_exe = b.addExecutable(.{
.name = "example-callback",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/callback.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(callback_exe);
const run_callback = b.step(
"run-callback",
"Run callback subscriptions example",
);
const callback_cmd = b.addRunArtifact(callback_exe);
run_callback.dependOn(&callback_cmd.step);
callback_cmd.step.dependOn(b.getInstallStep());
// 11. Request/Reply with Callback example
const req_rep_cb_exe = b.addExecutable(.{
.name = "example-request-reply-callback",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/request_reply_callback.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(req_rep_cb_exe);
const run_req_rep_cb = b.step(
"run-request-reply-callback",
"Run request/reply callback example",
);
const req_rep_cb_cmd = b.addRunArtifact(req_rep_cb_exe);
run_req_rep_cb.dependOn(&req_rep_cb_cmd.step);
req_rep_cb_cmd.step.dependOn(b.getInstallStep());
// 12. JetStream Publish example
const js_pub_exe = b.addExecutable(.{
.name = "example-jetstream-publish",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/jetstream_publish.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(js_pub_exe);
const run_js_pub = b.step(
"run-jetstream-publish",
"Run JetStream publish example",
);
const js_pub_cmd = b.addRunArtifact(js_pub_exe);
run_js_pub.dependOn(&js_pub_cmd.step);
js_pub_cmd.step.dependOn(b.getInstallStep());
// 13. JetStream Consume example
const js_consume_exe = b.addExecutable(.{
.name = "example-jetstream-consume",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/jetstream_consume.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(js_consume_exe);
const run_js_consume = b.step(
"run-jetstream-consume",
"Run JetStream pull consumer example",
);
const js_consume_cmd = b.addRunArtifact(
js_consume_exe,
);
run_js_consume.dependOn(&js_consume_cmd.step);
js_consume_cmd.step.dependOn(b.getInstallStep());
// 14. JetStream Push Consumer example
const js_push_exe = b.addExecutable(.{
.name = "example-jetstream-push",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/jetstream_push.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(js_push_exe);
const run_js_push = b.step(
"run-jetstream-push",
"Run JetStream push consumer example",
);
const js_push_cmd = b.addRunArtifact(js_push_exe);
run_js_push.dependOn(&js_push_cmd.step);
js_push_cmd.step.dependOn(b.getInstallStep());
// 15. JetStream Async Publish example
const js_async_exe = b.addExecutable(.{
.name = "example-jetstream-async-publish",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/jetstream_async_publish.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(js_async_exe);
const run_js_async = b.step(
"run-jetstream-async-publish",
"Run JetStream async publish example",
);
const js_async_cmd = b.addRunArtifact(
js_async_exe,
);
run_js_async.dependOn(&js_async_cmd.step);
js_async_cmd.step.dependOn(b.getInstallStep());
// 16. Key-Value Store example
const kv_exe = b.addExecutable(.{
.name = "example-kv",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/kv.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(kv_exe);
const run_kv = b.step(
"run-kv",
"Run KV store example",
);
const kv_cmd = b.addRunArtifact(kv_exe);
run_kv.dependOn(&kv_cmd.step);
kv_cmd.step.dependOn(b.getInstallStep());
// 17. Key-Value Watch example
const kv_watch_exe = b.addExecutable(.{
.name = "example-kv-watch",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/kv_watch.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(kv_watch_exe);
const run_kv_watch = b.step(
"run-kv-watch",
"Run KV watch example",
);
const kv_watch_cmd = b.addRunArtifact(
kv_watch_exe,
);
run_kv_watch.dependOn(&kv_watch_cmd.step);
kv_watch_cmd.step.dependOn(b.getInstallStep());
// 18. Microservices echo example
const micro_echo_exe = b.addExecutable(.{
.name = "example-micro-echo",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/examples/micro_echo.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(micro_echo_exe);
const run_micro_echo = b.step(
"run-micro-echo",
"Run microservices echo example",
);
const micro_echo_cmd = b.addRunArtifact(
micro_echo_exe,
);
run_micro_echo.dependOn(µ_echo_cmd.step);
micro_echo_cmd.step.dependOn(b.getInstallStep());
// NATS by Example: Pub-Sub messaging
const nbe_pubsub_exe = b.addExecutable(.{
.name = "nbe-messaging-pub-sub",
.root_module = b.createModule(.{
.root_source_file = b.path(
"doc/nats-by-example/messaging/pub-sub.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(nbe_pubsub_exe);
const run_nbe_pubsub = b.step(
"run-nbe-messaging-pub-sub",
"Run NATS by Example: Pub-Sub messaging",
);
const nbe_pubsub_cmd = b.addRunArtifact(nbe_pubsub_exe);
run_nbe_pubsub.dependOn(&nbe_pubsub_cmd.step);
nbe_pubsub_cmd.step.dependOn(b.getInstallStep());
// NATS by Example: Request-Reply
const nbe_reqrep_exe = b.addExecutable(.{
.name = "nbe-messaging-request-reply",
.root_module = b.createModule(.{
.root_source_file = b.path(
"doc/nats-by-example/messaging/request-reply.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(nbe_reqrep_exe);
const run_nbe_reqrep = b.step(
"run-nbe-messaging-request-reply",
"Run NATS by Example: Request-Reply",
);
const nbe_reqrep_cmd = b.addRunArtifact(nbe_reqrep_exe);
run_nbe_reqrep.dependOn(&nbe_reqrep_cmd.step);
nbe_reqrep_cmd.step.dependOn(b.getInstallStep());
// NATS by Example: JSON payloads
const nbe_json_exe = b.addExecutable(.{
.name = "nbe-messaging-json",
.root_module = b.createModule(.{
.root_source_file = b.path(
"doc/nats-by-example/messaging/json.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(nbe_json_exe);
const run_nbe_json = b.step(
"run-nbe-messaging-json",
"Run NATS by Example: JSON payloads",
);
const nbe_json_cmd = b.addRunArtifact(nbe_json_exe);
run_nbe_json.dependOn(&nbe_json_cmd.step);
nbe_json_cmd.step.dependOn(b.getInstallStep());
// NATS by Example: Concurrent processing
const nbe_concurrent_exe = b.addExecutable(.{
.name = "nbe-messaging-concurrent",
.root_module = b.createModule(.{
.root_source_file = b.path(
"doc/nats-by-example/messaging/concurrent.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(nbe_concurrent_exe);
const run_nbe_concurrent = b.step(
"run-nbe-messaging-concurrent",
"Run NATS by Example: Concurrent processing",
);
const nbe_concurrent_cmd = b.addRunArtifact(
nbe_concurrent_exe,
);
run_nbe_concurrent.dependOn(&nbe_concurrent_cmd.step);
nbe_concurrent_cmd.step.dependOn(b.getInstallStep());
// NATS by Example: Iterating multiple subscriptions
const nbe_multisub_exe = b.addExecutable(.{
.name = "nbe-messaging-iterating-multiple-subscriptions",
.root_module = b.createModule(.{
.root_source_file = b.path(
"doc/nats-by-example/messaging/" ++
"iterating-multiple-subscriptions.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(nbe_multisub_exe);
const run_nbe_multisub = b.step(
"run-nbe-messaging-iterating-multiple-subscriptions",
"Run NATS by Example: Multiple subscriptions",
);
const nbe_multisub_cmd = b.addRunArtifact(
nbe_multisub_exe,
);
run_nbe_multisub.dependOn(&nbe_multisub_cmd.step);
nbe_multisub_cmd.step.dependOn(b.getInstallStep());
// NATS by Example: NKeys and JWTs (auth)
const nbe_nkeys_jwts_exe = b.addExecutable(.{
.name = "nbe-auth-nkeys-jwts",
.root_module = b.createModule(.{
.root_source_file = b.path(
"doc/nats-by-example/auth/nkeys-jwts.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
},
}),
});
b.installArtifact(nbe_nkeys_jwts_exe);
const run_nbe_nkeys_jwts = b.step(
"run-nbe-auth-nkeys-jwts",
"Run NATS by Example: NKeys and JWTs",
);
const nbe_nkeys_jwts_cmd = b.addRunArtifact(
nbe_nkeys_jwts_exe,
);
run_nbe_nkeys_jwts.dependOn(&nbe_nkeys_jwts_cmd.step);
nbe_nkeys_jwts_cmd.step.dependOn(b.getInstallStep());
const fmt = b.addFmt(.{
.paths = &.{ "src", "doc", "build.zig" },
.check = false,
});
const fmt_step = b.step("fmt", "Format source code");
fmt_step.dependOn(&fmt.step);
const fmt_check = b.addFmt(.{
.paths = &.{ "src", "doc", "build.zig" },
.check = true,
});
const fmt_check_step = b.step("fmt-check", "Check formatting");
fmt_check_step.dependOn(&fmt_check.step);
// Integration tests (requires nats-server)
const integration_exe = b.addExecutable(.{
.name = "integration-test",
.root_module = b.createModule(.{
.root_source_file = b.path("src/testing/integration_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(integration_exe);
const run_integration = b.step(
"test-integration",
"Run integration tests (requires nats-server)",
);
const integration_cmd = b.addRunArtifact(integration_exe);
run_integration.dependOn(&integration_cmd.step);
// Micro-only integration tests (faster; just the micro suite).
const micro_integration_exe = b.addExecutable(.{
.name = "micro-integration-test",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/testing/micro_integration_test.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(micro_integration_exe);
const run_micro_integration = b.step(
"test-integration-micro",
"Run only the micro integration tests",
);
const micro_integration_cmd = b.addRunArtifact(
micro_integration_exe,
);
run_micro_integration.dependOn(µ_integration_cmd.step);
// Focused JWT/TLS integration tests.
const tls_integration_exe = b.addExecutable(.{
.name = "tls-integration-test",
.root_module = b.createModule(.{
.root_source_file = b.path(
"src/testing/tls_integration_test.zig",
),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "nats", .module = nats },
.{ .name = "io_backend", .module = io_backend_mod },
},
}),
});
b.installArtifact(tls_integration_exe);
const run_tls_integration = b.step(
"test-integration-tls",
"Run only the TLS integration tests",
);
const tls_integration_cmd = b.addRunArtifact(
tls_integration_exe,
);
run_tls_integration.dependOn(&tls_integration_cmd.step);
}
================================================
FILE: build.zig.zon
================================================
.{
.name = .nats,
.version = "0.1.0",
// Changing fingerprint has security and trust implications.
.fingerprint = 0x31f7624fb15addf7,
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{
"CONTRIBUTING.md",
"LICENSE",
"README.md",
"SECURITY.md",
"build.zig",
"build.zig.zon",
"doc",
"logo",
"src",
},
}
================================================
FILE: doc/JetStream.md
================================================
# JetStream Guide for nats.zig
JetStream is NATS' persistence and streaming layer. It provides
at-least-once delivery, message replay, and durable consumers --
all through a JSON request/reply API layered on core NATS. No new
wire protocol; everything goes through `$JS.API.*` subjects.
This guide covers the nats.zig JetStream API with side-by-side
Go comparisons for developers familiar with nats.go.
It is a focused companion to the comprehensive root
[README](../README.md). For Key-Value Store coverage, see the
README JetStream section and `src/examples/kv*.zig`.
## Table of Contents
- [Quick Start](#quick-start)
- [JetStream Context](#jetstream-context)
- [Streams](#streams)
- [Consumers](#consumers)
- [Publishing](#publishing)
- [Pull Subscription](#pull-subscription)
- [Message Acknowledgment](#message-acknowledgment)
- [Error Handling](#error-handling)
- [Response Ownership](#response-ownership)
- [Type Reference](#type-reference)
---
## Quick Start
A complete example: create a stream, publish a message, create a
consumer, fetch the message, and acknowledge it.
**Zig:**
```zig
const nats = @import("nats");
const js_mod = nats.jetstream;
// Assumes `client` is already connected
var js = try js_mod.JetStream.init(client, .{});
// Create a stream
var stream = try js.createStream(.{
.name = "ORDERS",
.subjects = &.{"orders.>"},
.storage = .memory,
});
defer stream.deinit();
// Publish a message
var ack = try js.publish("orders.new", "order-1");
defer ack.deinit();
// ack.value.seq == 1, ack.value.stream == "ORDERS"
// Create a consumer
var cons = try js.createConsumer("ORDERS", .{
.name = "processor",
.durable_name = "processor",
.ack_policy = .explicit,
});
defer cons.deinit();
// Fetch messages
var pull = js_mod.PullSubscription{
.js = &js,
.stream = "ORDERS",
};
try pull.setConsumer("processor");
var result = try pull.fetch(.{
.max_messages = 10,
.timeout_ms = 5000,
});
defer result.deinit();
for (result.messages) |*msg| {
// msg.data() returns the payload
try msg.ack();
}
```
**Go:**
```go
js, _ := jetstream.New(nc)
// Create a stream
stream, _ := js.CreateStream(ctx, jetstream.StreamConfig{
Name: "ORDERS",
Subjects: []string{"orders.>"},
Storage: jetstream.MemoryStorage,
})
// Publish a message
ack, _ := js.Publish(ctx, "orders.new", []byte("order-1"))
// ack.Stream == "ORDERS", ack.Sequence == 1
// Create a consumer
cons, _ := js.CreateConsumer(ctx, "ORDERS",
jetstream.ConsumerConfig{
Durable: "processor",
AckPolicy: jetstream.AckExplicitPolicy,
})
// Fetch messages
batch, _ := cons.Fetch(10)
for msg := range batch.Messages() {
msg.Ack()
}
```
---
## JetStream Context
The JetStream context is a lightweight struct (stack-allocated) that
holds a pointer to the NATS client, the API prefix, and timeout
settings. No heap allocation is needed. `JetStream.init()` is fallible because
it validates the API prefix or domain before storing it in the fixed-size
context buffer.
### Creating a Context
**Zig:**
```zig
const js_mod = nats.jetstream;
// Default settings
var js = try js_mod.JetStream.init(client, .{});
// Custom timeout
var js2 = try js_mod.JetStream.init(client, .{
.timeout_ms = 10000,
});
// With domain (multi-tenant)
var js3 = try js_mod.JetStream.init(client, .{
.domain = "hub",
});
// API prefix becomes: $JS.hub.API.
```
Stream, consumer, domain, and API-prefix names are validated at runtime.
Invalid names return `error.InvalidName`, `error.InvalidApiPrefix`, or
`error.NameTooLong` instead of relying on debug-only assertions.
**Go:**
```go
js, _ := jetstream.New(nc)
// With domain
js, _ = jetstream.NewWithDomain(nc, "hub")
```
### Options
| Field | Zig | Go | Default |
|-------|-----|-----|---------|
| API prefix | `.api_prefix` | `APIPrefix` | `$JS.API.` |
| Timeout | `.timeout_ms` | `DefaultTimeout` | 5000ms |
| Domain | `.domain` | via `NewWithDomain()` | none |
---
## Streams
Streams capture messages published to matching subjects.
### Create a Stream
**Zig:**
```zig
var resp = try js.createStream(.{
.name = "EVENTS",
.subjects = &.{"events.>"},
.retention = .limits,
.storage = .file,
.max_msgs = 100000,
.max_bytes = 1073741824, // 1GB
});
defer resp.deinit();
const info = resp.value;
// info.config.?.name == "EVENTS"
// info.state.?.messages == 0
```
**Go:**
```go
stream, _ := js.CreateStream(ctx, jetstream.StreamConfig{
Name: "EVENTS",
Subjects: []string{"events.>"},
Retention: jetstream.LimitsPolicy,
Storage: jetstream.FileStorage,
MaxMsgs: 100000,
MaxBytes: 1073741824,
})
info, _ := stream.Info(ctx)
```
### Get Stream Info
**Zig:**
```zig
var info = try js.streamInfo("EVENTS");
defer info.deinit();
if (info.value.state) |state| {
// state.messages, state.bytes, state.first_seq,
// state.last_seq, state.consumer_count
}
```
**Go:**
```go
stream, _ := js.Stream(ctx, "EVENTS")
info, _ := stream.Info(ctx)
// info.State.Msgs, info.State.Bytes, etc.
```
### Update a Stream
**Zig:**
```zig
var resp = try js.updateStream(.{
.name = "EVENTS",
.subjects = &.{ "events.>", "logs.>" },
.max_msgs = 200000,
});
defer resp.deinit();
```
**Go:**
```go
stream, _ := js.UpdateStream(ctx, jetstream.StreamConfig{
Name: "EVENTS",
Subjects: []string{"events.>", "logs.>"},
MaxMsgs: 200000,
})
```
### Purge a Stream
**Zig:**
```zig
var resp = try js.purgeStream("EVENTS");
defer resp.deinit();
// resp.value.purged == number of messages removed
```
**Go:**
```go
stream, _ := js.Stream(ctx, "EVENTS")
_ = stream.Purge(ctx)
```
### Delete a Stream
**Zig:**
```zig
var resp = try js.deleteStream("EVENTS");
defer resp.deinit();
// resp.value.success == true
```
**Go:**
```go
_ = js.DeleteStream(ctx, "EVENTS")
```
### StreamConfig Reference
| Field | Type | Description |
|-------|------|-------------|
| `name` | `[]const u8` | Stream name (required) |
| `subjects` | `?[]const []const u8` | Subjects to capture |
| `retention` | `?RetentionPolicy` | limits, interest, workqueue |
| `storage` | `?StorageType` | file, memory |
| `max_msgs` | `?i64` | Max messages in stream |
| `max_bytes` | `?i64` | Max total bytes |
| `max_age` | `?i64` | Max message age (nanoseconds) |
| `max_msg_size` | `?i32` | Max single message size |
| `max_msgs_per_subject` | `?i64` | Per-subject limit |
| `max_consumers` | `?i64` | Max consumers |
| `num_replicas` | `?i32` | Replica count |
| `discard` | `?DiscardPolicy` | old, new |
| `duplicate_window` | `?i64` | Dedup window (nanoseconds) |
| `no_ack` | `?bool` | Disable publish acks |
| `compression` | `?StoreCompression` | none, s2 |
All optional fields default to `null` and are omitted from the
JSON request (server applies its own defaults).
---
## Consumers
Consumers track read position in a stream and manage message
delivery.
### Create a Consumer
**Zig:**
```zig
var resp = try js.createConsumer("EVENTS", .{
.name = "my-worker",
.durable_name = "my-worker",
.ack_policy = .explicit,
.deliver_policy = .all,
.filter_subject = "events.orders.>",
.max_ack_pending = 1000,
});
defer resp.deinit();
if (resp.value.name) |name| {
// name == "my-worker"
}
```
**Go:**
```go
cons, _ := js.CreateConsumer(ctx, "EVENTS",
jetstream.ConsumerConfig{
Durable: "my-worker",
AckPolicy: jetstream.AckExplicitPolicy,
DeliverPolicy: jetstream.DeliverAllPolicy,
FilterSubject: "events.orders.>",
MaxAckPending: 1000,
})
```
### Get Consumer Info
**Zig:**
```zig
var info = try js.consumerInfo("EVENTS", "my-worker");
defer info.deinit();
// info.value.num_pending -- messages waiting
// info.value.num_ack_pending -- delivered but unacked
```
**Go:**
```go
cons, _ := js.Consumer(ctx, "EVENTS", "my-worker")
info, _ := cons.Info(ctx)
```
### Update a Consumer
**Zig:**
```zig
var resp = try js.updateConsumer("EVENTS", .{
.name = "my-worker",
.durable_name = "my-worker",
.ack_policy = .explicit,
.max_ack_pending = 2000,
});
defer resp.deinit();
```
**Go:**
```go
cons, _ := js.UpdateConsumer(ctx, "EVENTS",
jetstream.ConsumerConfig{
Durable: "my-worker",
AckPolicy: jetstream.AckExplicitPolicy,
MaxAckPending: 2000,
})
```
### Delete a Consumer
**Zig:**
```zig
var resp = try js.deleteConsumer("EVENTS", "my-worker");
defer resp.deinit();
// resp.value.success == true
```
**Go:**
```go
_ = js.DeleteConsumer(ctx, "EVENTS", "my-worker")
```
### ConsumerConfig Reference
| Field | Type | Description |
|-------|------|-------------|
| `name` | `?[]const u8` | Consumer name |
| `durable_name` | `?[]const u8` | Durable name (survives restarts) |
| `ack_policy` | `?AckPolicy` | none, all, explicit |
| `deliver_policy` | `?DeliverPolicy` | all, last, new, ... |
| `ack_wait` | `?i64` | Ack timeout (nanoseconds) |
| `max_deliver` | `?i64` | Max redelivery attempts |
| `filter_subject` | `?[]const u8` | Subject filter |
| `filter_subjects` | `?[]const []const u8` | Multiple filters |
| `replay_policy` | `?ReplayPolicy` | instant, original |
| `max_waiting` | `?i64` | Max pull requests waiting |
| `max_ack_pending` | `?i64` | Max unacked messages |
| `inactive_threshold` | `?i64` | Idle cleanup (nanoseconds) |
| `headers_only` | `?bool` | Deliver headers only |
---
## Publishing
JetStream publish goes directly to the stream subject (not through
`$JS.API`). The server returns a `PubAck` confirming storage.
### Simple Publish
**Zig:**
```zig
var ack = try js.publish("orders.new", payload);
defer ack.deinit();
// Check the ack
if (ack.value.stream) |stream| {
// stream name that stored the message
}
const seq = ack.value.seq; // sequence number
```
**Go:**
```go
ack, _ := js.Publish(ctx, "orders.new", payload)
// ack.Stream, ack.Sequence
```
### Publish with Options
Use `publishWithOpts` for idempotency and optimistic concurrency.
**Zig:**
```zig
var ack = try js.publishWithOpts(
"orders.new",
payload,
.{
.msg_id = "order-123",
.expected_stream = "ORDERS",
.expected_last_seq = 41,
},
);
defer ack.deinit();
// Check for duplicate
if (ack.value.duplicate) |dup| {
if (dup) {
// Message was already stored (idempotent)
}
}
```
**Go:**
```go
ack, _ := js.Publish(ctx, "orders.new", payload,
jetstream.WithMsgID("order-123"),
jetstream.WithExpectStream("ORDERS"),
jetstream.WithExpectLastSequence(41),
)
```
### Publish Option Headers
| Zig field | Header sent | Purpose |
|-----------|------------|---------|
| `msg_id` | `Nats-Msg-Id` | Deduplication key |
| `expected_stream` | `Nats-Expected-Stream` | Verify target stream |
| `expected_last_seq` | `Nats-Expected-Last-Sequence` | Optimistic concurrency |
| `expected_last_msg_id` | `Nats-Expected-Last-Msg-Id` | Sequence by msg ID |
| `expected_last_subj_seq` | `Nats-Expected-Last-Subject-Sequence` | Per-subject sequence |
---
## Pull Subscription
Pull consumers fetch messages on demand. Create a
`PullSubscription`, then call `fetch()` to get a batch.
### Setup and Fetch
**Zig:**
```zig
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "ORDERS",
};
try pull.setConsumer("processor");
var result = try pull.fetch(.{
.max_messages = 100,
.timeout_ms = 5000,
});
defer result.deinit();
for (result.messages) |*msg| {
const data = msg.data();
// process data...
try msg.ack();
}
```
**Go:**
```go
cons, _ := js.Consumer(ctx, "ORDERS", "processor")
batch, _ := cons.Fetch(100)
for msg := range batch.Messages() {
data := msg.Data()
// process data...
msg.Ack()
}
```
### FetchOpts
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_messages` | `u32` | 1 | Batch size |
| `timeout_ms` | `u32` | 5000 | Timeout in milliseconds |
### How Fetch Works
1. Subscribes to a temporary inbox
2. Publishes a pull request JSON to
`$JS.API.CONSUMER.MSG.NEXT.{stream}.{consumer}`
3. Collects messages until batch is full or a status signal
arrives:
- **404** -- no messages available (stop)
- **408** -- request expired (stop)
- **409** -- leadership change (stop)
- **100** -- idle heartbeat (skip, continue)
4. Returns `FetchResult` with collected messages
### FetchResult
```zig
const FetchResult = struct {
messages: []JsMsg,
allocator: Allocator,
pub fn count(self: *const FetchResult) usize;
pub fn deinit(self: *FetchResult) void;
};
```
Call `deinit()` to free all messages and the backing slice.
---
## Message Acknowledgment
JetStream messages must be acknowledged to confirm processing.
All ack methods publish a protocol token to the message's
reply-to subject.
### Ack Methods
| Method | Zig | Go | Payload | Repeatable |
|--------|-----|-----|---------|------------|
| Acknowledge | `msg.ack()` | `msg.Ack()` | `+ACK` | No |
| Negative ack | `msg.nak()` | `msg.Nak()` | `-NAK` | No |
| NAK with delay | `msg.nakWithDelay(ns)` | `msg.NakWithDelay(d)` | `-NAK {"delay":N}` | No |
| In progress | `msg.inProgress()` | `msg.InProgress()` | `+WPI` | Yes |
| Terminate | `msg.term()` | `msg.Term()` | `+TERM` | No |
| Terminate + reason | `msg.termWithReason(r)` | `msg.TermWithReason(r)` | `+TERM reason` | No |
### Examples
**Zig:**
```zig
for (result.messages) |*msg| {
const data = msg.data();
if (isValid(data)) {
try msg.ack();
} else if (isRetryable(data)) {
// Retry after 5 seconds
try msg.nakWithDelay(5_000_000_000);
} else {
try msg.termWithReason("invalid payload");
}
}
```
**Go:**
```go
for msg := range batch.Messages() {
data := msg.Data()
if isValid(data) {
msg.Ack()
} else if isRetryable(data) {
msg.NakWithDelay(5 * time.Second)
} else {
msg.TermWithReason("invalid payload")
}
}
```
### Extending the Ack Deadline
For long-running processing, send periodic `inProgress()` signals
to prevent redelivery:
**Zig:**
```zig
try msg.inProgress(); // Reset ack timer
// ... do work ...
try msg.inProgress(); // Reset again
// ... finish work ...
try msg.ack();
```
### JsMsg Accessors
| Method | Returns | Description |
|--------|---------|-------------|
| `data()` | `[]const u8` | Message payload |
| `subject()` | `[]const u8` | Original subject |
| `headers()` | `?[]const u8` | Raw headers |
| `replyTo()` | `?[]const u8` | Ack reply subject |
| `deinit()` | `void` | Free message memory |
---
## Error Handling
nats.zig uses a two-layer error system for JetStream:
1. **Zig error unions** -- transport/protocol failures
2. **ApiError struct** -- server-side JetStream errors
### Layer 1: Zig Errors
```zig
pub const Error = error{
Timeout,
NoResponders,
ApiError,
JsonParseError,
SubjectTooLong,
NoHeartbeat,
ConsumerDeleted,
OrderedReset,
InvalidKey,
InvalidData,
KeyNotFound,
WrongLastRevision,
ThreadSpawnFailed,
};
```
### Layer 2: API Errors
When `error.ApiError` is returned, call `js.lastApiError()` to
get the server-side error details:
**Zig:**
```zig
var info = js.streamInfo("NONEXISTENT");
if (info) |*r| {
defer r.deinit();
// use r.value...
} else |err| {
if (err == error.ApiError) {
if (js.lastApiError()) |api_err| {
// api_err.code -- HTTP-like status (404)
// api_err.err_code -- JetStream error code
// api_err.description() -- error message
}
}
}
```
**Go:**
```go
_, err := js.Stream(ctx, "NONEXISTENT")
if err != nil {
var jsErr jetstream.JetStreamError
if errors.As(err, &jsErr) {
apiErr := jsErr.APIError()
// apiErr.Code, apiErr.ErrorCode, apiErr.Description
}
}
```
### Common Error Codes
| Constant | Code | Meaning |
|----------|------|---------|
| `ErrCode.stream_not_found` | 10059 | Stream does not exist |
| `ErrCode.stream_name_in_use` | 10058 | Stream name taken |
| `ErrCode.consumer_not_found` | 10014 | Consumer does not exist |
| `ErrCode.consumer_already_exists` | 10105 | Consumer name taken |
| `ErrCode.js_not_enabled` | 10076 | JetStream not enabled |
| `ErrCode.bad_request` | 10003 | Invalid request |
| `ErrCode.stream_wrong_last_seq` | 10071 | Sequence mismatch |
| `ErrCode.message_not_found` | 10037 | Message not in stream |
Full list in `src/jetstream/errors.zig`.
### Checking Specific Errors
**Zig:**
```zig
const ErrCode = nats.jetstream.errors.ErrCode;
if (js.lastApiError()) |api_err| {
if (api_err.err_code == ErrCode.stream_not_found) {
// Handle missing stream
}
}
```
**Go:**
```go
if errors.Is(err, jetstream.ErrStreamNotFound) {
// Handle missing stream
}
```
---
## Response Ownership
Every JetStream operation that returns a `Response(T)` owns
parsed JSON memory. All string slices in `resp.value` point
into the parsed arena.
**You must call `deinit()` when done:**
```zig
var resp = try js.createStream(.{ .name = "TEST" });
defer resp.deinit(); // Frees parsed JSON arena
// Access data through resp.value
if (resp.value.config) |cfg| {
// cfg.name is valid until resp.deinit()
}
```
If you need to keep data beyond `deinit()`, copy it first:
```zig
var resp = try js.streamInfo("TEST");
const msg_count = resp.value.state.?.messages;
resp.deinit(); // Safe -- msg_count is a u64 (copied)
```
String data requires explicit copying:
```zig
var resp = try js.streamInfo("TEST");
const name = try allocator.dupe(
u8,
resp.value.config.?.name,
);
resp.deinit(); // Safe -- name is independently owned
defer allocator.free(name);
```
---
## Type Reference
### Enums
| Zig Enum | Values | Go Equivalent |
|----------|--------|---------------|
| `RetentionPolicy` | limits, interest, workqueue | `LimitsPolicy`, `InterestPolicy`, `WorkQueuePolicy` |
| `StorageType` | file, memory | `FileStorage`, `MemoryStorage` |
| `DiscardPolicy` | old, new | `DiscardOld`, `DiscardNew` |
| `StoreCompression` | none, s2 | `NoCompression`, `S2Compression` |
| `DeliverPolicy` | all, last, new, by_start_sequence, by_start_time, last_per_subject | `DeliverAllPolicy`, `DeliverLastPolicy`, ... |
| `AckPolicy` | none, all, explicit | `AckNonePolicy`, `AckAllPolicy`, `AckExplicitPolicy` |
| `ReplayPolicy` | instant, original | `ReplayInstantPolicy`, `ReplayOriginalPolicy` |
### Key Differences from Go
| Aspect | Zig (nats.zig) | Go (nats.go) |
|--------|----------------|--------------|
| Context | Stack struct, `try JetStream.init()` | Interface, `jetstream.New()` |
| Timeout | `timeout_ms: u32` on JetStream | `context.Context` per call |
| Responses | `Response(T)` with `defer deinit()` | Go GC handles memory |
| Errors | `error.ApiError` + `lastApiError()` | `JetStreamError` interface |
| Pull | `PullSubscription.fetch()` | `consumer.Fetch()` |
| Options | Struct fields with `?T = null` | Functional options pattern |
| Enums | Lowercase tags (`.file`) | PascalCase constants (`FileStorage`) |
| Durations | Nanoseconds (`i64`) | `time.Duration` |
### Duration Conversion
JetStream JSON uses nanoseconds for all duration fields:
```zig
// 30 seconds
const thirty_sec: i64 = 30 * std.time.ns_per_s;
// 5 minutes
const five_min: i64 = 5 * 60 * std.time.ns_per_s;
// Use in config
var stream = try js.createStream(.{
.name = "TEST",
.max_age = five_min,
.duplicate_window = thirty_sec,
});
```
================================================
FILE: doc/nats-by-example/README.md
================================================
# NATS by Example
Ports of [natsbyexample.com](https://natsbyexample.com) examples.
| Example | Run | Server? |
|---------|-----|---------|
| [Pub-Sub](messaging/Pub-Sub.md) | `run-nbe-messaging-pub-sub` | Yes |
| [Request-Reply](messaging/Request-Reply.md) | `run-nbe-messaging-request-reply` | Yes |
| [JSON](messaging/Json.md) | `run-nbe-messaging-json` | Yes |
| [Concurrent](messaging/Concurrent.md) | `run-nbe-messaging-concurrent` | Yes |
| [Multiple Subscriptions](messaging/Iterating-Multiple-Subscriptions.md) | `run-nbe-messaging-iterating-multiple-subscriptions` | Yes |
| [NKeys & JWTs](auth/NKeys-JWTs.md) | `run-nbe-auth-nkeys-jwts` | No |
================================================
FILE: doc/nats-by-example/auth/NKeys-JWTs.md
================================================
# NKeys and JWTs
NATS supports decentralized authentication using a three-level trust hierarchy:
- **Operator** - Top-level entity that manages accounts
- **Account** - Groups users and defines resource limits
- **User** - Authenticates to NATS with permissions
Each entity has an NKey keypair (Ed25519). Operators sign account JWTs,
and accounts sign user JWTs. This creates a chain of trust without
requiring a central authority.
## How It Works
1. Generate an **operator** keypair (prefix `SO`)
2. Generate an **account** keypair (prefix `SA`)
3. The operator signs an **account JWT** containing the account's public key
4. Generate a **user** keypair (prefix `SU`)
5. The account signs a **user JWT** with publish/subscribe permissions
6. Format a **credentials file** (`.creds`) containing the user JWT and seed
The credentials file is what a NATS client uses to authenticate. The server
validates the JWT signature chain back to a trusted operator.
## Running
No NATS server required - this is a pure cryptography example.
```sh
zig build run-nbe-auth-nkeys-jwts
```
## Output
```
== Operator ==
operator public key:
operator seed:
== Account ==
account public key:
account seed:
account JWT:
== User ==
user public key:
user seed:
user JWT:
== Credentials File ==
-----BEGIN NATS USER JWT-----
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
************************************************************
-----BEGIN USER NKEY SEED-----
------END USER NKEY SEED------
```
## What's Happening
1. Three NKey keypairs are generated: operator, account, and user. Each uses
Ed25519 with type-specific base32 prefixes (`SO`, `SA`, `SU`).
2. An account JWT is created with default limits (unlimited) and signed by the
operator's private key. The JWT contains the account's public key as subject
and the operator's public key as issuer.
3. A user JWT is created with publish permissions on `app.>` and subscribe
permissions on `app.>` and `_INBOX.>`, signed by the account's private key.
4. A credentials file is formatted containing the user JWT and seed. This file
can be passed to a NATS client via `--creds` for authentication.
## Source
See [nkeys-jwts.zig](nkeys-jwts.zig) for the full example.
Based on [natsbyexample.com/examples/auth/nkeys-jwts](https://natsbyexample.com/examples/auth/nkeys-jwts/go).
================================================
FILE: doc/nats-by-example/auth/nkeys-jwts.zig
================================================
//! NKeys and JWTs
//!
//! This example demonstrates NATS decentralized authentication using
//! the operator/account/user keypair hierarchy. It generates NKey
//! keypairs, encodes JWTs, and formats a credentials file - all
//! using pure Zig cryptography with zero external dependencies.
//!
//! No NATS server is needed - this is a pure cryptography example.
//!
//! Key concepts shown:
//! - NKey generation for operator, account, and user entities
//! - JWT encoding with Ed25519 signatures
//! - Credentials file formatting for client authentication
//! - The three-level trust hierarchy: operator > account > user
//!
//! Based on:
//! https://natsbyexample.com/examples/auth/nkeys-jwts/go
//!
//! Run with: zig build run-nbe-auth-nkeys-jwts
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const io = init.io;
// Set up buffered stdout writer
var stdout_buf: [8192]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(
io,
&stdout_buf,
);
const stdout = &stdout_writer.interface;
// The operator is the top-level entity that manages
// accounts. It signs account JWTs.
try stdout.print(
"== Operator ==\n",
.{},
);
var op_kp = nats.auth.KeyPair.generate(io, .operator);
defer op_kp.wipe();
var op_pk_buf: [56]u8 = undefined;
const op_pub = op_kp.publicKey(&op_pk_buf);
try stdout.print(
"operator public key: {s}\n",
.{op_pub},
);
var op_seed_buf: [58]u8 = undefined;
const op_seed = op_kp.encodeSeed(&op_seed_buf);
try stdout.print(
"operator seed: {s}\n\n",
.{op_seed},
);
// An account groups users and defines resource limits.
// The operator signs account JWTs.
try stdout.print(
"== Account ==\n",
.{},
);
var acct_kp = nats.auth.KeyPair.generate(io, .account);
defer acct_kp.wipe();
var acct_pk_buf: [56]u8 = undefined;
const acct_pub = acct_kp.publicKey(&acct_pk_buf);
try stdout.print(
"account public key: {s}\n",
.{acct_pub},
);
var acct_seed_buf: [58]u8 = undefined;
const acct_seed = acct_kp.encodeSeed(&acct_seed_buf);
try stdout.print(
"account seed: {s}\n\n",
.{acct_seed},
);
// Encode account JWT (signed by operator)
const ts = std.Io.Timestamp.now(io, .real);
const iat: i64 = @intCast(
@as(u64, @intCast(ts.nanoseconds)) /
std.time.ns_per_s,
);
var acct_jwt_buf: [2048]u8 = undefined;
const acct_jwt = try nats.auth.jwt.encodeAccountClaims(
&acct_jwt_buf,
acct_pub,
"my-account",
op_kp,
iat,
.{},
);
try stdout.print(
"account JWT:\n{s}\n\n",
.{acct_jwt},
);
// A user belongs to an account. The account signs
// user JWTs with publish/subscribe permissions.
try stdout.print(
"== User ==\n",
.{},
);
var user_kp = nats.auth.KeyPair.generate(io, .user);
defer user_kp.wipe();
var user_pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&user_pk_buf);
try stdout.print(
"user public key: {s}\n",
.{user_pub},
);
var user_seed_buf: [58]u8 = undefined;
const user_seed = user_kp.encodeSeed(&user_seed_buf);
try stdout.print(
"user seed: {s}\n\n",
.{user_seed},
);
// Encode user JWT with permissions (signed by account)
var user_jwt_buf: [2048]u8 = undefined;
const user_jwt = try nats.auth.jwt.encodeUserClaims(
&user_jwt_buf,
user_pub,
"my-user",
acct_kp,
iat,
.{
.pub_allow = &.{"app.>"},
.sub_allow = &.{ "app.>", "_INBOX.>" },
},
);
try stdout.print(
"user JWT:\n{s}\n\n",
.{user_jwt},
);
// Format a .creds file containing the user JWT and seed.
// This file is what a NATS client uses to authenticate.
try stdout.print(
"== Credentials File ==\n",
.{},
);
var creds_buf: [4096]u8 = undefined;
const creds = nats.auth.creds.format(
&creds_buf,
user_jwt,
user_seed,
) catch return;
try stdout.print("{s}\n", .{creds});
try stdout.flush();
}
================================================
FILE: doc/nats-by-example/messaging/Concurrent.md
================================================
# Concurrent Message Processing
By default, messages from a subscription are processed sequentially -
each message must finish before the next one starts. For workloads
where message processing takes variable time (API calls, database
queries, computation), concurrent processing can significantly improve
throughput.
This example uses `io.concurrent()` to spawn worker threads that
process messages in parallel. Each worker simulates variable
processing time with a random delay, causing messages to complete
out of their original order.
## Running
Prerequisites: `nats-server` running on `localhost:4222`.
```sh
nats-server &
zig build run-nbe-messaging-concurrent
```
## Output (order varies per run)
```
received message: "hello 3"
received message: "hello 0"
received message: "hello 7"
received message: "hello 1"
received message: "hello 5"
received message: "hello 8"
received message: "hello 4"
received message: "hello 9"
received message: "hello 6"
received message: "hello 2"
processed 10 messages concurrently
```
**Note**: The message order is non-deterministic. Each run produces
a different sequence because workers process with random delays.
## What's Happening
1. 10 messages are published to `greet.joe`.
2. All 10 are received on the main thread and copied into work items.
3. Three concurrent workers are spawned via `io.concurrent()`.
4. Each worker processes its assigned messages with a random delay
(0-100ms) to simulate variable work.
5. Workers write directly to stdout using `writeStreamingAll` (atomic
per-line writes avoid interleaved output).
6. The main thread waits for all workers to complete.
## Source
See [concurrent.zig](concurrent.zig) for the full example.
Based on [natsbyexample.com/examples/messaging/concurrent](https://natsbyexample.com/examples/messaging/concurrent/rust).
================================================
FILE: doc/nats-by-example/messaging/Iterating-Multiple-Subscriptions.md
================================================
# Iterating Over Multiple Subscriptions
NATS wildcards cover many routing cases, but sometimes you need
separate subscriptions. For example, you want `transport.cars`,
`transport.planes`, and `transport.ships` but not
`transport.spaceships`.
This example shows how to poll multiple subscriptions in a unified
loop using `tryNext()` - Zig's equivalent of merging async streams
into one iteration. Messages from all subscriptions are processed
in round-robin fashion without blocking.
## Running
Prerequisites: `nats-server` running on `localhost:4222`.
```sh
nats-server &
zig build run-nbe-messaging-iterating-multiple-subscriptions
```
## Output
```
received on cars.0: car number 0
received on planes.0: plane number 0
received on ships.0: ship number 0
received on cars.1: car number 1
received on planes.1: plane number 1
received on ships.1: ship number 1
...
received on cars.9: car number 9
received on planes.9: plane number 9
received on ships.9: ship number 9
received 30 messages from 3 subscriptions
```
## What's Happening
1. Three separate subscriptions are created: `cars.>`, `planes.>`,
and `ships.>`.
2. 10 messages are published to each category (30 total).
3. All three subscriptions are polled in round-robin using
`tryNext()` which returns instantly if no message is available.
4. Each message's subject and payload are printed as received.
5. A short sleep avoids busy-spinning when no messages are ready.
## Source
See [iterating-multiple-subscriptions.zig](iterating-multiple-subscriptions.zig)
for the full example.
Based on [natsbyexample.com/examples/messaging/iterating-multiple-subscriptions](https://natsbyexample.com/examples/messaging/iterating-multiple-subscriptions/rust).
================================================
FILE: doc/nats-by-example/messaging/Json.md
================================================
# JSON for Message Payloads
NATS message payloads are opaque byte sequences. It is up to the
application to define serialization. JSON is a natural choice for
cross-language compatibility.
Zig's `std.json` provides compile-time type-safe serialization and
deserialization:
- **Serialize**: `std.json.Stringify.value(struct, options, writer)`
writes JSON to any `Io.Writer` (including fixed-buffer writers).
- **Deserialize**: `std.json.parseFromSlice(T, allocator, data, options)`
parses JSON bytes into a typed struct, returning an error for
invalid input.
## Running
Prerequisites: `nats-server` running on `localhost:4222`.
```sh
nats-server &
zig build run-nbe-messaging-json
```
## Output
```
received valid payload: foo=bar, bar=27
received invalid payload: not json
```
## What's Happening
1. A `Payload` struct is defined with `foo` (string) and `bar` (int)
fields.
2. An instance is serialized to JSON using `Stringify.value` into a
stack-allocated buffer - no heap allocation needed.
3. The JSON bytes are published to the `greet` subject.
4. A second message with invalid content (`"not json"`) is published.
5. The receiver tries `parseFromSlice` on each message. The first
succeeds and prints the deserialized fields. The second fails
gracefully and prints the raw payload.
## Source
See [json.zig](json.zig) for the full example.
Based on [natsbyexample.com/examples/messaging/json](https://natsbyexample.com/examples/messaging/json/go).
================================================
FILE: doc/nats-by-example/messaging/Pub-Sub.md
================================================
# Publish-Subscribe
NATS implements publish-subscribe message distribution through subject-based
routing. Publishers send messages to named subjects. Subscribers express
interest in subjects (including wildcards) and receive matching messages.
The core guarantee is **at-most-once delivery**: if there is no subscriber
listening when a message is published, the message is silently discarded.
This is similar to UDP or MQTT QoS 0. For stronger delivery guarantees, see
JetStream.
## Wildcard Subscriptions
NATS supports two wildcard tokens in subscriptions:
- `*` matches a single token: `greet.*` matches `greet.joe`, `greet.pam`
- `>` matches one or more tokens: `greet.>` matches `greet.joe`,
`greet.joe.hello`
## Running
Prerequisites: `nats-server` running on `localhost:4222`.
```sh
nats-server &
zig build run-nbe-messaging-pub-sub
```
## Output
```
subscribed after a publish...
msg is null? true
msg data: "hello" on subject "greet.joe"
msg data: "hello" on subject "greet.pam"
msg data: "hello" on subject "greet.bob"
```
## What's Happening
1. A message is published to `greet.joe` **before** any subscription exists.
This message is lost - at-most-once delivery means no buffering.
2. A wildcard subscription on `greet.*` is created.
3. Attempting to receive returns `null` - the earlier message is gone.
4. Two messages are published to `greet.joe` and `greet.pam`. Both are
received because the subscription is now active and the wildcard matches.
5. A third message to `greet.bob` is also received via the same wildcard.
## Source
See [pub-sub.zig](pub-sub.zig) for the full example.
Based on [natsbyexample.com/examples/messaging/pub-sub](https://natsbyexample.com/examples/messaging/pub-sub/go).
================================================
FILE: doc/nats-by-example/messaging/README.md
================================================
# Messaging
Examples based on the [natsbyexample.com](https://natsbyexample.com/)
**Messaging** category, implemented in Zig using the nats.zig client.
## Building and Running
Prerequisites: a `nats-server` running on `localhost:4222`.
```sh
nats-server &
```
Each example is a standalone executable. Build and run with:
```sh
zig build run-nbe-messaging-
```
For example:
```sh
zig build run-nbe-messaging-pub-sub
zig build run-nbe-messaging-request-reply
```
## Examples
| Example | Description | Source |
|---------|-------------|--------|
| [Publish-Subscribe](Pub-Sub.md) | Subject-based pub/sub with wildcard routing and at-most-once delivery | [pub-sub.zig](pub-sub.zig) |
| [Request-Reply](Request-Reply.md) | RPC-style communication using temporary inbox subjects | [request-reply.zig](request-reply.zig) |
| [JSON for Message Payloads](Json.md) | Type-safe JSON serialization/deserialization with `std.json` | [json.zig](json.zig) |
| [Concurrent Message Processing](Concurrent.md) | Parallel message processing with `io.concurrent()` worker threads | [concurrent.zig](concurrent.zig) |
| [Iterating Over Multiple Subscriptions](Iterating-Multiple-Subscriptions.md) | Polling multiple subscriptions in a unified round-robin loop | [iterating-multiple-subscriptions.zig](iterating-multiple-subscriptions.zig) |
================================================
FILE: doc/nats-by-example/messaging/Request-Reply.md
================================================
# Request-Reply
The request-reply pattern enables RPC-style communication over NATS.
Under the hood, NATS implements this as an optimized pair of
publish-subscribe operations: the requester creates a temporary inbox
subject, subscribes to it, and publishes the request with a `reply_to`
header pointing at that inbox.
Unlike strict point-to-point protocols, multiple subscribers can
potentially respond to a request. The client receives the first reply
and discards the rest.
When no handler is subscribed, the server sends a "no responders"
notification (status 503) instead of silently timing out.
## Running
Prerequisites: `nats-server` running on `localhost:4222`.
```sh
nats-server &
zig build run-nbe-messaging-request-reply
```
## Output
```
hello, joe
hello, sue
hello, bob
no responders
```
## What's Happening
1. A subscription on `greet.*` handles incoming requests in a
background async task.
2. The handler extracts the name from the subject (`greet.joe` ->
`joe`) and responds with `"hello, joe"`.
3. Three requests are made - `greet.joe`, `greet.sue`, `greet.bob` -
each receiving a personalized greeting.
4. The handler subscription is unsubscribed.
5. A fourth request to `greet.joe` returns "no responders" because
no handler is listening anymore.
## Source
See [request-reply.zig](request-reply.zig) for the full example.
Based on [natsbyexample.com/examples/messaging/request-reply](https://natsbyexample.com/examples/messaging/request-reply/go).
================================================
FILE: doc/nats-by-example/messaging/concurrent.zig
================================================
//! Concurrent Message Processing
//!
//! By default, messages from a subscription are processed
//! sequentially. This example shows how to process messages
//! concurrently using multiple worker threads.
//!
//! The pattern: receive messages on the main thread, dispatch
//! them to concurrent workers via an Io.Queue, and collect
//! results. Each worker simulates variable processing time
//! with a random delay, causing messages to complete out of
//! their original order.
//!
//! Based on: https://natsbyexample.com/examples/messaging/concurrent/rust
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server
//!
//! Run with: zig build run-nbe-messaging-concurrent
const std = @import("std");
const nats = @import("nats");
const Io = std.Io;
const NUM_MSGS = 10;
const NUM_WORKERS = 3;
/// Work item passed from main thread to workers.
/// Contains a copy of the message data (the original
/// Message is freed after copying).
const WorkItem = struct {
data: [64]u8 = undefined,
len: usize = 0,
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
var stdout_buf: [4096]u8 = undefined;
var stdout_writer = Io.File.stdout().writer(
io,
&stdout_buf,
);
const stdout = &stdout_writer.interface;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
const sub = try client.subscribeSync("greet.*");
defer sub.deinit();
// Publish 10 messages
for (0..NUM_MSGS) |i| {
var buf: [32]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"hello {d}",
.{i},
) catch continue;
try client.publish("greet.joe", payload);
}
// Wait for messages to arrive
io.sleep(.fromMilliseconds(50), .awake) catch {};
// Receive all messages and copy data into work items.
// We copy because the Message backing buffer is freed
// on deinit, but workers need the data later.
var items: [NUM_MSGS]WorkItem = @splat(WorkItem{});
var received: usize = 0;
for (0..NUM_MSGS) |_| {
if (try sub.nextMsgTimeout(
1000,
)) |msg| {
defer msg.deinit();
const len = @min(msg.data.len, 64);
@memcpy(
items[received].data[0..len],
msg.data[0..len],
);
items[received].len = len;
received += 1;
}
}
// Dispatch work to 3 concurrent workers. Each worker
// gets a slice of the items array to process.
// io.concurrent() ensures true parallel execution.
const slice1_end = received / 3;
const slice2_end = (received * 2) / 3;
var w1 = try io.concurrent(processWorker, .{
io,
items[0..slice1_end],
});
defer w1.cancel(io);
var w2 = try io.concurrent(processWorker, .{
io,
items[slice1_end..slice2_end],
});
defer w2.cancel(io);
// Third worker runs on this thread (no extra thread needed)
processWorker(io, items[slice2_end..received]);
// Wait for concurrent workers to finish
w1.await(io);
w2.await(io);
try stdout.print(
"\nprocessed {d} messages concurrently\n",
.{received},
);
try stdout.flush();
}
/// Worker function that processes a slice of work items.
/// Each item is "processed" with a random delay to simulate
/// variable work, then printed. The random delays cause
/// messages to complete out of their original order.
fn processWorker(io: Io, items: []WorkItem) void {
const file_stdout = Io.File.stdout();
for (items) |item| {
// Random delay 0-100ms to simulate processing
var rnd: [1]u8 = undefined;
io.random(&rnd);
const delay_ms: i64 = @intCast(rnd[0] % 100);
io.sleep(
.fromMilliseconds(delay_ms),
.awake,
) catch {};
// Write directly to stdout (single write syscall
// per line avoids interleaved output)
var buf: [80]u8 = undefined;
const line = std.fmt.bufPrint(
&buf,
"received message: \"{s}\"\n",
.{item.data[0..item.len]},
) catch continue;
file_stdout.writeStreamingAll(io, line) catch {};
}
}
================================================
FILE: doc/nats-by-example/messaging/iterating-multiple-subscriptions.zig
================================================
//! Iterating Over Multiple Subscriptions
//!
//! NATS wildcards cover many routing cases, but sometimes you
//! need separate subscriptions - for example, you want
//! "transport.cars", "transport.planes", and "transport.ships"
//! but NOT "transport.spaceships".
//!
//! This example shows how to poll multiple subscriptions in
//! a unified loop using tryNext() - the Zig equivalent of
//! merging multiple async streams.
//!
//! Based on: https://natsbyexample.com/examples/messaging/iterating-multiple-subscriptions/rust
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server
//!
//! Run with: zig build run-nbe-messaging-iterating-multiple-subscriptions
const std = @import("std");
const nats = @import("nats");
const Io = std.Io;
const NUM_MSGS_PER_CATEGORY = 10;
const TOTAL_MSGS = NUM_MSGS_PER_CATEGORY * 3;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
var stdout_buf: [8192]u8 = undefined;
var stdout_writer = Io.File.stdout().writer(
io,
&stdout_buf,
);
const stdout = &stdout_writer.interface;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
// Create three separate subscriptions. We use ">"
// (multi-level wildcard) to match all sub-subjects.
const sub_cars = try client.subscribeSync("cars.>");
defer sub_cars.deinit();
const sub_planes = try client.subscribeSync("planes.>");
defer sub_planes.deinit();
const sub_ships = try client.subscribeSync("ships.>");
defer sub_ships.deinit();
// Publish 10 messages to each category
for (0..NUM_MSGS_PER_CATEGORY) |i| {
var buf: [64]u8 = undefined;
const cars_subj = std.fmt.bufPrint(
&buf,
"cars.{d}",
.{i},
) catch continue;
var payload_buf: [64]u8 = undefined;
const cars_payload = std.fmt.bufPrint(
&payload_buf,
"car number {d}",
.{i},
) catch continue;
try client.publish(cars_subj, cars_payload);
const planes_subj = std.fmt.bufPrint(
&buf,
"planes.{d}",
.{i},
) catch continue;
const planes_payload = std.fmt.bufPrint(
&payload_buf,
"plane number {d}",
.{i},
) catch continue;
try client.publish(planes_subj, planes_payload);
const ships_subj = std.fmt.bufPrint(
&buf,
"ships.{d}",
.{i},
) catch continue;
const ships_payload = std.fmt.bufPrint(
&payload_buf,
"ship number {d}",
.{i},
) catch continue;
try client.publish(ships_subj, ships_payload);
}
// Wait for messages to arrive
io.sleep(.fromMilliseconds(100), .awake) catch {};
// Poll all 3 subscriptions in round-robin fashion.
// tryNext() is non-blocking - returns null instantly if
// no message is available, letting us cycle to the next
// subscription without waiting.
const subs = [_]*nats.Client.Sub{
sub_cars,
sub_planes,
sub_ships,
};
var total: u32 = 0;
var idx: usize = 0;
var empty_cycles: u32 = 0;
while (total < TOTAL_MSGS) {
if (subs[idx].tryNextMsg()) |msg| {
defer msg.deinit();
total += 1;
empty_cycles = 0;
try stdout.print(
"received on {s}: {s}\n",
.{ msg.subject, msg.data },
);
}
idx = (idx + 1) % subs.len;
// Avoid busy-spinning when no messages are ready
if (idx == 0) {
empty_cycles += 1;
if (empty_cycles > 10) {
io.sleep(
.fromMilliseconds(10),
.awake,
) catch {};
}
}
// Safety: don't spin forever if messages are lost
if (empty_cycles > 100) break;
}
try stdout.print(
"\nreceived {d} messages from 3 subscriptions\n",
.{total},
);
try stdout.flush();
}
================================================
FILE: doc/nats-by-example/messaging/json.zig
================================================
//! JSON for Message Payloads
//!
//! NATS message payloads are opaque byte sequences - the application
//! decides how to serialize and deserialize them. JSON is a common
//! choice for its cross-language compatibility and readability.
//!
//! This example demonstrates:
//! - Defining a struct type for the message payload
//! - Serializing a struct to JSON using Stringify.value
//! - Receiving and deserializing JSON back to a struct
//! - Gracefully handling invalid JSON payloads
//!
//! Based on: https://natsbyexample.com/examples/messaging/json/go
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server
//!
//! Run with: zig build run-nbe-messaging-json
const std = @import("std");
const nats = @import("nats");
const Io = std.Io;
/// Application payload type. Zig's std.json will serialize
/// field names directly ("foo", "bar").
const Payload = struct {
foo: []const u8,
bar: i32,
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
var stdout_buf: [4096]u8 = undefined;
var stdout_writer = Io.File.stdout().writer(
io,
&stdout_buf,
);
const stdout = &stdout_writer.interface;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
const sub = try client.subscribeSync("greet");
defer sub.deinit();
// Create a payload and serialize it to JSON.
// Stringify.value writes JSON into a fixed buffer writer -
// no heap allocation needed.
const payload = Payload{ .foo = "bar", .bar = 27 };
var json_buf: [256]u8 = undefined;
var json_writer = Io.Writer.fixed(&json_buf);
try std.json.Stringify.value(
payload,
.{},
&json_writer,
);
const json = json_writer.buffered();
// Publish the valid JSON payload
try client.publish("greet", json);
// Publish an invalid (non-JSON) payload
try client.publish("greet", "not json");
// Receive the first message - valid JSON.
// parseFromSlice deserializes it back into a Payload struct.
if (try sub.nextMsgTimeout(1000)) |msg| {
defer msg.deinit();
if (std.json.parseFromSlice(
Payload,
allocator,
msg.data,
.{},
)) |parsed| {
defer parsed.deinit();
try stdout.print(
"received valid payload: " ++
"foo={s}, bar={d}\n",
.{ parsed.value.foo, parsed.value.bar },
);
} else |_| {
try stdout.print(
"received invalid payload: {s}\n",
.{msg.data},
);
}
}
// Receive the second message - invalid JSON.
// parseFromSlice returns an error, so we print raw data.
if (try sub.nextMsgTimeout(1000)) |msg| {
defer msg.deinit();
if (std.json.parseFromSlice(
Payload,
allocator,
msg.data,
.{},
)) |parsed| {
defer parsed.deinit();
try stdout.print(
"received valid payload: " ++
"foo={s}, bar={d}\n",
.{ parsed.value.foo, parsed.value.bar },
);
} else |_| {
try stdout.print(
"received invalid payload: {s}\n",
.{msg.data},
);
}
}
try stdout.flush();
}
================================================
FILE: doc/nats-by-example/messaging/pub-sub.zig
================================================
//! Publish-Subscribe
//!
//! This example demonstrates the core NATS publish-subscribe pattern.
//! Pub/Sub is the fundamental messaging pattern in NATS where publishers
//! send messages to subjects and subscribers receive them.
//!
//! Key concepts shown:
//! - At-most-once delivery: if no subscriber is listening, messages
//! are silently discarded (like UDP, or MQTT QoS 0)
//! - Wildcard subscriptions: "greet.*" matches "greet.joe",
//! "greet.pam", etc.
//! - Subject-based routing: messages are routed by their subject
//!
//! Based on: https://natsbyexample.com/examples/messaging/pub-sub/go
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server
//!
//! Run with: zig build run-nbe-messaging-pub-sub
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
// Set up buffered stdout writer for output
var stdout_buf: [4096]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(
io,
&stdout_buf,
);
const stdout = &stdout_writer.interface;
// Connect to NATS server
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
// Publish a message BEFORE subscribing.
// This message will be lost because NATS provides
// at-most-once delivery - there are no subscribers
// listening on this subject yet.
try client.publish("greet.joe", "hello");
// Subscribe using a wildcard subject. "greet.*" will
// match any subject with exactly one token after "greet.",
// for example: "greet.joe", "greet.pam", "greet.bob"
const sub = try client.subscribeSync("greet.*");
defer sub.deinit();
// Try to receive the message published before subscribing.
// The short timeout (10ms) confirms no message is available -
// it was published before our subscription existed.
const msg = try sub.nextMsgTimeout(10);
try stdout.print("subscribed after a publish...\n", .{});
try stdout.print("msg is null? {}\n", .{msg == null});
try stdout.flush();
// Now publish two messages AFTER subscribing.
// These will be received because the subscription is active.
try client.publish("greet.joe", "hello");
try client.publish("greet.pam", "hello");
// Receive both messages. The wildcard subscription
// matches both "greet.joe" and "greet.pam".
if (try sub.nextMsgTimeout(1000)) |m| {
defer m.deinit();
try stdout.print(
"msg data: \"{s}\" on subject \"{s}\"\n",
.{ m.data, m.subject },
);
}
if (try sub.nextMsgTimeout(1000)) |m| {
defer m.deinit();
try stdout.print(
"msg data: \"{s}\" on subject \"{s}\"\n",
.{ m.data, m.subject },
);
}
// Publish one more to a different subject that still
// matches our wildcard pattern.
try client.publish("greet.bob", "hello");
if (try sub.nextMsgTimeout(1000)) |m| {
defer m.deinit();
try stdout.print(
"msg data: \"{s}\" on subject \"{s}\"\n",
.{ m.data, m.subject },
);
}
try stdout.flush();
}
================================================
FILE: doc/nats-by-example/messaging/request-reply.zig
================================================
//! Request-Reply
//!
//! The request-reply pattern allows a client to send a request and
//! wait for a response. Under the hood, NATS implements this as an
//! optimized pair of publish-subscribe operations using an auto-
//! generated inbox subject for the reply.
//!
//! Key concepts shown:
//! - Subscribing to handle requests in a background task
//! - Extracting info from the subject (e.g. a name)
//! - Responding to requests with msg.respond()
//! - Detecting "no responders" when no handler is available
//!
//! Based on: https://natsbyexample.com/examples/messaging/request-reply/go
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server
//!
//! Run with: zig build run-nbe-messaging-request-reply
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
var stdout_buf: [4096]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(
io,
&stdout_buf,
);
const stdout = &stdout_writer.interface;
// Connect to NATS
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
// Subscribe to "greet.*" to handle incoming requests.
// The handler extracts the name from the subject and
// responds with a greeting.
const sub = try client.subscribeSync("greet.*");
defer sub.deinit();
// Run the request handler in a background async task.
// It will process exactly 3 requests then exit.
var handler = io.async(handleRequests, .{
client,
sub,
});
defer handler.cancel(io);
// Give the subscription time to register on the server
io.sleep(.fromMilliseconds(50), .awake) catch {};
// Send 3 requests - each will be handled by our
// background task and we'll get a personalized greeting.
if (try client.request(
"greet.joe",
"",
1000,
)) |reply| {
defer reply.deinit();
if (reply.isNoResponders()) {
try stdout.print("no responders\n", .{});
} else {
try stdout.print("{s}\n", .{reply.data});
}
}
if (try client.request(
"greet.sue",
"",
1000,
)) |reply| {
defer reply.deinit();
if (reply.isNoResponders()) {
try stdout.print("no responders\n", .{});
} else {
try stdout.print("{s}\n", .{reply.data});
}
}
if (try client.request(
"greet.bob",
"",
1000,
)) |reply| {
defer reply.deinit();
if (reply.isNoResponders()) {
try stdout.print("no responders\n", .{});
} else {
try stdout.print("{s}\n", .{reply.data});
}
}
// Unsubscribe the handler so no one is listening anymore
try sub.unsubscribe();
// This request will fail with "no responders" because
// we just unsubscribed the only handler.
if (try client.request(
"greet.joe",
"",
1000,
)) |reply| {
defer reply.deinit();
if (reply.isNoResponders()) {
try stdout.print("no responders\n", .{});
} else {
try stdout.print("{s}\n", .{reply.data});
}
}
try stdout.flush();
}
/// Background handler that processes incoming requests.
/// Extracts the name from the subject ("greet.joe" -> "joe")
/// and responds with "hello, ".
fn handleRequests(
client: *nats.Client,
sub: *nats.Client.Sub,
) void {
for (0..3) |_| {
const req = sub.nextMsgTimeout(
2000,
) catch return;
if (req) |r| {
defer r.deinit();
// "greet.joe" -> "joe"
const name = r.subject[6..];
var buf: [64]u8 = undefined;
const reply = std.fmt.bufPrint(
&buf,
"hello, {s}",
.{name},
) catch return;
r.respond(client, reply) catch {};
}
}
}
================================================
FILE: src/Client.zig
================================================
//! NATS Client
//!
//! High-level client API for connecting to NATS servers.
//! Uses std.Io for native async I/O with concurrent subscription support.
//!
//! Key features:
//! - Dedicated reader task routes messages to per-subscription Io.Queue
//! - Multiple subscriptions can call nextMsg() concurrently
//! - Reader task starts automatically on connect
//! - Colorblind async: works blocking or async based on Io implementation
//!
//! Connection-scoped: Allocator, Io, Reader, Writer stored for lifetime.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Io = std.Io;
const net = Io.net;
const tls = std.crypto.tls;
const Certificate = std.crypto.Certificate;
const protocol = @import("protocol.zig");
const Parser = protocol.Parser;
const ServerInfo = protocol.ServerInfo;
const connection = @import("connection.zig");
const State = connection.State;
const pubsub = @import("pubsub.zig");
const subscription_mod = @import("pubsub/subscription.zig");
const memory = @import("memory.zig");
const SidMap = memory.SidMap;
const TieredSlab = memory.TieredSlab;
const SpscQueue = @import("sync/spsc_queue.zig").SpscQueue;
const byte_ring = @import("sync/byte_ring.zig");
pub const ByteRing = byte_ring.ByteRing;
const RING_HDR_SIZE = byte_ring.HDR_SIZE;
const SpinLock = @import("sync/spin_lock.zig").SpinLock;
const dbg = @import("dbg.zig");
const defaults = @import("defaults.zig");
const events_mod = @import("events.zig");
pub const Event = events_mod.Event;
pub const EventHandler = events_mod.EventHandler;
const headers = @import("protocol/headers.zig");
pub const HeaderEntry = headers.Entry;
pub const HeaderMap = protocol.HeaderMap;
const nkey_auth = @import("auth.zig");
const creds_auth = nkey_auth.creds;
const Client = @This();
/// Type-erased message handler for callback subscriptions.
/// Uses comptime vtable pattern (same as EventHandler).
///
/// ```zig
/// const MyHandler = struct {
/// counter: *u32,
/// pub fn onMessage(self: *@This(), msg: *const Message) void {
/// self.counter.* += 1;
/// }
/// };
/// var handler = MyHandler{ .counter = &count };
/// const sub = try client.subscribe(
/// "subject", MsgHandler.init(MyHandler, &handler),
/// );
/// ```
pub const MsgHandler = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
onMessage: *const fn (*anyopaque, *const Message) void,
};
/// Create handler from concrete type using comptime.
pub fn init(comptime T: type, ptr: *T) MsgHandler {
const gen = struct {
fn onMessage(p: *anyopaque, msg: *const Message) void {
const self: *T = @ptrCast(@alignCast(p));
self.onMessage(msg);
}
};
return .{
.ptr = ptr,
.vtable = &.{ .onMessage = gen.onMessage },
};
}
/// Dispatch message to handler.
pub fn dispatch(
self: MsgHandler,
msg: *const Message,
) void {
self.vtable.onMessage(self.ptr, msg);
}
};
/// Checks if a file descriptor is valid using fcntl.
/// Returns false for negative fds or closed fds.
/// Used to guard shutdown/close calls that panic on
/// BADF in debug mode (Io.Threaded treats as bug).
fn isValidFd(fd: std.posix.fd_t) bool {
if (fd < 0) return false;
const F_GETFD = 1;
const rc = std.posix.system.fcntl(fd, F_GETFD, 0);
// fcntl returns the fd flags on success, or a
// large unsigned value (wrapped errno) on failure
return rc < 0x1000;
}
/// Gets current monotonic time in nanoseconds.
fn getNowNs(io: Io) u64 {
const ts = Io.Timestamp.now(io, .awake);
return @intCast(ts.nanoseconds);
}
/// Message received on a subscription. Call deinit() to free.
pub const Message = struct {
subject: []const u8,
sid: u64,
reply_to: ?[]const u8,
data: []const u8,
headers: ?[]const u8,
allocator: Allocator = undefined,
owned: bool = true,
/// Single backing buffer (all slices point into this).
backing_buf: ?[]u8 = null,
/// Return queue for thread-safe deallocation (reader thread frees).
return_queue: ?*SpscQueue([]u8) = null,
/// Spinlock for multi-thread return_queue.push() safety.
return_lock: ?*SpinLock = null,
/// Frees message data. Pushes to return queue for slab-allocated msgs.
/// Thread-safe: return_lock serializes concurrent push().
// REVIEWED(2025-03): SPSC queue used as MPSC here is safe.
// The spinlock's lock/unlock provides acquire/release ordering,
// ensuring each thread sees prior push() head updates.
pub fn deinit(self: *const Message) void {
if (!self.owned) return;
if (self.backing_buf) |buf| {
assert(self.return_queue != null);
const rq = self.return_queue.?;
if (self.return_lock) |sl| {
sl.lock();
defer sl.unlock();
while (!rq.push(buf)) {
sl.unlock();
std.Thread.yield() catch {};
sl.lock();
}
} else {
while (!rq.push(buf)) {
std.Thread.yield() catch {};
}
}
return;
}
const allocator = self.allocator;
allocator.free(self.subject);
allocator.free(self.data);
if (self.reply_to) |rt| allocator.free(rt);
if (self.headers) |h| allocator.free(h);
}
/// Sends a reply to this message using the reply_to subject.
/// Convenience method for request/reply pattern.
/// Returns error.NoReplyTo if message has no reply_to subject.
pub fn respond(
self: *const Message,
client: *Client,
payload: []const u8,
) !void {
const reply_to = self.reply_to orelse return error.NoReplyTo;
assert(reply_to.len > 0);
try client.publish(reply_to, payload);
}
/// Returns the total size of the message in bytes.
/// Includes subject, data, reply_to, and headers.
pub fn size(self: *const Message) usize {
var total: usize = self.subject.len + self.data.len;
if (self.reply_to) |rt| total += rt.len;
if (self.headers) |h| total += h.len;
return total;
}
/// Extracts HTTP-like status code from headers (on-demand parsing).
/// Returns null if no headers or no status code present.
/// Common codes: 503 (no responders), 408 (timeout), 404 (not found).
pub fn status(self: *const Message) ?u16 {
const hdrs = self.headers orelse return null;
return headers.extractStatus(hdrs);
}
/// Returns true if this is a no-responders message (status 503).
/// Used to detect when a request has no available responders.
pub fn isNoResponders(self: *const Message) bool {
return self.status() == 503;
}
};
/// Per-request waiter for the response multiplexer.
///
/// Lives on the request()'s stack. The dispatcher fills `msg`
/// with a cloned response Message and sets `done`; the request
/// task spin-yields on `done` until the response arrives or the
/// timeout fires.
pub const RespWaiter = struct {
msg: ?Message = null,
done: std.atomic.Value(bool) =
std.atomic.Value(bool).init(false),
};
/// Lazy response multiplexer for request/reply.
///
/// Replaces per-request SUB/UNSUB churn with one wildcard inbox
/// subscription per connection. The first request triggers
/// ensureRespMux which subscribes to "_INBOX..*" and does a
/// PING/PONG round-trip to confirm server registration; every
/// subsequent request just registers a waiter in `map` and
/// publishes. The dispatcher (handler on the wildcard sub) routes
/// incoming replies by extracting the token suffix from the
/// message subject and waking the matching waiter.
///
/// Owns its own mutex (NOT sub_mutex) so the demuxer cannot
/// contend with subscribe/unsubscribe.
pub const RespMux = struct {
/// Back-reference to the owning client. Set during the first
/// ensureRespMux call so the handler vtable can resolve back
/// to client.io and client.allocator.
client: ?*Client = null,
/// "_INBOX.." with trailing dot. Allocated on first
/// request, freed by closeRespMux.
prefix: ?[]u8 = null,
prefix_len: usize = 0,
/// Wildcard subscription "_INBOX..*". Lives for the
/// connection lifetime; cleaned up by closeRespMux.
sub: ?*Subscription = null,
/// Active waiters keyed by the 8-char base62 token.
/// Token slices borrow from the requester's stack buffer.
map: std.StringHashMapUnmanaged(*RespWaiter) = .empty,
/// Monotonic counter for unique token generation. Atomic so
/// concurrent request() calls can mint tokens without locking.
next_token: std.atomic.Value(u64) =
std.atomic.Value(u64).init(0),
/// Dedicated mutex protecting `map`. Separate from sub_mutex
/// so the dispatcher cannot deadlock against subscribe paths.
mutex: Io.Mutex = .init,
/// Set true once prefix/sub are valid. Acquire-loaded on the
/// fast path of every request() to skip re-initialization.
initialized: std.atomic.Value(bool) =
std.atomic.Value(bool).init(false),
/// Handler hook called by the standard callback drain task
/// for every reply arriving on the wildcard subscription.
/// Extracts the token suffix from msg.subject, looks up the
/// matching waiter under `mutex`, clones the message into the
/// waiter, and signals `done`. Late deliveries (waiter already
/// removed by timeout) are silently dropped.
///
/// IMPORTANT: clone+write happens INSIDE the lock so the
/// request() cleanup defer (which acquires the same mutex)
/// blocks until we are done writing to the waiter. This
/// prevents use-after-free on the stack-allocated waiter.
pub fn onMessage(
self: *RespMux,
msg: *const Message,
) void {
const client = self.client orelse return;
assert(self.prefix_len > 0);
const subj = msg.subject;
if (subj.len <= self.prefix_len) return;
const token = subj[self.prefix_len..];
const io = client.io;
self.mutex.lockUncancelable(io);
defer self.mutex.unlock(io);
const entry = self.map.fetchRemove(token);
const waiter = if (entry) |e| e.value else return;
const cloned = cloneMessageContents(
client.allocator,
msg,
) catch {
waiter.done.store(true, .release);
return;
};
waiter.msg = cloned;
waiter.done.store(true, .release);
}
};
/// Client connection options.
///
/// All fields have sensible defaults. Common customizations:
/// - name: Client identifier visible in server logs
/// - user/pass or auth_token: Authentication credentials
/// - reader_buffer_size/writer_buffer_size: tune protocol buffers
/// - sub_queue_size: Messages buffered per subscription (default 1024)
pub const Options = struct {
/// Client name for identification.
name: ?[]const u8 = null,
/// Enable verbose mode.
verbose: bool = false,
/// Enable pedantic mode.
pedantic: bool = false,
/// Username for auth.
user: ?[]const u8 = null,
/// Password for auth.
pass: ?[]const u8 = null,
/// Auth token.
auth_token: ?[]const u8 = null,
/// Connection timeout in nanoseconds.
connect_timeout_ns: u64 = defaults.Connection.timeout_ns,
/// Per-subscription queue size (messages buffered before dropping).
sub_queue_size: u32 = defaults.Memory.queue_size.value(),
/// Echo messages back to sender (default true).
echo: bool = true,
/// Enable message headers support.
headers: bool = true,
/// Request no_responders notification for requests.
no_responders: bool = true,
/// Require TLS connection.
tls_required: bool = false,
// TLS OPTIONS
/// Path to CA certificate file (PEM). Null = use system CAs.
tls_ca_file: ?[]const u8 = null,
/// Path to client certificate file for mTLS (PEM).
tls_cert_file: ?[]const u8 = null,
/// Path to client private key file for mTLS (PEM).
tls_key_file: ?[]const u8 = null,
/// Skip server certificate verification (INSECURE - testing only).
tls_insecure_skip_verify: bool = false,
/// Perform TLS handshake before NATS protocol (required by some proxies).
tls_handshake_first: bool = false,
/// NKey seed for authentication.
nkey_seed: ?[]const u8 = null,
/// NKey seed file path (alternative to nkey_seed).
nkey_seed_file: ?[]const u8 = null,
/// NKey public key for callback-based signing.
nkey_pubkey: ?[]const u8 = null,
/// NKey signing callback (returns true on success).
nkey_sign_fn: ?*const fn (nonce: []const u8, sig: *[64]u8) bool = null,
/// JWT for authentication.
jwt: ?[]const u8 = null,
/// Credentials file path (.creds file with JWT + NKey seed).
/// Mutually exclusive with jwt/nkey_seed options.
creds_file: ?[]const u8 = null,
/// Credentials content (alternative to file path).
/// Use when credentials are loaded from environment/memory.
creds: ?[]const u8 = null,
/// Read buffer size. Must be >= max message size you expect (1MB).
reader_buffer_size: usize = defaults.Connection.reader_buffer_size,
/// Write buffer size. Smaller values force more frequent flushes.
writer_buffer_size: usize = defaults.Connection.writer_buffer_size,
/// TCP receive buffer size hint. Larger values allow more messages to
/// queue in the kernel before backpressure kicks in. Default 1MB.
/// Set to 0 to use system default.
tcp_rcvbuf: u32 = defaults.Connection.tcp_rcvbuf,
// RECONNECTION OPTIONS
/// Enable automatic reconnection on disconnect.
reconnect: bool = defaults.Reconnection.enabled,
/// Maximum reconnection attempts (0 = infinite).
max_reconnect_attempts: u32 = defaults.Reconnection.max_attempts,
/// Initial wait between reconnect attempts (ms).
reconnect_wait_ms: u32 = defaults.Reconnection.wait_ms,
/// Maximum wait with exponential backoff (ms).
reconnect_wait_max_ms: u32 = defaults.Reconnection.wait_max_ms,
/// Jitter percentage for backoff (0-50).
reconnect_jitter_percent: u8 = defaults.Reconnection.jitter_percent,
/// Custom reconnect delay callback. If set, overrides default exponential
/// backoff. Called with attempt number (1-based), returns delay in ms.
/// Example: `fn(attempt: u32) u32 { return attempt * 1000; }`
custom_reconnect_delay: ?*const fn (attempt: u32) u32 = null,
/// Discover servers from INFO connect_urls.
discover_servers: bool = defaults.Reconnection.discover_servers,
/// Size of pending buffer for publishes during reconnect.
/// Set to 0 to disable buffering (publish returns error during reconnect).
pending_buffer_size: usize = defaults.Reconnection.pending_buffer_size,
// PING/PONG HEALTH CHECK
/// Interval between client-initiated PINGs (ms). 0 = disable.
ping_interval_ms: u32 = defaults.Connection.ping_interval_ms,
/// Max outstanding PINGs before connection is considered stale.
max_pings_outstanding: u8 = defaults.Connection.max_pings_outstanding,
// ERROR REPORTING
/// Messages between rate-limited error notifications.
/// After first error (alloc_failed, protocol_error), subsequent errors
/// only notify every N messages. Prevents event queue flooding.
error_notify_interval_msgs: u64 =
defaults.ErrorReporting.notify_interval_msgs,
// EVENT CALLBACKS
/// Event handler for connection lifecycle callbacks (optional).
/// Use EventHandler.init(T, &handler) to create from a handler struct.
event_handler: ?EventHandler = null,
// INBOX/REQUEST OPTIONS
/// Custom prefix for inbox subjects. Default is "_INBOX".
/// Used for request/reply pattern inbox generation.
inbox_prefix: []const u8 = "_INBOX",
// CONNECTION BEHAVIOR
/// Retry connection on initial connect failure (before returning error).
/// When true, connect() will retry using reconnect settings.
retry_on_failed_connect: bool = false,
/// Don't randomize server order for connection attempts.
/// When true, servers are tried in the order provided.
no_randomize: bool = false,
/// Ignore servers discovered via cluster INFO.
/// Only use explicitly configured servers.
ignore_discovered_servers: bool = false,
/// Default timeout for drain operations (ms).
drain_timeout_ms: u32 = 30_000,
/// Default timeout for flush operations (ms).
flush_timeout_ms: u32 = 10_000,
// ADDITIONAL SERVERS
/// Additional server URLs for reconnection pool.
/// These are added to the server pool after the primary URL.
/// Max MAX_SERVERS total (see connection/server_pool.zig).
servers: ?[]const []const u8 = null,
};
/// Connection statistics.
/// Thread ownership: io_task exclusively writes msgs_in/bytes_in.
/// msgs_out/bytes_out use atomics for multi-thread publish safety.
pub const Statistics = struct {
/// Total messages received (written by io_task only).
msgs_in: u64 = 0,
/// Total messages sent (atomic: multi-thread publish).
msgs_out: std.atomic.Value(u64) =
std.atomic.Value(u64).init(0),
/// Total bytes received (written by io_task only).
bytes_in: u64 = 0,
/// Total bytes sent (atomic: multi-thread publish).
bytes_out: std.atomic.Value(u64) =
std.atomic.Value(u64).init(0),
/// Number of reconnects.
reconnects: std.atomic.Value(u32) =
std.atomic.Value(u32).init(0),
/// Total successful connections (initial + reconnects).
connects: std.atomic.Value(u32) =
std.atomic.Value(u32).init(0),
/// Returns a snapshot of stats with atomics loaded.
pub fn snapshot(self: *const Statistics) StatsSnapshot {
return .{
.msgs_in = self.msgs_in,
.msgs_out = self.msgs_out.load(.monotonic),
.bytes_in = self.bytes_in,
.bytes_out = self.bytes_out.load(.monotonic),
.reconnects = self.reconnects.load(.monotonic),
.connects = self.connects.load(.monotonic),
};
}
};
/// Plain stats snapshot (no atomics). Returned by stats().
pub const StatsSnapshot = struct {
msgs_in: u64 = 0,
msgs_out: u64 = 0,
bytes_in: u64 = 0,
bytes_out: u64 = 0,
reconnects: u32 = 0,
connects: u32 = 0,
};
/// Debug counters for io_task buffer operations.
/// Only incremented when dbg.enabled.
/// Written exclusively by io_task thread, safe to read after deinit.
pub const IoTaskStats = struct {
/// Number of tryFillBuffer() calls.
fill_calls: u64 = 0,
/// Cumulative bytes already buffered (before read).
fill_buffered_hits: u64 = 0,
/// Poll timeouts (no data available).
fill_poll_timeouts: u64 = 0,
/// Successful socket reads.
fill_read_success: u64 = 0,
};
/// Subscription backup for restoration after reconnect.
/// Stores essential subscription state with inline buffers.
pub const SubBackup = struct {
sid: u64 = 0,
subject_buf: [256]u8 = undefined,
subject_len: u8 = 0,
queue_group_buf: [64]u8 = undefined,
queue_group_len: u8 = 0,
/// Get subject as slice.
pub fn getSubject(self: *const SubBackup) []const u8 {
return self.subject_buf[0..self.subject_len];
}
/// Get queue group as optional slice.
pub fn queueGroup(self: *const SubBackup) ?[]const u8 {
if (self.queue_group_len == 0) return null;
return self.queue_group_buf[0..self.queue_group_len];
}
};
/// Result of drain operation.
pub const DrainResult = struct {
/// Count of UNSUB commands that failed to encode.
unsub_failures: u16 = 0,
/// True if final flush failed (data may not have reached server).
flush_failed: bool = false,
/// Returns true if drain completed without any failures.
pub fn isClean(self: DrainResult) bool {
return self.unsub_failures == 0 and !self.flush_failed;
}
};
/// Subscribe command data (used by restoreSubscriptions).
pub const SubscribeCmd = struct {
sid: u64,
subject: []const u8,
queue_group: ?[]const u8,
};
/// Parse result for NATS URL.
pub const ParsedUrl = struct {
host: []const u8,
port: u16,
user: ?[]const u8,
pass: ?[]const u8,
use_tls: bool,
};
/// Fixed subscription limits (from defaults.zig).
pub const MAX_SUBSCRIPTIONS: u16 = defaults.Client.max_subscriptions;
pub const SIDMAP_CAPACITY: u32 = defaults.Client.sidmap_capacity;
/// Default queue size per subscription (messages buffered before dropping).
pub const DEFAULT_QUEUE_SIZE: u32 = defaults.Memory.queue_size.value();
comptime {
assert(SIDMAP_CAPACITY >= MAX_SUBSCRIPTIONS);
}
/// Parses a NATS URL like nats://user:pass@host:port or tls://host:port
pub fn parseUrl(url: []const u8) error{InvalidUrl}!ParsedUrl {
if (url.len == 0) return error.InvalidUrl;
var remaining = url;
var use_tls = false;
if (std.mem.startsWith(u8, remaining, "tls://")) {
remaining = remaining[6..];
use_tls = true;
} else if (std.mem.startsWith(u8, remaining, "nats://")) {
remaining = remaining[7..];
}
var user: ?[]const u8 = null;
var pass: ?[]const u8 = null;
if (std.mem.indexOf(u8, remaining, "@")) |at_pos| {
const auth = remaining[0..at_pos];
remaining = remaining[at_pos + 1 ..];
if (std.mem.indexOf(u8, auth, ":")) |colon_pos| {
user = auth[0..colon_pos];
pass = auth[colon_pos + 1 ..];
} else {
user = auth;
}
}
if (remaining.len == 0) return error.InvalidUrl;
var host: []const u8 = undefined;
var port: u16 = 4222;
if (remaining[0] == '[') {
const end = std.mem.indexOfScalar(u8, remaining, ']') orelse
return error.InvalidUrl;
host = remaining[1..end];
const after = remaining[end + 1 ..];
if (after.len > 0) {
if (after[0] != ':' or after.len == 1) return error.InvalidUrl;
port = std.fmt.parseInt(u16, after[1..], 10) catch {
return error.InvalidUrl;
};
}
} else {
var colon_count: u8 = 0;
var colon_pos: usize = 0;
for (remaining, 0..) |c, i| {
if (c == '[' or c == ']') return error.InvalidUrl;
if (c == ':') {
colon_count += 1;
colon_pos = i;
}
}
if (colon_count == 1) {
host = remaining[0..colon_pos];
if (colon_pos + 1 >= remaining.len) return error.InvalidUrl;
port = std.fmt.parseInt(u16, remaining[colon_pos + 1 ..], 10) catch {
return error.InvalidUrl;
};
} else {
host = remaining;
}
}
if (host.len == 0) return error.InvalidUrl;
assert(host.len > 0);
if (port == 0) return error.InvalidUrl;
return .{
.host = host,
.port = port,
.user = user,
.pass = pass,
.use_tls = use_tls,
};
}
fn connectToHost(io: Io, host: []const u8, port: u16) !net.Stream {
if (net.IpAddress.resolve(io, host, port)) |address| {
return net.IpAddress.connect(&address, io, .{
.mode = .stream,
.protocol = .tcp,
}) catch return error.ConnectionFailed;
} else |_| {}
const hostname = net.HostName.init(host) catch {
return error.InvalidAddress;
};
return net.HostName.connect(hostname, io, port, .{
.mode = .stream,
.protocol = .tcp,
}) catch return error.ConnectionFailed;
}
/// Subscription type alias.
pub const Sub = Subscription;
io: Io,
allocator: Allocator,
stream: net.Stream,
reader: net.Stream.Reader,
writer: net.Stream.Writer,
/// Active reader interface (TCP or TLS). Set once at connection, used by io_task.
active_reader: *Io.Reader = undefined,
/// Active writer interface (TCP or TLS). Set once at connection, used by io_task.
active_writer: *Io.Writer = undefined,
options: Options,
read_buffer: []u8,
write_buffer: []u8,
sidmap: SidMap,
sidmap_keys: [SIDMAP_CAPACITY]u64,
sidmap_vals: [SIDMAP_CAPACITY]u16,
free_slots: [MAX_SUBSCRIPTIONS]u16,
parser: Parser = .{},
server_info: ?ServerInfo = null,
state: State = .connecting,
sub_ptrs: [MAX_SUBSCRIPTIONS]?*Sub = [_]?*Sub{null} ** MAX_SUBSCRIPTIONS,
free_count: u16 = MAX_SUBSCRIPTIONS,
next_sid: u64 = 1,
/// Serializes reader-task routing with unsubscribe/deinit so a
/// loaded subscription pointer cannot be freed while in use.
read_mutex: Io.Mutex = .init,
statistics: Statistics = .{},
// Thread-safety mutexes for multi-thread publish/subscribe.
// Lock ordering: sub_mutex -> read_mutex -> write_mutex.
// publish_mutex and return_lock are independent.
/// Serializes multi-thread publish (encode-to-ring path).
publish_mutex: Io.Mutex = .init,
/// Serializes multi-thread subscribe/unsubscribe bookkeeping.
sub_mutex: Io.Mutex = .init,
/// Lazy response multiplexer for request/reply.
/// Owns its own mutex; does NOT share sub_mutex.
resp_mux: RespMux = .{},
/// Spinlock for multi-thread return_queue.push() in msg.deinit().
/// Atomic spinlock (not Io.Mutex) because Message.deinit() has no io.
return_lock: SpinLock = .{},
// Connection diagnostics
tcp_nodelay_set: bool = false,
tcp_rcvbuf_set: bool = false,
// Fast path cache for single-subscription case
cached_sub: ?*Sub = null,
// Cached max_payload from server_info
max_payload: usize = 1024 * 1024,
// Slab allocator for message buffers
tiered_slab: TieredSlab = undefined,
// Return queue for cross-thread buffer deallocation (main -> reader thread)
// Main thread pushes used buffers here, io_task drains and frees to slab
return_queue: SpscQueue([]u8) = undefined,
return_queue_buf: [][]u8 = undefined,
// Reconnection state
server_pool: connection.ServerPool = undefined,
server_pool_initialized: bool = false,
sub_backups: [MAX_SUBSCRIPTIONS]SubBackup =
[_]SubBackup{.{}} ** MAX_SUBSCRIPTIONS,
sub_backup_count: u16 = 0,
reconnect_attempt: u32 = 0,
original_url: [256]u8 = undefined,
original_url_len: u8 = 0,
// Pending buffer for publishes during reconnect
pending_buffer: ?[]u8 = null,
pending_buffer_pos: usize = 0,
pending_buffer_capacity: usize = 0,
// PING/PONG health check state (atomics for cross-thread access)
// Main thread reads during health check (~100ms), io_task writes on PONG.
// Uses monotonic ordering - exact timing not critical, eventual visibility suffices.
last_ping_sent_ns: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
last_pong_received_ns: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
pings_outstanding: std.atomic.Value(u8) = std.atomic.Value(u8).init(0),
/// Auto-flush signal: set by publish(), cleared by io_task after flush.
flush_requested: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
/// Lock-free publish ring buffer. Producer: main thread, Consumer: io_task.
/// Publishes encode directly into this ring; io_task drains to socket.
publish_ring: ByteRing = undefined,
publish_ring_buf: ?[]u8 = null,
// Debug counters for io_task (only used when dbg.enabled)
io_task_stats: IoTaskStats = .{},
// Error rate-limiting state (written by io_task only)
/// Count of protocol parse errors encountered.
protocol_errors: u64 = 0,
/// msgs_in value when protocol_error event was last pushed (rate-limit).
last_parse_error_notified_at: u64 = 0,
// Last async error tracking (written by io_task,
// cleared by user via clearLastError)
/// Last async error that occurred on the connection.
last_error: ?anyerror = null,
/// Message associated with last error (inline buffer, no allocation).
last_error_msg: [256]u8 = undefined,
/// Length of last_error_msg content.
last_error_msg_len: u8 = 0,
// Background I/O task infrastructure
write_mutex: Io.Mutex = .init,
/// Future for background I/O task (for proper cancellation in deinit).
io_task_future: ?Io.Future(void) = null,
// Event callback infrastructure
/// Event queue for io_task -> callback_task communication.
/// SpscQueue for non-blocking push from io_task hot path.
event_queue: ?*SpscQueue(Event) = null,
/// Serializes event producers so the SPSC queue still sees a single writer.
event_queue_mutex: Io.Mutex = .init,
/// Buffer backing the event queue.
event_queue_buf: ?[]Event = null,
/// Future for callback task (dispatches events to user handler).
callback_task_future: ?Io.Future(void) = null,
/// Event handler (copied from options for callback_task access).
event_handler: ?EventHandler = null,
/// Flag to track if lame duck event has been fired.
lame_duck_notified: bool = false,
// TLS state
/// TLS client instance (owns decryption state).
tls_client: ?tls.Client = null,
/// TLS read buffer (must be at least tls.Client.min_buffer_len).
tls_read_buffer: ?[]u8 = null,
/// TLS write buffer (must be at least tls.Client.min_buffer_len).
tls_write_buffer: ?[]u8 = null,
/// CA certificate bundle for verification.
ca_bundle: ?Certificate.Bundle = null,
ca_bundle_lock: Io.RwLock = .init,
/// Whether TLS is enabled for this connection.
use_tls: bool = false,
/// Host for TLS SNI and certificate verification.
tls_host: [256]u8 = undefined,
/// Length of tls_host.
tls_host_len: u8 = 0,
/// Connects to a NATS server.
///
/// Arguments:
/// allocator: Allocator for client and buffer memory
/// io: Io interface for async I/O operations
/// url: NATS server URL (e.g., "nats://localhost:4222")
/// opts: Connection options (timeouts, auth, buffer sizes)
///
/// Returns pointer to connected Client. Caller owns and must call deinit().
pub fn connect(
allocator: Allocator,
io: Io,
url: []const u8,
opts: Options,
) !*Client {
// Validate URL length - reject rather than truncate
if (url.len >= defaults.Server.max_url_len) return error.UrlTooLong;
const parsed = try parseUrl(url);
const wants_tls = parsed.use_tls or opts.tls_required or
opts.tls_ca_file != null or opts.tls_handshake_first;
if (wants_tls and parsed.host.len > 255) return error.HostTooLong;
const client = try allocator.create(Client);
client.allocator = allocator;
client.server_info = null;
client.parser = .{};
client.state = .connecting;
client.sub_ptrs = [_]?*Sub{null} ** MAX_SUBSCRIPTIONS;
client.free_count = MAX_SUBSCRIPTIONS;
client.next_sid = 1;
client.read_mutex = .init;
client.sub_mutex = .init;
client.publish_mutex = .init;
client.return_lock = .{};
client.statistics = .{};
client.cached_sub = null;
client.max_payload = 1024 * 1024;
client.tcp_nodelay_set = false;
client.tcp_rcvbuf_set = false;
client.flush_requested = std.atomic.Value(bool).init(false);
client.resp_mux = .{};
// Initialize reconnection state
client.server_pool = undefined;
client.server_pool_initialized = false;
client.sub_backups = [_]SubBackup{.{}} ** MAX_SUBSCRIPTIONS;
client.sub_backup_count = 0;
client.reconnect_attempt = 0;
client.original_url = undefined;
client.original_url_len = 0;
// Initialize pending buffer state
client.pending_buffer = null;
client.pending_buffer_pos = 0;
client.pending_buffer_capacity = 0;
// Initialize health check state (atomics)
client.last_ping_sent_ns.raw = 0;
client.last_pong_received_ns.raw = 0;
client.pings_outstanding.raw = 0;
client.io_task_stats = .{};
// Initialize error tracking state
client.protocol_errors = 0;
client.last_parse_error_notified_at = 0;
client.last_error = null;
client.last_error_msg_len = 0;
// Initialize background I/O task infrastructure
client.write_mutex = .init;
client.io_task_future = null;
client.publish_ring_buf = null;
// Initialize event callback infrastructure
client.event_queue = null;
client.event_queue_mutex = .init;
client.event_queue_buf = null;
client.callback_task_future = null;
client.event_handler = opts.event_handler;
client.lame_duck_notified = false;
// Initialize TLS state
client.tls_client = null;
client.tls_read_buffer = null;
client.tls_write_buffer = null;
client.ca_bundle = null;
client.ca_bundle_lock = .init;
// Determine if TLS should be used: URL scheme, explicit option, or CA file set
client.use_tls = wants_tls;
client.tls_host = undefined;
client.tls_host_len = 0;
// Store host for TLS SNI and certificate verification
if (client.use_tls) {
const host_len: u8 = @intCast(parsed.host.len);
@memcpy(client.tls_host[0..host_len], parsed.host);
client.tls_host_len = host_len;
}
// Initialize slab allocator (critical for O(1) message allocation)
client.tiered_slab = TieredSlab.init(allocator) catch |err| {
allocator.destroy(client);
return err;
};
// Initialize return queue for cross-thread buffer deallocation
// Size must exceed slab tier capacity to avoid blocking when buffers
// are split between sub_queue, processing, and return_queue
const rq_size = opts.sub_queue_size * 2;
client.return_queue_buf = allocator.alloc([]u8, rq_size) catch |err| {
client.tiered_slab.deinit();
allocator.destroy(client);
return err;
};
client.return_queue = SpscQueue([]u8).init(client.return_queue_buf);
errdefer {
allocator.free(client.return_queue_buf);
client.tiered_slab.deinit();
if (client.server_info) |*info| {
info.deinit(allocator);
}
// TLS cleanup
if (client.tls_read_buffer) |buf| allocator.free(buf);
if (client.tls_write_buffer) |buf| allocator.free(buf);
if (client.ca_bundle) |*bundle| bundle.deinit(allocator);
allocator.destroy(client);
}
client.stream = try connectToHost(io, parsed.host, parsed.port);
errdefer client.stream.close(io);
// TCP_NODELAY
const enable: u32 = 1;
client.tcp_nodelay_set = true;
std.posix.setsockopt(
client.stream.socket.handle,
std.posix.IPPROTO.TCP,
std.posix.TCP.NODELAY,
std.mem.asBytes(&enable),
) catch {
client.tcp_nodelay_set = false;
};
// Set TCP receive buffer size for better backpressure handling
client.tcp_rcvbuf_set = opts.tcp_rcvbuf > 0;
if (opts.tcp_rcvbuf > 0) {
std.posix.setsockopt(
client.stream.socket.handle,
std.posix.SOL.SOCKET,
std.posix.SO.RCVBUF,
std.mem.asBytes(&opts.tcp_rcvbuf),
) catch {
client.tcp_rcvbuf_set = false;
};
}
client.read_buffer = allocator.alloc(u8, opts.reader_buffer_size) catch {
return error.OutOfMemory;
};
errdefer allocator.free(client.read_buffer);
client.write_buffer = allocator.alloc(u8, opts.writer_buffer_size) catch {
return error.OutOfMemory;
};
errdefer allocator.free(client.write_buffer);
// Publish ring: power-of-2, must be > 2x largest
// possible entry. The ring rejects entries that
// exceed capacity/2. Entry size = RING_HDR_SIZE +
// PUB overhead + payload. Add 512 bytes headroom
// for subject, reply-to, and length digits.
const min_ring =
(defaults.Protocol.max_payload + 512) * 2;
const ring_size = std.math.ceilPowerOfTwo(
usize,
@max(min_ring, opts.writer_buffer_size),
) catch {
return error.OutOfMemory;
};
client.publish_ring_buf = allocator.alloc(
u8,
ring_size,
) catch {
return error.OutOfMemory;
};
errdefer if (client.publish_ring_buf) |b| allocator.free(b);
client.publish_ring = ByteRing.init(
client.publish_ring_buf.?,
);
client.io = io;
client.reader = client.stream.reader(io, client.read_buffer);
client.writer = client.stream.writer(io, client.write_buffer);
// Default to TCP reader/writer (updated by upgradeTls if TLS is used)
client.active_reader = &client.reader.interface;
client.active_writer = &client.writer.interface;
client.options = opts;
client.sidmap_keys = undefined;
client.sidmap_vals = undefined;
client.sidmap = .init(&client.sidmap_keys, &client.sidmap_vals);
for (0..MAX_SUBSCRIPTIONS) |i| {
client.free_slots[i] = @intCast(MAX_SUBSCRIPTIONS - 1 - i);
}
// TLS-first mode: upgrade to TLS before NATS protocol
if (client.use_tls and opts.tls_handshake_first) {
try client.upgradeTls(opts);
}
try client.handshake(opts, parsed);
// Note: TLS upgrade (if needed) now happens inside handshake(),
// between receiving INFO and sending CONNECT per NATS protocol.
assert(url.len <= defaults.Server.max_url_len);
const url_len: u8 = @intCast(url.len);
@memcpy(client.original_url[0..url_len], url);
client.original_url_len = url_len;
client.server_pool = connection.ServerPool.init(url) catch {
return error.InvalidUrl;
};
client.server_pool_initialized = true;
// Add additional servers from options
if (opts.servers) |servers| {
for (servers) |server_url| {
client.server_pool.addServer(server_url) catch continue;
}
}
if (opts.discover_servers) {
if (client.server_info) |info| {
const new_servers = client.server_pool.addFromConnectUrls(
&info.connect_urls,
&info.connect_urls_lens,
info.connect_urls_count,
);
if (new_servers > 0) {
client.pushEvent(
.{ .discovered_servers = .{ .count = new_servers } },
);
}
}
}
try client.initPendingBuffer();
const now_ns = getNowNs(io);
client.last_ping_sent_ns.store(now_ns, .monotonic);
client.last_pong_received_ns.store(now_ns, .monotonic);
// concurrent() required - async() may deadlock on flush()
client.io_task_future = io.concurrent(
connection.io_task.run,
.{client},
) catch blk: {
dbg.print("WARNING: concurrent() failed, using async()", .{});
break :blk io.async(connection.io_task.run, .{client});
};
// Spawn callback task if event handler provided
if (opts.event_handler != null) {
// Allocate event queue buffer (256 events is plenty for lifecycle)
const eq_buf = try allocator.alloc(Event, 256);
client.event_queue_buf = eq_buf;
errdefer {
allocator.free(eq_buf);
client.event_queue_buf = null;
}
// Create event queue (SpscQueue for non-blocking push from io_task)
const eq = try allocator.create(SpscQueue(Event));
eq.* = SpscQueue(Event).init(eq_buf);
client.event_queue = eq;
errdefer {
allocator.destroy(eq);
client.event_queue = null;
}
// Spawn callback task
client.callback_task_future = io.concurrent(
callbackTaskFn,
.{client},
) catch blk: {
dbg.print(
"WARNING: callback concurrent() failed, using async()",
.{},
);
break :blk io.async(callbackTaskFn, .{client});
};
// Push initial connected event
_ = eq.push(.{ .connected = {} });
// Push socket option warnings (non-fatal, performance impact)
if (!client.tcp_nodelay_set) {
_ = eq.push(.{
.err = .{
.err = events_mod.Error.TcpNoDelayFailed,
.msg = null,
},
});
}
if (!client.tcp_rcvbuf_set and opts.tcp_rcvbuf > 0) {
_ = eq.push(.{
.err = .{
.err = events_mod.Error.TcpRcvBufFailed,
.msg = null,
},
});
}
}
assert(client.next_sid >= 1);
assert(client.state == .connected);
return client;
}
/// Push event to callback queue (called by io_task).
/// Non-blocking, drops event if queue is full.
// REVIEWED(2025-03): Silent drop is intentional. Blocking
// would stall the io_task hot path. Users can monitor via
// subscription dropped_msgs counters.
pub fn pushEvent(self: *Client, event: Event) void {
self.event_queue_mutex.lock(self.io) catch return;
defer self.event_queue_mutex.unlock(self.io);
if (self.event_queue) |q| {
_ = q.push(event);
}
}
/// Callback task: drains event queue and dispatches to user handler.
/// Runs concurrently, uses io.sleep(0) for async-aware yield with cancellation.
/// Exits on .closed event, null queue (deinit), or when canceled during shutdown.
fn callbackTaskFn(client: *Client) void {
dbg.print("callback_task: STARTED", .{});
const handler = client.event_handler orelse return;
while (State.atomicLoad(&client.state) != .closed) {
// Check if queue was nulled by deinit() - must exit immediately
const queue = client.event_queue orelse break;
// Drain all pending events
while (queue.pop()) |event| {
switch (event) {
.connected => handler.dispatchConnect(),
.disconnected => |e| handler.dispatchDisconnect(e.err),
.reconnected => handler.dispatchReconnect(),
.closed => {
handler.dispatchClose();
dbg.print("callback_task: EXITED (closed event)", .{});
return;
},
.slow_consumer => {
handler.dispatchError(events_mod.Error.SlowConsumer);
},
.err => |e| handler.dispatchError(e.err),
.lame_duck => handler.dispatchLameDuck(),
.alloc_failed => {
handler.dispatchError(events_mod.Error.AllocationFailed);
},
.protocol_error => {
handler.dispatchError(events_mod.Error.ProtocolParseError);
},
.discovered_servers => |e| {
handler.dispatchDiscoveredServers(e.count);
},
.draining => handler.dispatchDraining(),
.subscription_complete => |e| {
handler.dispatchSubscriptionComplete(e.sid);
},
}
}
// REVIEWED(2025-03): yield-based polling is intentional.
// Blocking alternatives (futex/condvar) add complexity
// for the event dispatch path with minimal benefit.
std.Thread.yield() catch {};
}
// Drain any remaining events queued during shutdown
if (client.event_queue) |queue| {
while (queue.pop()) |event| {
switch (event) {
.connected => handler.dispatchConnect(),
.disconnected => |e| handler.dispatchDisconnect(e.err),
.reconnected => handler.dispatchReconnect(),
.closed => {}, // Will dispatch below
.slow_consumer => {
handler.dispatchError(events_mod.Error.SlowConsumer);
},
.err => |e| handler.dispatchError(e.err),
.lame_duck => handler.dispatchLameDuck(),
.alloc_failed => {
handler.dispatchError(events_mod.Error.AllocationFailed);
},
.protocol_error => {
handler.dispatchError(events_mod.Error.ProtocolParseError);
},
.discovered_servers => |e| {
handler.dispatchDiscoveredServers(e.count);
},
.draining => handler.dispatchDraining(),
.subscription_complete => |e| {
handler.dispatchSubscriptionComplete(e.sid);
},
}
}
}
// Dispatch final close event if not already done
handler.dispatchClose();
dbg.print("callback_task: EXITED (state closed)", .{});
}
/// Drain loop for MsgHandler callback subscriptions.
/// Runs as io.async task, pops messages and dispatches to handler.
/// Automatically frees each message after dispatch.
fn callbackDrainFn(
sub: *Subscription,
handler: MsgHandler,
) void {
assert(sub.mode == .callback);
const io = sub.client.io;
while (sub.state == .active or sub.state == .draining) {
const msg = sub.nextRaw(io) catch |err| {
if (err == error.Canceled or
err == error.Closed) break;
continue;
};
handler.dispatch(&msg);
msg.deinit();
}
}
/// Drain loop for plain fn callback subscriptions.
/// Same as callbackDrainFn but calls a plain function pointer.
fn callbackDrainFnPlain(
sub: *Subscription,
cb: *const fn (*const Message) void,
) void {
assert(sub.mode == .callback);
const io = sub.client.io;
while (sub.state == .active or sub.state == .draining) {
const msg = sub.nextRaw(io) catch |err| {
if (err == error.Canceled or
err == error.Closed) break;
continue;
};
cb(&msg);
msg.deinit();
}
}
/// Clones a Message into freshly allocated buffers owned by the
/// caller. Used by RespMux.onMessage to transfer a borrowed reply
/// (freed by the callback drain) into a Message the requester
/// can keep beyond the handler return.
fn cloneMessageContents(
allocator: Allocator,
src: *const Message,
) !Message {
assert(src.subject.len > 0);
const subject = try allocator.dupe(u8, src.subject);
errdefer allocator.free(subject);
const data = try allocator.dupe(u8, src.data);
errdefer allocator.free(data);
const reply_to: ?[]const u8 = if (src.reply_to) |rt|
try allocator.dupe(u8, rt)
else
null;
errdefer if (reply_to) |rt| allocator.free(rt);
const hdrs: ?[]const u8 = if (src.headers) |h|
try allocator.dupe(u8, h)
else
null;
return .{
.subject = subject,
.sid = src.sid,
.reply_to = reply_to,
.data = data,
.headers = hdrs,
.allocator = allocator,
.owned = true,
.backing_buf = null,
.return_queue = null,
.return_lock = null,
};
}
/// Encodes a u64 into an 8-character base62 token in `buf`.
/// Used by request() to generate unique reply suffixes from a
/// monotonic counter without allocation or RNG state.
fn generateRespToken(buf: *[8]u8, n: u64) []const u8 {
const digits =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ++
"abcdefghijklmnopqrstuvwxyz";
assert(digits.len == 62);
var x = n;
var i: usize = 8;
while (i > 0) {
i -= 1;
buf[i] = digits[@intCast(x % 62)];
x /= 62;
}
return buf[0..];
}
/// Upgrades the connection to TLS.
/// Allocates TLS buffers, loads CA certificates, and performs handshake.
fn upgradeTls(
self: *Client,
opts: Options,
) !void {
const allocator = self.allocator;
assert(self.use_tls);
assert(self.tls_client == null);
assert(self.tls_host_len > 0);
// mTLS not yet implemented
if (opts.tls_cert_file != null or
opts.tls_key_file != null)
return error.MtlsNotImplemented;
// Allocate TLS buffers if not already done
if (self.tls_read_buffer == null) {
self.tls_read_buffer =
try allocator.alloc(u8, defaults.Tls.buffer_size);
}
errdefer if (self.tls_read_buffer) |buf| {
allocator.free(buf);
self.tls_read_buffer = null;
};
if (self.tls_write_buffer == null) {
self.tls_write_buffer =
try allocator.alloc(u8, defaults.Tls.buffer_size);
}
errdefer if (self.tls_write_buffer) |buf| {
allocator.free(buf);
self.tls_write_buffer = null;
};
// Load CA bundle (unless insecure mode)
if (!opts.tls_insecure_skip_verify) {
if (self.ca_bundle == null) {
self.ca_bundle = .empty;
}
const now = Io.Clock.real.now(self.io);
if (opts.tls_ca_file) |ca_path| {
// Load custom CA bundle from file (propagates file system errors)
try self.ca_bundle.?.addCertsFromFilePathAbsolute(
allocator,
self.io,
now,
ca_path,
);
} else {
// Use system CAs
try self.ca_bundle.?.rescan(allocator, self.io, now);
}
}
// Generate entropy for TLS handshake
var entropy: [tls.Client.Options.entropy_len]u8 = undefined;
self.io.randomSecure(&entropy) catch
return error.SecureEntropyUnavailable;
// Get current timestamp for certificate validation
const now = Io.Clock.real.now(self.io);
// Build TLS options with inline unions
const tls_opts: tls.Client.Options = .{
.host = if (opts.tls_insecure_skip_verify)
.no_verification
else
.{ .explicit = self.tls_host[0..self.tls_host_len] },
.ca = if (opts.tls_insecure_skip_verify)
.no_verification
else
.{ .bundle = .{
.gpa = self.allocator,
.io = self.io,
.lock = &self.ca_bundle_lock,
.bundle = &self.ca_bundle.?,
} },
.read_buffer = self.tls_read_buffer.?,
.write_buffer = self.tls_write_buffer.?,
.entropy = &entropy,
.realtime_now = now,
};
// Perform TLS handshake (propagates TLS errors)
self.tls_client = try tls.Client.init(
&self.reader.interface,
&self.writer.interface,
tls_opts,
);
// Update active reader/writer to TLS (no branching in io_task hot path)
self.active_reader = &self.tls_client.?.reader;
self.active_writer = &self.tls_client.?.writer;
dbg.print("TLS handshake completed", .{});
}
/// Performs NATS handshake (INFO/CONNECT exchange).
fn handshake(
self: *Client,
opts: Options,
parsed: ParsedUrl,
) !void {
const allocator = self.allocator;
// Allow both initial connect and reconnection states
assert(self.state == .connecting or self.state == .reconnecting);
assert(parsed.host.len > 0);
// Use active reader (TLS or TCP depending on connection state)
// Note: writer is fetched later after potential TLS upgrade
const reader = self.active_reader;
// Read INFO from server with connection timeout
const info_data =
try self.peekWithTimeout(reader, opts.connect_timeout_ns);
var consumed: usize = 0;
const cmd = self.parser.parse(allocator, info_data, &consumed) catch {
return error.ProtocolError;
};
assert(consumed <= info_data.len);
reader.toss(consumed);
if (cmd) |c| {
switch (c) {
.info => |parsed_info| {
// Free old server_info if reconnecting
if (self.server_info) |*old| {
old.deinit(allocator);
}
self.server_info = parsed_info;
self.max_payload = parsed_info.max_payload;
self.state = .connected;
_ = self.statistics.connects.fetchAdd(1, .monotonic);
},
else => return error.UnexpectedCommand,
}
} else {
return error.NoInfoReceived;
}
// REVIEWED(2025-03): TLS upgrade occurs HERE, before CONNECT.
// Credentials in CONNECT are sent over TLS when required.
// Server sends INFO in plain text, then expects TLS handshake.
if (self.use_tls and self.tls_client == null) {
const server_tls =
if (self.server_info) |info| info.tls_required else false;
const client_tls_required =
parsed.use_tls or opts.tls_required or opts.tls_ca_file != null;
if (server_tls or client_tls_required) {
try self.upgradeTls(opts);
}
}
// Send CONNECT (now over TLS if upgraded)
// Re-fetch writer since TLS upgrade may have changed active_writer
const writer_for_connect = self.active_writer;
const pass = opts.pass orelse parsed.pass;
var user = opts.user orelse parsed.user;
var auth_token = opts.auth_token;
if (parsed.user != null and parsed.pass == null and opts.user == null) {
auth_token = parsed.user;
user = null;
}
// Authentication: sign nonce if credentials provided
// Priority: creds_file > creds > nkey_seed > nkey_seed_file > nkey_sign_fn
var sig_buf: [86]u8 = undefined;
var pubkey_buf: [56]u8 = undefined;
var sig_slice: ?[]const u8 = null;
var pubkey_slice: ?[]const u8 = null;
// Buffer for credentials file (must outlive signing operation)
var creds_buf: [8192]u8 = undefined;
defer std.crypto.secureZero(u8, &creds_buf);
// Buffer for seed from file (must outlive signing operation)
var file_seed_buf: [128]u8 = undefined;
defer std.crypto.secureZero(u8, &file_seed_buf);
// JWT to send (may come from opts.jwt or parsed credentials)
var jwt_to_send: ?[]const u8 = opts.jwt;
if (opts.creds_file) |path| {
// Load credentials from file (propagates file system errors)
const creds = try creds_auth.loadFile(self.io, path, &creds_buf);
jwt_to_send = creds.jwt;
if (self.server_info.?.nonce) |nonce| {
var kp = nkey_auth.KeyPair.fromSeed(creds.seed) catch {
return error.InvalidNKeySeed;
};
defer kp.wipe();
sig_slice = kp.signEncoded(nonce, &sig_buf);
pubkey_slice = kp.publicKey(&pubkey_buf);
}
// Note: creds_buf contains JWT (not secret) and seed.
// Seed is wiped via kp.wipe(). Buffer on stack gets overwritten.
} else if (opts.creds) |content| {
// Parse credentials from provided content
const creds = try creds_auth.parse(content);
jwt_to_send = creds.jwt;
if (self.server_info.?.nonce) |nonce| {
var kp = nkey_auth.KeyPair.fromSeed(creds.seed) catch {
return error.InvalidNKeySeed;
};
defer kp.wipe();
sig_slice = kp.signEncoded(nonce, &sig_buf);
pubkey_slice = kp.publicKey(&pubkey_buf);
}
} else if (opts.nkey_seed) |seed| {
if (self.server_info.?.nonce) |nonce| {
var kp = nkey_auth.KeyPair.fromSeed(seed) catch {
return error.InvalidNKeySeed;
};
defer kp.wipe();
sig_slice = kp.signEncoded(nonce, &sig_buf);
pubkey_slice = kp.publicKey(&pubkey_buf);
}
} else if (opts.nkey_seed_file) |path| {
if (self.server_info.?.nonce) |nonce| {
const seed = try readSeedFile(self.io, path, &file_seed_buf);
defer std.crypto.secureZero(u8, file_seed_buf[0..seed.len]);
var kp = nkey_auth.KeyPair.fromSeed(seed) catch {
return error.InvalidNKeySeed;
};
defer kp.wipe();
sig_slice = kp.signEncoded(nonce, &sig_buf);
pubkey_slice = kp.publicKey(&pubkey_buf);
}
} else if (opts.nkey_sign_fn) |sign_fn| {
if (self.server_info.?.nonce) |nonce| {
var raw_sig: [64]u8 = undefined;
if (!sign_fn(nonce, &raw_sig)) {
return error.NKeySigningFailed;
}
sig_slice = std.base64.url_safe_no_pad.Encoder.encode(
&sig_buf,
&raw_sig,
);
pubkey_slice = opts.nkey_pubkey;
}
}
const connect_opts = protocol.ConnectOptions{
.verbose = opts.verbose,
.pedantic = opts.pedantic,
.name = opts.name,
.user = user,
.pass = pass,
.auth_token = auth_token,
.lang = "zig",
.version = "0.1.0",
.protocol = 1,
.echo = opts.echo,
.headers = opts.headers,
.no_responders = opts.no_responders,
.tls_required = opts.tls_required or parsed.use_tls,
.jwt = jwt_to_send,
.nkey = pubkey_slice,
.sig = sig_slice,
};
protocol.Encoder.encodeConnect(writer_for_connect, connect_opts) catch {
return error.EncodingFailed;
};
writer_for_connect.flush() catch {
return error.WriteFailed;
};
// Check for auth rejection
if (self.server_info.?.auth_required) {
try self.checkAuthRejection(opts.connect_timeout_ns);
}
}
/// Checks auth by sending a bounded PING and waiting for PONG or -ERR.
fn checkAuthRejection(self: *Client, timeout_ns: u64) !void {
assert(self.state == .connected);
assert(timeout_ns > 0);
const reader = self.active_reader;
const writer = self.active_writer;
writer.writeAll("PING\r\n") catch return error.WriteFailed;
writer.flush() catch return error.WriteFailed;
if (self.tls_client != null) {
self.writer.interface.flush() catch return error.WriteFailed;
}
const start_ns = getNowNs(self.io);
while (true) {
const now_ns = getNowNs(self.io);
if (now_ns - start_ns >= timeout_ns) {
return error.ConnectionTimeout;
}
const remaining = timeout_ns - (now_ns - start_ns);
const response = try self.peekWithTimeout(reader, remaining);
var consumed: usize = 0;
const cmd = self.parser.parse(
self.allocator,
response,
&consumed,
) catch return error.ProtocolError;
if (consumed > 0) reader.toss(consumed);
if (cmd) |c| {
switch (c) {
.pong => return,
.err => {
self.state = .closed;
return error.AuthorizationViolation;
},
.ok => continue,
.ping => {
writer.writeAll("PONG\r\n") catch return error.WriteFailed;
writer.flush() catch return error.WriteFailed;
},
.info => |info| {
var owned = info;
owned.deinit(self.allocator);
},
else => continue,
}
} else {
self.io.sleep(.fromNanoseconds(0), .awake) catch {};
}
}
}
/// Reads from socket with connection timeout using Io.Select.
/// Returns data or error.ConnectionTimeout if timeout expires.
fn peekWithTimeout(
self: *Client,
reader: *Io.Reader,
timeout_ns: u64,
) ![]u8 {
assert(timeout_ns > 0);
const Sel = Io.Select(union(enum) {
read: anyerror![]u8,
timeout: void,
});
var buf: [2]Sel.Union = undefined;
var sel = Sel.init(self.io, &buf);
sel.async(.read, peekGreedyAsync, .{ reader, self.io });
sel.async(.timeout, sleepNs, .{ self.io, timeout_ns });
const result = sel.await() catch {
sel.cancelDiscard();
return error.ConnectionFailed;
};
sel.cancelDiscard();
switch (result) {
.read => |read_result| {
return read_result catch error.ConnectionFailed;
},
.timeout => {
return error.ConnectionTimeout;
},
}
}
/// Async wrapper for peekGreedy (used with io.async).
fn peekGreedyAsync(reader: *Io.Reader, io: Io) ![]u8 {
_ = io;
return reader.peekGreedy(1);
}
/// Cleanup subscription resources after failed registration.
/// Inline to avoid function call overhead in error path.
inline fn cleanupFailedSub(
self: *Client,
sub: *Sub,
slot_idx: u16,
queue_buf: []Message,
owned_queue: ?[]const u8,
owned_subject: []const u8,
remove_from_sidmap: bool,
) void {
const allocator = self.allocator;
if (remove_from_sidmap) {
_ = self.sidmap.remove(sub.sid);
self.sub_ptrs[slot_idx] = null;
}
if (self.cached_sub == sub) self.cached_sub = null;
self.free_slots[self.free_count] = slot_idx;
self.free_count += 1;
allocator.free(queue_buf);
if (owned_queue) |qg| allocator.free(qg);
allocator.free(owned_subject);
allocator.destroy(sub);
}
/// Subscribes to a subject.
///
/// Arguments:
/// subject: Subject pattern to subscribe to (wildcards allowed: *, >)
///
/// Returns subscription pointer. Caller must call sub.deinit() when done.
pub fn subscribeSync(
self: *Client,
subject: []const u8,
) !*Sub {
return self.queueSubscribeSync(subject, null);
}
/// Subscribes with queue group for load balancing.
///
/// Arguments:
/// subject: Subject pattern to subscribe to
/// queue_group: Queue group name (messages distributed among members)
///
/// Queue groups allow multiple subscribers to share the message load.
/// Only one subscriber in the group receives each message.
pub fn queueSubscribeSync(
self: *Client,
subject: []const u8,
queue_group: ?[]const u8,
) !*Sub {
const allocator = self.allocator;
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
try pubsub.validateSubscribe(subject);
if (queue_group) |qg| try pubsub.validateQueueGroup(qg);
// Validate lengths for backup buffer compatibility
if (subject.len >= defaults.Limits.max_subject_len)
return error.SubjectTooLong;
if (queue_group) |qg| {
if (qg.len > defaults.Limits.max_queue_group_len) {
return error.QueueGroupTooLong;
}
}
assert(self.next_sid >= 1);
// sub_mutex serializes slot/SID allocation and SUB encoding
// for multi-thread subscribe safety.
self.sub_mutex.lockUncancelable(self.io);
defer self.sub_mutex.unlock(self.io);
// Allocate slot
if (self.free_count == 0) {
return error.TooManySubscriptions;
}
self.free_count -= 1;
const slot_idx = self.free_slots[self.free_count];
const sid = self.next_sid;
self.next_sid += 1;
// Create subscription
const sub = try allocator.create(Sub);
errdefer {
allocator.destroy(sub);
self.free_slots[self.free_count] = slot_idx;
self.free_count += 1;
}
const owned_subject = try allocator.dupe(u8, subject);
errdefer allocator.free(owned_subject);
const owned_queue = if (queue_group) |qg|
try allocator.dupe(u8, qg)
else
null;
errdefer if (owned_queue) |qg| allocator.free(qg);
// Allocate Io.Queue buffer
const queue_size = self.options.sub_queue_size;
const queue_buf = try allocator.alloc(Message, queue_size);
errdefer allocator.free(queue_buf);
sub.* = .{
.client = self,
.sid = sid,
.subject = owned_subject,
.queue_group = owned_queue,
.queue_buf = queue_buf,
.queue = .init(queue_buf),
.state = .active,
.received_msgs = 0,
};
// Store in SidMap
self.sidmap.put(sid, slot_idx) catch {
self.cleanupFailedSub(
sub,
slot_idx,
queue_buf,
owned_queue,
owned_subject,
false,
);
return error.TooManySubscriptions;
};
self.sub_ptrs[slot_idx] = sub;
self.cached_sub = sub;
// Enqueue SUB onto the ordered outbound ring so it cannot
// overtake or be overtaken by queued publishes.
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
self.encodeSubToRing(.{
.subject = subject,
.queue_group = queue_group,
.sid = sid,
}) catch {
self.cleanupFailedSub(
sub,
slot_idx,
queue_buf,
owned_queue,
owned_subject,
true,
);
return error.EncodingFailed;
};
// Signal auto-flush to register subscription promptly
self.flush_requested.store(true, .release);
return sub;
}
/// Subscribes with a MsgHandler callback.
/// Messages are dispatched to handler.onMessage() automatically.
/// The drain task frees each message after dispatch.
///
/// Do NOT call nextMsg()/tryNextMsg() on the returned subscription.
pub fn subscribe(
self: *Client,
subject: []const u8,
handler: MsgHandler,
) !*Sub {
return self.queueSubscribe(
subject,
null,
handler,
);
}
/// Subscribes with a MsgHandler callback and queue group.
pub fn queueSubscribe(
self: *Client,
subject: []const u8,
queue_group: ?[]const u8,
handler: MsgHandler,
) !*Sub {
const sub = try self.queueSubscribeSync(
subject,
queue_group,
);
errdefer sub.deinit();
sub.mode = .callback;
sub.callback_future = self.io.concurrent(
callbackDrainFn,
.{ sub, handler },
) catch self.io.async(
callbackDrainFn,
.{ sub, handler },
);
return sub;
}
/// Subscribes with a plain function callback.
/// Simpler alternative when no handler state is needed.
pub fn subscribeFn(
self: *Client,
subject: []const u8,
cb: *const fn (*const Message) void,
) !*Sub {
return self.queueSubscribeFn(
subject,
null,
cb,
);
}
/// Subscribes with a plain function callback and queue group.
pub fn queueSubscribeFn(
self: *Client,
subject: []const u8,
queue_group: ?[]const u8,
cb: *const fn (*const Message) void,
) !*Sub {
const sub = try self.queueSubscribeSync(
subject,
queue_group,
);
errdefer sub.deinit();
sub.mode = .callback;
sub.callback_future = self.io.concurrent(
callbackDrainFnPlain,
.{ sub, cb },
) catch self.io.async(
callbackDrainFnPlain,
.{ sub, cb },
);
return sub;
}
/// Publishes a message to a subject.
///
/// Thread-safe: serialized by publish_mutex. Encodes into
/// the publish ring buffer; io_task drains to socket.
pub fn publish(
self: *Client,
subject: []const u8,
payload: []const u8,
) !void {
const state = State.atomicLoad(&self.state);
try pubsub.validatePublish(subject);
if (payload.len > self.max_payload)
return error.PayloadTooLarge;
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
if (!state.canSend()) {
if (self.options.reconnect and
(state == .reconnecting or state == .disconnected))
{
try self.bufferPendingPublish(subject, payload);
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(payload.len, .monotonic);
return;
}
return error.NotConnected;
}
try self.encodePubToRing(
subject,
null,
payload,
);
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
payload.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
/// Publishes with a reply-to subject.
/// Thread-safe: serialized by publish_mutex.
pub fn publishRequest(
self: *Client,
subject: []const u8,
reply_to: []const u8,
payload: []const u8,
) !void {
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
try pubsub.validatePublish(subject);
try pubsub.validateReplyTo(reply_to);
if (payload.len > self.max_payload)
return error.PayloadTooLarge;
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
try self.encodePubToRing(
subject,
reply_to,
payload,
);
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
payload.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
/// Publishes a message with headers.
/// Thread-safe: serialized by publish_mutex.
pub fn publishWithHeaders(
self: *Client,
subject: []const u8,
hdrs: []const headers.Entry,
payload: []const u8,
) !void {
if (!State.atomicLoad(&self.state).canSend()) return error.NotConnected;
try pubsub.validatePublish(subject);
try headers.validateEntries(hdrs);
const hdr_size = headers.encodedSize(hdrs);
if (hdr_size + payload.len > self.max_payload)
return error.PayloadTooLarge;
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
try self.encodeHPubToRing(
subject,
null,
hdrs,
payload,
);
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
payload.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
/// Publishes with headers and reply-to subject.
/// Thread-safe: serialized by publish_mutex.
pub fn publishRequestWithHeaders(
self: *Client,
subject: []const u8,
reply_to: []const u8,
hdrs: []const headers.Entry,
payload: []const u8,
) !void {
if (!State.atomicLoad(&self.state).canSend()) return error.NotConnected;
try pubsub.validatePublish(subject);
try pubsub.validateReplyTo(reply_to);
try headers.validateEntries(hdrs);
const hdr_size = headers.encodedSize(hdrs);
if (hdr_size + payload.len > self.max_payload)
return error.PayloadTooLarge;
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
try self.encodeHPubToRing(
subject,
reply_to,
hdrs,
payload,
);
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
payload.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
/// Publishes with a HeaderMap builder.
/// Thread-safe: serialized by publish_mutex.
pub fn publishWithHeaderMap(
self: *Client,
subject: []const u8,
header_map: *const protocol.HeaderMap,
payload: []const u8,
) !void {
if (header_map.isEmpty()) return error.EmptyHeaders;
if (!State.atomicLoad(&self.state).canSend()) return error.NotConnected;
try pubsub.validatePublish(subject);
const hdr_bytes = try header_map.encode();
defer header_map.allocator.free(hdr_bytes);
if (hdr_bytes.len + payload.len > self.max_payload)
return error.PayloadTooLarge;
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
try self.encodeHPubRawToRing(
subject,
null,
hdr_bytes,
payload,
);
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
payload.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
/// Publishes a Message object (convenience for forwarding).
/// Thread-safe: serialized by publish_mutex.
pub fn publishMsg(
self: *Client,
msg: *const Message,
) !void {
if (!State.atomicLoad(&self.state).canSend()) return error.NotConnected;
try pubsub.validatePublish(msg.subject);
const total_size = if (msg.headers) |h|
h.len + msg.data.len
else
msg.data.len;
if (total_size > self.max_payload)
return error.PayloadTooLarge;
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
if (msg.headers) |hdrs| {
try self.encodeHPubRawToRing(
msg.subject,
null,
hdrs,
msg.data,
);
} else {
try self.encodePubToRing(
msg.subject,
null,
msg.data,
);
}
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
msg.data.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
/// Sends a request with headers and waits for a reply with timeout.
///
/// Arguments:
/// subject: Request destination subject
/// hdrs: Header entries to include in request
/// payload: Request data
/// timeout_ms: Maximum time to wait for reply in milliseconds
///
/// Sends request with reply-to and headers, then waits for the
/// response multiplexer using a direct deadline loop.
/// Returns null on timeout.
pub fn requestWithHeaders(
self: *Client,
subject: []const u8,
hdrs: []const headers.Entry,
payload: []const u8,
timeout_ms: u32,
) !?Message {
assert(timeout_ms > 0);
assert(subject.len > 0);
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
var waiter: RespWaiter = .{};
var reply_buf: [256]u8 = undefined;
const reply = try self.requestPrepReply(&waiter, &reply_buf);
errdefer self.cleanupRespWaiter(&waiter, reply);
try self.publishRequestWithHeaders(
subject,
reply,
hdrs,
payload,
);
try self.flushBuffer();
return self.requestAwaitResp(&waiter, reply, timeout_ms);
}
/// Flushes pending writes to the server.
///
/// Sends all buffered data to the TCP socket. This is a simple TCP flush
/// without PING/PONG verification - for maximum performance.
/// Sends all buffered data to the network (buffer-to-socket only).
///
/// This is a fast flush that does not wait for server confirmation.
/// For confirmed delivery, use flush() which sends PING and waits for PONG.
pub fn flushBuffer(self: *Client) !void {
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
try self.write_mutex.lock(self.io);
defer self.write_mutex.unlock(self.io);
try self.flushBufferLocked();
}
fn flushBufferLocked(self: *Client) !void {
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
while (self.publish_ring.peek()) |data| {
self.active_writer.writeAll(data) catch return error.WriteFailed;
self.publish_ring.advance();
}
self.active_writer.flush() catch return error.WriteFailed;
// TLS: active_writer.flush() only encrypts to TCP buffer.
// Must also flush the underlying TCP writer to send to network.
if (self.use_tls) {
self.writer.interface.flush() catch return error.WriteFailed;
}
}
/// Flushes all buffered data and confirms server received it.
///
/// Sends all buffered data, then sends PING and waits for PONG response.
/// This confirms the server received all messages up to this point.
/// Safe in both sync and async contexts.
///
/// This matches Go/C client Flush() behavior (PING/PONG confirmation).
/// For fast buffer-only flush without confirmation, use flushBuffer().
///
/// Note: Concurrent calls may have PONG mismatch issues. Use single-caller
/// semantics or serialize calls externally.
pub fn flush(
self: *Client,
timeout_ns: u64,
) !void {
assert(timeout_ns > 0);
if (!State.atomicLoad(&self.state).canSend()) return error.NotConnected;
const old_pong_ns = self.last_pong_received_ns.load(.acquire);
// Step 1: Drain publish ring + flush + send PING (holding mutex)
try self.write_mutex.lock(self.io);
// Drain any pending publishes from the ring first
while (self.publish_ring.peek()) |data| {
self.active_writer.writeAll(data) catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
self.publish_ring.advance();
}
self.active_writer.flush() catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
if (self.use_tls) {
self.writer.interface.flush() catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
}
// Send PING while still holding mutex (ensures ordering)
self.active_writer.writeAll("PING\r\n") catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
self.active_writer.flush() catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
if (self.use_tls) {
self.writer.interface.flush() catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
}
self.write_mutex.unlock(self.io);
// Step 2: Poll for PONG with timeout (direct loop, no Io.Select).
// Using direct polling avoids layering a second async wait inside
// a synchronous flush call on this client's Io.
const deadline_ns = getNowNs(self.io) +| timeout_ns;
var iteration: u32 = 0;
dbg.print("flush: waiting for PONG, old_pong_ns={d}", .{old_pong_ns});
while (true) {
// Check for PONG
const current = self.last_pong_received_ns.load(.acquire);
if (current > old_pong_ns) {
dbg.print("flush: got PONG, current={d}", .{current});
return; // Success!
}
// Check timeout
const now = getNowNs(self.io);
if (now >= deadline_ns) return error.Timeout;
// Yield periodically to allow io_task to process incoming PONG
iteration += 1;
if (iteration >= 100) {
iteration = 0;
std.Thread.yield() catch {};
}
std.atomic.spinLoopHint();
}
}
/// Forces an immediate reconnection attempt.
/// Closes the current connection and triggers reconnection logic.
/// Subscriptions will be restored automatically.
pub fn forceReconnect(self: *Client) !void {
const state = State.atomicLoad(&self.state);
if (state == .closed) return error.ConnectionClosed;
if (state == .reconnecting) return;
if (state != .connected) return error.NotConnected;
assert(self.next_sid >= 1);
// State first: canSend() returns false, no new writes
State.atomicStore(&self.state, .reconnecting);
// Shutdown read only -- in-flight writes stay safe.
// io_task sees EndOfStream, enters handleDisconnect
// which calls cleanupForReconnect for full close
// with write_mutex. Guard: fd may be invalid.
if (isValidFd(self.stream.socket.handle)) {
self.stream.shutdown(self.io, .recv) catch {};
}
}
/// Gracefully drains with a timeout.
/// Returns error.Timeout if drain doesn't complete in time.
pub fn drainTimeout(
self: *Client,
timeout_ns: u64,
) !DrainResult {
assert(timeout_ns > 0);
if (State.atomicLoad(&self.state) != .connected) {
return error.NotConnected;
}
const Sel = Io.Select(union(enum) {
drain: anyerror!DrainResult,
timeout: void,
});
var buf: [2]Sel.Union = undefined;
var sel = Sel.init(self.io, &buf);
sel.async(.drain, drainHelper, .{self});
sel.async(.timeout, sleepNs, .{ self.io, timeout_ns });
const select_result = sel.await() catch {
sel.cancelDiscard();
return error.Canceled;
};
sel.cancelDiscard();
switch (select_result) {
.drain => |result| {
return result;
},
.timeout => {
return error.Timeout;
},
}
}
/// Helper for async drain.
fn drainHelper(self: *Client) !DrainResult {
return self.drain();
}
/// Lazily initializes the response multiplexer on the first
/// request() call. Subsequent calls fast-path on the atomic
/// `initialized` flag.
///
/// Init steps (run once per connection lifetime):
/// 1. Build the inbox prefix and wildcard subject
/// 2. subscribe(wildcard, RespMux as handler)
/// 3. flush() - PING/PONG round-trip confirms server has the SUB
/// 4. Publish init under resp_mux.mutex with double-checked
/// pattern (concurrent first-requesters race; loser cleans up)
///
/// Lock discipline: resp_mux.mutex is NOT held across the slow
/// flush() call. Holding it would risk deadlock if the io_task
/// were spinning trying to call respMux.onMessage while flush()
/// spin-waits on the PONG atomic. The window is safe because the
/// wildcard "_INBOX..*" is unique to this connection and no
/// peer can publish to it before the first request.
fn ensureRespMux(self: *Client) !void {
if (self.resp_mux.initialized.load(.acquire)) return;
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
const allocator = self.allocator;
const base = try self.newInbox();
defer allocator.free(base);
assert(base.len > 0);
const prefix = try std.fmt.allocPrint(
allocator,
"{s}.",
.{base},
);
errdefer allocator.free(prefix);
// wildcard = ".*" — the trailing dot is in prefix already.
var wildcard_buf: [256]u8 = undefined;
const wildcard = std.fmt.bufPrint(
&wildcard_buf,
"{s}*",
.{prefix},
) catch return error.SubjectTooLong;
self.resp_mux.client = self;
const sub = try self.subscribe(
wildcard,
MsgHandler.init(RespMux, &self.resp_mux),
);
errdefer sub.deinit();
try self.flush(5 * std.time.ns_per_s);
self.resp_mux.mutex.lockUncancelable(self.io);
defer self.resp_mux.mutex.unlock(self.io);
if (self.resp_mux.initialized.load(.acquire)) {
sub.deinit();
allocator.free(prefix);
return;
}
self.resp_mux.prefix = prefix;
self.resp_mux.prefix_len = prefix.len;
self.resp_mux.sub = sub;
self.resp_mux.initialized.store(true, .release);
}
/// Inner helper: registers a stack-allocated waiter in the resp
/// map and returns the assembled reply subject. The caller MUST
/// invoke requestAwaitResp (or manually clean up the map) before
/// the waiter goes out of scope.
///
/// Note: the map key is a slice of reply_buf (the caller's stack
/// buffer). The cleanup defer in requestAwaitResp removes the
/// entry before the caller's frame is unwound.
fn requestPrepReply(
self: *Client,
waiter: *RespWaiter,
reply_buf: *[256]u8,
) ![]const u8 {
try self.ensureRespMux();
assert(self.resp_mux.prefix != null);
assert(self.resp_mux.prefix_len > 0);
var token_buf: [8]u8 = undefined;
const token = generateRespToken(
&token_buf,
self.resp_mux.next_token.fetchAdd(1, .monotonic),
);
const reply = std.fmt.bufPrint(
reply_buf,
"{s}{s}",
.{ self.resp_mux.prefix.?, token },
) catch return error.SubjectTooLong;
// Register using a slice of `reply` (lives in caller's frame).
// This must outlive the map entry — caller's deferred cleanup
// in requestAwaitResp removes the entry before reply_buf dies.
const map_key = reply[self.resp_mux.prefix_len..];
self.resp_mux.mutex.lockUncancelable(self.io);
self.resp_mux.map.put(
self.allocator,
map_key,
waiter,
) catch {
self.resp_mux.mutex.unlock(self.io);
return error.OutOfMemory;
};
self.resp_mux.mutex.unlock(self.io);
return reply;
}
/// Removes a waiter from the resp map. Used as an error-path
/// cleanup helper when publish fails after the waiter is
/// registered but before requestAwaitResp would run. If the
/// dispatcher already removed it (and possibly populated msg),
/// the returned message is freed by the caller via the same
/// path requestAwaitResp would use.
fn cleanupRespWaiter(
self: *Client,
waiter: *RespWaiter,
reply: []const u8,
) void {
assert(self.resp_mux.prefix_len > 0);
const map_key = reply[self.resp_mux.prefix_len..];
self.resp_mux.mutex.lockUncancelable(self.io);
_ = self.resp_mux.map.remove(map_key);
self.resp_mux.mutex.unlock(self.io);
if (waiter.msg) |m| {
m.deinit();
waiter.msg = null;
}
}
/// Inner helper: waits for the waiter or deadline, cleans up the
/// map entry on every exit path, and returns the response or null.
/// Acquiring resp_mux.mutex in the cleanup defer serializes against
/// any concurrent dispatcher (which holds the same mutex during
/// clone+write), preventing use-after-free of the stack-allocated
/// waiter.
fn requestAwaitResp(
self: *Client,
waiter: *RespWaiter,
reply: []const u8,
timeout_ms: u32,
) ?Message {
assert(self.resp_mux.prefix_len > 0);
const map_key = reply[self.resp_mux.prefix_len..];
const timeout_ns: u64 =
@as(u64, timeout_ms) * std.time.ns_per_ms;
const start = getNowNs(self.io);
var spin_count: u32 = 0;
defer {
self.resp_mux.mutex.lockUncancelable(self.io);
_ = self.resp_mux.map.remove(map_key);
self.resp_mux.mutex.unlock(self.io);
}
while (!waiter.done.load(.acquire)) {
spin_count += 1;
if (spin_count < defaults.Spin.max_spins) {
std.atomic.spinLoopHint();
} else {
self.io.sleep(
.fromNanoseconds(0),
.awake,
) catch |err| {
if (err == error.Canceled)
return waiter.msg;
};
spin_count = 0;
const now = getNowNs(self.io);
if (now -| start >= timeout_ns)
return waiter.msg;
}
}
return waiter.msg;
}
/// Cleans up the response multiplexer. Called from Client.deinit
/// before subscriptions are torn down. Signals all in-flight
/// waiters with null msg so any blocked request() calls unblock
/// and return null cleanly.
fn closeRespMux(self: *Client) void {
const io = self.io;
self.resp_mux.mutex.lockUncancelable(io);
var it = self.resp_mux.map.iterator();
while (it.next()) |entry| {
entry.value_ptr.*.done.store(true, .release);
}
self.resp_mux.map.clearAndFree(self.allocator);
self.resp_mux.mutex.unlock(io);
if (self.resp_mux.sub) |sub| {
sub.deinit();
self.resp_mux.sub = null;
}
if (self.resp_mux.prefix) |p| {
self.allocator.free(p);
self.resp_mux.prefix = null;
self.resp_mux.prefix_len = 0;
}
self.resp_mux.initialized.store(false, .release);
self.resp_mux.client = null;
}
/// Sends a request and waits for a reply with timeout.
///
/// Arguments:
/// subject: Request destination subject
/// payload: Request data
/// timeout_ms: Maximum time to wait for reply in milliseconds
///
/// Uses the response multiplexer: a single wildcard inbox
/// subscription is created on first call (with PING/PONG sync)
/// and reused for the connection lifetime. Each call registers
/// a stack-allocated waiter in the resp_map keyed by an 8-char
/// token, publishes the request with reply-to set to the
/// per-request inbox, and races the response against the timeout.
/// Returns null on timeout, cancellation, or no-responders.
pub fn request(
self: *Client,
subject: []const u8,
payload: []const u8,
timeout_ms: u32,
) !?Message {
assert(timeout_ms > 0);
assert(subject.len > 0);
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
var waiter: RespWaiter = .{};
var reply_buf: [256]u8 = undefined;
const reply = try self.requestPrepReply(&waiter, &reply_buf);
errdefer self.cleanupRespWaiter(&waiter, reply);
try self.publishRequest(subject, reply, payload);
try self.flushBuffer();
return self.requestAwaitResp(&waiter, reply, timeout_ms);
}
/// Sends a request using a Message object and waits for a reply.
///
/// Arguments:
/// msg: Message to send (uses subject, data, and headers if present)
/// timeout_ms: Maximum time to wait for reply in milliseconds
///
/// Useful for forwarding request messages or republishing with same
/// content. Routes through the response multiplexer (see request()).
/// The msg.headers field carries pre-encoded raw header bytes; this
/// preserves them verbatim across the request/reply round-trip.
pub fn requestMsg(
self: *Client,
msg: *const Message,
timeout_ms: u32,
) !?Message {
assert(msg.subject.len > 0);
assert(timeout_ms > 0);
if (!State.atomicLoad(&self.state).canSend()) {
return error.NotConnected;
}
var waiter: RespWaiter = .{};
var reply_buf: [256]u8 = undefined;
const reply = try self.requestPrepReply(&waiter, &reply_buf);
errdefer self.cleanupRespWaiter(&waiter, reply);
{
self.publish_mutex.lockUncancelable(self.io);
defer self.publish_mutex.unlock(self.io);
if (msg.headers) |hdrs| {
try self.encodeHPubRawToRing(
msg.subject,
reply,
hdrs,
msg.data,
);
} else {
try self.encodePubToRing(
msg.subject,
reply,
msg.data,
);
}
_ = self.statistics.msgs_out.fetchAdd(1, .monotonic);
_ = self.statistics.bytes_out.fetchAdd(
msg.data.len,
.monotonic,
);
self.flush_requested.store(true, .release);
}
try self.flushBuffer();
return self.requestAwaitResp(&waiter, reply, timeout_ms);
}
/// Helper for connection timeout (nanoseconds).
fn sleepNs(io: Io, timeout_ns: u64) void {
io.sleep(.fromNanoseconds(timeout_ns), .awake) catch {};
}
/// Reads NKey seed from file, trimming whitespace.
/// Returns slice into buf containing the seed.
/// File system errors (FileNotFound, AccessDenied, etc.) propagate directly.
/// Returns InvalidNKeySeedFile only for content issues (empty/whitespace-only).
fn readSeedFile(io: Io, path: []const u8, buf: *[128]u8) ![]const u8 {
assert(path.len > 0);
const data = try Io.Dir.readFile(.cwd(), io, path, buf);
if (data.len == 0) return error.InvalidNKeySeedFile;
assert(data.len > 0);
// Trim leading/trailing whitespace
var start: usize = 0;
var end: usize = data.len;
while (start < end and std.ascii.isWhitespace(buf[start])) {
start += 1;
}
while (end > start and std.ascii.isWhitespace(buf[end - 1])) {
end -= 1;
}
if (start >= end) return error.InvalidNKeySeedFile;
assert(start < end);
return buf[start..end];
}
/// Gracefully drains subscriptions and closes the connection.
///
/// Returns DrainResult indicating any failures during cleanup.
/// Unsubscribes all active subscriptions, drains remaining messages,
/// and closes the connection. Use for graceful shutdown.
pub fn drain(self: *Client) !DrainResult {
const alloc = self.allocator;
if (State.atomicLoad(&self.state) != .connected) {
return error.NotConnected;
}
assert(self.next_sid >= 1);
var result: DrainResult = .{};
// Lock ordering: sub_mutex -> read_mutex -> write_mutex.
self.sub_mutex.lockUncancelable(self.io);
// Acquire mutex for subscription cleanup (prevents races with nextMsg())
self.read_mutex.lockUncancelable(self.io);
// Acquire write mutex for thread-safe UNSUB encoding
self.write_mutex.lockUncancelable(self.io);
const writer = self.active_writer;
// Unsubscribe all active subscriptions
for (self.sub_ptrs, 0..) |maybe_sub, slot_idx| {
if (maybe_sub) |sub| {
// Buffer UNSUB command (no I/O yet)
protocol.Encoder.encodeUnsub(writer, .{
.sid = sub.sid,
.max_msgs = null,
}) catch {
result.unsub_failures += 1;
};
// Close queue and clear from data structures
sub.queue.close(self.io);
_ = self.sidmap.remove(sub.sid);
self.sub_ptrs[slot_idx] = null;
if (self.cached_sub == sub) self.cached_sub = null;
self.free_slots[self.free_count] = @intCast(slot_idx);
self.free_count += 1;
// Drain remaining messages from queue (in-memory, no socket I/O)
var drain_buf: [1]Message = undefined;
while (true) {
const n = sub.queue.popBatch(&drain_buf);
if (n == 0) break;
drain_buf[0].deinit();
}
// Mark subscription state - sub.deinit() frees resources
sub.state = .unsubscribed;
sub.client_destroyed = true;
}
}
// Flush while still holding write mutex
writer.flush() catch {
result.flush_failed = true;
};
self.write_mutex.unlock(self.io);
self.read_mutex.unlock(self.io);
self.sub_mutex.unlock(self.io);
State.atomicStore(&self.state, .draining);
self.pushEvent(.{ .draining = {} });
State.atomicStore(&self.state, .closed);
// Shutdown read to unblock io_task. Guard: fd may
// already be closed by io_task during disconnect.
if (isValidFd(self.stream.socket.handle)) {
self.stream.shutdown(self.io, .recv) catch {};
}
// Wait for io_task to exit
if (self.io_task_future) |*future| {
_ = future.cancel(self.io);
self.io_task_future = null;
}
// Full close -- io_task exited
self.stream.close(self.io);
if (self.server_info) |*info| {
info.deinit(alloc);
self.server_info = null;
}
// Push err event if drain had failures
if (!result.isClean()) {
self.pushEvent(.{
.err = .{
.err = events_mod.Error.DrainIncomplete,
.msg = null,
},
});
}
return result;
}
/// Returns true if connected.
pub fn isConnected(self: *const Client) bool {
assert(self.next_sid >= 1);
// Use atomic load for cross-thread visibility (io_task may update state)
return @atomicLoad(State, &self.state, .acquire) == .connected;
}
/// Returns connection statistics snapshot.
pub fn stats(self: *const Client) StatsSnapshot {
assert(self.next_sid >= 1);
return self.statistics.snapshot();
}
/// Returns server info.
pub fn serverInfo(self: *const Client) ?*const ServerInfo {
assert(self.next_sid >= 1);
if (self.server_info) |*info| {
return info;
}
return null;
}
/// Returns true if connection is using TLS.
pub fn isTls(self: *const Client) bool {
assert(self.next_sid >= 1);
return self.use_tls;
}
/// Returns true if TCP_NODELAY was successfully set.
pub fn isTcpNoDelaySet(self: *const Client) bool {
return self.tcp_nodelay_set;
}
/// Returns true if TCP receive buffer was successfully set.
pub fn isTcpRcvBufSet(self: *const Client) bool {
return self.tcp_rcvbuf_set;
}
// Connection Info Getters
/// Returns the currently connected server URL.
/// Returns the original URL used to connect, or null if not connected.
pub fn connectedUrl(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.original_url_len == 0) return null;
return self.original_url[0..self.original_url_len];
}
/// Returns the connected server's unique ID.
/// This is the `server_id` from the INFO response.
pub fn connectedServerId(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
if (info.server_id.len > 0) return info.server_id;
}
return null;
}
/// Returns the connected server's name.
/// This is the `server_name` from the INFO response.
pub fn connectedServerName(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
if (info.server_name.len > 0) return info.server_name;
}
return null;
}
/// Returns the connected server's version string.
/// This is the `version` from the INFO response (e.g., "2.10.0").
pub fn connectedServerVersion(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
if (info.version.len > 0) return info.version;
}
return null;
}
/// Checks if the server version meets minimum requirements.
///
/// Arguments:
/// min_major: Minimum major version required
/// min_minor: Minimum minor version required
/// min_patch: Minimum patch version required
///
/// Returns true if server version >= min_major.min_minor.min_patch.
/// Returns false if not connected or version cannot be parsed.
///
/// Example: `client.checkCompatibility(2, 10, 0)` checks for NATS 2.10.0+.
pub fn checkCompatibility(
self: *const Client,
min_major: u16,
min_minor: u16,
min_patch: u16,
) bool {
assert(self.next_sid >= 1);
const version = self.connectedServerVersion() orelse return false;
// Parse version string (e.g., "2.10.0" or "2.10.0-beta")
var parts = std.mem.splitScalar(u8, version, '.');
const major_str = parts.next() orelse return false;
const minor_str = parts.next() orelse return false;
const patch_str = parts.next() orelse "0";
// Parse major
const major = std.fmt.parseInt(u16, major_str, 10) catch return false;
// Parse minor
const minor = std.fmt.parseInt(u16, minor_str, 10) catch return false;
// Parse patch (strip suffix like "-beta" if present)
var patch_clean = patch_str;
if (std.mem.indexOfScalar(u8, patch_str, '-')) |idx| {
patch_clean = patch_str[0..idx];
}
const patch = std.fmt.parseInt(u16, patch_clean, 10) catch 0;
// Compare: major > min OR (major == min AND minor > min) OR ...
if (major > min_major) return true;
if (major < min_major) return false;
if (minor > min_minor) return true;
if (minor < min_minor) return false;
return patch >= min_patch;
}
/// Returns the maximum payload size allowed by the server.
/// Defaults to 1MB if not yet connected.
pub fn maxPayload(self: *const Client) usize {
assert(self.next_sid >= 1);
return self.max_payload;
}
/// Returns true if the server supports message headers (NATS 2.2+).
pub fn headersSupported(self: *const Client) bool {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.headers;
}
return false;
}
/// Returns the number of known servers in the connection pool.
/// This includes the original server and any discovered via cluster INFO.
pub fn serverCount(self: *const Client) u8 {
assert(self.next_sid >= 1);
if (self.server_pool_initialized) {
return self.server_pool.serverCount();
}
return 0;
}
/// Returns a server URL from the pool at the given index.
/// Use with serverCount() to iterate all known servers.
pub fn serverUrl(self: *const Client, index: u8) ?[]const u8 {
assert(self.next_sid >= 1);
if (!self.server_pool_initialized) return null;
if (index >= self.server_pool.count) return null;
return self.server_pool.servers[index].getUrl();
}
/// Returns the count of discovered servers from cluster INFO.
/// These are additional servers beyond the original connection URL.
pub fn discoveredServerCount(self: *const Client) u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.connect_urls_count;
}
return 0;
}
/// Returns a discovered server URL at the given index.
/// Use with discoveredServerCount() to iterate discovered servers.
pub fn discoveredServerUrl(self: *const Client, index: u8) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.getConnectUrl(index);
}
return null;
}
/// Returns the connected server's cluster name.
/// This is the `cluster` from the INFO response.
pub fn connectedClusterName(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.cluster;
}
return null;
}
/// Returns true if the server requires authentication.
/// Derived from `auth_required` in the INFO response.
pub fn authRequired(self: *const Client) bool {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.auth_required;
}
return false;
}
/// Returns true if the server requires TLS.
/// Derived from `tls_required` in the INFO response.
pub fn tlsRequired(self: *const Client) bool {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.tls_required;
}
return false;
}
/// Returns the client name from options.
/// This is the name used in the CONNECT command.
pub fn name(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
return self.options.name;
}
/// Returns the client ID assigned by the server.
/// This is the `client_id` from the INFO response.
pub fn clientId(self: *const Client) ?u64 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.client_id;
}
return null;
}
/// Returns the client IP as seen by the server.
/// This is the `client_ip` from the INFO response.
pub fn clientIp(self: *const Client) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
return info.client_ip;
}
return null;
}
/// Returns the connected server address as "host:port" string.
/// Writes to the provided buffer and returns the slice.
/// Returns null if not connected or buffer too small.
pub fn connectedAddr(
self: *const Client,
buf: []u8,
) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.server_info) |info| {
if (info.host.len == 0) return null;
assert(info.port > 0);
// Format "host:port" into buffer
const result = std.fmt.bufPrint(buf, "{s}:{d}", .{
info.host,
info.port,
}) catch return null;
return result;
}
return null;
}
/// Returns the connected URL with password redacted.
/// Replaces password with "***" for safe logging.
/// Writes to the provided buffer and returns the slice.
pub fn connectedUrlRedacted(
self: *const Client,
buf: []u8,
) ?[]const u8 {
assert(self.next_sid >= 1);
if (self.original_url_len == 0) return null;
const url = self.original_url[0..self.original_url_len];
// Check if URL has credentials (look for @ in URL)
const at_pos = std.mem.indexOf(u8, url, "@") orelse {
// No credentials, return as-is
if (buf.len < url.len) return null;
@memcpy(buf[0..url.len], url);
return buf[0..url.len];
};
// Find protocol prefix (nats:// or tls://)
var prefix_len: usize = 0;
if (std.mem.startsWith(u8, url, "nats://")) {
prefix_len = 7;
} else if (std.mem.startsWith(u8, url, "tls://")) {
prefix_len = 6;
}
const auth_part = url[prefix_len..at_pos];
const colon_pos = std.mem.indexOf(u8, auth_part, ":") orelse {
// No password (just token or user), return as-is
if (buf.len < url.len) return null;
@memcpy(buf[0..url.len], url);
return buf[0..url.len];
};
// Redact password: "user:pass@host" -> "user:***@host"
const user = auth_part[0..colon_pos];
const host_part = url[at_pos..];
const redacted_pass = "***";
const new_len =
prefix_len + user.len + 1 + redacted_pass.len + host_part.len;
if (buf.len < new_len) return null;
var pos: usize = 0;
@memcpy(buf[pos..][0..prefix_len], url[0..prefix_len]);
pos += prefix_len;
@memcpy(buf[pos..][0..user.len], user);
pos += user.len;
buf[pos] = ':';
pos += 1;
@memcpy(buf[pos..][0..redacted_pass.len], redacted_pass);
pos += redacted_pass.len;
@memcpy(buf[pos..][0..host_part.len], host_part);
pos += host_part.len;
assert(pos == new_len);
return buf[0..new_len];
}
/// Last error info returned by lastError().
pub const LastErrorInfo = struct {
err: anyerror,
msg: ?[]const u8,
};
/// Returns the last async error that occurred on the connection.
/// This includes server -ERR messages and other async errors.
/// The error message is from the server (e.g., permission violation).
/// Returns null if no error has occurred since last clear.
pub fn lastError(self: *const Client) ?LastErrorInfo {
assert(self.next_sid >= 1);
if (self.last_error) |err| {
const msg: ?[]const u8 = if (self.last_error_msg_len > 0)
self.last_error_msg[0..self.last_error_msg_len]
else
null;
return .{ .err = err, .msg = msg };
}
return null;
}
/// Clears the last error.
/// Call after handling the error to reset state.
pub fn clearLastError(self: *Client) void {
assert(self.next_sid >= 1);
self.last_error = null;
self.last_error_msg_len = 0;
}
// Connection State Methods
/// Returns the current connection state.
/// Thread-safe: uses atomic load for cross-thread visibility.
pub fn status(self: *const Client) State {
assert(self.next_sid >= 1);
return State.atomicLoad(&self.state);
}
/// Returns true if the connection is permanently closed.
/// Once closed, the client cannot be reconnected.
pub fn isClosed(self: *const Client) bool {
assert(self.next_sid >= 1);
return State.atomicLoad(&self.state) == .closed;
}
/// Returns true if the connection is draining.
/// During drain, no new subscriptions allowed but existing messages are delivered.
pub fn isDraining(self: *const Client) bool {
assert(self.next_sid >= 1);
return State.atomicLoad(&self.state) == .draining;
}
/// Returns true if the connection is attempting to reconnect.
pub fn isReconnecting(self: *const Client) bool {
assert(self.next_sid >= 1);
return State.atomicLoad(&self.state) == .reconnecting;
}
/// Returns the number of active subscriptions.
pub fn numSubscriptions(self: *const Client) usize {
assert(self.next_sid >= 1);
// free_count tracks available slots; total - free = active
return MAX_SUBSCRIPTIONS - self.free_count;
}
/// Measures round-trip time to the server by sending PING and waiting for PONG.
/// Returns RTT in nanoseconds.
/// This is a blocking operation that waits for the server to respond.
pub fn rtt(self: *Client) !u64 {
if (!State.atomicLoad(&self.state).canSend()) return error.NotConnected;
assert(self.next_sid >= 1);
// Record start time
const start_ns = getNowNs(self.io);
const old_pong_ns = self.last_pong_received_ns.load(.acquire);
// Send PING
try self.write_mutex.lock(self.io);
self.active_writer.writeAll("PING\r\n") catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
self.active_writer.flush() catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
if (self.use_tls) {
self.writer.interface.flush() catch {
self.write_mutex.unlock(self.io);
return error.WriteFailed;
};
}
self.write_mutex.unlock(self.io);
// Wait for PONG - poll last_pong_received_ns
// io_task handles PONG and updates the timestamp
const timeout_ns: u64 = 5_000_000_000;
var spin_count: u32 = 0;
while (true) {
const current_pong_ns = self.last_pong_received_ns.load(.acquire);
if (current_pong_ns > old_pong_ns) {
const end_ns = getNowNs(self.io);
return end_ns - start_ns;
}
spin_count += 1;
if (spin_count < defaults.Spin.max_spins) {
std.atomic.spinLoopHint();
} else {
self.io.sleep(
.fromNanoseconds(0),
.awake,
) catch {};
spin_count = 0;
const now_ns = getNowNs(self.io);
if (now_ns - start_ns >= timeout_ns) {
return error.Timeout;
}
}
}
}
/// Generates a new unique inbox subject using the configured prefix.
/// Caller owns returned memory.
pub fn newInbox(self: *Client) ![]u8 {
const allocator = self.allocator;
assert(self.next_sid >= 1);
const prefix = self.options.inbox_prefix;
const random_len = 22;
const total_len = prefix.len + 1 + random_len; // prefix.random
const result = try allocator.alloc(u8, total_len);
@memcpy(result[0..prefix.len], prefix);
result[prefix.len] = '.';
// Fill random portion with base62 characters
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ++
"abcdefghijklmnopqrstuvwxyz";
self.io.random(result[prefix.len + 1 ..]);
for (result[prefix.len + 1 ..]) |*b| {
b.* = alphabet[@mod(b.*, alphabet.len)];
}
return result;
}
/// Reset rate-limit counters, allowing errors to re-trigger events.
/// Call this if you want immediate re-notification of ongoing errors.
/// Resets both subscription alloc_failed and client protocol_error thresholds.
pub fn resetErrorNotifications(self: *Client) void {
for (self.sub_ptrs) |maybe_sub| {
if (maybe_sub) |sub| {
sub.last_alloc_notified_at = 0;
}
}
self.last_parse_error_notified_at = 0;
}
/// Get subscription by SID.
/// Uses cached pointer for fast path when single subscription matches.
// Caller must hold read_mutex while using the returned pointer if
// another task could unsubscribe/deinit the subscription concurrently.
pub inline fn getSubscriptionBySid(self: *Client, sid: u64) ?*Sub {
assert(sid > 0);
// Fast path: cached subscription (common in benchmarks)
if (self.cached_sub) |sub| {
if (sub.sid == sid) return sub;
}
// Normal hash lookup
if (self.sidmap.get(sid)) |slot_idx| {
return self.sub_ptrs[slot_idx];
}
return null;
}
/// Sends PONG response.
/// Sends PING for health check.
fn sendPing(self: *Client) !void {
assert(self.state == .connected);
self.write_mutex.lock(self.io) catch {
return error.WriteFailed;
};
defer self.write_mutex.unlock(self.io);
const writer = self.active_writer;
writer.writeAll("PING\r\n") catch {
return error.WriteFailed;
};
writer.flush() catch {
return error.WriteFailed;
};
if (self.use_tls) {
self.writer.interface.flush() catch {
return error.WriteFailed;
};
}
const now = getNowNs(self.io);
self.last_ping_sent_ns.store(now, .monotonic);
const new_outstanding =
self.pings_outstanding.fetchAdd(1, .monotonic) + 1;
dbg.pingPong("PING_SENT", new_outstanding);
}
/// Handles PONG response from server.
fn handlePong(self: *Client) void {
const now = getNowNs(self.io);
self.last_pong_received_ns.store(now, .release);
self.pings_outstanding.store(0, .monotonic);
dbg.pingPong("PONG_RECEIVED", 0);
}
/// Checks connection health, sends PING if needed.
/// Returns true if connection is stale (should trigger disconnect).
/// Called from io_task loop with throttling.
pub fn checkHealthAndDetectStale(self: *Client) bool {
if (self.options.ping_interval_ms == 0) return false;
if (State.atomicLoad(&self.state) != .connected) return false;
const now_ns = getNowNs(self.io);
const interval_ns: u64 =
@as(u64, self.options.ping_interval_ms) * 1_000_000;
// Check if too many PINGs outstanding (connection stale)
const outstanding = self.pings_outstanding.load(.monotonic);
if (outstanding >= self.options.max_pings_outstanding) {
dbg.print(
"[HEALTH] STALE: pings_outstanding={d} >= max={d}",
.{ outstanding, self.options.max_pings_outstanding },
);
return true;
}
// Check if it's time to send PING
const last_ping = self.last_ping_sent_ns.load(.monotonic);
if (now_ns - last_ping >= interval_ns) {
dbg.print(
"[HEALTH] Sending PING, pings_outstanding={d}",
.{outstanding},
);
self.sendPing() catch |err| {
dbg.print("[HEALTH] PING failed: {s}", .{@errorName(err)});
// Write failure indicates disconnection
if (err == error.BrokenPipe or err == error.ConnectionResetByPeer) {
return true;
}
};
}
return false;
}
/// Closes all subscription queues (wakes waiters with error).
pub fn closeAllQueues(self: *Client) void {
for (self.sub_ptrs) |maybe_sub| {
if (maybe_sub) |sub| {
sub.queue.close(self.io);
}
}
}
/// Closes the connection and frees all resources.
///
/// Closes connection, stops io_task, frees buffers.
/// Uses shutdown-recv-then-close pattern for reliable shutdown.
/// Safe to call multiple times.
pub fn deinit(self: *Client) void {
const alloc = self.allocator;
assert(self.next_sid >= 1);
// SHUTDOWN-RECV-THEN-CLOSE PATTERN
// shutdown(.recv) unblocks fillMore() (returns EndOfStream)
// while keeping write fd valid for in-flight writes.
// 1. Atomic state transition
const was_open = State.atomicLoad(&self.state) != .closed;
State.atomicStore(&self.state, .closed);
// 2. Shutdown read side only -- unblocks fillMore()
// Write fd stays valid for in-flight writes.
// Guard: fd may already be closed by io_task during
// disconnect or failed reconnect. BADF panics in
// debug mode (Io.Threaded treats it as programmer
// bug, not catchable). Validate fd before syscall.
if (was_open and isValidFd(self.stream.socket.handle)) {
self.stream.shutdown(self.io, .recv) catch {};
}
// 3. Wait for io_task to exit (no concurrent writers after)
if (self.io_task_future) |*future| {
_ = future.cancel(self.io);
self.io_task_future = null;
}
// 4. Full close -- io_task exited, no concurrent writers
if (was_open and isValidFd(self.stream.socket.handle)) {
self.stream.close(self.io);
}
// 5. Cancel callback task and free event queue
// SAFETY: Set event_queue = null BEFORE canceling to signal callback_task
// to exit. This prevents use-after-free if callback_task is mid-loop.
self.event_queue_mutex.lockUncancelable(self.io);
const eq = self.event_queue;
self.event_queue = null; // Signal callback_task to exit
if (eq) |queue| {
// Push closed event so callback_task can dispatch final onClose
_ = queue.push(.{ .closed = {} });
}
self.event_queue_mutex.unlock(self.io);
if (self.callback_task_future) |*future| {
_ = future.cancel(self.io);
self.callback_task_future = null;
}
// Now safe to free - callback_task has exited (state=closed + null check)
if (eq) |queue| {
alloc.destroy(queue);
}
if (self.event_queue_buf) |buf| {
alloc.free(buf);
self.event_queue_buf = null;
}
// 5b. Tear down the response multiplexer.
// Owns a wildcard subscription that we destroy here so the
// step-6 sub_ptrs loop sees a cleared slot. Signals all
// in-flight waiters so blocked request() calls return null.
self.closeRespMux();
// 6. Cleanup subscriptions (io_task is now gone)
self.closeAllQueues();
for (self.sub_ptrs) |maybe_sub| {
if (maybe_sub) |sub| {
sub.state = .unsubscribed;
sub.client_destroyed = true;
}
}
// Free client resources
if (self.server_info) |*info| {
info.deinit(alloc);
}
// Drain return queue before destroying slab (free any pending buffers)
while (self.return_queue.pop()) |buf| {
self.tiered_slab.free(buf);
}
alloc.free(self.return_queue_buf);
self.tiered_slab.deinit();
// Free pending buffer
self.deinitPendingBuffer();
// Free TLS resources
self.tls_client = null;
if (self.tls_read_buffer) |buf| {
alloc.free(buf);
self.tls_read_buffer = null;
}
if (self.tls_write_buffer) |buf| {
alloc.free(buf);
self.tls_write_buffer = null;
}
if (self.ca_bundle) |*bundle| {
bundle.deinit(alloc);
self.ca_bundle = null;
}
alloc.free(self.read_buffer);
alloc.free(self.write_buffer);
if (self.publish_ring_buf) |b| alloc.free(b);
alloc.destroy(self);
}
// -- Reconnection Support
/// Backup all active subscriptions for restoration after reconnect.
/// Stores SID, subject, and queue_group in inline buffers (no allocation).
/// Returns error if any subject > 256 bytes or queue_group > 64 bytes.
pub fn backupSubscriptions(self: *Client) error{ SubjectTooLong, QueueGroupTooLong }!void {
self.sub_mutex.lockUncancelable(self.io);
defer self.sub_mutex.unlock(self.io);
self.sub_backup_count = 0;
for (self.sub_ptrs) |maybe_sub| {
if (maybe_sub) |sub| {
if (sub.state != .active) continue;
if (self.sub_backup_count >= MAX_SUBSCRIPTIONS) break;
// Validate lengths - reject truncation
if (sub.subject.len >= defaults.Limits.max_subject_len) {
self.pushEvent(.{
.err = .{
.err = events_mod.Error.SubjectTooLong,
.msg = null,
},
});
return error.SubjectTooLong;
}
if (sub.queue_group) |qg| {
if (qg.len > defaults.Limits.max_queue_group_len) {
self.pushEvent(.{
.err = .{
.err = events_mod.Error.QueueGroupTooLong,
.msg = null,
},
});
return error.QueueGroupTooLong;
}
}
var backup = &self.sub_backups[self.sub_backup_count];
backup.sid = sub.sid;
// Copy subject (validated above)
const subj_len: u8 = @intCast(sub.subject.len);
@memcpy(backup.subject_buf[0..subj_len], sub.subject);
backup.subject_len = subj_len;
// Copy queue_group if present (validated above)
if (sub.queue_group) |qg| {
const qg_len: u8 = @intCast(qg.len);
@memcpy(backup.queue_group_buf[0..qg_len], qg);
backup.queue_group_len = qg_len;
} else {
backup.queue_group_len = 0;
}
self.sub_backup_count += 1;
}
}
}
/// Restore subscriptions after reconnect (preserves original SIDs).
/// Re-sends SUB commands to server with the same SIDs so existing
/// subscription pointers continue to work.
pub fn restoreSubscriptions(self: *Client) !void {
if (self.sub_backup_count == 0) return;
// Acquire write_mutex — restoreSubscriptions writes to socket.
try self.write_mutex.lock(self.io);
defer self.write_mutex.unlock(self.io);
const writer = self.active_writer;
for (self.sub_backups[0..self.sub_backup_count]) |*backup| {
if (backup.sid == 0) continue;
const subject = backup.getSubject();
const queue_group = backup.queueGroup();
// Send SUB with SAME SID
protocol.Encoder.encodeSub(writer, .{
.subject = subject,
.queue_group = queue_group,
.sid = backup.sid,
}) catch return error.RestoreSubscriptionsFailed;
}
writer.flush() catch return error.RestoreSubscriptionsFailed;
}
/// Initialize pending buffer for publishes during reconnect.
fn initPendingBuffer(self: *Client) !void {
const allocator = self.allocator;
if (self.options.pending_buffer_size == 0) return;
if (self.pending_buffer != null) return;
self.pending_buffer = try allocator.alloc(
u8,
self.options.pending_buffer_size,
);
self.pending_buffer_capacity = self.options.pending_buffer_size;
self.pending_buffer_pos = 0;
}
/// Free pending buffer.
fn deinitPendingBuffer(self: *Client) void {
const allocator = self.allocator;
if (self.pending_buffer) |buf| {
allocator.free(buf);
self.pending_buffer = null;
self.pending_buffer_pos = 0;
self.pending_buffer_capacity = 0;
}
}
/// Fast integer-to-string (avoids std.fmt overhead).
fn writeUsizeToSlice(buf: *[20]u8, value: usize) []const u8 {
if (value == 0) {
buf[19] = '0';
return buf[19..20];
}
var v = value;
var i: usize = 20;
while (v > 0) : (v /= 10) {
i -= 1;
buf[i] = @intCast((v % 10) + '0');
}
return buf[i..20];
}
/// Max encoded size for PUB subject [reply] len\r\npayload\r\n
fn pubEncodedSize(
subject: []const u8,
reply_to: ?[]const u8,
payload: []const u8,
) usize {
// "PUB " + subject + " " + [reply + " "] + len(max 20) + "\r\n" + payload + "\r\n"
var size: usize = 4 + subject.len + 1 + 20 + 2 + payload.len + 2;
if (reply_to) |r| size += r.len + 1;
return size;
}
/// Encode PUB into publish ring (lock-free).
/// Spins briefly if ring is full, then returns error.
fn encodePubToRing(
self: *Client,
subject: []const u8,
reply_to: ?[]const u8,
payload: []const u8,
) !void {
const max_size = pubEncodedSize(
subject,
reply_to,
payload,
);
const entry = self.reserveRingEntry(max_size) orelse
return error.PublishBufferFull;
const buf = entry[RING_HDR_SIZE..];
var pos: usize = 0;
// "PUB "
@memcpy(buf[pos..][0..4], "PUB ");
pos += 4;
// subject
@memcpy(buf[pos..][0..subject.len], subject);
pos += subject.len;
// [reply_to]
if (reply_to) |r| {
buf[pos] = ' ';
pos += 1;
@memcpy(buf[pos..][0..r.len], r);
pos += r.len;
}
// " " + payload length
buf[pos] = ' ';
pos += 1;
var num_buf: [20]u8 = undefined;
const len_str = writeUsizeToSlice(
&num_buf,
payload.len,
);
@memcpy(buf[pos..][0..len_str.len], len_str);
pos += len_str.len;
// "\r\n"
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
// payload
@memcpy(buf[pos..][0..payload.len], payload);
pos += payload.len;
// "\r\n"
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
self.publish_ring.commit(entry, pos);
}
/// Encode HPUB with structured header entries into ring.
fn encodeHPubToRing(
self: *Client,
subject: []const u8,
reply_to: ?[]const u8,
hdrs: []const headers.Entry,
payload: []const u8,
) !void {
try headers.validateEntries(hdrs);
const hdr_size = headers.encodedSize(hdrs);
const total_len = hdr_size + payload.len;
// "HPUB " + subject + " " + [reply + " "] + hdr_len(20) + " " + total_len(20) + "\r\n" + headers + payload + "\r\n"
var max_size: usize = 5 + subject.len + 1 + 20 + 1 + 20 + 2 + hdr_size + payload.len + 2;
if (reply_to) |r| max_size += r.len + 1;
const entry = self.reserveRingEntry(max_size) orelse
return error.PublishBufferFull;
const buf = entry[RING_HDR_SIZE..];
var pos: usize = 0;
@memcpy(buf[pos..][0..5], "HPUB ");
pos += 5;
@memcpy(buf[pos..][0..subject.len], subject);
pos += subject.len;
if (reply_to) |r| {
buf[pos] = ' ';
pos += 1;
@memcpy(buf[pos..][0..r.len], r);
pos += r.len;
}
var num_buf: [20]u8 = undefined;
buf[pos] = ' ';
pos += 1;
const hdr_str = writeUsizeToSlice(&num_buf, hdr_size);
@memcpy(buf[pos..][0..hdr_str.len], hdr_str);
pos += hdr_str.len;
buf[pos] = ' ';
pos += 1;
const tot_str = writeUsizeToSlice(&num_buf, total_len);
@memcpy(buf[pos..][0..tot_str.len], tot_str);
pos += tot_str.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
// Encode headers into buffer
const hdr_buf = buf[pos .. pos + hdr_size];
headers.encodeToBuf(hdr_buf, hdrs);
pos += hdr_size;
@memcpy(buf[pos..][0..payload.len], payload);
pos += payload.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
self.publish_ring.commit(entry, pos);
}
/// Encode HPUB with raw pre-encoded header bytes into ring.
fn encodeHPubRawToRing(
self: *Client,
subject: []const u8,
reply_to: ?[]const u8,
hdr_bytes: []const u8,
payload: []const u8,
) !void {
const total_len = hdr_bytes.len + payload.len;
var max_size: usize = 5 + subject.len + 1 + 20 + 1 + 20 + 2 + hdr_bytes.len + payload.len + 2;
if (reply_to) |r| max_size += r.len + 1;
const entry = self.reserveRingEntry(max_size) orelse
return error.PublishBufferFull;
const buf = entry[RING_HDR_SIZE..];
var pos: usize = 0;
@memcpy(buf[pos..][0..5], "HPUB ");
pos += 5;
@memcpy(buf[pos..][0..subject.len], subject);
pos += subject.len;
if (reply_to) |r| {
buf[pos] = ' ';
pos += 1;
@memcpy(buf[pos..][0..r.len], r);
pos += r.len;
}
var num_buf: [20]u8 = undefined;
buf[pos] = ' ';
pos += 1;
const hdr_str = writeUsizeToSlice(
&num_buf,
hdr_bytes.len,
);
@memcpy(buf[pos..][0..hdr_str.len], hdr_str);
pos += hdr_str.len;
buf[pos] = ' ';
pos += 1;
const tot_str = writeUsizeToSlice(
&num_buf,
total_len,
);
@memcpy(buf[pos..][0..tot_str.len], tot_str);
pos += tot_str.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
@memcpy(buf[pos..][0..hdr_bytes.len], hdr_bytes);
pos += hdr_bytes.len;
@memcpy(buf[pos..][0..payload.len], payload);
pos += payload.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
self.publish_ring.commit(entry, pos);
}
/// Encode SUB into publish ring.
fn encodeSubToRing(
self: *Client,
args: protocol.SubArgs,
) !void {
try pubsub.validateSubscribe(args.subject);
if (args.sid == 0) return error.InvalidSid;
if (args.queue_group) |queue| {
if (queue.len > 0) {
try pubsub.validateQueueGroup(queue);
}
}
var max_size: usize = 4 + args.subject.len + 1 + 20 + 2;
if (args.queue_group) |queue| {
if (queue.len > 0) max_size += queue.len + 1;
}
const entry = self.reserveRingEntry(max_size) orelse
return error.PublishBufferFull;
const buf = entry[RING_HDR_SIZE..];
var pos: usize = 0;
@memcpy(buf[pos..][0..4], "SUB ");
pos += 4;
@memcpy(buf[pos..][0..args.subject.len], args.subject);
pos += args.subject.len;
if (args.queue_group) |queue| {
if (queue.len > 0) {
buf[pos] = ' ';
pos += 1;
@memcpy(buf[pos..][0..queue.len], queue);
pos += queue.len;
}
}
buf[pos] = ' ';
pos += 1;
var num_buf: [20]u8 = undefined;
const sid_str = writeUsizeToSlice(&num_buf, args.sid);
@memcpy(buf[pos..][0..sid_str.len], sid_str);
pos += sid_str.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
self.publish_ring.commit(entry, pos);
}
/// Encode UNSUB into publish ring.
fn encodeUnsubToRing(
self: *Client,
args: protocol.commands.UnsubArgs,
) !void {
if (args.sid == 0) return error.InvalidSid;
var max_size: usize = 6 + 20 + 2;
if (args.max_msgs != null) max_size += 1 + 20;
const entry = self.reserveRingEntry(max_size) orelse
return error.PublishBufferFull;
const buf = entry[RING_HDR_SIZE..];
var pos: usize = 0;
@memcpy(buf[pos..][0..6], "UNSUB ");
pos += 6;
var num_buf: [20]u8 = undefined;
const sid_str = writeUsizeToSlice(&num_buf, args.sid);
@memcpy(buf[pos..][0..sid_str.len], sid_str);
pos += sid_str.len;
if (args.max_msgs) |max| {
buf[pos] = ' ';
pos += 1;
const max_str = writeUsizeToSlice(&num_buf, max);
@memcpy(buf[pos..][0..max_str.len], max_str);
pos += max_str.len;
}
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
self.publish_ring.commit(entry, pos);
}
/// Reserve a ring entry with brief spin on full.
/// Returns the entry slice or null if still full after ~1ms.
fn reserveRingEntry(
self: *Client,
max_size: usize,
) ?[]u8 {
// Try immediately
if (self.publish_ring.reserve(max_size)) |e| return e;
// Backpressure: spin → yield → sleep → fail
for (0..256) |_| {
std.atomic.spinLoopHint();
if (self.publish_ring.reserve(max_size)) |e| return e;
}
for (0..64) |_| {
std.Thread.yield() catch {};
if (self.publish_ring.reserve(max_size)) |e| return e;
}
// Sleep in 10us increments to let io_task drain.
// 1000 iterations = ~10ms budget, well above the
// io_task's ~1ms drain cycle.
for (0..1000) |_| {
var ts: std.posix.timespec = .{
.sec = 0,
.nsec = 10_000,
};
_ = std.posix.system.nanosleep(&ts, &ts);
if (self.publish_ring.reserve(max_size)) |e|
return e;
}
return null;
}
/// Buffer a publish during reconnect.
/// Returns error if buffer is full or not initialized.
fn bufferPendingPublish(
self: *Client,
subject: []const u8,
payload: []const u8,
) !void {
const buf = self.pending_buffer orelse return error.NotConnected;
const remaining = self.pending_buffer_capacity - self.pending_buffer_pos;
// Estimate encoded size: "PUB subject len\r\npayload\r\n"
// PUB + space + subject + space + len(max 10 digits) + \r\n + payload + \r\n
const encoded_size = 4 + subject.len + 1 + 10 + 2 + payload.len + 2;
if (encoded_size > remaining) {
return error.PendingBufferFull;
}
var writer = Io.Writer.fixed(buf[self.pending_buffer_pos..]);
// Write PUB command manually (simpler than using Encoder)
try writer.writeAll("PUB ");
try writer.writeAll(subject);
try writer.writeByte(' ');
// Write payload length
var len_buf: [10]u8 = undefined;
const len_str = std.fmt.bufPrint(&len_buf, "{d}", .{payload.len}) catch {
return error.EncodingFailed;
};
try writer.writeAll(len_str);
try writer.writeAll("\r\n");
try writer.writeAll(payload);
try writer.writeAll("\r\n");
self.pending_buffer_pos += writer.end;
}
/// Flush pending buffer after reconnect.
fn flushPendingBuffer(self: *Client) !void {
if (self.pending_buffer_pos == 0) return;
const buf = self.pending_buffer orelse return;
self.active_writer.writeAll(buf[0..self.pending_buffer_pos]) catch {
return error.WriteFailed;
};
self.active_writer.flush() catch return error.WriteFailed;
self.pending_buffer_pos = 0;
dbg.pendingBuffer("FLUSHED", 0, self.pending_buffer_capacity);
}
/// Cleanup client state for reconnection.
/// Closes old stream but preserves subscriptions and pending buffer.
pub fn cleanupForReconnect(self: *Client) void {
dbg.stateChange("cleanup", "for_reconnect");
// Wait for in-flight main-thread writes.
// State is already .disconnected -- no new
// writes will start (canSend() returns false).
self.write_mutex.lock(self.io) catch {
self.stream.close(self.io);
self.tls_client = null;
self.active_reader = &self.reader.interface;
self.active_writer = &self.writer.interface;
return;
};
self.stream.close(self.io);
self.tls_client = null;
// Reset to raw TCP to avoid dangling TLS pointers
self.active_reader = &self.reader.interface;
self.active_writer = &self.writer.interface;
self.write_mutex.unlock(self.io);
}
/// Attempt connection to a single server.
/// Returns true on success, error on failure.
pub fn tryConnect(
self: *Client,
server: *connection.server_pool.Server,
) !void {
const raw_host = server.getHost();
const port = server.port;
dbg.reconnectEvent(
"CONNECTING",
self.reconnect_attempt + 1,
server.getUrl(),
);
// Connect
self.stream = try connectToHost(self.io, raw_host, port);
errdefer self.stream.close(self.io);
// Set TCP_NODELAY
const enable: u32 = 1;
std.posix.setsockopt(
self.stream.socket.handle,
std.posix.IPPROTO.TCP,
std.posix.TCP.NODELAY,
std.mem.asBytes(&enable),
) catch {};
// Reinitialize reader/writer with existing buffers
self.reader = self.stream.reader(self.io, self.read_buffer);
self.writer = self.stream.writer(self.io, self.write_buffer);
// Reset to TCP reader/writer (upgradeTls will update if needed)
self.active_reader = &self.reader.interface;
self.active_writer = &self.writer.interface;
// Parse URL to get auth info and TLS flag
const parsed = parseUrl(server.getUrl()) catch ParsedUrl{
.host = raw_host,
.port = port,
.user = null,
.pass = null,
.use_tls = self.use_tls,
};
self.use_tls = self.use_tls or parsed.use_tls or
self.options.tls_required or self.options.tls_ca_file != null or
self.options.tls_handshake_first;
// Update TLS host for reconnection (server might have different hostname)
if (self.use_tls) {
const actual_host = parsed.host;
if (actual_host.len == 0 or actual_host.len > 255)
return error.HostTooLong;
const host_len: u8 = @intCast(actual_host.len);
@memcpy(self.tls_host[0..host_len], actual_host);
self.tls_host_len = host_len;
}
// TLS-first mode: upgrade to TLS before NATS protocol
if (self.use_tls and self.options.tls_handshake_first) {
try self.upgradeTls(self.options);
}
// Perform handshake (includes TLS upgrade after INFO if needed)
try self.handshake(self.options, parsed);
// Initialize health check timestamps (atomics)
const now_ns = getNowNs(self.io);
self.last_ping_sent_ns.store(now_ns, .monotonic);
self.last_pong_received_ns.store(now_ns, .monotonic);
self.pings_outstanding.store(0, .monotonic);
dbg.reconnectEvent(
"CONNECTED",
self.reconnect_attempt + 1,
server.getUrl(),
);
}
/// Wait with exponential backoff + jitter.
pub fn waitBackoff(self: *Client) void {
const opts = self.options;
const attempt = @min(self.reconnect_attempt, 10);
const base: u64 = opts.reconnect_wait_ms;
const exp_wait = base << @as(u6, @intCast(attempt));
const capped = @min(exp_wait, opts.reconnect_wait_max_ms);
// Add jitter using Io.random()
var rand_buf: [4]u8 = undefined;
self.io.random(&rand_buf);
const rand = std.mem.readInt(u32, &rand_buf, .little);
const jitter_range = capped * opts.reconnect_jitter_percent / 100;
// Calculate final wait with jitter
var final_wait = capped;
if (jitter_range > 0) {
const jitter_val = rand % (jitter_range * 2 + 1);
if (jitter_val > jitter_range) {
final_wait = capped + (jitter_val - jitter_range);
} else {
final_wait = capped -| jitter_range + jitter_val;
}
}
final_wait = @max(100, final_wait);
dbg.print(
"Backoff wait: {d}ms (attempt {d})",
.{ final_wait, attempt + 1 },
);
self.io.sleep(.fromMilliseconds(final_wait), .awake) catch {};
}
/// Attempt reconnection with exponential backoff.
/// Can be called automatically (from io_task) or manually by user.
pub fn reconnect(self: *Client) !void {
if (self.state != .disconnected and self.state != .reconnecting) {
if (self.state == .connected) return;
return error.InvalidState;
}
if (!self.options.reconnect) {
return error.ReconnectDisabled;
}
// Initialize server pool if not already done
if (!self.server_pool_initialized) {
const url = self.original_url[0..self.original_url_len];
self.server_pool = connection.ServerPool.init(url) catch {
return error.InvalidUrl;
};
self.server_pool_initialized = true;
}
// Backup subscriptions before cleanup (error = subs won't restore)
self.backupSubscriptions() catch |err| {
dbg.print("backupSubscriptions failed: {s}", .{@errorName(err)});
// Error event already pushed by backupSubscriptions
};
self.cleanupForReconnect();
State.atomicStore(&self.state, .reconnecting);
const max = self.options.max_reconnect_attempts;
const infinite = max == 0;
dbg.print(
"Starting reconnect (max_attempts={d}, infinite={any})",
.{ max, infinite },
);
while (infinite or self.reconnect_attempt < max) {
// Get next server
const now_ns = getNowNs(self.io);
const server = self.server_pool.nextServer(now_ns) orelse {
dbg.print("All servers on cooldown, waiting...", .{});
self.waitBackoff();
continue;
};
dbg.reconnectEvent(
"ATTEMPT",
self.reconnect_attempt + 1,
server.getUrl(),
);
// Attempt connection
if (self.tryConnect(server)) {
// Connection succeeded
self.restoreSubscriptions() catch |err| {
dbg.print(
"Failed to restore subscriptions: {s}",
.{@errorName(err)},
);
// Notify user - subscriptions may be broken, they can re-sub
self.pushEvent(.{
.err = .{
.err = events_mod.Error.SubscriptionRestoreFailed,
.msg = null,
},
});
};
self.flushPendingBuffer() catch |err| {
dbg.print(
"Failed to flush pending buffer: {s}",
.{@errorName(err)},
);
};
State.atomicStore(&self.state, .connected);
self.reconnect_attempt = 0;
const reconnects =
self.statistics.reconnects.fetchAdd(1, .monotonic) + 1;
self.server_pool.resetFailures();
dbg.stateChange("reconnecting", "connected");
dbg.print(
"Reconnect successful (total reconnects: {d})",
.{reconnects},
);
return;
} else |err| {
dbg.reconnectEvent(
"FAILED",
self.reconnect_attempt + 1,
server.getUrl(),
);
dbg.print("Connection attempt failed: {s}", .{@errorName(err)});
self.server_pool.markCurrentFailed();
self.reconnect_attempt += 1;
self.waitBackoff();
}
}
// All attempts exhausted
self.state = .closed;
dbg.stateChange("reconnecting", "closed");
dbg.print("Reconnect failed: max attempts ({d}) exhausted", .{max});
return error.ReconnectFailed;
}
/// Subscription state.
pub const SubscriptionState = enum {
active,
draining,
unsubscribed,
};
/// Whether a subscription is manually polled or callback-driven.
pub const SubscriptionMode = enum {
manual,
callback,
};
/// Subscription with Io.Queue for async message delivery.
///
/// Supports multiple concurrent consumers via inline routing:
/// - First subscriber to call nextMsg() reads from socket
/// - Messages for other subscriptions are routed to their queues
/// - Io.Mutex ensures only one reader at a time
///
/// Use nextMsg() for blocking receive, nextMsgTimeout() for bounded waits,
/// or tryNextMsg() for non-blocking poll.
pub const Subscription = struct {
client: *Client,
sid: u64,
subject: []const u8,
queue_group: ?[]const u8,
queue_buf: []Message,
queue: SpscQueue(Message),
state: SubscriptionState,
received_msgs: u64,
dropped_msgs: u64 = 0,
alloc_failed_msgs: u64 = 0,
client_destroyed: bool = false,
/// msgs_in value when alloc_failed event was last pushed (rate-limit).
last_alloc_notified_at: u64 = 0,
// Callback subscription support
/// Whether this sub is manually polled or callback-driven.
mode: SubscriptionMode = .manual,
/// Future for the callback drain task (set for callback subs).
callback_future: ?Io.Future(void) = null,
// Auto-unsubscribe support
/// Maximum messages before auto-unsubscribe. Null = no limit.
/// When set, subscription auto-unsubscribes after this many messages.
max_msgs: ?u64 = null,
/// Count of delivered messages for auto-unsubscribe tracking.
/// Only tracked when max_msgs is set (opt-in for performance).
delivered_count: u64 = 0,
/// Flag set when auto-unsubscribe triggered (for io_task to send UNSUB).
auto_unsub_triggered: bool = false,
// Pending limits (flow control)
/// Maximum pending messages allowed in queue. 0 = no limit.
pending_limit: usize = 0,
/// Maximum pending bytes allowed in queue. 0 = no limit.
pending_bytes_limit: usize = 0,
// REVIEWED(2025-03): pending_bytes is non-atomic by design.
// io_task writes (+), user thread reads/decrements (-|).
// Race is bounded: worst case slightly over/under-counts,
// acceptable for flow control approximation. Atomics would
// add overhead to every message push/pop on the hot path.
pending_bytes: u64 = 0,
/// High watermark for pending message count.
max_pending_msgs: u64 = 0,
/// High watermark for pending bytes.
max_pending_bytes: u64 = 0,
/// Spin-yield loop to pop next message from queue.
/// Internal: used by both nextMsg() and callback drain tasks.
fn nextRaw(self: *Subscription, io: Io) !Message {
assert(self.state == .active or self.state == .draining);
dbg.print(
"Sub.nextRaw: ENTERED, queue len={d}",
.{self.queue.len()},
);
var spin_count: u32 = 0;
var yield_count: u32 = 0;
while (true) {
if (self.queue.pop()) |msg| {
const msg_size =
if (msg.backing_buf) |buf| buf.len else msg.size();
self.pending_bytes -|= msg_size;
dbg.print(
"Sub.nextRaw: GOT MESSAGE after {d} yields",
.{yield_count},
);
return msg;
}
if (self.queue.isClosed()) {
return error.Closed;
}
if (self.state != .active and
self.state != .draining)
{
return error.Closed;
}
spin_count += 1;
if (spin_count < defaults.Spin.max_spins) {
std.atomic.spinLoopHint();
} else {
io.sleep(
.fromNanoseconds(0),
.awake,
) catch |err| {
if (err == error.Canceled)
return error.Canceled;
};
spin_count = 0;
yield_count += 1;
if (yield_count % 10000 == 0) {
dbg.print(
"Sub.nextRaw: still waiting, yields={d}",
.{yield_count},
);
}
}
}
}
/// Blocks until a message is available or connection is closed.
///
/// Only valid for manual-mode subscriptions (not callback).
/// Returns owned Message that caller must free via msg.deinit().
pub fn nextMsg(
self: *Subscription,
) !Message {
assert(self.mode == .manual);
if (self.mode != .manual)
return error.SubscriptionModeConflict;
return self.nextRaw(self.client.io);
}
/// Try receive without blocking. Returns null if no message.
/// Only valid for manual-mode subscriptions.
pub fn tryNextMsg(self: *Subscription) ?Message {
assert(self.mode == .manual);
if (self.mode != .manual) return null;
if (self.queue.pop()) |msg| {
const msg_size =
if (msg.backing_buf) |buf| buf.len else msg.size();
self.pending_bytes -|= msg_size;
return msg;
}
return null;
}
/// Batch receive - waits for at least 1, returns up to buf.len.
/// Only valid for manual-mode subscriptions.
pub fn nextMsgBatch(
self: *Subscription,
io: Io,
buf: []Message,
) !usize {
assert(self.mode == .manual);
if (self.mode != .manual)
return error.SubscriptionModeConflict;
assert(self.state == .active or self.state == .draining);
assert(buf.len > 0);
var spin_count: u32 = 0;
while (true) {
const count = self.queue.popBatch(buf);
if (count > 0) {
for (buf[0..count]) |msg| {
const msg_size =
if (msg.backing_buf) |buf_|
buf_.len
else
msg.size();
self.pending_bytes -|= msg_size;
}
return count;
}
if (self.queue.isClosed()) {
return error.Closed;
}
if (self.state != .active and
self.state != .draining)
{
return error.Closed;
}
spin_count += 1;
if (spin_count < defaults.Spin.max_spins) {
std.atomic.spinLoopHint();
} else {
io.sleep(
.fromNanoseconds(0),
.awake,
) catch |err| {
if (err == error.Canceled)
return error.Canceled;
};
spin_count = 0;
}
}
}
/// Non-blocking batch receive.
/// Only valid for manual-mode subscriptions.
pub fn tryNextMsgBatch(self: *Subscription, buf: []Message) usize {
assert(self.mode == .manual);
if (self.mode != .manual) return 0;
const count = self.queue.popBatch(buf);
for (buf[0..count]) |msg| {
const msg_size =
if (msg.backing_buf) |buf_| buf_.len else msg.size();
self.pending_bytes -|= msg_size;
}
return count;
}
/// Receive with timeout. Spins briefly for low
/// latency, then yields to the IO event loop.
/// Returns null on timeout. Supports cancellation
/// via io.sleep yield points.
pub fn nextMsgTimeout(
self: *Subscription,
timeout_ms: u32,
) !?Message {
assert(self.mode == .manual);
if (self.mode != .manual)
return error.SubscriptionModeConflict;
assert(self.state == .active or
self.state == .draining);
assert(timeout_ms > 0);
const io = self.client.io;
const start = getNowNs(io);
const timeout_ns: u64 =
@as(u64, timeout_ms) * std.time.ns_per_ms;
var spin_count: u32 = 0;
while (true) {
if (self.queue.pop()) |msg| {
const msg_size =
if (msg.backing_buf) |buf|
buf.len
else
msg.size();
self.pending_bytes -|= msg_size;
return msg;
}
if (self.queue.isClosed()) {
return error.Closed;
}
if (self.state != .active and
self.state != .draining)
{
return error.Closed;
}
spin_count += 1;
if (spin_count < defaults.Spin.max_spins) {
std.atomic.spinLoopHint();
} else {
// Yield to IO event loop. This:
// - stops burning CPU
// - enables future.cancel()
// - works in Debug mode
io.sleep(
.fromNanoseconds(0),
.awake,
) catch |err| {
if (err == error.Canceled)
return error.Canceled;
};
spin_count = 0;
// Check timeout after yielding
const now = getNowNs(io);
if (now -| start >= timeout_ns)
return null;
}
}
}
/// Returns queue capacity.
pub fn capacity(self: *const Subscription) usize {
return self.queue.capacity;
}
/// Returns count of messages dropped due to queue overflow.
/// Only incremented when other subscriptions route messages to this one
/// and the queue is full. The reading subscription bypasses its queue.
pub fn dropped(self: *const Subscription) u64 {
return self.dropped_msgs;
}
/// Returns count of messages dropped due to allocation failure.
pub fn allocFailed(self: *const Subscription) u64 {
return self.alloc_failed_msgs;
}
// Subscription Control Methods
/// Auto-unsubscribe after receiving max messages.
/// Sends UNSUB with max_msgs to server for server-side enforcement.
/// Client also tracks and triggers local cleanup.
pub fn autoUnsubscribe(self: *Subscription, max: u64) !void {
assert(max > 0);
if (self.state != .active) return error.InvalidState;
if (self.client_destroyed) return error.InvalidState;
self.max_msgs = max;
self.delivered_count = 0;
// Enqueue UNSUB with max_msgs so it stays ordered with publishes.
const client = self.client;
if (State.atomicLoad(&client.state).canSend()) {
client.publish_mutex.lockUncancelable(client.io);
defer client.publish_mutex.unlock(client.io);
client.encodeUnsubToRing(.{
.sid = self.sid,
.max_msgs = max,
}) catch return error.EncodingFailed;
// Signal auto-flush to send UNSUB promptly
client.flush_requested.store(true, .release);
}
}
/// Gracefully drain this subscription.
/// Stops receiving new messages but delivers already-queued messages.
pub fn drain(self: *Subscription) !void {
if (self.state != .active) return;
if (self.client_destroyed) return error.InvalidState;
const client = self.client;
if (State.atomicLoad(&client.state).canSend()) {
client.publish_mutex.lockUncancelable(client.io);
defer client.publish_mutex.unlock(client.io);
client.encodeUnsubToRing(.{
.sid = self.sid,
.max_msgs = null,
}) catch return error.EncodingFailed;
// Signal auto-flush to send UNSUB promptly
client.flush_requested.store(true, .release);
}
self.state = .draining;
}
/// Blocks until the subscription queue is empty (drained) or timeout.
///
/// Call after drain() to wait for all queued messages to be consumed.
/// Returns error.Timeout if the queue is not empty after timeout_ms.
/// Returns error.NotDraining if subscription is not in draining state.
///
/// Note: This only waits for the queue to empty. Call unsubscribe() or
/// deinit() afterward to fully clean up the subscription.
///
/// Example:
/// ```
/// try sub.drain();
/// // ... consume messages with nextMsg() ...
/// try sub.waitDrained(5000); // Wait up to 5 seconds
/// sub.deinit(allocator); // Clean up
/// ```
pub fn waitDrained(self: *Subscription, timeout_ms: u32) !void {
assert(timeout_ms > 0);
if (self.state != .draining) {
return error.NotDraining;
}
// Already drained
if (self.queue.len() == 0) {
return;
}
const io = self.client.io;
const start = getNowNs(io);
const timeout_ns: u64 =
@as(u64, timeout_ms) * std.time.ns_per_ms;
var spin_count: u32 = 0;
while (true) {
if (self.queue.len() == 0) {
return;
}
spin_count += 1;
if (spin_count < defaults.Spin.max_spins) {
std.atomic.spinLoopHint();
} else {
io.sleep(
.fromNanoseconds(0),
.awake,
) catch {};
spin_count = 0;
const now = getNowNs(io);
if (now -| start >= timeout_ns)
return error.Timeout;
}
}
}
/// Returns the number of messages pending in the queue.
pub fn pending(self: *const Subscription) usize {
return self.queue.len();
}
/// Returns the count of delivered messages.
/// Only tracked when autoUnsubscribe is set.
pub fn delivered(self: *const Subscription) u64 {
return self.delivered_count;
}
/// Sets the maximum pending message limit.
/// When exceeded, new messages are dropped (slow consumer).
/// Set to 0 for no limit (default).
pub fn setPendingLimits(self: *Subscription, msg_limit: usize) void {
self.pending_limit = msg_limit;
}
/// Returns the current pending message limit. 0 means no limit.
pub fn pendingLimits(self: *const Subscription) usize {
return self.pending_limit;
}
/// Sets the maximum pending bytes limit.
/// When exceeded, new messages are dropped (slow consumer).
/// Set to 0 for no limit (default).
pub fn setPendingBytesLimit(self: *Subscription, bytes_limit: usize) void {
self.pending_bytes_limit = bytes_limit;
}
/// Returns the current pending bytes limit. 0 means no limit.
pub fn pendingBytesLimit(self: *const Subscription) usize {
return self.pending_bytes_limit;
}
/// Returns true if the subscription is valid and can receive messages.
pub fn isValid(self: *const Subscription) bool {
if (self.client_destroyed) return false;
return self.state == .active or self.state == .draining;
}
// Subscription Info Getters
/// Returns the subscription ID (SID).
/// Unique within the connection, assigned during subscribe.
pub fn getSid(self: *const Subscription) u64 {
assert(self.sid > 0);
return self.sid;
}
/// Returns the subscription subject pattern.
/// May contain wildcards (* and >).
pub fn getSubject(self: *const Subscription) []const u8 {
assert(self.subject.len > 0);
return self.subject;
}
/// Returns the queue group name if subscribed as a queue subscriber.
/// Returns null for regular subscriptions.
pub fn queueGroup(self: *const Subscription) ?[]const u8 {
return self.queue_group;
}
/// Returns true if this subscription is currently draining.
/// A draining subscription will deliver queued messages but not receive new ones.
pub fn isDraining(self: *const Subscription) bool {
return self.state == .draining;
}
// Subscription Statistics
/// Subscription statistics snapshot.
pub const SubStats = struct {
/// Current messages pending in queue.
pending_msgs: usize,
/// Current bytes pending in queue.
pending_bytes: u64,
/// High watermark for pending message count.
max_pending_msgs: u64,
/// High watermark for pending bytes.
max_pending_bytes: u64,
/// Total messages delivered to this subscription.
delivered: u64,
/// Messages dropped due to slow consumer (queue overflow).
dropped: u64,
/// Messages lost due to allocation failure.
alloc_failed: u64,
};
/// Returns current pending bytes in queue.
pub fn pendingBytes(self: *const Subscription) u64 {
return self.pending_bytes;
}
/// Returns high watermarks for pending messages and bytes.
pub fn maxPending(self: *const Subscription) struct {
msgs: u64,
bytes: u64,
} {
return .{
.msgs = self.max_pending_msgs,
.bytes = self.max_pending_bytes,
};
}
/// Resets high watermark counters to current values.
pub fn clearMaxPending(self: *Subscription) void {
self.max_pending_msgs = self.queue.len();
self.max_pending_bytes = self.pending_bytes;
}
/// Returns a snapshot of subscription statistics.
pub fn subStats(self: *const Subscription) SubStats {
return .{
.pending_msgs = self.queue.len(),
.pending_bytes = self.pending_bytes,
.max_pending_msgs = self.max_pending_msgs,
.max_pending_bytes = self.max_pending_bytes,
.delivered = self.delivered_count,
.dropped = self.dropped_msgs,
.alloc_failed = self.alloc_failed_msgs,
};
}
/// Push message to queue (called by io_task).
/// Lock-free, never blocks.
pub fn pushMessage(self: *Subscription, msg: Message) !void {
if (self.pending_limit > 0 or
self.pending_bytes_limit > 0)
{
// Slow path: flow control active
const queue_len = self.queue.len();
if (self.pending_limit > 0 and
queue_len >= self.pending_limit)
return error.QueueFull;
const msg_size =
if (msg.backing_buf) |buf|
buf.len
else
msg.size();
if (self.pending_bytes_limit > 0 and
self.pending_bytes + msg_size >
self.pending_bytes_limit)
{
return error.QueueFull;
}
if (!self.queue.push(msg))
return error.QueueFull;
self.pending_bytes += msg_size;
// Watermarks (reuse queue_len)
const new_len = queue_len + 1;
if (new_len > self.max_pending_msgs)
self.max_pending_msgs = new_len;
if (self.pending_bytes > self.max_pending_bytes)
self.max_pending_bytes = self.pending_bytes;
} else {
// Fast path: no flow control (3 atomic ops)
if (!self.queue.push(msg))
return error.QueueFull;
const msg_size =
if (msg.backing_buf) |buf|
buf.len
else
msg.size();
self.pending_bytes += msg_size;
if (self.pending_bytes > self.max_pending_bytes)
self.max_pending_bytes = self.pending_bytes;
}
// Auto-unsubscribe (only when max_msgs set)
if (self.max_msgs != null) {
self.delivered_count += 1;
if (self.delivered_count >= self.max_msgs.? and
!self.auto_unsub_triggered)
{
self.auto_unsub_triggered = true;
self.client.pushEvent(.{
.subscription_complete = .{
.sid = self.sid,
},
});
}
}
}
/// Unsubscribes from the subject.
///
/// Sends UNSUB to server, removes from client tracking, closes queue.
/// Idempotent - returns immediately if already unsubscribed.
/// Does NOT free memory - call deinit() for that.
///
/// Returns error.NotConnected if UNSUB couldn't be sent (local cleanup
/// still succeeds). Returns error.EncodingFailed for protocol errors.
pub fn unsubscribe(self: *Subscription) !void {
// Idempotent - already unsubscribed
if (self.state == .unsubscribed) return;
// Client already destroyed, mark state only
if (self.client_destroyed) {
self.state = .unsubscribed;
return;
}
const client = self.client;
const can_send = State.atomicLoad(&client.state).canSend();
// sub_mutex serializes with subscribe for
// sidmap/sub_ptrs/free_slots safety.
// Lock ordering: sub_mutex -> read_mutex -> write_mutex.
client.sub_mutex.lockUncancelable(client.io);
defer client.sub_mutex.unlock(client.io);
// Acquire mutex for thread-safe cleanup
client.read_mutex.lockUncancelable(client.io);
defer client.read_mutex.unlock(client.io);
// Track UNSUB enqueue success
var send_failed = false;
// Enqueue UNSUB protocol if connected
if (can_send) {
client.publish_mutex.lockUncancelable(client.io);
client.encodeUnsubToRing(.{
.sid = self.sid,
.max_msgs = null,
}) catch {
send_failed = true;
};
client.publish_mutex.unlock(client.io);
// Signal auto-flush to send UNSUB promptly
client.flush_requested.store(true, .release);
}
// Always remove from client tracking (inside mutex)
// This must happen even if not connected to prevent use-after-free
if (client.sidmap.get(self.sid)) |slot_idx| {
client.sub_ptrs[slot_idx] = null;
if (client.cached_sub == self) client.cached_sub = null;
_ = client.sidmap.remove(self.sid);
client.free_slots[client.free_count] = slot_idx;
client.free_count += 1;
}
// Close queue (inside mutex)
self.queue.close(client.io);
// Mark as unsubscribed
self.state = .unsubscribed;
// Report errors after cleanup completes
if (!can_send) return error.NotConnected;
if (send_failed) return error.EncodingFailed;
}
/// Frees all memory resources.
///
/// If not yet unsubscribed, calls unsubscribe() and ignores errors.
/// Safe to use in defer blocks (like Rust's Drop trait).
pub fn deinit(self: *Subscription) void {
const allocator = self.client.allocator;
// Cancel callback drain task before unsubscribe
if (self.callback_future) |*future| {
_ = future.cancel(self.client.io);
self.callback_future = null;
}
// Ensure unsubscribed (errors ignored - like Rust Drop)
if (self.state != .unsubscribed) {
self.unsubscribe() catch |err| {
dbg.print(
"deinit: unsubscribe failed: {s}",
.{@errorName(err)},
);
};
}
// Drain remaining messages (return buffers to pool)
var drain_buf: [1]Message = undefined;
while (true) {
const n = self.queue.popBatch(&drain_buf);
if (n == 0) break;
drain_buf[0].deinit();
}
// Free subscription resources
allocator.free(self.queue_buf);
allocator.free(self.subject);
if (self.queue_group) |qg| allocator.free(qg);
allocator.destroy(self);
}
};
test "parse url" {
{
const parsed = try parseUrl("nats://localhost:4222");
try std.testing.expectEqualSlices(u8, "localhost", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
try std.testing.expect(parsed.user == null);
try std.testing.expect(!parsed.use_tls);
}
{
const parsed = try parseUrl("nats://user:pass@localhost:4222");
try std.testing.expectEqualSlices(u8, "localhost", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
try std.testing.expectEqualSlices(u8, "user", parsed.user.?);
try std.testing.expectEqualSlices(u8, "pass", parsed.pass.?);
try std.testing.expect(!parsed.use_tls);
}
{
const parsed = try parseUrl("localhost");
try std.testing.expectEqualSlices(u8, "localhost", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
try std.testing.expect(!parsed.use_tls);
}
{
const parsed = try parseUrl("127.0.0.1:4223");
try std.testing.expectEqualSlices(u8, "127.0.0.1", parsed.host);
try std.testing.expectEqual(@as(u16, 4223), parsed.port);
try std.testing.expect(!parsed.use_tls);
}
}
test "parse url tls scheme" {
{
const parsed = try parseUrl("tls://secure.example.com:4222");
try std.testing.expectEqualSlices(u8, "secure.example.com", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
try std.testing.expect(parsed.use_tls);
}
{
const parsed = try parseUrl("tls://user:pass@secure.example.com:4222");
try std.testing.expectEqualSlices(u8, "secure.example.com", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
try std.testing.expectEqualSlices(u8, "user", parsed.user.?);
try std.testing.expectEqualSlices(u8, "pass", parsed.pass.?);
try std.testing.expect(parsed.use_tls);
}
{
const parsed = try parseUrl("tls://localhost");
try std.testing.expectEqualSlices(u8, "localhost", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
try std.testing.expect(parsed.use_tls);
}
}
test "parse url bracketed ipv6 host" {
const parsed = try parseUrl("tls://user:pass@[::1]:4223");
try std.testing.expectEqualSlices(u8, "::1", parsed.host);
try std.testing.expectEqual(@as(u16, 4223), parsed.port);
try std.testing.expectEqualSlices(u8, "user", parsed.user.?);
try std.testing.expectEqualSlices(u8, "pass", parsed.pass.?);
try std.testing.expect(parsed.use_tls);
}
test "parse url with user only" {
const parsed = try parseUrl("nats://admin@localhost:4222");
try std.testing.expectEqualSlices(u8, "localhost", parsed.host);
try std.testing.expectEqualSlices(u8, "admin", parsed.user.?);
try std.testing.expect(parsed.pass == null);
}
test "parse url invalid" {
try std.testing.expectError(error.InvalidUrl, parseUrl("nats://"));
try std.testing.expectError(error.InvalidUrl, parseUrl("nats://:4222"));
}
test "parse url default port" {
const parsed = try parseUrl("nats://myserver");
try std.testing.expectEqualSlices(u8, "myserver", parsed.host);
try std.testing.expectEqual(@as(u16, 4222), parsed.port);
}
test "options defaults" {
const opts: Options = .{};
try std.testing.expect(opts.name == null);
try std.testing.expect(!opts.verbose);
try std.testing.expect(!opts.pedantic);
try std.testing.expect(opts.user == null);
try std.testing.expect(opts.pass == null);
try std.testing.expectEqual(
defaults.Connection.timeout_ns,
opts.connect_timeout_ns,
);
try std.testing.expectEqual(
defaults.Memory.queue_size.value(),
opts.sub_queue_size,
);
}
test "stats defaults" {
const s: Statistics = .{};
const snap = s.snapshot();
try std.testing.expectEqual(@as(u64, 0), snap.msgs_in);
try std.testing.expectEqual(@as(u64, 0), snap.msgs_out);
try std.testing.expectEqual(@as(u64, 0), snap.bytes_in);
try std.testing.expectEqual(@as(u64, 0), snap.bytes_out);
try std.testing.expectEqual(@as(u32, 0), snap.reconnects);
try std.testing.expectEqual(@as(u32, 0), snap.connects);
}
================================================
FILE: src/auth/base32.zig
================================================
//! Base32 encoder/decoder (RFC 4648).
//!
//! Uses standard alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
const std = @import("std");
const assert = std.debug.assert;
pub const Error = error{
InvalidCharacter,
InvalidPadding,
OutputTooSmall,
};
/// Standard RFC 4648 base32 alphabet.
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/// Decoding lookup table (256 entries, 0xFF = invalid).
const decode_table: [256]u8 = blk: {
var t: [256]u8 = .{0xFF} ** 256;
for (alphabet, 0..) |c, i| {
t[c] = @intCast(i);
// Also accept lowercase
if (c >= 'A' and c <= 'Z') {
t[c + 32] = @intCast(i);
}
}
break :blk t;
};
/// Calculates decoded byte length from encoded character length.
/// Does not account for padding characters.
pub fn decodedLen(encoded_len: usize) usize {
return (encoded_len * 5) / 8;
}
/// Calculates encoded character length from decoded byte length.
pub fn encodedLen(decoded_len: usize) usize {
return (decoded_len * 8 + 4) / 5;
}
/// Decodes base32 string into dest buffer.
/// Returns slice of decoded bytes.
pub fn decode(dest: []u8, source: []const u8) Error![]u8 {
if (source.len == 0) return dest[0..0];
const needed = decodedLen(source.len);
if (dest.len < needed) return error.OutputTooSmall;
assert(dest.len >= needed);
var bits: u32 = 0;
var bit_count: u5 = 0;
var out_idx: usize = 0;
for (source) |c| {
if (c == '=') break;
const val = decode_table[c];
if (val == 0xFF) return error.InvalidCharacter;
bits = (bits << 5) | val;
bit_count += 5;
if (bit_count >= 8) {
bit_count -= 8;
dest[out_idx] = @intCast((bits >> bit_count) & 0xFF);
out_idx += 1;
}
}
return dest[0..out_idx];
}
/// Encodes bytes into base32 string in dest buffer.
/// Returns slice of encoded characters.
pub fn encode(dest: []u8, source: []const u8) Error![]u8 {
if (source.len == 0) return dest[0..0];
const needed = encodedLen(source.len);
if (dest.len < needed) return error.OutputTooSmall;
assert(dest.len >= needed);
var bits: u32 = 0;
var bit_count: u5 = 0;
var out_idx: usize = 0;
for (source) |byte| {
bits = (bits << 8) | byte;
bit_count += 8;
while (bit_count >= 5) {
bit_count -= 5;
dest[out_idx] = alphabet[(bits >> bit_count) & 0x1F];
out_idx += 1;
}
}
// Handle remaining bits (if any)
if (bit_count > 0) {
dest[out_idx] = alphabet[(bits << (5 - bit_count)) & 0x1F];
out_idx += 1;
}
return dest[0..out_idx];
}
test "decode empty" {
var buf: [1]u8 = undefined;
const result = try decode(&buf, "");
try std.testing.expectEqual(@as(usize, 0), result.len);
}
test "decode single char" {
var buf: [1]u8 = undefined;
const result = try decode(&buf, "ME");
try std.testing.expectEqual(@as(usize, 1), result.len);
try std.testing.expectEqual(@as(u8, 'a'), result[0]);
}
test "decode lowercase accepted" {
var buf: [1]u8 = undefined;
const result = try decode(&buf, "me");
try std.testing.expectEqual(@as(u8, 'a'), result[0]);
}
test "decode test string" {
var buf: [16]u8 = undefined;
const result = try decode(&buf, "ORSXG5A");
try std.testing.expectEqualSlices(u8, "test", result);
}
test "decode invalid character" {
var buf: [16]u8 = undefined;
try std.testing.expectError(error.InvalidCharacter, decode(&buf, "ME!!"));
}
test "decode with padding" {
var buf: [16]u8 = undefined;
const result = try decode(&buf, "ORSXG5A=");
try std.testing.expectEqualSlices(u8, "test", result);
}
test "encode empty" {
var buf: [1]u8 = undefined;
const result = try encode(&buf, "");
try std.testing.expectEqual(@as(usize, 0), result.len);
}
test "encode single byte" {
var buf: [8]u8 = undefined;
const result = try encode(&buf, "a");
try std.testing.expectEqualSlices(u8, "ME", result);
}
test "encode test string" {
var buf: [16]u8 = undefined;
const result = try encode(&buf, "test");
try std.testing.expectEqualSlices(u8, "ORSXG5A", result);
}
test "encode decode roundtrip" {
const original = "Hello, World!";
var enc_buf: [64]u8 = undefined;
var dec_buf: [64]u8 = undefined;
const encoded = try encode(&enc_buf, original);
const decoded = try decode(&dec_buf, encoded);
try std.testing.expectEqualSlices(u8, original, decoded);
}
test "decodedLen" {
try std.testing.expectEqual(@as(usize, 0), decodedLen(0));
try std.testing.expectEqual(@as(usize, 0), decodedLen(1));
try std.testing.expectEqual(@as(usize, 1), decodedLen(2));
try std.testing.expectEqual(@as(usize, 2), decodedLen(4));
try std.testing.expectEqual(@as(usize, 5), decodedLen(8));
// NKey seed: 57 chars -> 35 bytes
try std.testing.expectEqual(@as(usize, 35), decodedLen(57));
}
test "encodedLen" {
try std.testing.expectEqual(@as(usize, 0), encodedLen(0));
try std.testing.expectEqual(@as(usize, 2), encodedLen(1));
try std.testing.expectEqual(@as(usize, 8), encodedLen(5));
// Public key: 35 bytes -> 56 chars
try std.testing.expectEqual(@as(usize, 56), encodedLen(35));
}
================================================
FILE: src/auth/crc16.zig
================================================
//! CRC16-CCITT checksum for NKey validation.
//!
//! Thin wrapper around std.hash.crc.Crc16Xmodem.
const std = @import("std");
const assert = std.debug.assert;
const Crc16Xmodem = std.hash.crc.Crc16Xmodem;
/// Computes CRC16-CCITT (XMODEM) checksum over data.
pub fn compute(data: []const u8) u16 {
assert(data.len > 0);
return Crc16Xmodem.hash(data);
}
/// Validates CRC16 checksum against expected value.
pub fn validate(data: []const u8, expected: u16) bool {
assert(data.len > 0);
return compute(data) == expected;
}
test "compute single byte" {
const result = compute(&.{0x31});
try std.testing.expectEqual(@as(u16, 0x2672), result);
}
test "compute ascii string" {
const result = compute("123456789");
// CRC16-CCITT (XMODEM) of "123456789" = 0x31C3
try std.testing.expectEqual(@as(u16, 0x31C3), result);
}
test "validate correct checksum" {
try std.testing.expect(validate("123456789", 0x31C3));
}
test "validate incorrect checksum" {
try std.testing.expect(!validate("123456789", 0x0000));
}
================================================
FILE: src/auth/creds.zig
================================================
//! Credentials file parser and formatter for NATS JWT authentication.
//!
//! Parses and formats .creds files containing JWT and NKey seed.
const std = @import("std");
const assert = std.debug.assert;
const Io = std.Io;
pub const Error = error{
InvalidCredentials,
MissingJwt,
MissingSeed,
};
/// Parsed credentials containing JWT and NKey seed.
/// Slices point into the original content buffer.
pub const Credentials = struct {
jwt: []const u8,
seed: []const u8,
};
const JWT_BEGIN = "-----BEGIN NATS USER JWT-----";
const JWT_END = "------END NATS USER JWT------";
const SEED_BEGIN = "-----BEGIN USER NKEY SEED-----";
const SEED_END = "------END USER NKEY SEED------";
/// Parses credentials from content buffer.
/// Returns slices into the input buffer.
pub fn parse(content: []const u8) Error!Credentials {
assert(content.len > 0);
// Find JWT section
const jwt_start_idx = std.mem.indexOf(u8, content, JWT_BEGIN) orelse {
return error.MissingJwt;
};
const jwt_content_start = jwt_start_idx + JWT_BEGIN.len;
const jwt_end_idx = std.mem.indexOfPos(
u8,
content,
jwt_content_start,
JWT_END,
) orelse {
return error.MissingJwt;
};
// Find seed section
const seed_start_idx = std.mem.indexOf(u8, content, SEED_BEGIN) orelse {
return error.MissingSeed;
};
const seed_content_start = seed_start_idx + SEED_BEGIN.len;
const seed_end_idx = std.mem.indexOfPos(
u8,
content,
seed_content_start,
SEED_END,
) orelse {
return error.MissingSeed;
};
// Extract and trim JWT
const jwt_raw = content[jwt_content_start..jwt_end_idx];
const jwt = trimWhitespace(jwt_raw);
if (jwt.len == 0) return error.MissingJwt;
// Extract and trim seed
const seed_raw = content[seed_content_start..seed_end_idx];
const seed = trimWhitespace(seed_raw);
if (seed.len == 0) return error.MissingSeed;
assert(jwt.len > 0);
assert(seed.len > 0);
return .{
.jwt = jwt,
.seed = seed,
};
}
/// Trims leading and trailing ASCII whitespace.
/// Returns slice into original buffer.
fn trimWhitespace(s: []const u8) []const u8 {
var start: usize = 0;
var end: usize = s.len;
while (start < end and std.ascii.isWhitespace(s[start])) {
start += 1;
}
while (end > start and std.ascii.isWhitespace(s[end - 1])) {
end -= 1;
}
return s[start..end];
}
const WARN_TEXT =
"\n\n" ++
"************************* IMPORTANT ****" ++
"*********************\n" ++
" NKEY Seed printed below can be used to" ++
" sign and prove identity.\n" ++
" NKEYs are sensitive and should be treat" ++
"ed as secrets.\n\n" ++
" *************************************" ++
"************************\n\n";
/// Formats a credentials file from JWT and seed strings.
/// Writes into caller-provided buffer, returns slice.
pub fn format(
buf: []u8,
jwt_str: []const u8,
seed_str: []const u8,
) error{BufferTooSmall}![]const u8 {
assert(jwt_str.len > 0);
assert(seed_str.len > 0);
if (buf.len < jwt_str.len + seed_str.len + 256)
return error.BufferTooSmall;
var w = Io.Writer.fixed(buf);
w.writeAll(JWT_BEGIN) catch unreachable;
w.writeAll("\n") catch unreachable;
w.writeAll(jwt_str) catch unreachable;
w.writeAll("\n") catch unreachable;
w.writeAll(JWT_END) catch unreachable;
w.writeAll(WARN_TEXT) catch unreachable;
w.writeAll(SEED_BEGIN) catch unreachable;
w.writeAll("\n") catch unreachable;
w.writeAll(seed_str) catch unreachable;
w.writeAll("\n") catch unreachable;
w.writeAll(SEED_END) catch unreachable;
w.writeAll("\n") catch unreachable;
const result = w.buffered();
assert(result.len > 0);
return result;
}
/// Loads and parses credentials from file path.
/// Caller provides buffer for file content.
/// Returns slices pointing into buf.
pub fn loadFile(
io: Io,
path: []const u8,
buf: *[8192]u8,
) !Credentials {
assert(path.len > 0);
const data = try Io.Dir.readFile(.cwd(), io, path, buf);
if (data.len == 0) return error.InvalidCredentials;
return parse(data);
}
test "parse valid credentials" {
const content =
\\-----BEGIN NATS USER JWT-----
\\eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJBQkNERUZHIiw
\\------END NATS USER JWT------
\\
\\************************* IMPORTANT *************************
\\NKEY Seed printed below can be used to sign and prove identity.
\\
\\-----BEGIN USER NKEY SEED-----
\\SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY
\\------END USER NKEY SEED------
;
const creds = try parse(content);
try std.testing.expectEqualStrings(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJBQkNERUZHIiw",
creds.jwt,
);
try std.testing.expectEqualStrings(
"SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY",
creds.seed,
);
}
test "parse credentials with extra whitespace" {
const content =
\\-----BEGIN NATS USER JWT-----
\\
\\ eyJhbGciOiJlZDI1NTE5In0.eyJzdWIiOiJVQSJ9
\\
\\------END NATS USER JWT------
\\
\\-----BEGIN USER NKEY SEED-----
\\ SUATEST1234567890ABCDEF
\\------END USER NKEY SEED------
;
const creds = try parse(content);
try std.testing.expectEqualStrings(
"eyJhbGciOiJlZDI1NTE5In0.eyJzdWIiOiJVQSJ9",
creds.jwt,
);
try std.testing.expectEqualStrings("SUATEST1234567890ABCDEF", creds.seed);
}
test "parse credentials missing JWT" {
const content =
\\-----BEGIN USER NKEY SEED-----
\\SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY
\\------END USER NKEY SEED------
;
try std.testing.expectError(error.MissingJwt, parse(content));
}
test "parse credentials missing seed" {
const content =
\\-----BEGIN NATS USER JWT-----
\\eyJhbGciOiJlZDI1NTE5In0
\\------END NATS USER JWT------
;
try std.testing.expectError(error.MissingSeed, parse(content));
}
test "parse credentials empty JWT" {
const content =
\\-----BEGIN NATS USER JWT-----
\\
\\------END NATS USER JWT------
\\-----BEGIN USER NKEY SEED-----
\\SUATEST
\\------END USER NKEY SEED------
;
try std.testing.expectError(error.MissingJwt, parse(content));
}
test "parse credentials empty seed" {
const content =
\\-----BEGIN NATS USER JWT-----
\\eyJhbGciOiJlZDI1NTE5In0
\\------END NATS USER JWT------
\\-----BEGIN USER NKEY SEED-----
\\
\\------END USER NKEY SEED------
;
try std.testing.expectError(error.MissingSeed, parse(content));
}
test "parse credentials malformed - no end marker for JWT" {
const content =
\\-----BEGIN NATS USER JWT-----
\\eyJhbGciOiJlZDI1NTE5In0
\\-----BEGIN USER NKEY SEED-----
\\SUATEST
\\------END USER NKEY SEED------
;
try std.testing.expectError(error.MissingJwt, parse(content));
}
test "trimWhitespace" {
try std.testing.expectEqualStrings("hello", trimWhitespace(" hello "));
try std.testing.expectEqualStrings("hello", trimWhitespace("hello"));
try std.testing.expectEqualStrings("hello", trimWhitespace("\n\thello\r\n"));
try std.testing.expectEqualStrings("", trimWhitespace(" "));
try std.testing.expectEqualStrings("", trimWhitespace(""));
}
test "format and parse roundtrip" {
const jwt_str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5In0.test";
const seed_str =
"SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV" ++
"7NSWFFEW63UXMRLFM2XLAXK4GY";
var buf: [2048]u8 = undefined;
const formatted = try format(&buf, jwt_str, seed_str);
const creds = try parse(formatted);
try std.testing.expectEqualStrings(jwt_str, creds.jwt);
try std.testing.expectEqualStrings(
seed_str,
creds.seed,
);
}
test "realistic generated content roundtrip" {
const nkey = @import("nkey.zig");
const jwt = @import("jwt.zig");
const Ed25519 = std.crypto.sign.Ed25519;
// Deterministic account keypair
const acct_seed = [_]u8{30} ** 32;
const acct_ed = Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_ed,
.key_type = .account,
};
// Deterministic user keypair
const user_seed = [_]u8{40} ** 32;
const user_ed = Ed25519.KeyPair.generateDeterministic(
user_seed,
) catch unreachable;
const user_kp = nkey.KeyPair{
.kp = user_ed,
.key_type = .user,
};
// Encode user JWT
var pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt_str = try jwt.encodeUserClaims(
&jwt_buf,
user_pub,
"roundtrip-user",
acct_kp,
1700000000,
.{ .pub_allow = &.{">"} },
);
// Encode user seed
var seed_buf: [58]u8 = undefined;
const seed_str = user_kp.encodeSeed(&seed_buf);
// Format credentials
var creds_buf: [4096]u8 = undefined;
const formatted = try format(
&creds_buf,
jwt_str,
seed_str,
);
// Parse back
const parsed = try parse(formatted);
try std.testing.expectEqualStrings(
jwt_str,
parsed.jwt,
);
try std.testing.expectEqualStrings(
seed_str,
parsed.seed,
);
// Verify parsed seed creates valid keypair
var kp = try nkey.KeyPair.fromSeed(parsed.seed);
defer kp.wipe();
var pk2: [56]u8 = undefined;
try std.testing.expectEqualStrings(
user_pub,
kp.publicKey(&pk2),
);
}
================================================
FILE: src/auth/jwt.zig
================================================
//! JWT encoding for NATS decentralized authentication.
//!
//! Encodes account and user JWTs signed with NKey Ed25519 keypairs.
//! No allocator needed - all encoding uses caller-provided buffers.
const std = @import("std");
const assert = std.debug.assert;
const Io = std.Io;
const Ed25519 = std.crypto.sign.Ed25519;
const Sha512_256 = std.crypto.hash.sha2.Sha512_256;
const base64 = std.base64.url_safe_no_pad;
const nkey = @import("nkey.zig");
const base32 = @import("base32.zig");
pub const Error = error{
BufferTooSmall,
WriteFailed,
};
/// Pre-encoded JWT header: {"typ":"JWT","alg":"ed25519-nkey"}
const HEADER_B64 =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ";
/// Account JWT options (NATS account limits).
pub const AccountOptions = struct {
subs: i64 = -1,
data: i64 = -1,
payload: i64 = -1,
imports: i64 = -1,
exports: i64 = -1,
conn: i64 = -1,
leaf: i64 = -1,
mem_storage: i64 = -1,
disk_storage: i64 = -1,
wildcards: bool = true,
};
/// Operator JWT options.
pub const OperatorOptions = struct {
system_account: []const u8 = "",
};
/// User JWT options (permissions and limits).
pub const UserOptions = struct {
pub_allow: []const []const u8 = &.{},
sub_allow: []const []const u8 = &.{},
subs: i64 = -1,
data: i64 = -1,
payload: i64 = -1,
};
/// Encodes a NATS account JWT signed by an operator keypair.
pub fn encodeAccountClaims(
buf: []u8,
subject: []const u8,
name: []const u8,
signer: nkey.KeyPair,
iat: i64,
opts: AccountOptions,
) Error![]const u8 {
assert(subject.len > 0);
assert(name.len > 0);
if (buf.len < 512) return error.BufferTooSmall;
var pk_buf: [56]u8 = undefined;
const iss = signer.publicKey(&pk_buf);
// Pass 1: build payload without JTI to compute hash
var tmp: [1024]u8 = undefined;
const pre_jti = writeAccountJson(
&tmp,
"",
iat,
iss,
name,
subject,
opts,
) orelse return error.WriteFailed;
const jti = computeJti(pre_jti);
// Pass 2: build payload with JTI
var payload_buf: [1024]u8 = undefined;
const payload = writeAccountJson(
&payload_buf,
&jti.str,
iat,
iss,
name,
subject,
opts,
) orelse return error.WriteFailed;
return assembleJwt(buf, payload, signer);
}
/// Encodes a NATS user JWT signed by an account keypair.
pub fn encodeUserClaims(
buf: []u8,
subject: []const u8,
name: []const u8,
signer: nkey.KeyPair,
iat: i64,
opts: UserOptions,
) Error![]const u8 {
assert(subject.len > 0);
assert(name.len > 0);
if (buf.len < 512) return error.BufferTooSmall;
var pk_buf: [56]u8 = undefined;
const iss = signer.publicKey(&pk_buf);
// Pass 1: build payload without JTI
var tmp: [1024]u8 = undefined;
const pre_jti = writeUserJson(
&tmp,
"",
iat,
iss,
name,
subject,
opts,
) orelse return error.WriteFailed;
const jti = computeJti(pre_jti);
// Pass 2: build payload with JTI
var payload_buf: [1024]u8 = undefined;
const payload = writeUserJson(
&payload_buf,
&jti.str,
iat,
iss,
name,
subject,
opts,
) orelse return error.WriteFailed;
return assembleJwt(buf, payload, signer);
}
/// Encodes a NATS operator JWT (self-signed by operator).
pub fn encodeOperatorClaims(
buf: []u8,
subject: []const u8,
name: []const u8,
signer: nkey.KeyPair,
iat: i64,
opts: OperatorOptions,
) Error![]const u8 {
assert(subject.len > 0);
assert(name.len > 0);
if (buf.len < 512) return error.BufferTooSmall;
var pk_buf: [56]u8 = undefined;
const iss = signer.publicKey(&pk_buf);
// Pass 1: build payload without JTI
var tmp: [1024]u8 = undefined;
const pre_jti = writeOperatorJson(
&tmp,
"",
iat,
iss,
name,
subject,
opts,
) orelse return error.WriteFailed;
const jti = computeJti(pre_jti);
// Pass 2: build payload with JTI
var payload_buf: [1024]u8 = undefined;
const payload = writeOperatorJson(
&payload_buf,
&jti.str,
iat,
iss,
name,
subject,
opts,
) orelse return error.WriteFailed;
return assembleJwt(buf, payload, signer);
}
const Jti = struct { str: [52]u8 };
/// SHA-512/256 hash of payload, base32-encoded.
fn computeJti(payload: []const u8) Jti {
assert(payload.len > 0);
var hash: [32]u8 = undefined;
Sha512_256.hash(payload, &hash, .{});
var result: Jti = undefined;
_ = base32.encode(&result.str, &hash) catch unreachable;
return result;
}
/// Assembles header.payload.signature into buf.
fn assembleJwt(
buf: []u8,
payload: []const u8,
signer: nkey.KeyPair,
) Error![]const u8 {
assert(payload.len > 0);
if (buf.len < 512) return error.BufferTooSmall;
const payload_b64_len = base64.Encoder.calcSize(
payload.len,
);
// header + "." + payload_b64 + "." + sig_b64(86)
const sig_b64_len = 86;
const total = HEADER_B64.len + 1 + payload_b64_len +
1 + sig_b64_len;
if (buf.len < total) return error.BufferTooSmall;
var pos: usize = 0;
// Header
@memcpy(buf[pos..][0..HEADER_B64.len], HEADER_B64);
pos += HEADER_B64.len;
// Dot
buf[pos] = '.';
pos += 1;
// Base64url-encoded payload
_ = base64.Encoder.encode(
buf[pos..][0..payload_b64_len],
payload,
);
pos += payload_b64_len;
// Sign everything before the second dot
const sign_data = buf[0..pos];
const sig = signer.kp.sign(
sign_data,
null,
) catch unreachable;
const sig_bytes = sig.toBytes();
// Second dot
buf[pos] = '.';
pos += 1;
// Base64url-encoded signature
_ = base64.Encoder.encode(
buf[pos..][0..sig_b64_len],
&sig_bytes,
);
pos += sig_b64_len;
assert(pos == total);
return buf[0..pos];
}
/// Writes account claims JSON into buf. Returns slice or null.
// REVIEWED(2025-03): name/sub/iss fields come from NKey
// public keys (hex-encoded), never user-controlled input.
// No JSON injection risk — no escaping needed.
fn writeAccountJson(
buf: []u8,
jti: []const u8,
iat: i64,
iss: []const u8,
name: []const u8,
sub: []const u8,
opts: AccountOptions,
) ?[]const u8 {
assert(iss.len > 0);
assert(sub.len > 0);
if (buf.len < 256) return null;
var w = Io.Writer.fixed(buf);
w.writeAll("{\"jti\":\"") catch return null;
w.writeAll(jti) catch return null;
w.writeAll("\",\"iat\":") catch return null;
w.print("{d}", .{iat}) catch return null;
w.writeAll(",\"iss\":\"") catch return null;
w.writeAll(iss) catch return null;
w.writeAll("\",\"name\":\"") catch return null;
w.writeAll(name) catch return null;
w.writeAll("\",\"sub\":\"") catch return null;
w.writeAll(sub) catch return null;
w.writeAll("\",\"nats\":{\"limits\":{") catch return null;
w.print("\"subs\":{d}", .{opts.subs}) catch return null;
w.print(",\"data\":{d}", .{opts.data}) catch return null;
w.print(
",\"payload\":{d}",
.{opts.payload},
) catch return null;
w.print(
",\"imports\":{d}",
.{opts.imports},
) catch return null;
w.print(
",\"exports\":{d}",
.{opts.exports},
) catch return null;
w.print(",\"conn\":{d}", .{opts.conn}) catch return null;
w.print(",\"leaf\":{d}", .{opts.leaf}) catch return null;
w.print(
",\"mem_storage\":{d}",
.{opts.mem_storage},
) catch return null;
w.print(
",\"disk_storage\":{d}",
.{opts.disk_storage},
) catch return null;
if (opts.wildcards) {
w.writeAll(",\"wildcards\":true") catch return null;
} else {
w.writeAll(",\"wildcards\":false") catch return null;
}
w.writeAll(
"},\"type\":\"account\",\"version\":2}}",
) catch return null;
const result = w.buffered();
assert(result.len > 0);
return result;
}
/// Writes user claims JSON into buf. Returns slice or null.
fn writeUserJson(
buf: []u8,
jti: []const u8,
iat: i64,
iss: []const u8,
name: []const u8,
sub: []const u8,
opts: UserOptions,
) ?[]const u8 {
assert(iss.len > 0);
assert(sub.len > 0);
if (buf.len < 256) return null;
var w = Io.Writer.fixed(buf);
w.writeAll("{\"jti\":\"") catch return null;
w.writeAll(jti) catch return null;
w.writeAll("\",\"iat\":") catch return null;
w.print("{d}", .{iat}) catch return null;
w.writeAll(",\"iss\":\"") catch return null;
w.writeAll(iss) catch return null;
w.writeAll("\",\"name\":\"") catch return null;
w.writeAll(name) catch return null;
w.writeAll("\",\"sub\":\"") catch return null;
w.writeAll(sub) catch return null;
w.writeAll("\",\"nats\":{") catch return null;
// Publish permissions
if (opts.pub_allow.len > 0) {
w.writeAll("\"pub\":{\"allow\":[") catch return null;
writeStringArray(&w, opts.pub_allow) orelse
return null;
w.writeAll("]},") catch return null;
}
// Subscribe permissions
if (opts.sub_allow.len > 0) {
w.writeAll("\"sub\":{\"allow\":[") catch return null;
writeStringArray(&w, opts.sub_allow) orelse
return null;
w.writeAll("]},") catch return null;
}
w.print("\"subs\":{d}", .{opts.subs}) catch return null;
w.print(",\"data\":{d}", .{opts.data}) catch return null;
w.print(
",\"payload\":{d}",
.{opts.payload},
) catch return null;
w.writeAll(
",\"type\":\"user\",\"version\":2}}",
) catch return null;
const result = w.buffered();
assert(result.len > 0);
return result;
}
/// Writes a JSON string array (without brackets).
fn writeStringArray(
w: *Io.Writer,
items: []const []const u8,
) ?void {
assert(items.len > 0);
for (items, 0..) |item, i| {
if (i > 0) w.writeAll(",") catch return null;
w.writeAll("\"") catch return null;
w.writeAll(item) catch return null;
w.writeAll("\"") catch return null;
}
}
/// Writes operator claims JSON into buf.
fn writeOperatorJson(
buf: []u8,
jti: []const u8,
iat: i64,
iss: []const u8,
name: []const u8,
sub: []const u8,
opts: OperatorOptions,
) ?[]const u8 {
assert(iss.len > 0);
assert(sub.len > 0);
if (buf.len < 256) return null;
var w = Io.Writer.fixed(buf);
w.writeAll("{\"jti\":\"") catch return null;
w.writeAll(jti) catch return null;
w.writeAll("\",\"iat\":") catch return null;
w.print("{d}", .{iat}) catch return null;
w.writeAll(",\"iss\":\"") catch return null;
w.writeAll(iss) catch return null;
w.writeAll("\",\"name\":\"") catch return null;
w.writeAll(name) catch return null;
w.writeAll("\",\"sub\":\"") catch return null;
w.writeAll(sub) catch return null;
w.writeAll("\",\"nats\":{") catch return null;
if (opts.system_account.len > 0) {
w.writeAll(
"\"system_account\":\"",
) catch return null;
w.writeAll(
opts.system_account,
) catch return null;
w.writeAll("\",") catch return null;
}
w.writeAll(
"\"type\":\"operator\",\"version\":2}}",
) catch return null;
const result = w.buffered();
assert(result.len > 0);
return result;
}
test "encode account JWT structure" {
const test_seed = [_]u8{10} ** 32;
const op_kp_inner =
Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
const op_kp = nkey.KeyPair{
.kp = op_kp_inner,
.key_type = .operator,
};
const acct_seed = [_]u8{20} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
var pk_buf: [56]u8 = undefined;
const acct_pub = acct_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeAccountClaims(
&jwt_buf,
acct_pub,
"test-account",
op_kp,
1700000000,
.{},
);
// Split on dots
assert(jwt.len > 0);
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
// Verify header decodes to expected JSON
const hdr_exp =
"{\"typ\":\"JWT\",\"alg\":\"ed25519-nkey\"}";
var hdr_buf: [64]u8 = undefined;
const hdr_len = base64.Decoder.calcSizeForSlice(
jwt[0..dot1],
) catch unreachable;
base64.Decoder.decode(
hdr_buf[0..hdr_len],
jwt[0..dot1],
) catch unreachable;
try std.testing.expectEqualStrings(
hdr_exp,
hdr_buf[0..hdr_len],
);
// Verify payload contains expected fields
var pay_buf: [1024]u8 = undefined;
const payload_b64 = jwt[dot1 + 1 .. dot2];
const pay_len = base64.Decoder.calcSizeForSlice(
payload_b64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..pay_len],
payload_b64,
) catch unreachable;
const pay = pay_buf[0..pay_len];
// Check key fields exist
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"jti\":\"") != null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"iat\":1700000000") !=
null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"name\":\"test-account\"") !=
null,
);
try std.testing.expect(
std.mem.indexOf(
u8,
pay,
"\"type\":\"account\"",
) != null,
);
// Verify Ed25519 signature
const sig_b64 = jwt[dot2 + 1 ..];
var sig_raw: [64]u8 = undefined;
base64.Decoder.decode(
&sig_raw,
sig_b64,
) catch unreachable;
const sig = Ed25519.Signature.fromBytes(sig_raw);
sig.verify(jwt[0..dot2], op_kp.kp.public_key) catch {
return error.WriteFailed;
};
}
test "encode user JWT with permissions" {
const acct_seed = [_]u8{30} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
const user_seed = [_]u8{40} ** 32;
const user_kp_inner =
Ed25519.KeyPair.generateDeterministic(
user_seed,
) catch unreachable;
const user_kp = nkey.KeyPair{
.kp = user_kp_inner,
.key_type = .user,
};
var pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeUserClaims(
&jwt_buf,
user_pub,
"test-user",
acct_kp,
1700000000,
.{
.pub_allow = &.{ "foo.>", "bar.>" },
.sub_allow = &.{"_INBOX.>"},
},
);
assert(jwt.len > 0);
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
var pay_buf: [1024]u8 = undefined;
const payload_b64 = jwt[dot1 + 1 .. dot2];
const pay_len = base64.Decoder.calcSizeForSlice(
payload_b64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..pay_len],
payload_b64,
) catch unreachable;
const pay = pay_buf[0..pay_len];
// Verify permissions in payload
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"pub\":{\"allow\":[") !=
null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"foo.>\"") != null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"_INBOX.>\"") != null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"type\":\"user\"") !=
null,
);
// Verify signature
const sig_b64 = jwt[dot2 + 1 ..];
var sig_raw: [64]u8 = undefined;
base64.Decoder.decode(
&sig_raw,
sig_b64,
) catch unreachable;
const sig = Ed25519.Signature.fromBytes(sig_raw);
sig.verify(
jwt[0..dot2],
acct_kp.kp.public_key,
) catch {
return error.WriteFailed;
};
}
test "encode operator JWT self-signed" {
const op_seed = [_]u8{10} ** 32;
const op_kp_inner =
Ed25519.KeyPair.generateDeterministic(
op_seed,
) catch unreachable;
const op_kp = nkey.KeyPair{
.kp = op_kp_inner,
.key_type = .operator,
};
var pk_buf: [56]u8 = undefined;
const op_pub = op_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeOperatorClaims(
&jwt_buf,
op_pub,
"test-operator",
op_kp,
1700000000,
.{},
);
assert(jwt.len > 0);
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
// Decode and verify payload
var pay_buf: [1024]u8 = undefined;
const payload_b64 = jwt[dot1 + 1 .. dot2];
const pay_len = base64.Decoder.calcSizeForSlice(
payload_b64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..pay_len],
payload_b64,
) catch unreachable;
const pay = pay_buf[0..pay_len];
try std.testing.expect(
std.mem.indexOf(
u8,
pay,
"\"type\":\"operator\"",
) != null,
);
try std.testing.expect(
std.mem.indexOf(
u8,
pay,
"\"name\":\"test-operator\"",
) != null,
);
// Self-signed: verify with operator's own key
const sig_b64 = jwt[dot2 + 1 ..];
var sig_raw: [64]u8 = undefined;
base64.Decoder.decode(
&sig_raw,
sig_b64,
) catch unreachable;
const sig = Ed25519.Signature.fromBytes(sig_raw);
sig.verify(
jwt[0..dot2],
op_kp.kp.public_key,
) catch {
return error.WriteFailed;
};
}
test "JTI determinism - same input same output" {
const op_seed = [_]u8{10} ** 32;
const op_kp_inner =
Ed25519.KeyPair.generateDeterministic(
op_seed,
) catch unreachable;
const op_kp = nkey.KeyPair{
.kp = op_kp_inner,
.key_type = .operator,
};
const acct_seed = [_]u8{20} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
var pk_buf: [56]u8 = undefined;
const acct_pub = acct_kp.publicKey(&pk_buf);
var buf1: [2048]u8 = undefined;
const jwt1 = try encodeAccountClaims(
&buf1,
acct_pub,
"det-test",
op_kp,
1700000000,
.{},
);
var buf2: [2048]u8 = undefined;
const jwt2 = try encodeAccountClaims(
&buf2,
acct_pub,
"det-test",
op_kp,
1700000000,
.{},
);
try std.testing.expectEqualStrings(jwt1, jwt2);
}
test "JTI uniqueness - different names different JTI" {
const op_seed = [_]u8{10} ** 32;
const op_kp_inner =
Ed25519.KeyPair.generateDeterministic(
op_seed,
) catch unreachable;
const op_kp = nkey.KeyPair{
.kp = op_kp_inner,
.key_type = .operator,
};
const acct_seed = [_]u8{20} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
var pk_buf: [56]u8 = undefined;
const acct_pub = acct_kp.publicKey(&pk_buf);
var buf1: [2048]u8 = undefined;
const jwt1 = try encodeAccountClaims(
&buf1,
acct_pub,
"account-alpha",
op_kp,
1700000000,
.{},
);
var buf2: [2048]u8 = undefined;
const jwt2 = try encodeAccountClaims(
&buf2,
acct_pub,
"account-beta",
op_kp,
1700000000,
.{},
);
// JWTs must differ (different name -> different JTI)
try std.testing.expect(
!std.mem.eql(u8, jwt1, jwt2),
);
}
test "custom account limits in payload" {
const op_seed = [_]u8{10} ** 32;
const op_kp_inner =
Ed25519.KeyPair.generateDeterministic(
op_seed,
) catch unreachable;
const op_kp = nkey.KeyPair{
.kp = op_kp_inner,
.key_type = .operator,
};
const acct_seed = [_]u8{20} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
var pk_buf: [56]u8 = undefined;
const acct_pub = acct_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeAccountClaims(
&jwt_buf,
acct_pub,
"limited-acct",
op_kp,
1700000000,
.{
.subs = 100,
.conn = 50,
.wildcards = false,
},
);
// Decode payload
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
var pay_buf: [1024]u8 = undefined;
const pb64 = jwt[dot1 + 1 .. dot2];
const plen = base64.Decoder.calcSizeForSlice(
pb64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..plen],
pb64,
) catch unreachable;
const pay = pay_buf[0..plen];
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"subs\":100") !=
null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"conn\":50") !=
null,
);
try std.testing.expect(
std.mem.indexOf(
u8,
pay,
"\"wildcards\":false",
) != null,
);
}
test "user JWT with empty permissions" {
const acct_seed = [_]u8{30} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
const user_seed = [_]u8{40} ** 32;
const user_kp_inner =
Ed25519.KeyPair.generateDeterministic(
user_seed,
) catch unreachable;
const user_kp = nkey.KeyPair{
.kp = user_kp_inner,
.key_type = .user,
};
var pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeUserClaims(
&jwt_buf,
user_pub,
"no-perms-user",
acct_kp,
1700000000,
.{},
);
// Decode payload
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
var pay_buf: [1024]u8 = undefined;
const pb64 = jwt[dot1 + 1 .. dot2];
const plen = base64.Decoder.calcSizeForSlice(
pb64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..plen],
pb64,
) catch unreachable;
const pay = pay_buf[0..plen];
// No "pub": or "sub":{ permission blocks
// (only "subs":, "sub":" for subject)
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"pub\":{") == null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"sub\":{") == null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"type\":\"user\"") !=
null,
);
}
test "user JWT with single permission" {
const acct_seed = [_]u8{30} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
const user_seed = [_]u8{40} ** 32;
const user_kp_inner =
Ed25519.KeyPair.generateDeterministic(
user_seed,
) catch unreachable;
const user_kp = nkey.KeyPair{
.kp = user_kp_inner,
.key_type = .user,
};
var pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeUserClaims(
&jwt_buf,
user_pub,
"single-perm-user",
acct_kp,
1700000000,
.{
.pub_allow = &.{"single.subject"},
},
);
// Decode payload
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
var pay_buf: [1024]u8 = undefined;
const pb64 = jwt[dot1 + 1 .. dot2];
const plen = base64.Decoder.calcSizeForSlice(
pb64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..plen],
pb64,
) catch unreachable;
const pay = pay_buf[0..plen];
try std.testing.expect(
std.mem.indexOf(
u8,
pay,
"\"pub\":{\"allow\":[\"single.subject\"]}",
) != null,
);
}
test "custom user limits in payload" {
const acct_seed = [_]u8{30} ** 32;
const acct_kp_inner =
Ed25519.KeyPair.generateDeterministic(
acct_seed,
) catch unreachable;
const acct_kp = nkey.KeyPair{
.kp = acct_kp_inner,
.key_type = .account,
};
const user_seed = [_]u8{40} ** 32;
const user_kp_inner =
Ed25519.KeyPair.generateDeterministic(
user_seed,
) catch unreachable;
const user_kp = nkey.KeyPair{
.kp = user_kp_inner,
.key_type = .user,
};
var pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&pk_buf);
var jwt_buf: [2048]u8 = undefined;
const jwt = try encodeUserClaims(
&jwt_buf,
user_pub,
"limited-user",
acct_kp,
1700000000,
.{
.subs = 10,
.data = 1024,
.payload = 512,
},
);
// Decode payload
const dot1 = std.mem.indexOf(u8, jwt, ".") orelse
unreachable;
const dot2 = std.mem.indexOfPos(
u8,
jwt,
dot1 + 1,
".",
) orelse unreachable;
var pay_buf: [1024]u8 = undefined;
const pb64 = jwt[dot1 + 1 .. dot2];
const plen = base64.Decoder.calcSizeForSlice(
pb64,
) catch unreachable;
base64.Decoder.decode(
pay_buf[0..plen],
pb64,
) catch unreachable;
const pay = pay_buf[0..plen];
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"subs\":10") !=
null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"data\":1024") !=
null,
);
try std.testing.expect(
std.mem.indexOf(u8, pay, "\"payload\":512") !=
null,
);
}
================================================
FILE: src/auth/nkey.zig
================================================
//! NKey authentication for NATS.
//!
//! Generates, parses, and encodes NKey seeds. Signs server nonces
//! for authentication. NKeys use Ed25519 with base32-encoded keys.
const std = @import("std");
const assert = std.debug.assert;
const Ed25519 = std.crypto.sign.Ed25519;
const base32 = @import("base32.zig");
const crc16 = @import("crc16.zig");
pub const Error = error{
InvalidSeed,
InvalidPrefix,
InvalidChecksum,
InvalidKeyType,
InvalidLength,
IdentityElement,
};
/// NKey entity types (encoded in seed prefix).
pub const KeyType = enum(u8) {
user = 160, // 20 << 3
account = 0, // 0 << 3
server = 104, // 13 << 3
cluster = 16, // 2 << 3
operator = 112, // 14 << 3
/// Converts byte value to KeyType enum.
pub fn fromByte(b: u8) ?KeyType {
return switch (b) {
160 => .user,
0 => .account,
104 => .server,
16 => .cluster,
112 => .operator,
else => null,
};
}
};
/// Seed prefix byte value (S = 18 << 3 = 144).
const SEED_PREFIX: u8 = 144;
/// NKey keypair for signing and verification.
pub const KeyPair = struct {
kp: Ed25519.KeyPair,
key_type: KeyType,
/// Generates a random Ed25519 keypair for the given type.
pub fn generate(io: std.Io, key_type: KeyType) KeyPair {
assert(KeyType.fromByte(@intFromEnum(key_type)) != null);
const kp = Ed25519.KeyPair.generate(io);
return .{ .kp = kp, .key_type = key_type };
}
/// Encodes the keypair's seed in NKey format (base32).
///
/// Reverse of `fromSeed`. Packs prefix + seed + CRC16,
/// then base32-encodes to 58-character string.
pub fn encodeSeed(
self: KeyPair,
out: *[58]u8,
) []const u8 {
assert(KeyType.fromByte(
@intFromEnum(self.key_type),
) != null);
var raw: [36]u8 = undefined;
const kt = @intFromEnum(self.key_type);
raw[0] = (SEED_PREFIX & 0xF8) | ((kt >> 5) & 0x07);
raw[1] = (kt & 0x1F) << 3;
raw[2..34].* = self.kp.secret_key.seed();
const crc = crc16.compute(raw[0..34]);
std.mem.writeInt(u16, raw[34..36], crc, .little);
defer std.crypto.secureZero(
u8,
@volatileCast(&raw),
);
return base32.encode(out, &raw) catch unreachable;
}
/// Parses an NKey seed and derives the Ed25519 keypair.
///
/// Seed format (57 chars base32):
/// - Bytes 0-1: Packed prefix (seed prefix + key type)
/// - Bytes 2-33: 32-byte Ed25519 seed
/// - Bytes 34-35: CRC16 checksum (little-endian)
pub fn fromSeed(encoded_seed: []const u8) Error!KeyPair {
assert(encoded_seed.len > 0);
// Base32 decode the seed
var raw: [64]u8 = undefined;
defer std.crypto.secureZero(
u8,
@volatileCast(&raw),
);
const decoded = base32.decode(&raw, encoded_seed) catch {
return error.InvalidSeed;
};
// Expect 35 bytes: 2 prefix + 32 seed + 2 CRC (57 chars / 8 * 5 = 35)
if (decoded.len < 35) return error.InvalidLength;
assert(decoded.len >= 35);
// Extract prefix bytes
const b1 = decoded[0] & 0xF8;
const b2 = ((decoded[0] & 0x07) << 5) | ((decoded[1] & 0xF8) >> 3);
// Validate seed prefix
if (b1 != SEED_PREFIX) return error.InvalidPrefix;
// Validate key type
const key_type = KeyType.fromByte(b2) orelse {
return error.InvalidKeyType;
};
// Validate CRC16 checksum (last 2 bytes, little-endian)
const data_len = decoded.len - 2;
const stored_crc = std.mem.readInt(
u16,
decoded[data_len..][0..2],
.little,
);
if (!crc16.validate(decoded[0..data_len], stored_crc)) {
return error.InvalidChecksum;
}
// Extract 32-byte seed
const seed: [32]u8 = decoded[2..34].*;
// Derive Ed25519 keypair
const kp = Ed25519.KeyPair.generateDeterministic(seed) catch {
return error.IdentityElement;
};
return .{ .kp = kp, .key_type = key_type };
}
/// Signs data and returns raw 64-byte signature.
pub fn sign(self: KeyPair, data: []const u8) [64]u8 {
assert(data.len > 0);
// Deterministic signature (null noise)
const sig = self.kp.sign(data, null) catch unreachable;
return sig.toBytes();
}
/// Signs data and returns base64url-encoded signature (no padding).
/// Writes to provided buffer and returns slice.
pub fn signEncoded(
self: KeyPair,
data: []const u8,
out: *[86]u8,
) []const u8 {
assert(data.len > 0);
assert(out.len >= 86);
const sig = self.sign(data);
return std.base64.url_safe_no_pad.Encoder.encode(out, &sig);
}
/// Returns base32-encoded public key.
/// Format: [key_type_prefix][32-byte-pubkey][crc16]
pub fn publicKey(self: KeyPair, out: *[56]u8) []const u8 {
assert(out.len >= 56);
// Build raw: 1 byte type + 32 bytes pubkey + 2 bytes CRC = 35 bytes
var raw: [35]u8 = undefined;
raw[0] = @intFromEnum(self.key_type);
raw[1..33].* = self.kp.public_key.toBytes();
const crc = crc16.compute(raw[0..33]);
std.mem.writeInt(u16, raw[33..35], crc, .little);
// Base32 encode: 35 bytes -> 56 chars
return base32.encode(out, &raw) catch unreachable;
}
/// Securely wipes keypair from memory.
pub fn wipe(self: *KeyPair) void {
std.crypto.secureZero(
u8,
@volatileCast(&self.kp.secret_key.bytes),
);
}
};
// Test vectors from NATS C client
test "parse valid user seed" {
const seed = "SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY";
var kp = try KeyPair.fromSeed(seed);
defer kp.wipe();
try std.testing.expectEqual(KeyType.user, kp.key_type);
}
test "sign nonce matches test vector" {
const seed = "SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY";
var kp = try KeyPair.fromSeed(seed);
defer kp.wipe();
const sig = kp.sign("nonce");
// First bytes from C client test
try std.testing.expectEqual(@as(u8, 155), sig[0]);
try std.testing.expectEqual(@as(u8, 157), sig[1]);
}
test "sign encoded" {
const seed = "SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY";
var kp = try KeyPair.fromSeed(seed);
defer kp.wipe();
var buf: [86]u8 = undefined;
const encoded = kp.signEncoded("nonce", &buf);
// Base64url encoding of 64 bytes = 86 chars (no padding)
try std.testing.expectEqual(@as(usize, 86), encoded.len);
// First char should correspond to sig[0]=155
try std.testing.expect(encoded[0] == 'm');
}
test "public key format" {
const seed = "SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY";
var kp = try KeyPair.fromSeed(seed);
defer kp.wipe();
var buf: [56]u8 = undefined;
const pubkey = kp.publicKey(&buf);
// User public keys start with 'U'
try std.testing.expectEqual(@as(u8, 'U'), pubkey[0]);
try std.testing.expectEqual(@as(usize, 56), pubkey.len);
}
test "invalid seed - bad prefix" {
// Valid 57-char base32 but wrong prefix (starts with 'N' instead of 'S')
// NAAA... decodes to prefix byte that is not SEED_PREFIX (144)
const bad = "NAAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY";
try std.testing.expectError(error.InvalidPrefix, KeyPair.fromSeed(bad));
}
test "invalid seed - too short" {
const bad = "SUAMK2FG";
try std.testing.expectError(error.InvalidLength, KeyPair.fromSeed(bad));
}
test "invalid seed - bad characters" {
// Contains invalid base32 char '!'
const bad = "SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4!Y";
try std.testing.expectError(error.InvalidSeed, KeyPair.fromSeed(bad));
}
test "encodeSeed roundtrip with known seed" {
const seed =
"SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV" ++
"7NSWFFEW63UXMRLFM2XLAXK4GY";
var kp = try KeyPair.fromSeed(seed);
defer kp.wipe();
var buf: [58]u8 = undefined;
const encoded = kp.encodeSeed(&buf);
try std.testing.expectEqualStrings(seed, encoded);
}
test "encodeSeed roundtrip with deterministic key" {
const test_seed = [_]u8{1} ** 32;
const ed_kp = Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
const kp = KeyPair{ .kp = ed_kp, .key_type = .user };
var seed_buf: [58]u8 = undefined;
const encoded_seed = kp.encodeSeed(&seed_buf);
var kp2 = try KeyPair.fromSeed(encoded_seed);
defer kp2.wipe();
var pk1: [56]u8 = undefined;
var pk2: [56]u8 = undefined;
try std.testing.expectEqualStrings(
kp.publicKey(&pk1),
kp2.publicKey(&pk2),
);
}
test "seed prefix chars per key type" {
const test_seed = [_]u8{42} ** 32;
const ed_kp = Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
var buf: [58]u8 = undefined;
// User seed starts with SU
const user_kp = KeyPair{
.kp = ed_kp,
.key_type = .user,
};
const user_enc = user_kp.encodeSeed(&buf);
try std.testing.expectEqual(@as(u8, 'S'), user_enc[0]);
try std.testing.expectEqual(@as(u8, 'U'), user_enc[1]);
// Account seed starts with SA
const acct_kp = KeyPair{
.kp = ed_kp,
.key_type = .account,
};
const acct_enc = acct_kp.encodeSeed(&buf);
try std.testing.expectEqual(@as(u8, 'S'), acct_enc[0]);
try std.testing.expectEqual(@as(u8, 'A'), acct_enc[1]);
// Operator seed starts with SO
const op_kp = KeyPair{
.kp = ed_kp,
.key_type = .operator,
};
const op_enc = op_kp.encodeSeed(&buf);
try std.testing.expectEqual(@as(u8, 'S'), op_enc[0]);
try std.testing.expectEqual(@as(u8, 'O'), op_enc[1]);
}
test "server and cluster seed prefix roundtrip" {
const test_seed = [_]u8{99} ** 32;
const ed_kp = Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
var buf: [58]u8 = undefined;
// Server seed starts with SN
const srv_kp = KeyPair{
.kp = ed_kp,
.key_type = .server,
};
const srv_enc = srv_kp.encodeSeed(&buf);
try std.testing.expectEqual(@as(u8, 'S'), srv_enc[0]);
try std.testing.expectEqual(@as(u8, 'N'), srv_enc[1]);
var srv_kp2 = try KeyPair.fromSeed(srv_enc);
defer srv_kp2.wipe();
try std.testing.expectEqual(
KeyType.server,
srv_kp2.key_type,
);
// Cluster seed starts with SC
const cls_kp = KeyPair{
.kp = ed_kp,
.key_type = .cluster,
};
const cls_enc = cls_kp.encodeSeed(&buf);
try std.testing.expectEqual(@as(u8, 'S'), cls_enc[0]);
try std.testing.expectEqual(@as(u8, 'C'), cls_enc[1]);
var cls_kp2 = try KeyPair.fromSeed(cls_enc);
defer cls_kp2.wipe();
try std.testing.expectEqual(
KeyType.cluster,
cls_kp2.key_type,
);
}
test "public key prefix for all key types" {
const test_seed = [_]u8{77} ** 32;
const ed_kp = Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
var pk_buf: [56]u8 = undefined;
const Expected = struct { kt: KeyType, ch: u8 };
const cases = [_]Expected{
.{ .kt = .user, .ch = 'U' },
.{ .kt = .account, .ch = 'A' },
.{ .kt = .operator, .ch = 'O' },
.{ .kt = .server, .ch = 'N' },
.{ .kt = .cluster, .ch = 'C' },
};
for (cases) |c| {
const kp = KeyPair{
.kp = ed_kp,
.key_type = c.kt,
};
const pk = kp.publicKey(&pk_buf);
try std.testing.expectEqual(c.ch, pk[0]);
}
}
test "full crypto chain: encode parse sign verify" {
const test_seed = [_]u8{55} ** 32;
const ed_kp = Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
const kp = KeyPair{
.kp = ed_kp,
.key_type = .user,
};
// Encode seed
var seed_buf: [58]u8 = undefined;
const encoded_seed = kp.encodeSeed(&seed_buf);
// Parse back
var kp2 = try KeyPair.fromSeed(encoded_seed);
defer kp2.wipe();
// Public keys must match
var pk1: [56]u8 = undefined;
var pk2: [56]u8 = undefined;
try std.testing.expectEqualStrings(
kp.publicKey(&pk1),
kp2.publicKey(&pk2),
);
// Sign with parsed keypair, verify with original
const data = "test payload for signing";
const sig_bytes = kp2.sign(data);
const sig = Ed25519.Signature.fromBytes(sig_bytes);
try sig.verify(data, kp.kp.public_key);
}
test "CRC16 corruption detection" {
const test_seed = [_]u8{88} ** 32;
const ed_kp = Ed25519.KeyPair.generateDeterministic(
test_seed,
) catch unreachable;
const kp = KeyPair{
.kp = ed_kp,
.key_type = .user,
};
var seed_buf: [58]u8 = undefined;
const encoded = kp.encodeSeed(&seed_buf);
// Copy and corrupt one byte in the middle
var corrupt: [58]u8 = undefined;
@memcpy(corrupt[0..encoded.len], encoded);
corrupt[20] = if (corrupt[20] == 'A') 'B' else 'A';
try std.testing.expectError(
error.InvalidChecksum,
KeyPair.fromSeed(corrupt[0..encoded.len]),
);
}
================================================
FILE: src/auth.zig
================================================
//! Authentication modules for NATS.
//!
//! Provides NKey authentication (Ed25519 signatures) and credentials
//! file parsing for JWT authentication.
pub const nkey = @import("auth/nkey.zig");
pub const creds = @import("auth/creds.zig");
pub const jwt = @import("auth/jwt.zig");
pub const base32 = @import("auth/base32.zig");
pub const crc16 = @import("auth/crc16.zig");
pub const KeyPair = nkey.KeyPair;
pub const KeyType = nkey.KeyType;
pub const Credentials = creds.Credentials;
pub const parseCredentials = creds.parse;
pub const loadCredentialsFile = creds.loadFile;
pub const formatCredentials = creds.format;
test {
_ = nkey;
_ = creds;
_ = jwt;
_ = base32;
_ = crc16;
}
================================================
FILE: src/connection/errors.zig
================================================
//! Connection Errors
//!
//! Error types for connection-related failures including authentication,
//! timeouts, and connection state issues.
const std = @import("std");
/// Connection-related errors.
pub const Error = error{
/// Connection to server was closed unexpectedly.
ConnectionClosed,
/// Connection attempt timed out.
ConnectionTimeout,
/// Server refused the connection.
ConnectionRefused,
/// Authentication with the server failed.
AuthenticationFailed,
/// Stale connection detected.
StaleConnection,
/// Server is in lame duck mode and will shut down soon.
LameDuckMode,
/// TCP_NODELAY socket option failed
TcpNoDelayFailed,
/// TCP receive buffer option failed
TcpRcvBufFailed,
/// URL too long
UrlTooLong,
/// Queue group too long
QueueGroupTooLong,
/// Subject too long
SubjectTooLong,
};
/// Parses auth-related errors from server -ERR message.
/// Returns null if the message is not an auth error.
pub fn parseAuthError(msg: []const u8) ?Error {
if (std.mem.indexOf(u8, msg, "Authentication")) |_| {
return error.AuthenticationFailed;
}
if (std.mem.indexOf(u8, msg, "Stale Connection")) |_| {
return error.StaleConnection;
}
return null;
}
/// Returns true if the error is retryable (connection can be re-established).
pub fn isRetryable(err: Error) bool {
return switch (err) {
error.ConnectionClosed,
error.ConnectionTimeout,
error.StaleConnection,
=> true,
else => false,
};
}
test "parseAuthError authentication" {
const err = parseAuthError("Authentication Timeout");
try std.testing.expectEqual(error.AuthenticationFailed, err.?);
}
test "parseAuthError stale connection" {
const err = parseAuthError("Stale Connection");
try std.testing.expectEqual(error.StaleConnection, err.?);
}
test "parseAuthError non-auth" {
const err = parseAuthError("Some Other Error");
try std.testing.expectEqual(@as(?Error, null), err);
}
test "isRetryable connection errors" {
try std.testing.expect(isRetryable(error.ConnectionClosed));
try std.testing.expect(isRetryable(error.ConnectionTimeout));
try std.testing.expect(isRetryable(error.StaleConnection));
}
test "isRetryable non-retryable errors" {
try std.testing.expect(!isRetryable(error.AuthenticationFailed));
try std.testing.expect(!isRetryable(error.ConnectionRefused));
try std.testing.expect(!isRetryable(error.LameDuckMode));
}
================================================
FILE: src/connection/events.zig
================================================
//! Connection Events
//!
//! Event queue for connection lifecycle and message events.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const commands = @import("../protocol/commands.zig");
const ServerInfo = commands.ServerInfo;
/// Connection events that can be polled by the user.
pub const Event = union(enum) {
/// Successfully connected to server.
connected: ConnectedInfo,
/// Disconnected from server.
disconnected: DisconnectedInfo,
/// Received a message.
message: MessageInfo,
/// Received an error from server.
server_error: []const u8,
/// Reconnecting to server.
reconnecting: ReconnectingInfo,
/// Lamport drain started.
drain_started,
/// Lamport drain completed.
drain_completed,
};
/// Information about successful connection.
pub const ConnectedInfo = struct {
/// Server information received during handshake.
server_id: []const u8,
server_name: []const u8,
version: []const u8,
/// True if this is a reconnection.
is_reconnect: bool,
};
/// Information about disconnection.
pub const DisconnectedInfo = struct {
/// Reason for disconnection.
reason: DisconnectReason,
/// Error message if applicable.
error_msg: ?[]const u8,
};
/// Reasons for disconnection.
pub const DisconnectReason = enum {
/// Normal close requested by user.
user_close,
/// Server closed connection.
server_close,
/// Network error occurred.
network_error,
/// Authentication failed.
auth_failed,
/// Protocol error.
protocol_error,
/// Connection timeout.
timeout,
};
/// Information about received message.
pub const MessageInfo = struct {
/// Message subject.
subject: []const u8,
/// Subscription ID that matched.
sid: u64,
/// Optional reply-to subject.
reply_to: ?[]const u8,
/// Message payload.
data: []const u8,
/// Header data if present (HMSG).
headers: ?[]const u8,
};
/// Information about reconnection attempt.
pub const ReconnectingInfo = struct {
/// Current attempt number.
attempt: u32,
/// Maximum attempts configured.
max_attempts: u32,
/// Server being connected to.
server: []const u8,
};
================================================
FILE: src/connection/io_task.zig
================================================
//! Background I/O Task for NATS Client
//!
//! Async task that handles:
//! - All socket reads (fillMore)
//! - Message routing (MSG/HMSG to subscription queues)
//! - PONG responses to server PING
//! - Reconnection (including handshake writes)
//!
//! Caller context handles:
//! - PUB, SUB, UNSUB writes
//! - Client-initiated PING
//! - Flush operations
//!
//! Both contexts share the socket writer via write_mutex.
//! Runs as async task started by Client.connect().
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const native_os = builtin.os.tag;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Client = @import("../Client.zig");
const State = @import("state.zig").State;
const protocol = @import("../protocol.zig");
const dbg = @import("../dbg.zig");
const memory = @import("../memory.zig");
const TieredSlab = memory.TieredSlab;
const defaults = @import("../defaults.zig");
const Message = Client.Message;
/// Poll timeout when buffer empty (milliseconds).
/// Derived from defaults.Poll.timeout_us for configurability.
/// 0 = busy poll (from timeout_us=0), >=1 otherwise.
const POLL_TIMEOUT_MS: i32 = if (defaults.Poll.timeout_us == 0)
0
else
@max(1, @divFloor(defaults.Poll.timeout_us + 999, 1000));
const Io = std.Io;
/// Gets current monotonic time in nanoseconds.
fn getNowNs(io: Io) u64 {
const ts = Io.Timestamp.now(io, .awake);
return @intCast(ts.nanoseconds);
}
/// Drain return queue - free returned buffers back to slab.
/// Called periodically from read loop to reclaim memory.
/// Uses batch pop to reduce atomic operations from N to ceil(N/64).
inline fn drainReturnQueue(client: *Client) void {
const slab = &client.tiered_slab;
var batch_buf: [64][]u8 = undefined;
while (true) {
const count = client.return_queue.popBatch(&batch_buf);
if (count == 0) break;
for (batch_buf[0..count]) |buf| {
slab.free(buf);
}
}
}
/// Drain publish ring to socket. Returns false on write error
/// (caller should continue :outer to retry). Also handles
/// auto-flush for non-publish writes (SUB, UNSUB).
inline fn drainPublishRing(client: *Client) bool {
if (!client.publish_ring.isEmpty()) {
if (State.atomicLoad(&client.state) == .connected) {
client.write_mutex.lock(
client.io,
) catch return true;
if (State.atomicLoad(&client.state) ==
.connected)
{
while (client.publish_ring.peek()) |data| {
client.active_writer.writeAll(
data,
) catch {
client.write_mutex.unlock(
client.io,
);
return false;
};
client.publish_ring.advance();
}
// REVIEWED(2025-03): flush catch {} is intentional.
// Next write detects failure and triggers reconnect.
client.active_writer.flush() catch {};
if (client.use_tls) {
client.writer.interface.flush() catch {};
}
}
client.write_mutex.unlock(client.io);
_ = client.flush_requested.swap(
false,
.acquire,
);
}
} else if (client.flush_requested.load(.monotonic)) {
// Auto-flush for non-publish writes (SUB, UNSUB)
if (client.flush_requested.swap(false, .acquire)) {
if (State.atomicLoad(&client.state) ==
.connected)
{
client.write_mutex.lock(
client.io,
) catch return true;
defer client.write_mutex.unlock(client.io);
if (State.atomicLoad(&client.state) ==
.connected)
{
client.active_writer.flush() catch {};
if (client.use_tls) {
client.writer.interface.flush() catch {};
}
}
}
}
}
return true;
}
/// Main I/O task entry point. Called via io.async() from connect().
/// Reader: reads socket, routes MSG, responds to PING with PONG.
/// Exits cleanly when stream is closed (close then cancel).
pub fn run(client: *Client) void {
dbg.print("io_task[fd={d}]: STARTED", .{client.stream.socket.handle});
var loop_count: u64 = 0;
// Health check throttling (100ms interval to avoid hot-loop impact)
// Use iteration counter to avoid syscall every loop (~10ms at 1M loops/sec)
const health_check_interval_ns: u64 = 100_000_000;
var last_health_check_ns: u64 = 0;
var health_check_counter: u32 = 0;
outer: while (true) {
if (dbg.enabled) loop_count += 1;
// HOT PATH: Exit check - intentionally non-atomic for performance.
// Safe because of "close-then-cancel" pattern (see module doc).
// Stale .closed read causes socket op failure, task exits anyway.
if (client.state == .closed) break :outer;
// Periodic health check (detects stale connections when server killed)
// Only check timestamp every N iterations to avoid syscall overhead
health_check_counter +%= 1;
if (health_check_counter >= defaults.Spin.health_check_iterations) {
health_check_counter = 0;
const now_ns = getNowNs(client.io);
if (now_ns - last_health_check_ns >= health_check_interval_ns) {
last_health_check_ns = now_ns;
if (client.checkHealthAndDetectStale()) {
// Connection stale - trigger disconnect/reconnect
const state = State.atomicLoad(&client.state);
if (client.options.reconnect and state != .closed) {
if (!handleDisconnect(client)) break :outer;
continue :outer;
}
// Reconnect disabled - set closed state before exiting
@atomicStore(State, &client.state, .closed, .release);
client.pushEvent(.{ .closed = {} });
break :outer;
}
}
}
// Drain ring BEFORE reads to minimize write latency.
// Without this, ring drains only after the inner
// loop's 1ms poll timeout, starving the producer.
if (!drainPublishRing(client)) continue :outer;
var made_progress = true;
while (made_progress) {
made_progress = false;
// Exit inner loop when ring has pending data
// so we drain it without blocking in poll().
if (!client.publish_ring.isEmpty()) break;
drainReturnQueue(client);
const route_result =
tryRouteBufferedMessages(client);
if (route_result == .progress) {
dbg.print(
"io_task[fd={d}]: routed messages",
.{client.stream.socket.handle},
);
made_progress = true;
}
if (route_result == .disconnected) {
const state = State.atomicLoad(
&client.state,
);
if (client.options.reconnect and
state != .closed)
{
if (!handleDisconnect(client))
break :outer;
continue :outer;
}
@atomicStore(
State,
&client.state,
.closed,
.release,
);
client.pushEvent(.{ .closed = {} });
break :outer;
}
if (!made_progress) {
const read_result = tryFillBuffer(client);
if (read_result == .progress) {
dbg.print(
"io_task[fd={d}]: read data" ++
" from socket",
.{client.stream.socket.handle},
);
}
if (read_result == .canceled)
break :outer;
if (read_result == .disconnected) {
const state = State.atomicLoad(
&client.state,
);
if (client.options.reconnect and
state != .closed)
{
if (!handleDisconnect(client))
break :outer;
continue :outer;
}
@atomicStore(
State,
&client.state,
.closed,
.release,
);
client.pushEvent(
.{ .closed = {} },
);
break :outer;
}
if (read_result == .progress)
made_progress = true;
}
}
// Drain ring AFTER reads (catches new entries
// produced during the inner loop).
if (!drainPublishRing(client)) continue :outer;
// No progress - yield to allow other threads
std.Thread.yield() catch {};
}
if (dbg.enabled) {
const stats = &client.io_task_stats;
dbg.print(
"io_task: EXITED loops={d} fill_calls={d} buffered_hits={d} " ++
"poll_timeouts={d} read_ok={d}",
.{
loop_count,
stats.fill_calls,
stats.fill_buffered_hits,
stats.fill_poll_timeouts,
stats.fill_read_success,
},
);
}
}
/// Result of read/route operations.
const ReadResult = enum {
progress,
no_progress,
disconnected,
canceled,
};
/// Result of poll() operation for disconnect detection.
const PollResult = enum {
has_data,
no_data,
disconnected,
};
/// Cross-platform socket poll.
/// std.posix.poll has a Windows codepath that calls
/// windows.poll which does not exist in this Zig version.
/// Use WSAPoll directly on Windows.
fn pollSockets(
fds: []posix.pollfd,
timeout: i32,
) posix.PollError!usize {
if (native_os == .windows) {
const ws2 = std.os.windows.ws2_32;
const rc = ws2.WSAPoll(
fds.ptr,
@intCast(fds.len),
timeout,
);
if (rc == ws2.SOCKET_ERROR)
return error.NetworkDown;
return @intCast(rc);
}
return posix.poll(fds, timeout);
}
/// Poll socket for readable data with timeout (cross-platform).
/// Also detects disconnect via POLLHUP/POLLERR.
/// On Linux, POLLIN and POLLHUP can both be set when there's
/// buffered data AND the connection is closing. POLLHUP is prioritized
/// to detect dead connections even with buffered data.
inline fn pollForData(
fd: posix.socket_t,
timeout_ms: i32,
) PollResult {
var fds = [_]posix.pollfd{.{
.fd = fd,
.events = posix.POLL.IN,
.revents = 0,
}};
const ready = pollSockets(&fds, timeout_ms) catch return .no_data;
if (ready == 0) return .no_data;
// Single load, combined checks (avoid 3 separate loads)
const revents = fds[0].revents;
// REVIEWED(2025-03): POLLHUP prioritized over POLLIN intentionally.
// Dead connection detection is more important than draining
// partial data; reconnect recovers cleanly.
if ((revents & (posix.POLL.HUP | posix.POLL.ERR)) != 0)
return .disconnected;
if ((revents & posix.POLL.IN) != 0) return .has_data;
return .no_data;
}
/// Try to fill buffer without blocking forever.
/// Uses poll() to check for data, then fillMore() to read.
/// For TLS: loops until we get decrypted data or no more TCP data available.
/// This handles TLS record fragmentation where a record spans multiple TCP segments.
inline fn tryFillBuffer(client: *Client) ReadResult {
if (dbg.enabled) client.io_task_stats.fill_calls += 1;
// HOT PATH: Non-atomic read - see module doc "State checks (hot path)"
if (client.state == .closed) return .canceled;
const reader = client.active_reader;
const fd = client.stream.socket.handle;
const before = reader.buffered().len;
if (dbg.enabled) client.io_task_stats.fill_buffered_hits += before;
// TLS: loop until we decrypt data or truly no more data available.
// Key insight: encrypted data may be in TCP reader's buffer (not socket).
// After TLS decrypts one record, more encrypted records may remain in the
// TCP buffer. poll() only sees the socket, not the TCP reader's buffer!
// So we must: 1) check TCP buffer first, 2) only poll if TCP buffer empty.
if (client.use_tls) {
while (true) {
// Atomic read: race with deinit closing socket before fillMore()
if (State.atomicLoad(&client.state) == .closed) return .canceled;
// Check if TCP reader has buffered encrypted data (poll can't see this)
const tcp_buffered = client.reader.interface.buffered().len;
if (tcp_buffered == 0) {
// TCP buffer empty - poll socket for more encrypted data
const poll_result = pollForData(fd, POLL_TIMEOUT_MS);
if (poll_result == .disconnected) return .disconnected;
if (poll_result == .no_data) {
if (dbg.enabled)
client.io_task_stats.fill_poll_timeouts += 1;
// No more data anywhere - return what we have
return if (reader.buffered().len > before)
.progress
else
.no_progress;
}
}
// Either TCP buffer has data, or poll said socket has data
reader.fillMore() catch |err| {
if (err == error.Canceled) return .canceled;
if (err == error.EndOfStream or
err == error.ConnectionResetByPeer or
err == error.BrokenPipe or
err == error.NotOpenForReading)
{
return .disconnected;
}
// Other errors: check if we made progress before failing
return if (reader.buffered().len > before)
.progress
else
.no_progress;
};
// Got decrypted data - success
if (reader.buffered().len > before) {
if (dbg.enabled) client.io_task_stats.fill_read_success += 1;
return .progress;
}
// No decrypted data yet - TLS needs more data, loop
}
}
// Non-TLS: simple poll + read
const poll_result = pollForData(fd, POLL_TIMEOUT_MS);
if (poll_result == .disconnected) {
return .disconnected;
}
if (poll_result == .no_data) {
if (dbg.enabled) client.io_task_stats.fill_poll_timeouts += 1;
return .no_progress;
}
// Atomic read: race with deinit closing socket before fillMore()
if (State.atomicLoad(&client.state) == .closed) return .canceled;
// Socket has data -> fillMore() will return immediately
reader.fillMore() catch |err| {
if (err == error.Canceled) return .canceled;
if (err == error.EndOfStream or
err == error.ConnectionResetByPeer or
err == error.BrokenPipe or
err == error.NotOpenForReading)
{
return .disconnected;
}
return .no_progress;
};
const after = reader.buffered().len;
if (after > before) {
if (dbg.enabled) client.io_task_stats.fill_read_success += 1;
return .progress;
}
return .no_progress;
}
/// Route buffered messages (no I/O, buffer processing only).
/// Handles: MSG -> route to queue, PING -> write PONG.
/// Uses lock-free SpscQueue - no yields needed.
inline fn tryRouteBufferedMessages(
client: *Client,
) ReadResult {
const allocator = client.allocator;
const reader = client.active_reader;
const slab = &client.tiered_slab;
// HOT PATH: Non-atomic read - see module doc "State checks (hot path)"
if (client.state == .closed) return .canceled;
const data = reader.buffered();
if (data.len == 0) return .no_progress;
var offset: usize = 0;
while (offset < data.len) {
var consumed: usize = 0;
const result = client.parser.parse(
allocator,
data[offset..],
&consumed,
) catch {
// Scan to next CRLF for recovery (skip corrupted data)
// Uses SIMD on supported platforms
if (std.mem.indexOf(u8, data[offset..], "\r\n")) |crlf_pos| {
const bytes_skipped = crlf_pos + 2;
offset += bytes_skipped;
// Track and rate-limit protocol error notifications
client.protocol_errors += 1;
const msgs_since = client.statistics.msgs_in -|
client.last_parse_error_notified_at;
const interval = client.options.error_notify_interval_msgs;
if (client.protocol_errors == 1 or msgs_since >= interval) {
client.last_parse_error_notified_at = client.statistics.msgs_in;
client.pushEvent(.{
.protocol_error = .{
.bytes_skipped = bytes_skipped,
.count = client.protocol_errors,
},
});
}
dbg.print(
"parse error (#{d}, skipped {d} bytes, rate-limited)",
.{ client.protocol_errors, bytes_skipped },
);
} else {
break;
}
continue;
};
if (result) |cmd| {
switch (cmd) {
.msg => |args| {
routeMessageToSub(client, slab, args);
client.statistics.msgs_in += 1;
client.statistics.bytes_in += args.payload.len;
},
.hmsg => |args| {
routeHMessageToSub(client, slab, args);
client.statistics.msgs_in += 1;
client.statistics.bytes_in += args.total_len;
},
.ping => {
client.write_mutex.lock(client.io) catch
return .disconnected;
defer client.write_mutex.unlock(client.io);
client.active_writer.writeAll("PONG\r\n") catch {
return .disconnected;
};
client.active_writer.flush() catch return .disconnected;
},
.pong => {
const now = getNowNs(client.io);
dbg.print("Got PONG, storing timestamp={d}", .{now});
client.pings_outstanding.store(0, .monotonic);
client.last_pong_received_ns.store(now, .release);
},
.info => |info| {
// REVIEWED(2025-03): server_info replacement
// races with user reads. Risk: user holding a
// slice from serverInfo() getters gets dangling
// pointer when old strings are freed. Window is
// narrow (reconnect only). Locking would add
// overhead to every getter for a rare event.
// x86_64 only; aarch64 risk is strictly worse.
if (client.server_info) |*old| {
old.deinit(allocator);
}
client.server_info = info;
client.max_payload = info.max_payload;
client.parser.max_payload = info.max_payload;
},
.ok => {},
.err => |err_msg| {
if (handleServerError(client, err_msg)) {
return .disconnected;
}
},
}
offset += consumed;
} else {
break;
}
}
if (offset > 0) {
reader.toss(offset);
return .progress;
}
return .no_progress;
}
/// Route MSG to subscription queue.
inline fn routeMessageToSub(
client: *Client,
slab: *TieredSlab,
args: protocol.MsgArgs,
) void {
client.read_mutex.lockUncancelable(client.io);
defer client.read_mutex.unlock(client.io);
dbg.print("routeMsg[fd={d}]: sid={d} subject={s}", .{ client.stream.socket.handle, args.sid, args.subject });
const sub = client.getSubscriptionBySid(args.sid) orelse {
dbg.print("routeMsg[fd={d}]: NO SUB FOUND for sid={d}", .{ client.stream.socket.handle, args.sid });
return;
};
const subj_len = args.subject.len;
const payload_len = args.payload.len;
const reply_len = if (args.reply_to) |rt| rt.len else 0;
const total_size = subj_len + payload_len + reply_len;
// Bounds verification - assert our arithmetic is correct
const subj_end = subj_len;
const payload_end = subj_end + payload_len;
const reply_end = payload_end + reply_len;
assert(reply_end == total_size);
const buf = slab.alloc(total_size) orelse {
sub.alloc_failed_msgs += 1;
// Rate-limit: push event on 1st failure OR after interval msgs
const msgs_since = client.statistics.msgs_in -| sub.last_alloc_notified_at;
const interval = client.options.error_notify_interval_msgs;
if (sub.alloc_failed_msgs == 1 or msgs_since >= interval) {
sub.last_alloc_notified_at = client.statistics.msgs_in;
client.pushEvent(.{
.alloc_failed = .{
.sid = args.sid,
.count = sub.alloc_failed_msgs,
},
});
}
dbg.print(
"alloc failed sid={d} (#{d}, rate-limited every {d} msgs)",
.{ args.sid, sub.alloc_failed_msgs, interval },
);
return;
};
@memcpy(buf[0..subj_end], args.subject);
@memcpy(buf[subj_end..payload_end], args.payload);
if (args.reply_to) |rt| {
@memcpy(buf[payload_end..reply_end], rt);
}
const subject = buf[0..subj_end];
const data_slice = buf[subj_end..payload_end];
const reply_to: ?[]const u8 = if (reply_len > 0)
buf[payload_end..reply_end]
else
null;
const msg = Message{
.subject = subject,
.sid = args.sid,
.reply_to = reply_to,
.data = data_slice,
.headers = null,
.owned = true,
.backing_buf = buf,
.return_queue = &client.return_queue,
.return_lock = &client.return_lock,
};
sub.pushMessage(msg) catch {
dbg.print("routeMsg: PUSH FAILED (slow consumer) sid={d}", .{args.sid});
sub.dropped_msgs += 1;
slab.free(buf);
// REVIEWED(2025-03): Single notification is intentional.
// Avoids flooding event queue during slow consumer.
// Users monitor sub.dropped_msgs for ongoing counts.
if (sub.dropped_msgs == 1) {
client.pushEvent(.{ .slow_consumer = .{ .sid = args.sid } });
}
return;
};
// REVIEWED(2025-03): Non-atomic stats are safe here.
// io_task is the sole writer; user reads after drain.
dbg.print("routeMsg: pushed to queue, sid={d}", .{args.sid});
sub.received_msgs += 1;
}
/// Route HMSG to subscription queue.
inline fn routeHMessageToSub(
client: *Client,
slab: *TieredSlab,
args: protocol.HMsgArgs,
) void {
client.read_mutex.lockUncancelable(client.io);
defer client.read_mutex.unlock(client.io);
const sub = client.getSubscriptionBySid(args.sid) orelse return;
const subj_len = args.subject.len;
const data_len = args.payload.len;
const hdr_len = args.headers.len;
const reply_len = if (args.reply_to) |rt| rt.len else 0;
const total_size = subj_len + data_len + hdr_len + reply_len;
// Bounds verification - assert our arithmetic is correct
const subj_end = subj_len;
const data_end = subj_end + data_len;
const hdr_end = data_end + hdr_len;
const reply_end = hdr_end + reply_len;
assert(reply_end == total_size);
const buf = slab.alloc(total_size) orelse {
sub.alloc_failed_msgs += 1;
// Rate-limit: push event on 1st failure OR after interval msgs
const msgs_since = client.statistics.msgs_in -| sub.last_alloc_notified_at;
const interval = client.options.error_notify_interval_msgs;
if (sub.alloc_failed_msgs == 1 or msgs_since >= interval) {
sub.last_alloc_notified_at = client.statistics.msgs_in;
client.pushEvent(.{
.alloc_failed = .{
.sid = args.sid,
.count = sub.alloc_failed_msgs,
},
});
}
dbg.print(
"alloc failed sid={d} (#{d}, rate-limited every {d} msgs)",
.{ args.sid, sub.alloc_failed_msgs, interval },
);
return;
};
@memcpy(buf[0..subj_end], args.subject);
@memcpy(buf[subj_end..data_end], args.payload);
@memcpy(buf[data_end..hdr_end], args.headers);
if (args.reply_to) |rt| {
@memcpy(buf[hdr_end..reply_end], rt);
}
const subject = buf[0..subj_end];
const data_slice = buf[subj_end..data_end];
const headers = buf[data_end..hdr_end];
const reply_to: ?[]const u8 = if (reply_len > 0)
buf[hdr_end..reply_end]
else
null;
const msg = Message{
.subject = subject,
.sid = args.sid,
.reply_to = reply_to,
.data = data_slice,
.headers = headers,
.owned = true,
.backing_buf = buf,
.return_queue = &client.return_queue,
.return_lock = &client.return_lock,
};
sub.pushMessage(msg) catch {
sub.dropped_msgs += 1;
slab.free(buf);
if (sub.dropped_msgs == 1) {
client.pushEvent(.{ .slow_consumer = .{ .sid = args.sid } });
}
return;
};
sub.received_msgs += 1;
}
/// Handle disconnect - backup subs, attempt reconnection, restore subs.
/// Returns true if reconnected successfully, false if should exit task.
fn handleDisconnect(client: *Client) bool {
@atomicStore(State, &client.state, .disconnected, .release);
client.pushEvent(.{ .disconnected = .{ .err = null } });
client.backupSubscriptions() catch |err| {
dbg.print("backupSubscriptions failed: {s}", .{@errorName(err)});
};
// Close old stream before reconnect to prevent FD leak
// (matches reconnect() ordering: backup then cleanup)
client.cleanupForReconnect();
if (tryReconnectLoop(client)) {
client.restoreSubscriptions() catch {
dbg.print("Failed to restore subscriptions after reconnect", .{});
};
client.pushEvent(.{ .reconnected = {} });
return true;
} else {
@atomicStore(State, &client.state, .closed, .release);
client.pushEvent(.{ .closed = {} });
return false;
}
}
/// Attempt reconnection loop with backoff.
/// Returns true if reconnected, false if failed or canceled.
fn tryReconnectLoop(client: *Client) bool {
@atomicStore(State, &client.state, .reconnecting, .release);
const max_attempts = if (client.options.max_reconnect_attempts == 0)
std.math.maxInt(u32)
else
client.options.max_reconnect_attempts;
var attempt: u32 = 0;
while (attempt < max_attempts) {
attempt += 1;
client.reconnect_attempt = attempt;
// Wait with backoff (except first attempt) - cancellation point
if (attempt > 1) {
const delay_ms = calculateReconnectDelay(client, attempt);
client.io.sleep(
.fromMilliseconds(delay_ms),
.awake,
) catch |err| {
if (err == error.Canceled) return false;
};
}
for (client.server_pool.servers[0..client.server_pool.count]) |*server| {
client.tryConnect(server) catch continue;
@atomicStore(State, &client.state, .connected, .release);
_ = client.statistics.reconnects.fetchAdd(1, .monotonic);
client.reconnect_attempt = 0;
return true;
}
}
return false;
}
/// Calculate reconnect delay with exponential backoff and jitter.
/// If custom_reconnect_delay callback is set, uses that instead.
fn calculateReconnectDelay(client: *Client, attempt: u32) u32 {
assert(attempt > 0);
// Use custom callback if provided
if (client.options.custom_reconnect_delay) |cb| {
return cb(attempt);
}
// Exponential backoff: base * 2^(attempt-1), capped at max
const base_ms = client.options.reconnect_wait_ms;
const max_ms = client.options.reconnect_wait_max_ms;
const jitter_pct = client.options.reconnect_jitter_percent;
// Calculate exponential delay: base * 2^(attempt-2) for attempt > 1
// attempt 2 -> base, attempt 3 -> base*2, attempt 4 -> base*4, etc.
const shift: u5 = @intCast(@min(attempt -| 2, 30));
const exp_delay: u64 = @as(u64, base_ms) << shift;
const capped_delay: u32 = @intCast(@min(exp_delay, max_ms));
// Apply jitter: delay +/- jitter_pct%
if (jitter_pct == 0) return capped_delay;
const jitter_range = (capped_delay * jitter_pct) / 100;
if (jitter_range == 0) return capped_delay;
var rand_buf: [4]u8 = undefined;
client.io.random(&rand_buf);
const rand_val = std.mem.readInt(
u32,
&rand_buf,
.little,
);
const jitter_offset = rand_val % (jitter_range * 2 + 1);
const jitter: i64 = @as(i64, jitter_offset) -
@as(i64, jitter_range);
const final_delay: i64 = @as(i64, capped_delay) + jitter;
return @intCast(@max(final_delay, 1));
}
/// Handle server -ERR message. Categorizes error and pushes event.
/// Also stores as last_error for later retrieval via getLastError().
/// Returns true if error is fatal (should disconnect), false otherwise.
fn handleServerError(client: *Client, msg: []const u8) bool {
const events = @import("../events.zig");
// Categorize error (case-insensitive matching like Go/C clients)
const err_type: anyerror = blk: {
if (containsIgnoreCase(msg, "authorization")) {
break :blk events.Error.AuthorizationViolation;
}
if (containsIgnoreCase(msg, "permissions violation")) {
break :blk events.Error.PermissionViolation;
}
if (containsIgnoreCase(msg, "stale connection")) {
break :blk events.Error.StaleConnection;
}
if (containsIgnoreCase(msg, "maximum connections")) {
break :blk events.Error.MaxConnectionsExceeded;
}
break :blk events.Error.ServerError;
};
// REVIEWED(2025-03): last_error written without sync.
// Acceptable: errors rare, x86_64 TSO ensures coherent
// reads, msg is copied into fixed buffer before use.
client.last_error = err_type;
if (msg.len < 256) {
const len: u8 = @intCast(msg.len);
@memcpy(client.last_error_msg[0..len], msg);
client.last_error_msg_len = len;
} else {
// Truncate to fit u8 length field
@memcpy(
client.last_error_msg[0..255],
msg[0..255],
);
client.last_error_msg_len = 255;
}
// Use already-copied last_error_msg to avoid
// dangling pointer into recycled parser buffer
const safe_msg = if (client.last_error_msg_len > 0)
client.last_error_msg[0..client.last_error_msg_len]
else
null;
client.pushEvent(.{ .err = .{ .err = err_type, .msg = safe_msg } });
// Fatal errors trigger disconnect/reconnect
return err_type == events.Error.AuthorizationViolation or
err_type == events.Error.StaleConnection or
err_type == events.Error.MaxConnectionsExceeded;
}
/// Case-insensitive substring search (no allocations).
fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool {
if (needle.len > haystack.len) return false;
var i: usize = 0;
while (i <= haystack.len - needle.len) : (i += 1) {
var match = true;
for (0..needle.len) |j| {
const h = haystack[i + j];
const n = needle[j];
const hl = if (h >= 'A' and h <= 'Z') h + 32 else h;
const nl = if (n >= 'A' and n <= 'Z') n + 32 else n;
if (hl != nl) {
match = false;
break;
}
}
if (match) return true;
}
return false;
}
================================================
FILE: src/connection/reconnect_test.zig
================================================
//! Reconnection Logic Unit Tests
//!
//! Tests for subscription backup/restore, backoff calculations,
//! and pending buffer operations.
const std = @import("std");
const Client = @import("../Client.zig");
const SubBackup = Client.SubBackup;
// SubBackup Structure Tests
test "SubBackup default initialization" {
const backup: SubBackup = .{};
try std.testing.expectEqual(@as(u64, 0), backup.sid);
try std.testing.expectEqual(@as(u8, 0), backup.subject_len);
try std.testing.expectEqual(@as(u8, 0), backup.queue_group_len);
try std.testing.expect(backup.max_msgs == null);
try std.testing.expectEqual(@as(u64, 0), backup.received_msgs);
}
test "SubBackup getSubject returns correct slice" {
var backup: SubBackup = .{};
const subject = "test.subject.name";
@memcpy(backup.subject_buf[0..subject.len], subject);
backup.subject_len = subject.len;
try std.testing.expectEqualStrings(subject, backup.getSubject());
}
test "SubBackup getSubject empty" {
const backup: SubBackup = .{};
try std.testing.expectEqualStrings("", backup.getSubject());
}
test "SubBackup getQueueGroup returns correct slice" {
var backup: SubBackup = .{};
const qg = "my-queue-group";
@memcpy(backup.queue_group_buf[0..qg.len], qg);
backup.queue_group_len = qg.len;
try std.testing.expectEqualStrings(qg, backup.queueGroup().?);
}
test "SubBackup getQueueGroup returns null when empty" {
const backup: SubBackup = .{};
try std.testing.expect(backup.queueGroup() == null);
}
test "SubBackup max subject length" {
var backup: SubBackup = .{};
// Fill entire buffer
@memset(&backup.subject_buf, 'x');
backup.subject_len = 255;
try std.testing.expectEqual(@as(usize, 255), backup.getSubject().len);
}
test "SubBackup max queue group length" {
var backup: SubBackup = .{};
@memset(&backup.queue_group_buf, 'q');
backup.queue_group_len = 64;
try std.testing.expectEqual(@as(usize, 64), backup.queueGroup().?.len);
}
test "SubBackup preserves SID" {
var backup: SubBackup = .{};
backup.sid = 12345;
try std.testing.expectEqual(@as(u64, 12345), backup.sid);
}
test "SubBackup preserves max_msgs" {
var backup: SubBackup = .{};
backup.max_msgs = 100;
try std.testing.expectEqual(@as(u64, 100), backup.max_msgs.?);
}
test "SubBackup preserves received_msgs" {
var backup: SubBackup = .{};
backup.received_msgs = 42;
try std.testing.expectEqual(@as(u64, 42), backup.received_msgs);
}
test "SubBackup max SID value" {
var backup: SubBackup = .{};
backup.sid = std.math.maxInt(u64);
try std.testing.expectEqual(std.math.maxInt(u64), backup.sid);
}
// Backoff Calculation Tests
// These test the backoff calculation formula:
// exp_wait = base << attempt (capped at 10)
// capped = min(exp_wait, max_wait)
// jitter = +/-(capped * jitter_percent / 100)
test "backoff base case" {
const base_ms: u64 = 2000;
const attempt: u4 = 0;
const exp_wait = base_ms << attempt;
try std.testing.expectEqual(@as(u64, 2000), exp_wait);
}
test "backoff exponential growth" {
const base_ms: u64 = 2000;
try std.testing.expectEqual(@as(u64, 2000), base_ms << @as(u4, 0));
try std.testing.expectEqual(@as(u64, 4000), base_ms << @as(u4, 1));
try std.testing.expectEqual(@as(u64, 8000), base_ms << @as(u4, 2));
try std.testing.expectEqual(@as(u64, 16000), base_ms << @as(u4, 3));
try std.testing.expectEqual(@as(u64, 32000), base_ms << @as(u4, 4));
}
test "backoff capped at max" {
const base_ms: u64 = 2000;
const max_ms: u64 = 30000;
// Attempt 5 would be 64000, capped to 30000
const exp_wait = base_ms << @as(u4, 5);
const capped = @min(exp_wait, max_ms);
try std.testing.expectEqual(@as(u64, 30000), capped);
}
test "backoff attempt capped at 10" {
const base_ms: u64 = 2000;
const max_attempt: u4 = 10;
// Attempt 10 = 2000 << 10 = 2,048,000ms
const exp_wait = base_ms << max_attempt;
try std.testing.expectEqual(@as(u64, 2048000), exp_wait);
}
test "backoff jitter range calculation" {
const capped: u64 = 30000;
const jitter_percent: u8 = 10;
const jitter_range = capped * jitter_percent / 100;
try std.testing.expectEqual(@as(u64, 3000), jitter_range);
}
test "backoff jitter bounds" {
const capped: u64 = 30000;
const jitter_percent: u8 = 10;
const jitter_range = capped * jitter_percent / 100;
// Jitter should be in range [-3000, +3000]
// Min wait = 30000 - 3000 = 27000
// Max wait = 30000 + 3000 = 33000
const min_wait = capped - jitter_range;
const max_wait = capped + jitter_range;
try std.testing.expectEqual(@as(u64, 27000), min_wait);
try std.testing.expectEqual(@as(u64, 33000), max_wait);
}
test "backoff zero jitter" {
const capped: u64 = 30000;
const jitter_percent: u8 = 0;
const jitter_range = capped * jitter_percent / 100;
try std.testing.expectEqual(@as(u64, 0), jitter_range);
}
test "backoff max jitter 50 percent" {
const capped: u64 = 30000;
const jitter_percent: u8 = 50;
const jitter_range = capped * jitter_percent / 100;
try std.testing.expectEqual(@as(u64, 15000), jitter_range);
}
// Reconnection Options Tests
test "default reconnection options" {
const opts: Client.Options = .{};
try std.testing.expect(opts.reconnect);
try std.testing.expectEqual(@as(u32, 60), opts.max_reconnect_attempts);
try std.testing.expectEqual(@as(u32, 2000), opts.reconnect_wait_ms);
try std.testing.expectEqual(@as(u32, 30000), opts.reconnect_wait_max_ms);
try std.testing.expectEqual(@as(u8, 10), opts.reconnect_jitter_percent);
try std.testing.expect(opts.discover_servers);
try std.testing.expectEqual(@as(usize, 8 * 1024 * 1024), opts.pending_buffer_size);
}
test "disable reconnection" {
const opts: Client.Options = .{ .reconnect = false };
try std.testing.expect(!opts.reconnect);
}
test "infinite reconnect attempts" {
const opts: Client.Options = .{ .max_reconnect_attempts = 0 };
try std.testing.expectEqual(@as(u32, 0), opts.max_reconnect_attempts);
}
test "custom reconnect timing" {
const opts: Client.Options = .{
.reconnect_wait_ms = 500,
.reconnect_wait_max_ms = 10000,
.reconnect_jitter_percent = 25,
};
try std.testing.expectEqual(@as(u32, 500), opts.reconnect_wait_ms);
try std.testing.expectEqual(@as(u32, 10000), opts.reconnect_wait_max_ms);
try std.testing.expectEqual(@as(u8, 25), opts.reconnect_jitter_percent);
}
test "disable pending buffer" {
const opts: Client.Options = .{ .pending_buffer_size = 0 };
try std.testing.expectEqual(@as(usize, 0), opts.pending_buffer_size);
}
test "custom pending buffer size" {
const opts: Client.Options = .{ .pending_buffer_size = 1024 * 1024 };
try std.testing.expectEqual(@as(usize, 1024 * 1024), opts.pending_buffer_size);
}
// Health Check Options Tests
test "default health check options" {
const opts: Client.Options = .{};
try std.testing.expectEqual(@as(u32, 120000), opts.ping_interval_ms);
try std.testing.expectEqual(@as(u8, 2), opts.max_pings_outstanding);
}
test "disable health check" {
const opts: Client.Options = .{ .ping_interval_ms = 0 };
try std.testing.expectEqual(@as(u32, 0), opts.ping_interval_ms);
}
test "aggressive health check" {
const opts: Client.Options = .{
.ping_interval_ms = 1000,
.max_pings_outstanding = 1,
};
try std.testing.expectEqual(@as(u32, 1000), opts.ping_interval_ms);
try std.testing.expectEqual(@as(u8, 1), opts.max_pings_outstanding);
}
// Stats Tests
test "stats default reconnects zero" {
const stats: Client.Statistics = .{};
try std.testing.expectEqual(@as(u32, 0), stats.reconnects);
}
// Subscription Remaining Messages Calculation Tests
test "remaining messages calculation" {
// Test the -| saturating subtraction pattern used in restoreSubscriptions
const max_msgs: u64 = 100;
const received: u64 = 30;
const remaining = max_msgs -| received;
try std.testing.expectEqual(@as(u64, 70), remaining);
}
test "remaining messages at zero" {
const max_msgs: u64 = 100;
const received: u64 = 100;
const remaining = max_msgs -| received;
try std.testing.expectEqual(@as(u64, 0), remaining);
}
test "remaining messages saturates" {
const max_msgs: u64 = 100;
const received: u64 = 150; // More than max (shouldn't happen but test anyway)
const remaining = max_msgs -| received;
try std.testing.expectEqual(@as(u64, 0), remaining);
}
test "remaining messages max values" {
const max_msgs: u64 = std.math.maxInt(u64);
const received: u64 = 1;
const remaining = max_msgs -| received;
try std.testing.expectEqual(std.math.maxInt(u64) - 1, remaining);
}
// Pending Buffer Size Estimation Tests
// Tests for the size estimation: "PUB subject len\r\npayload\r\n"
// encoded_size = 4 + subject.len + 1 + 10 + 2 + payload.len + 2
test "pending buffer size estimation minimal" {
const subject = "x";
const payload = "";
// "PUB x 0\r\n\r\n" = 4 + 1 + 1 + 10 + 2 + 0 + 2 = 20 (estimate)
const estimate = 4 + subject.len + 1 + 10 + 2 + payload.len + 2;
try std.testing.expectEqual(@as(usize, 19), estimate);
}
test "pending buffer size estimation typical" {
const subject = "my.test.subject";
const payload = "hello world";
const estimate = 4 + subject.len + 1 + 10 + 2 + payload.len + 2;
try std.testing.expectEqual(@as(usize, 45), estimate);
}
test "pending buffer size estimation large payload" {
const subject = "data.stream";
const payload_size: usize = 1024 * 1024; // 1MB
const estimate = 4 + subject.len + 1 + 10 + 2 + payload_size + 2;
try std.testing.expectEqual(@as(usize, 1048606), estimate);
}
// Multiple Backup Array Tests
test "backup array initialization" {
const backups = [_]SubBackup{.{}} ** Client.MAX_SUBSCRIPTIONS;
try std.testing.expectEqual(Client.MAX_SUBSCRIPTIONS, backups.len);
// All should be zeroed
for (backups) |backup| {
try std.testing.expectEqual(@as(u64, 0), backup.sid);
try std.testing.expectEqual(@as(u8, 0), backup.subject_len);
}
}
test "backup array modification" {
var backups = [_]SubBackup{.{}} ** 4;
backups[0].sid = 1;
backups[1].sid = 2;
backups[2].sid = 3;
backups[3].sid = 4;
try std.testing.expectEqual(@as(u64, 1), backups[0].sid);
try std.testing.expectEqual(@as(u64, 2), backups[1].sid);
try std.testing.expectEqual(@as(u64, 3), backups[2].sid);
try std.testing.expectEqual(@as(u64, 4), backups[3].sid);
}
// Edge Case Tests
test "subject with special characters in backup" {
var backup: SubBackup = .{};
const subject = "test.*.>";
@memcpy(backup.subject_buf[0..subject.len], subject);
backup.subject_len = subject.len;
try std.testing.expectEqualStrings("test.*.>", backup.getSubject());
}
test "queue group with hyphens and numbers" {
var backup: SubBackup = .{};
const qg = "worker-group-123";
@memcpy(backup.queue_group_buf[0..qg.len], qg);
backup.queue_group_len = qg.len;
try std.testing.expectEqualStrings(qg, backup.queueGroup().?);
}
test "backup with all fields populated" {
var backup: SubBackup = .{};
backup.sid = 42;
const subject = "orders.*.shipped";
@memcpy(backup.subject_buf[0..subject.len], subject);
backup.subject_len = subject.len;
const qg = "processors";
@memcpy(backup.queue_group_buf[0..qg.len], qg);
backup.queue_group_len = qg.len;
backup.max_msgs = 1000;
backup.received_msgs = 500;
try std.testing.expectEqual(@as(u64, 42), backup.sid);
try std.testing.expectEqualStrings(subject, backup.getSubject());
try std.testing.expectEqualStrings(qg, backup.queueGroup().?);
try std.testing.expectEqual(@as(u64, 1000), backup.max_msgs.?);
try std.testing.expectEqual(@as(u64, 500), backup.received_msgs);
}
================================================
FILE: src/connection/server_pool.zig
================================================
//! Server Pool for Reconnection
//!
//! Manages multiple servers for reconnection with round-robin rotation.
//! Servers are discovered from initial URL and INFO connect_urls.
const std = @import("std");
const assert = std.debug.assert;
const defaults = @import("../defaults.zig");
/// Maximum number of servers in the pool.
pub const MAX_SERVERS: u8 = defaults.Server.max_pool_size;
/// Maximum URL length.
pub const MAX_URL_LEN: u16 = defaults.Server.max_url_len;
/// Cooldown period after failure before retry (ns).
const FAILURE_COOLDOWN_NS: u64 = defaults.Server.failure_cooldown_ns;
/// Server entry in the pool.
pub const Server = struct {
url: [MAX_URL_LEN]u8 = undefined,
url_len: u8 = 0,
host_start: u8 = 0,
host_len: u8 = 0,
port: u16 = defaults.Protocol.port,
consecutive_failures: u8 = 0,
last_attempt_ns: u64 = 0,
/// Whether this server uses TLS (from tls:// scheme).
use_tls: bool = false,
/// Get the URL as a slice.
pub fn getUrl(self: *const Server) []const u8 {
return self.url[0..self.url_len];
}
/// Get the host as a slice.
pub fn getHost(self: *const Server) []const u8 {
return self.url[self.host_start..][0..self.host_len];
}
};
/// Server pool for reconnection rotation.
pub const ServerPool = struct {
servers: [MAX_SERVERS]Server = undefined,
count: u8 = 0,
current_idx: u8 = 0,
primary_idx: u8 = 0,
/// Initialize server pool with primary server URL.
pub fn init(primary_url: []const u8) error{InvalidUrl}!ServerPool {
var pool: ServerPool = .{};
if (primary_url.len == 0 or primary_url.len >= MAX_URL_LEN) {
return error.InvalidUrl;
}
pool.addServer(primary_url) catch return error.InvalidUrl;
pool.primary_idx = 0;
assert(pool.count > 0);
return pool;
}
/// Add a server to the pool. Returns false if pool is full or URL invalid.
pub fn addServer(self: *ServerPool, url: []const u8) !void {
if (self.count >= MAX_SERVERS) return error.PoolFull;
if (url.len == 0 or url.len >= MAX_URL_LEN) return error.InvalidUrl;
for (self.servers[0..self.count]) |*existing| {
if (std.mem.eql(u8, existing.getUrl(), url)) {
return;
}
}
var server: Server = .{};
const url_len: u8 = @intCast(url.len);
@memcpy(server.url[0..url_len], url);
server.url_len = url_len;
var remaining = url;
if (std.mem.startsWith(u8, remaining, "tls://")) {
remaining = remaining[6..];
server.host_start = 6;
server.use_tls = true;
} else if (std.mem.startsWith(u8, remaining, "nats://")) {
remaining = remaining[7..];
server.host_start = 7;
}
if (std.mem.indexOf(u8, remaining, "@")) |at_pos| {
remaining = remaining[at_pos + 1 ..];
server.host_start += @intCast(at_pos + 1);
}
if (remaining.len > 0 and remaining[0] == '[') {
// IPv6 literal: [::1]:port
if (std.mem.indexOf(
u8,
remaining,
"]",
)) |bracket_end| {
server.host_start += 1; // skip '['
server.host_len = @intCast(
bracket_end - 1,
);
const after = remaining[bracket_end + 1 ..];
if (after.len > 1 and after[0] == ':') {
server.port = std.fmt.parseInt(
u16,
after[1..],
10,
) catch 4222;
}
} else {
// Malformed IPv6 — treat as host
server.host_len = @intCast(
remaining.len,
);
}
} else if (std.mem.indexOf(
u8,
remaining,
":",
)) |colon_pos| {
if (colon_pos > 255) return error.InvalidUrl;
server.host_len = @intCast(colon_pos);
server.port = std.fmt.parseInt(
u16,
remaining[colon_pos + 1 ..],
10,
) catch 4222;
} else {
if (remaining.len > 255)
return error.InvalidUrl;
server.host_len = @intCast(remaining.len);
server.port = 4222;
}
assert(server.host_len > 0);
assert(server.port > 0);
self.servers[self.count] = server;
self.count += 1;
}
/// Add servers from ServerInfo connect_urls.
/// Returns the number of new servers that were added (not duplicates).
pub fn addFromConnectUrls(
self: *ServerPool,
urls: []const [256]u8,
lens: []const u8,
count: u8,
) u8 {
assert(urls.len >= count);
assert(lens.len >= count);
const before = self.count;
for (0..count) |i| {
const len = lens[i];
if (len == 0) continue;
const url = urls[i][0..len];
self.addServer(url) catch continue;
}
return self.count - before;
}
/// Get next server for connection attempt (round-robin).
/// Skips servers that failed recently (cooldown).
/// Returns null if all servers are on cooldown.
pub fn nextServer(self: *ServerPool, now_ns: u64) ?*Server {
if (self.count == 0) return null;
assert(self.count > 0);
assert(self.current_idx < self.count);
var attempts: u8 = 0;
while (attempts < self.count) : (attempts += 1) {
self.current_idx = (self.current_idx + 1) % self.count;
var server = &self.servers[self.current_idx];
if (server.consecutive_failures > 0) {
const cooldown = FAILURE_COOLDOWN_NS *
@as(u64, server.consecutive_failures);
if (now_ns - server.last_attempt_ns < cooldown) {
continue;
}
}
server.last_attempt_ns = now_ns;
return server;
}
return null;
}
/// Mark current server as failed.
pub fn markCurrentFailed(self: *ServerPool) void {
if (self.count == 0) return;
assert(self.current_idx < self.count);
var server = &self.servers[self.current_idx];
if (server.consecutive_failures < 255) {
server.consecutive_failures += 1;
}
}
/// Reset all failure counts (called on successful connect).
pub fn resetFailures(self: *ServerPool) void {
for (self.servers[0..self.count]) |*server| {
server.consecutive_failures = 0;
}
}
/// Get current server URL as slice.
pub fn currentUrl(self: *const ServerPool) []const u8 {
if (self.count == 0) return "none";
assert(self.current_idx < self.count);
return self.servers[self.current_idx].getUrl();
}
/// Get current server.
pub fn current(self: *ServerPool) ?*Server {
if (self.count == 0) return null;
assert(self.current_idx < self.count);
return &self.servers[self.current_idx];
}
/// Get server count.
pub fn serverCount(self: *const ServerPool) u8 {
return self.count;
}
};
test "server pool init" {
const pool = try ServerPool.init("nats://localhost:4222");
try std.testing.expectEqual(@as(u8, 1), pool.count);
try std.testing.expectEqualStrings(
"nats://localhost:4222",
pool.servers[0].getUrl(),
);
try std.testing.expectEqualStrings("localhost", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "server pool init with auth" {
const pool = try ServerPool.init("nats://user:pass@localhost:4222");
try std.testing.expectEqual(@as(u8, 1), pool.count);
try std.testing.expectEqualStrings("localhost", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "server pool init without port" {
const pool = try ServerPool.init("nats://localhost");
try std.testing.expectEqual(@as(u8, 1), pool.count);
try std.testing.expectEqualStrings("localhost", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "server pool init without scheme" {
const pool = try ServerPool.init("localhost:4222");
try std.testing.expectEqual(@as(u8, 1), pool.count);
try std.testing.expectEqualStrings("localhost", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "server pool add servers" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
try pool.addServer("nats://server3:4222");
try std.testing.expectEqual(@as(u8, 3), pool.count);
}
test "server pool deduplication" {
var pool = try ServerPool.init("nats://localhost:4222");
try pool.addServer("nats://localhost:4222"); // Duplicate
try pool.addServer("nats://localhost:4222"); // Duplicate
try std.testing.expectEqual(@as(u8, 1), pool.count);
}
test "server pool rotation" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
try pool.addServer("nats://server3:4222");
const now: u64 = 1000000000000;
// Should rotate through servers
const s1 = pool.nextServer(now).?;
try std.testing.expectEqualStrings("nats://server2:4222", s1.getUrl());
const s2 = pool.nextServer(now).?;
try std.testing.expectEqualStrings("nats://server3:4222", s2.getUrl());
const s3 = pool.nextServer(now).?;
try std.testing.expectEqualStrings("nats://server1:4222", s3.getUrl());
}
test "server pool failure tracking" {
var pool = try ServerPool.init("nats://server1:4222");
var now: u64 = 1000000000000;
// Get server and mark as failed
_ = pool.nextServer(now);
pool.markCurrentFailed();
try std.testing.expectEqual(@as(u8, 1), pool.servers[0].consecutive_failures);
// Should be on cooldown
now += 1000000000; // +1 second (cooldown is 5 seconds)
try std.testing.expect(pool.nextServer(now) == null);
// After cooldown, should be available
now += 10000000000; // +10 seconds
try std.testing.expect(pool.nextServer(now) != null);
}
test "server pool reset failures" {
var pool = try ServerPool.init("nats://server1:4222");
_ = pool.nextServer(0);
pool.markCurrentFailed();
pool.markCurrentFailed();
try std.testing.expectEqual(@as(u8, 2), pool.servers[0].consecutive_failures);
pool.resetFailures();
try std.testing.expectEqual(@as(u8, 0), pool.servers[0].consecutive_failures);
}
test "server pool empty url" {
const result = ServerPool.init("");
try std.testing.expectError(error.InvalidUrl, result);
}
test "server pool tls scheme" {
const pool = try ServerPool.init("tls://secure.example.com:4222");
try std.testing.expectEqual(@as(u8, 1), pool.count);
try std.testing.expectEqualStrings("secure.example.com", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
try std.testing.expect(pool.servers[0].use_tls);
}
test "server pool nats scheme not tls" {
const pool = try ServerPool.init("nats://localhost:4222");
try std.testing.expect(!pool.servers[0].use_tls);
}
test "server pool mixed schemes" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("tls://server2:4222");
try std.testing.expectEqual(@as(u8, 2), pool.count);
try std.testing.expect(!pool.servers[0].use_tls);
try std.testing.expect(pool.servers[1].use_tls);
}
================================================
FILE: src/connection/server_pool_test.zig
================================================
//! Server Pool Tests
//!
//! Unit tests for ServerPool including edge cases,
//! failure tracking, cooldown behavior, and URL parsing.
const std = @import("std");
const ServerPool = @import("server_pool.zig").ServerPool;
const Server = @import("server_pool.zig").Server;
const MAX_SERVERS = @import("server_pool.zig").MAX_SERVERS;
const MAX_URL_LEN = @import("server_pool.zig").MAX_URL_LEN;
// URL Parsing Tests
test "parse URL with IPv4 address" {
const pool = try ServerPool.init("nats://192.168.1.100:4222");
try std.testing.expectEqualStrings("192.168.1.100", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "parse URL with different port" {
const pool = try ServerPool.init("nats://localhost:5222");
try std.testing.expectEqual(@as(u16, 5222), pool.servers[0].port);
}
test "parse URL with max port" {
const pool = try ServerPool.init("nats://localhost:65535");
try std.testing.expectEqual(@as(u16, 65535), pool.servers[0].port);
}
test "parse URL with port 1" {
const pool = try ServerPool.init("nats://localhost:1");
try std.testing.expectEqual(@as(u16, 1), pool.servers[0].port);
}
test "parse URL with invalid port uses default" {
const pool = try ServerPool.init("nats://localhost:invalid");
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "parse URL with port overflow uses default" {
const pool = try ServerPool.init("nats://localhost:99999");
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "parse URL with user only" {
const pool = try ServerPool.init("nats://user@localhost:4222");
try std.testing.expectEqualStrings("localhost", pool.servers[0].getHost());
}
test "parse URL with complex auth" {
const pool = try ServerPool.init("nats://user:p@ss:word@localhost:4222");
try std.testing.expectEqualStrings("localhost", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "parse URL with just host no scheme no port" {
const pool = try ServerPool.init("myserver");
try std.testing.expectEqualStrings("myserver", pool.servers[0].getHost());
try std.testing.expectEqual(@as(u16, 4222), pool.servers[0].port);
}
test "parse URL preserves original" {
const original = "nats://demo.nats.io:4222";
const pool = try ServerPool.init(original);
try std.testing.expectEqualStrings(original, pool.servers[0].getUrl());
}
test "pool starts empty after primary" {
const pool = try ServerPool.init("nats://localhost:4222");
try std.testing.expectEqual(@as(u8, 1), pool.serverCount());
}
test "pool can hold MAX_SERVERS" {
var pool = try ServerPool.init("nats://server0:4222");
var i: u8 = 1;
while (i < MAX_SERVERS) : (i += 1) {
var buf: [32]u8 = undefined;
const url = std.fmt.bufPrint(&buf, "nats://server{d}:4222", .{i}) catch
unreachable;
try pool.addServer(url);
}
try std.testing.expectEqual(MAX_SERVERS, pool.serverCount());
}
test "pool full returns error" {
var pool = try ServerPool.init("nats://server0:4222");
var i: u8 = 1;
while (i < MAX_SERVERS) : (i += 1) {
var buf: [32]u8 = undefined;
const url = std.fmt.bufPrint(&buf, "nats://server{d}:4222", .{i}) catch
unreachable;
try pool.addServer(url);
}
const result = pool.addServer("nats://overflow:4222");
try std.testing.expectError(error.PoolFull, result);
}
test "URL too long returns error" {
var long_url: [MAX_URL_LEN + 10]u8 = undefined;
@memset(&long_url, 'a');
const result = ServerPool.init(&long_url);
try std.testing.expectError(error.InvalidUrl, result);
}
test "URL exactly max length succeeds" {
var url: [MAX_URL_LEN]u8 = undefined;
@memset(&url, 'a');
@memcpy(url[0..7], "server:");
const pool = try ServerPool.init(&url);
try std.testing.expectEqual(@as(u8, 1), pool.serverCount());
}
test "exact duplicate rejected" {
var pool = try ServerPool.init("nats://localhost:4222");
try pool.addServer("nats://localhost:4222");
try std.testing.expectEqual(@as(u8, 1), pool.serverCount());
}
test "different port not duplicate" {
var pool = try ServerPool.init("nats://localhost:4222");
try pool.addServer("nats://localhost:4223");
try std.testing.expectEqual(@as(u8, 2), pool.serverCount());
}
test "different host not duplicate" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
try std.testing.expectEqual(@as(u8, 2), pool.serverCount());
}
test "case sensitive URLs" {
var pool = try ServerPool.init("nats://Server1:4222");
try pool.addServer("nats://server1:4222");
try std.testing.expectEqual(@as(u8, 2), pool.serverCount());
}
test "rotation starts from second server" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
try pool.addServer("nats://server3:4222");
const now: u64 = 1_000_000_000_000;
const s1 = pool.nextServer(now).?;
try std.testing.expectEqualStrings("nats://server2:4222", s1.getUrl());
}
test "rotation wraps around" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
const now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now); // server2
_ = pool.nextServer(now); // server1
const s3 = pool.nextServer(now).?; // server2 again
try std.testing.expectEqualStrings("nats://server2:4222", s3.getUrl());
}
test "single server rotation returns same" {
var pool = try ServerPool.init("nats://only:4222");
const now: u64 = 1_000_000_000_000;
const s1 = pool.nextServer(now).?;
const s2 = pool.nextServer(now).?;
const s3 = pool.nextServer(now).?;
try std.testing.expectEqualStrings("nats://only:4222", s1.getUrl());
try std.testing.expectEqualStrings("nats://only:4222", s2.getUrl());
try std.testing.expectEqualStrings("nats://only:4222", s3.getUrl());
}
// Failure Tracking Tests
test "failure count increments" {
var pool = try ServerPool.init("nats://server:4222");
const now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
try std.testing.expectEqual(@as(u8, 0), pool.servers[0].consecutive_failures);
pool.markCurrentFailed();
try std.testing.expectEqual(@as(u8, 1), pool.servers[0].consecutive_failures);
pool.markCurrentFailed();
try std.testing.expectEqual(@as(u8, 2), pool.servers[0].consecutive_failures);
}
test "failure count saturates at 255" {
var pool = try ServerPool.init("nats://server:4222");
const now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
var i: u16 = 0;
while (i < 300) : (i += 1) {
pool.markCurrentFailed();
}
try std.testing.expectEqual(@as(u8, 255), pool.servers[0].consecutive_failures);
}
test "reset failures clears all" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
var now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
pool.markCurrentFailed();
pool.markCurrentFailed();
now += 100_000_000_000;
_ = pool.nextServer(now);
pool.markCurrentFailed();
pool.resetFailures();
try std.testing.expectEqual(@as(u8, 0), pool.servers[0].consecutive_failures);
try std.testing.expectEqual(@as(u8, 0), pool.servers[1].consecutive_failures);
}
test "cooldown increases with failures" {
var pool = try ServerPool.init("nats://server:4222");
var now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
pool.markCurrentFailed();
now += 4_000_000_000;
try std.testing.expect(pool.nextServer(now) == null);
now += 2_000_000_000;
try std.testing.expect(pool.nextServer(now) != null);
pool.markCurrentFailed();
now += 8_000_000_000;
try std.testing.expect(pool.nextServer(now) == null);
now += 4_000_000_000;
try std.testing.expect(pool.nextServer(now) != null);
}
test "all servers on cooldown returns null" {
var pool = try ServerPool.init("nats://server1:4222");
try pool.addServer("nats://server2:4222");
var now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
pool.markCurrentFailed();
_ = pool.nextServer(now);
pool.markCurrentFailed();
now += 1_000_000_000;
try std.testing.expect(pool.nextServer(now) == null);
}
test "cooldown expires allows retry" {
var pool = try ServerPool.init("nats://server:4222");
var now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
pool.markCurrentFailed();
now += 6_000_000_000;
try std.testing.expect(pool.nextServer(now) != null);
}
test "healthy server chosen over failed" {
var pool = try ServerPool.init("nats://failed:4222");
try pool.addServer("nats://healthy:4222");
const now: u64 = 1_000_000_000_000;
_ = pool.nextServer(now);
pool.markCurrentFailed();
const server = pool.nextServer(now).?;
try std.testing.expectEqualStrings("nats://healthy:4222", server.getUrl());
}
test "add from connect_urls" {
var pool = try ServerPool.init("nats://primary:4222");
var urls: [16][256]u8 = undefined;
var lens: [16]u8 = [_]u8{0} ** 16;
const url1 = "nats://cluster1:4222";
const url2 = "nats://cluster2:4222";
@memcpy(urls[0][0..url1.len], url1);
lens[0] = url1.len;
@memcpy(urls[1][0..url2.len], url2);
lens[1] = url2.len;
pool.addFromConnectUrls(&urls, &lens, 2);
try std.testing.expectEqual(@as(u8, 3), pool.serverCount());
}
test "add from connect_urls skips empty" {
var pool = try ServerPool.init("nats://primary:4222");
var urls: [16][256]u8 = undefined;
var lens: [16]u8 = [_]u8{0} ** 16;
const url1 = "nats://cluster1:4222";
@memcpy(urls[0][0..url1.len], url1);
lens[0] = url1.len;
lens[2] = 0;
pool.addFromConnectUrls(&urls, &lens, 3);
try std.testing.expectEqual(@as(u8, 2), pool.serverCount());
}
test "add from connect_urls deduplicates" {
var pool = try ServerPool.init("nats://primary:4222");
var urls: [16][256]u8 = undefined;
var lens: [16]u8 = [_]u8{0} ** 16;
const url1 = "nats://primary:4222";
@memcpy(urls[0][0..url1.len], url1);
lens[0] = url1.len;
pool.addFromConnectUrls(&urls, &lens, 1);
try std.testing.expectEqual(@as(u8, 1), pool.serverCount());
}
// Current Server Access Tests
test "currentUrl on empty pool returns none" {
// Can't create empty pool directly, but test the behavior
var pool = try ServerPool.init("nats://server:4222");
// Manually clear for testing (don't do this in production!)
pool.count = 0;
try std.testing.expectEqualStrings("none", pool.currentUrl());
}
test "current returns server reference" {
var pool = try ServerPool.init("nats://server:4222");
const server = pool.current().?;
try std.testing.expectEqualStrings("nats://server:4222", server.getUrl());
}
test "current allows modification" {
var pool = try ServerPool.init("nats://server:4222");
const server = pool.current().?;
server.consecutive_failures = 5;
try std.testing.expectEqual(@as(u8, 5), pool.servers[0].consecutive_failures);
}
// Server Struct Tests
test "server default values" {
const server: Server = .{};
try std.testing.expectEqual(@as(u8, 0), server.url_len);
try std.testing.expectEqual(@as(u16, 4222), server.port);
try std.testing.expectEqual(@as(u8, 0), server.consecutive_failures);
try std.testing.expectEqual(@as(u64, 0), server.last_attempt_ns);
}
test "server getUrl returns correct slice" {
var server: Server = .{};
const url = "nats://test:1234";
@memcpy(server.url[0..url.len], url);
server.url_len = url.len;
try std.testing.expectEqualStrings(url, server.getUrl());
}
test "server getHost returns correct slice" {
var server: Server = .{};
const url = "nats://myhost:4222";
@memcpy(server.url[0..url.len], url);
server.url_len = url.len;
server.host_start = 7;
server.host_len = 6;
try std.testing.expectEqualStrings("myhost", server.getHost());
}
test "primary index preserved" {
var pool = try ServerPool.init("nats://primary:4222");
try pool.addServer("nats://secondary:4222");
try std.testing.expectEqual(@as(u8, 0), pool.primary_idx);
}
test "timestamps updated on next_server" {
var pool = try ServerPool.init("nats://server:4222");
const time1: u64 = 1_000_000_000_000;
_ = pool.nextServer(time1);
try std.testing.expectEqual(time1, pool.servers[0].last_attempt_ns);
const time2: u64 = 2_000_000_000_000;
_ = pool.nextServer(time2);
try std.testing.expectEqual(time2, pool.servers[0].last_attempt_ns);
}
test "zero time works" {
var pool = try ServerPool.init("nats://server:4222");
const server = pool.nextServer(0);
try std.testing.expect(server != null);
}
test "max time works" {
var pool = try ServerPool.init("nats://server:4222");
const server = pool.nextServer(std.math.maxInt(u64));
try std.testing.expect(server != null);
}
================================================
FILE: src/connection/state.zig
================================================
//! Connection State Machine
//!
//! Manages the connection state transitions for NATS protocol.
const std = @import("std");
const assert = std.debug.assert;
/// Connection states.
pub const State = enum {
/// Initial state, not connected.
disconnected,
/// TCP connection established, waiting for INFO.
connecting,
/// Received INFO, sent CONNECT, waiting for response.
authenticating,
/// Fully connected and ready.
connected,
/// Connection lost, attempting reconnect.
reconnecting,
/// Gracefully draining before close.
draining,
/// Permanently closed.
closed,
/// Thread-safe state read (use from io_task/callback_task).
pub inline fn atomicLoad(state_ptr: *const State) State {
return @atomicLoad(State, state_ptr, .acquire);
}
/// Thread-safe state write (use from io_task).
pub inline fn atomicStore(state_ptr: *State, new_state: State) void {
@atomicStore(State, state_ptr, new_state, .release);
}
/// Returns true if the connection can send messages.
pub fn canSend(self: State) bool {
return self == .connected or self == .draining;
}
/// Returns true if the connection can receive messages.
pub fn canReceive(self: State) bool {
return self == .connected or self == .draining;
}
/// Returns true if the connection is in a terminal state.
pub fn isTerminal(self: State) bool {
return self == .closed;
}
/// Returns true if the connection should attempt reconnection.
pub fn shouldReconnect(self: State) bool {
return self == .reconnecting;
}
};
/// State machine for connection lifecycle.
pub const StateMachine = struct {
state: State = .disconnected,
last_error: ?[]const u8 = null,
/// Transitions to connecting state.
pub fn startConnect(self: *StateMachine) !void {
switch (self.state) {
.disconnected, .reconnecting => {
self.state = .connecting;
self.last_error = null;
assert(self.state == .connecting);
},
.closed => return error.ConnectionClosed,
else => return error.InvalidState,
}
}
/// Called when INFO is received.
pub fn receivedInfo(self: *StateMachine) !void {
if (self.state != .connecting) return error.InvalidState;
self.state = .authenticating;
assert(self.state == .authenticating);
}
/// Called when CONNECT is acknowledged.
pub fn connectAcknowledged(self: *StateMachine) !void {
if (self.state != .authenticating) return error.InvalidState;
self.state = .connected;
assert(self.state == .connected);
}
/// Called when connection is lost.
pub fn connectionLost(self: *StateMachine, err: ?[]const u8) void {
self.last_error = err;
switch (self.state) {
.connected, .authenticating, .connecting => {
self.state = .reconnecting;
},
.draining => {
self.state = .closed;
},
else => {},
}
}
/// Starts graceful drain.
pub fn startDrain(self: *StateMachine) !void {
if (self.state != .connected) return error.InvalidState;
self.state = .draining;
assert(self.state == .draining);
}
/// Closes the connection permanently.
pub fn close(self: *StateMachine) void {
self.state = .closed;
assert(self.state.isTerminal());
}
/// Resets to disconnected for reconnection attempt.
pub fn resetForReconnect(self: *StateMachine) !void {
if (self.state != .reconnecting) return error.InvalidState;
self.state = .disconnected;
assert(self.state == .disconnected);
}
};
test "state machine happy path" {
var sm: StateMachine = .{};
try std.testing.expectEqual(State.disconnected, sm.state);
try sm.startConnect();
try std.testing.expectEqual(State.connecting, sm.state);
try sm.receivedInfo();
try std.testing.expectEqual(State.authenticating, sm.state);
try sm.connectAcknowledged();
try std.testing.expectEqual(State.connected, sm.state);
try std.testing.expect(sm.state.canSend());
try std.testing.expect(sm.state.canReceive());
}
test "state machine reconnect" {
var sm: StateMachine = .{};
try sm.startConnect();
try sm.receivedInfo();
try sm.connectAcknowledged();
sm.connectionLost("test error");
try std.testing.expectEqual(State.reconnecting, sm.state);
try std.testing.expect(sm.state.shouldReconnect());
try sm.resetForReconnect();
try std.testing.expectEqual(State.disconnected, sm.state);
}
test "state machine drain" {
var sm: StateMachine = .{};
try sm.startConnect();
try sm.receivedInfo();
try sm.connectAcknowledged();
try sm.startDrain();
try std.testing.expectEqual(State.draining, sm.state);
try std.testing.expect(sm.state.canSend());
sm.connectionLost(null);
try std.testing.expectEqual(State.closed, sm.state);
try std.testing.expect(sm.state.isTerminal());
}
test "state machine close" {
var sm: StateMachine = .{};
try sm.startConnect();
sm.close();
try std.testing.expectEqual(State.closed, sm.state);
try std.testing.expectError(error.ConnectionClosed, sm.startConnect());
}
================================================
FILE: src/connection.zig
================================================
//! Connection Module
//!
//! Provides connection state management and events for NATS.
const std = @import("std");
pub const state = @import("connection/state.zig");
pub const events = @import("connection/events.zig");
pub const errors = @import("connection/errors.zig");
pub const server_pool = @import("connection/server_pool.zig");
pub const io_task = @import("connection/io_task.zig");
pub const State = state.State;
pub const StateMachine = state.StateMachine;
pub const Event = events.Event;
pub const ConnectedInfo = events.ConnectedInfo;
pub const DisconnectedInfo = events.DisconnectedInfo;
pub const DisconnectReason = events.DisconnectReason;
pub const MessageInfo = events.MessageInfo;
pub const ReconnectingInfo = events.ReconnectingInfo;
pub const Error = errors.Error;
pub const parseAuthError = errors.parseAuthError;
pub const isRetryable = errors.isRetryable;
pub const ServerPool = server_pool.ServerPool;
test {
std.testing.refAllDecls(@This());
}
================================================
FILE: src/dbg.zig
================================================
//! Debug printing utilities for NATS client.
//!
//! Compile with -DEnableDebug=true to enable debug output.
//! When disabled, all debug calls are eliminated by dead code elimination.
const std = @import("std");
const build_options = @import("build_options");
/// Debug printing enabled at compile time.
pub const enabled = build_options.enable_debug;
/// Print debug message if debug is enabled.
/// Dead code eliminated when disabled.
pub inline fn print(comptime fmt: []const u8, args: anytype) void {
if (enabled) {
std.debug.print("[NATS] " ++ fmt ++ "\n", args);
}
}
/// Print reconnection event.
pub inline fn reconnectEvent(
comptime event: []const u8,
attempt: u32,
server: []const u8,
) void {
if (enabled) {
std.debug.print(
"[NATS:RECONNECT] {s} attempt={d} server={s}\n",
.{ event, attempt, server },
);
}
}
/// Print connection state change.
pub inline fn stateChange(
comptime from: []const u8,
comptime to: []const u8,
) void {
if (enabled) {
std.debug.print("[NATS:STATE] {s} -> {s}\n", .{ from, to });
}
}
/// Print PING/PONG event.
pub inline fn pingPong(comptime event: []const u8, outstanding: u8) void {
if (enabled) {
std.debug.print(
"[NATS:HEALTH] {s} outstanding={d}\n",
.{ event, outstanding },
);
}
}
/// Print subscription event.
pub inline fn subscription(
comptime event: []const u8,
sid: u64,
subject: []const u8,
) void {
if (enabled) {
std.debug.print(
"[NATS:SUB] {s} sid={d} subject={s}\n",
.{ event, sid, subject },
);
}
}
/// Print pending buffer event.
pub inline fn pendingBuffer(
comptime event: []const u8,
pos: usize,
capacity: usize,
) void {
if (enabled) {
std.debug.print(
"[NATS:BUFFER] {s} pos={d} capacity={d}\n",
.{ event, pos, capacity },
);
}
}
================================================
FILE: src/defaults.zig
================================================
//! Centralized Default Configuration
//!
//! Queue size is the master value. Slab tier counts derive from it.
//! Change queue_size once, all memory allocations adjust automatically.
const builtin = @import("builtin");
/// Predefined queue size options (power-of-2, 1K to 512K).
pub const QueueSize = enum(u32) {
k1 = 1024,
k2 = 2048,
k4 = 4096,
k8 = 8192,
k16 = 16384,
k32 = 32768,
k64 = 65536,
k128 = 131072,
k256 = 262144,
k512 = 524288,
/// Returns the numeric value.
pub fn value(self: QueueSize) u32 {
return @intFromEnum(self);
}
};
/// Memory and slab configuration.
pub const Memory = struct {
/// Master queue size (slab tiers derive from this).
pub const queue_size: QueueSize = .k8;
/// Slab tier sizes (fixed, power-of-2).
pub const tier_sizes = [_]u32{ 256, 512, 1024, 4096, 16384 };
pub const tier_count: usize = tier_sizes.len;
/// Slab tier counts (derived from queue_size).
pub const tier_counts = blk: {
const q = queue_size.value();
break :blk [_]u32{
q, // Tier 0: 256B
q, // Tier 1: 512B
q / 2, // Tier 2: 1KB
q / 4, // Tier 3: 4KB
q / 16, // Tier 4: 16KB
};
};
/// Max slab slice size (larger uses fallback allocator).
pub const max_slice_size: usize = 16384;
/// Total pre-allocated slab memory (comptime computed).
pub const total_memory: usize = blk: {
var total: usize = 0;
for (tier_sizes, tier_counts) |size, count| {
total += @as(usize, size) * count;
}
break :blk total;
};
};
/// Connection settings.
pub const Connection = struct {
/// Connection timeout (5 seconds).
pub const timeout_ns: u64 = 5_000_000_000;
/// Read buffer size. Must be > max_payload + protocol overhead.
/// Derived from Protocol.max_payload + 8KB headroom for MSG/HMSG headers.
pub const reader_buffer_size: usize = Protocol.max_payload + 8 * 1024;
/// Write buffer size. Same default as read buffer.
pub const writer_buffer_size: usize = Protocol.max_payload + 8 * 1024;
/// TCP receive buffer hint (1 MB for high throughput).
pub const tcp_rcvbuf: u32 = 1024 * 1024;
/// Ping interval (2 minutes).
pub const ping_interval_ms: u32 = 120_000;
/// Max outstanding pings before stale.
pub const max_pings_outstanding: u8 = 2;
};
/// Reconnection strategy.
pub const Reconnection = struct {
/// Enable automatic reconnection.
pub const enabled: bool = true;
/// Maximum reconnection attempts (0 = infinite).
pub const max_attempts: u32 = 60;
/// Initial wait between attempts (2 seconds).
pub const wait_ms: u32 = 2_000;
/// Maximum wait with backoff (30 seconds).
pub const wait_max_ms: u32 = 30_000;
/// Jitter percentage (0-50).
pub const jitter_percent: u8 = 10;
/// Discover servers from INFO connect_urls.
pub const discover_servers: bool = true;
/// Buffer for publishes during reconnect (8 MB).
pub const pending_buffer_size: usize = 8 * 1024 * 1024;
};
/// Server pool limits.
pub const Server = struct {
/// Max servers in pool.
pub const max_pool_size: u8 = 16;
/// Max URL string length.
pub const max_url_len: u16 = 256;
/// Cooldown after failure (5 seconds).
pub const failure_cooldown_ns: u64 = 5_000_000_000;
};
/// Client limits.
pub const Client = struct {
/// Max concurrent subscriptions per client.
pub const max_subscriptions: u16 = 16384;
/// SidMap hash table capacity.
pub const sidmap_capacity: u32 = 32768;
};
/// Protocol constants.
pub const Protocol = struct {
/// Default NATS server port.
pub const port: u16 = 4222;
/// Default max payload (1 MB).
pub const max_payload: u32 = 1048576;
/// Client version string.
pub const version: []const u8 = "0.1.0";
};
/// Spin/yield loop tuning constants.
pub const Spin = struct {
/// Spin iterations before yielding in subscription next() loop.
/// After this many spins, yields to I/O runtime for cancellation support.
pub const max_spins: u32 = 4096;
/// Loop iterations between health check timestamp
/// reads in io_task.
pub const health_check_iterations: u32 = 1000000;
// timeout_check_iterations removed -- all spin loops
// now use io.sleep yield after max_spins instead.
};
/// Poll timeout configuration for io_task.
pub const Poll = struct {
/// Poll timeout in microseconds.
/// 0 = busy poll (max throughput, high CPU)
/// 100-500 = low latency with reduced CPU
/// 1000 = 1ms, balanced (default)
/// Values < 1000 require ppoll() on Linux for sub-ms precision.
/// poll() rounds up to 1ms minimum.
pub const timeout_us: i32 = 1000;
};
/// Protocol limits for subjects and queue groups.
/// These are compile-time limits that define backup buffer sizes.
pub const Limits = struct {
/// Max subject length for backup buffers (reconnect support).
/// Subjects longer than this cannot be restored after reconnect.
pub const max_subject_len: u16 = 256;
/// Max queue group length for backup buffers.
pub const max_queue_group_len: u8 = 64;
};
/// Error reporting configuration.
pub const ErrorReporting = struct {
/// Messages between rate-limited error notifications.
/// After first error, subsequent errors only notify every N messages.
/// This prevents event queue flooding during sustained error conditions.
pub const notify_interval_msgs: u64 = 100_000;
};
/// TLS configuration.
pub const Tls = struct {
/// TLS read/write buffer size (must be >= tls.Client.min_buffer_len).
/// Using 32KB for good performance.
pub const buffer_size: usize = 32 * 1024;
};
================================================
FILE: src/events.zig
================================================
//! Event Callbacks for NATS Client
//!
//! Type-erased event handler using comptime vtable pattern (like std.mem.Allocator).
//! Enables callbacks without closures, maintaining Zig's no-hidden-allocation guarantee.
//!
//! ## Architecture
//!
//! io_task pushes events to SPSC queue (non-blocking).
//! callback_task drains queue and dispatches to user handlers.
//!
//! ## Usage
//!
//! ```zig
//! const MyHandler = struct {
//! counter: *u32,
//!
//! pub fn onConnect(self: *@This()) void {
//! self.counter.* += 1;
//! }
//! };
//!
//! var counter: u32 = 0;
//! var handler = MyHandler{ .counter = &counter };
//! const client = try nats.Client.connect(allocator, io, url, .{
//! .event_handler = nats.EventHandler.init(MyHandler, &handler),
//! });
//! ```
const std = @import("std");
const assert = std.debug.assert;
/// NATS-specific errors for event callbacks.
pub const Error = error{
/// Subscription queue full - messages being dropped (slow consumer).
SlowConsumer,
/// Server permission violation (publish/subscribe rejected).
PermissionViolation,
/// Connection is stale (ping timeout).
StaleConnection,
/// Server sent -ERR response.
ServerError,
/// Authorization failed (invalid credentials).
AuthorizationViolation,
/// Server connection limit reached.
MaxConnectionsExceeded,
/// Failed to restore subscriptions after reconnect.
/// User may need to re-subscribe manually.
SubscriptionRestoreFailed,
/// Message allocation failed (slab exhausted).
AllocationFailed,
/// Protocol parse error (malformed data skipped).
ProtocolParseError,
/// Subject too long for backup buffer (>256 bytes).
SubjectTooLong,
/// Queue group too long for backup buffer (>64 bytes).
QueueGroupTooLong,
/// Drain completed with failures (UNSUB or flush failed).
DrainIncomplete,
/// TCP_NODELAY socket option failed (performance impact).
TcpNoDelayFailed,
/// TCP receive buffer option failed (performance impact).
TcpRcvBufFailed,
/// URL too long (>256 bytes, would be truncated).
UrlTooLong,
};
/// Returns a human-readable description for NATS errors.
/// For errors not in the NATS Error set, returns the error name.
pub fn statusText(err: anyerror) []const u8 {
return switch (err) {
Error.SlowConsumer => "Slow consumer - subscription queue full",
Error.PermissionViolation => "Permission denied by server",
Error.StaleConnection => "Connection stale - ping timeout exceeded",
Error.ServerError => "Server error response",
Error.AuthorizationViolation => "Authorization failed",
Error.MaxConnectionsExceeded => "Server connection limit reached",
Error.SubscriptionRestoreFailed => "Failed to restore subscriptions",
Error.AllocationFailed => "Message allocation failed - slab exhausted",
Error.ProtocolParseError => "Protocol parse error - malformed data",
Error.SubjectTooLong => "Subject exceeds maximum length",
Error.QueueGroupTooLong => "Queue group exceeds maximum length",
Error.DrainIncomplete => "Drain completed with failures",
Error.TcpNoDelayFailed => "Failed to set TCP_NODELAY",
Error.TcpRcvBufFailed => "Failed to set TCP receive buffer",
Error.UrlTooLong => "URL exceeds maximum length",
else => @errorName(err),
};
}
/// Events pushed from io_task to callback_task.
/// These represent connection lifecycle changes and async errors.
pub const Event = union(enum) {
/// Initial connection established. Fired once, not on reconnect.
connected: void,
/// Connection lost. err is the I/O error that caused disconnect,
/// or null if clean close.
disconnected: struct { err: ?anyerror },
/// Successfully reconnected after disconnect.
/// Fired each time reconnection succeeds.
reconnected: void,
/// Connection permanently closed. No more events after this.
/// Fired exactly once when client becomes unusable.
closed: void,
/// Slow consumer - subscription queue full, message dropped.
/// sid identifies the affected subscription.
slow_consumer: struct { sid: u64 },
/// Async error that doesn't close connection.
/// Includes permission violations, server errors, stale connection.
err: struct { err: anyerror, msg: ?[]const u8 },
/// Server entering lame duck mode (graceful shutdown).
lame_duck: void,
/// Message allocation failed (slab exhausted). Rate-limited.
alloc_failed: struct { sid: u64, count: u64 },
/// Protocol parse error (malformed data recovered via CRLF skip).
/// Rate-limited: fires on first error, then every 100k messages.
protocol_error: struct { bytes_skipped: usize, count: u64 },
/// New servers discovered via cluster INFO (connect_urls).
/// count is the number of new servers added to the pool.
discovered_servers: struct { count: u8 },
/// Connection entering drain mode.
/// Fired when drain() is called on the client.
draining: void,
/// Subscription auto-unsubscribe limit reached.
/// Fired when a subscription hits its max messages limit.
subscription_complete: struct { sid: u64 },
};
/// Type-erased event handler using std.mem.Allocator vtable pattern.
/// All callbacks are optional - only implement what you need.
///
/// Handler struct can contain references to external state:
/// ```zig
/// const MyHandler = struct {
/// app_state: *AppState, // Reference to your state
///
/// pub fn onConnect(self: *@This()) void {
/// self.app_state.is_connected = true;
/// }
/// };
/// ```
pub const EventHandler = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
onConnect: ?*const fn (*anyopaque) void = null,
onDisconnect: ?*const fn (*anyopaque, ?anyerror) void = null,
onReconnect: ?*const fn (*anyopaque) void = null,
onClose: ?*const fn (*anyopaque) void = null,
onError: ?*const fn (*anyopaque, anyerror) void = null,
onLameDuck: ?*const fn (*anyopaque) void = null,
onDiscoveredServers: ?*const fn (*anyopaque, u8) void = null,
onDraining: ?*const fn (*anyopaque) void = null,
onSubscriptionComplete: ?*const fn (*anyopaque, u64) void = null,
};
/// Create handler from concrete type using comptime.
/// Only generates vtable entries for methods that exist on T.
pub fn init(comptime T: type, ptr: *T) EventHandler {
const gen = struct {
fn onConnect(p: *anyopaque) void {
const self: *T = @ptrCast(@alignCast(p));
self.onConnect();
}
fn onDisconnect(p: *anyopaque, err: ?anyerror) void {
const self: *T = @ptrCast(@alignCast(p));
self.onDisconnect(err);
}
fn onReconnect(p: *anyopaque) void {
const self: *T = @ptrCast(@alignCast(p));
self.onReconnect();
}
fn onClose(p: *anyopaque) void {
const self: *T = @ptrCast(@alignCast(p));
self.onClose();
}
fn onError(p: *anyopaque, err: anyerror) void {
const self: *T = @ptrCast(@alignCast(p));
self.onError(err);
}
fn onLameDuck(p: *anyopaque) void {
const self: *T = @ptrCast(@alignCast(p));
self.onLameDuck();
}
fn onDiscoveredServers(p: *anyopaque, count: u8) void {
const self: *T = @ptrCast(@alignCast(p));
self.onDiscoveredServers(count);
}
fn onDraining(p: *anyopaque) void {
const self: *T = @ptrCast(@alignCast(p));
self.onDraining();
}
fn onSubscriptionComplete(p: *anyopaque, sid: u64) void {
const self: *T = @ptrCast(@alignCast(p));
self.onSubscriptionComplete(sid);
}
};
const vtable = comptime blk: {
break :blk VTable{
.onConnect = if (@hasDecl(T, "onConnect"))
gen.onConnect
else
null,
.onDisconnect = if (@hasDecl(T, "onDisconnect"))
gen.onDisconnect
else
null,
.onReconnect = if (@hasDecl(T, "onReconnect"))
gen.onReconnect
else
null,
.onClose = if (@hasDecl(T, "onClose"))
gen.onClose
else
null,
.onError = if (@hasDecl(T, "onError"))
gen.onError
else
null,
.onLameDuck = if (@hasDecl(T, "onLameDuck"))
gen.onLameDuck
else
null,
.onDiscoveredServers = if (@hasDecl(T, "onDiscoveredServers"))
gen.onDiscoveredServers
else
null,
.onDraining = if (@hasDecl(T, "onDraining"))
gen.onDraining
else
null,
.onSubscriptionComplete = if (@hasDecl(T, "onSubscriptionComplete"))
gen.onSubscriptionComplete
else
null,
};
};
return .{
.ptr = ptr,
.vtable = &vtable,
};
}
/// Dispatch connected event to handler.
pub fn dispatchConnect(self: EventHandler) void {
if (self.vtable.onConnect) |f| f(self.ptr);
}
/// Dispatch disconnected event to handler.
pub fn dispatchDisconnect(self: EventHandler, err: ?anyerror) void {
if (self.vtable.onDisconnect) |f| f(self.ptr, err);
}
/// Dispatch reconnected event to handler.
pub fn dispatchReconnect(self: EventHandler) void {
if (self.vtable.onReconnect) |f| f(self.ptr);
}
/// Dispatch closed event to handler.
pub fn dispatchClose(self: EventHandler) void {
if (self.vtable.onClose) |f| f(self.ptr);
}
/// Dispatch error event to handler.
pub fn dispatchError(self: EventHandler, err: anyerror) void {
if (self.vtable.onError) |f| f(self.ptr, err);
}
/// Dispatch lame duck event to handler.
pub fn dispatchLameDuck(self: EventHandler) void {
if (self.vtable.onLameDuck) |f| f(self.ptr);
}
/// Dispatch discovered servers event to handler.
pub fn dispatchDiscoveredServers(self: EventHandler, count: u8) void {
if (self.vtable.onDiscoveredServers) |f| f(self.ptr, count);
}
/// Dispatch draining event to handler.
pub fn dispatchDraining(self: EventHandler) void {
if (self.vtable.onDraining) |f| f(self.ptr);
}
/// Dispatch subscription complete event to handler.
pub fn dispatchSubscriptionComplete(self: EventHandler, sid: u64) void {
if (self.vtable.onSubscriptionComplete) |f| f(self.ptr, sid);
}
};
test "EventHandler vtable generation" {
const FullHandler = struct {
connect_count: u32 = 0,
disconnect_count: u32 = 0,
last_error: ?anyerror = null,
pub fn onConnect(self: *@This()) void {
self.connect_count += 1;
}
pub fn onDisconnect(self: *@This(), err: ?anyerror) void {
self.disconnect_count += 1;
self.last_error = err;
}
pub fn onReconnect(self: *@This()) void {
self.connect_count += 1;
}
pub fn onClose(_: *@This()) void {}
pub fn onError(self: *@This(), err: anyerror) void {
self.last_error = err;
}
pub fn onLameDuck(_: *@This()) void {}
};
var handler = FullHandler{};
const eh = EventHandler.init(FullHandler, &handler);
try std.testing.expect(eh.vtable.onConnect != null);
try std.testing.expect(eh.vtable.onDisconnect != null);
try std.testing.expect(eh.vtable.onReconnect != null);
try std.testing.expect(eh.vtable.onClose != null);
try std.testing.expect(eh.vtable.onError != null);
try std.testing.expect(eh.vtable.onLameDuck != null);
eh.dispatchConnect();
try std.testing.expectEqual(@as(u32, 1), handler.connect_count);
eh.dispatchDisconnect(error.OutOfMemory);
try std.testing.expectEqual(@as(u32, 1), handler.disconnect_count);
try std.testing.expectEqual(error.OutOfMemory, handler.last_error.?);
}
test "EventHandler partial implementation" {
const MinimalHandler = struct {
called: bool = false,
pub fn onConnect(self: *@This()) void {
self.called = true;
}
};
var handler = MinimalHandler{};
const eh = EventHandler.init(MinimalHandler, &handler);
try std.testing.expect(eh.vtable.onConnect != null);
try std.testing.expect(eh.vtable.onDisconnect == null);
try std.testing.expect(eh.vtable.onReconnect == null);
try std.testing.expect(eh.vtable.onClose == null);
try std.testing.expect(eh.vtable.onError == null);
try std.testing.expect(eh.vtable.onLameDuck == null);
eh.dispatchConnect();
try std.testing.expect(handler.called);
eh.dispatchDisconnect(null); // Should be no-op
eh.dispatchReconnect(); // Should be no-op
eh.dispatchClose(); // Should be no-op
}
test "EventHandler with external state" {
const AppState = struct {
is_online: bool = false,
reconnect_count: u32 = 0,
};
const MyHandler = struct {
app: *AppState,
pub fn onConnect(self: *@This()) void {
self.app.is_online = true;
}
pub fn onDisconnect(self: *@This(), _: ?anyerror) void {
self.app.is_online = false;
}
pub fn onReconnect(self: *@This()) void {
self.app.is_online = true;
self.app.reconnect_count += 1;
}
};
var app_state = AppState{};
var handler = MyHandler{ .app = &app_state };
const eh = EventHandler.init(MyHandler, &handler);
try std.testing.expect(!app_state.is_online);
try std.testing.expectEqual(@as(u32, 0), app_state.reconnect_count);
eh.dispatchConnect();
try std.testing.expect(app_state.is_online);
eh.dispatchDisconnect(error.BrokenPipe);
try std.testing.expect(!app_state.is_online);
eh.dispatchReconnect();
try std.testing.expect(app_state.is_online);
try std.testing.expectEqual(@as(u32, 1), app_state.reconnect_count);
}
test "Event union" {
const events = [_]Event{
.{ .connected = {} },
.{ .disconnected = .{ .err = error.BrokenPipe } },
.{ .disconnected = .{ .err = null } },
.{ .reconnected = {} },
.{ .closed = {} },
.{ .slow_consumer = .{ .sid = 42 } },
.{ .err = .{ .err = Error.SlowConsumer, .msg = null } },
.{ .err = .{ .err = Error.PermissionViolation, .msg = "test" } },
.{ .lame_duck = {} },
.{ .alloc_failed = .{ .sid = 1, .count = 5 } },
.{ .protocol_error = .{ .bytes_skipped = 128, .count = 3 } },
.{ .discovered_servers = .{ .count = 3 } },
.{ .draining = {} },
.{ .subscription_complete = .{ .sid = 42 } },
};
for (events) |event| {
switch (event) {
.connected => {},
.disconnected => |d| {
if (d.err) |err| {
_ = @errorName(err);
}
},
.reconnected => {},
.closed => {},
.slow_consumer => |sc| try std.testing.expect(sc.sid >= 0),
.err => |e| {
_ = @errorName(e.err);
},
.lame_duck => {},
.alloc_failed => |af| {
try std.testing.expect(af.sid > 0);
try std.testing.expect(af.count > 0);
},
.protocol_error => |pe| {
try std.testing.expect(pe.bytes_skipped > 0);
try std.testing.expect(pe.count > 0);
},
.discovered_servers => |ds| {
try std.testing.expect(ds.count > 0);
},
.draining => {},
.subscription_complete => |sc| {
try std.testing.expect(sc.sid > 0);
},
}
}
}
test "statusText for known errors" {
// Test known NATS errors
try std.testing.expectEqualStrings(
"Slow consumer - subscription queue full",
statusText(Error.SlowConsumer),
);
try std.testing.expectEqualStrings(
"Permission denied by server",
statusText(Error.PermissionViolation),
);
try std.testing.expectEqualStrings(
"Authorization failed",
statusText(Error.AuthorizationViolation),
);
try std.testing.expectEqualStrings(
"Message allocation failed - slab exhausted",
statusText(Error.AllocationFailed),
);
}
test "statusText for unknown errors" {
// Unknown errors should return error name
try std.testing.expectEqualStrings(
"OutOfMemory",
statusText(error.OutOfMemory),
);
}
================================================
FILE: src/examples/README.md
================================================
# Examples
Run with `zig build run-` (requires `nats-server` on localhost:4222).
| Example | Run | Description |
|---------|-----|-------------|
| simple | `run-simple` | Basic pub/sub - connect, `subscribeSync`, publish, receive |
| request_reply | `run-request-reply` | RPC pattern with automatic inbox handling |
| headers | `run-headers` | Publish, receive, and parse NATS headers |
| queue_groups | `run-queue-groups` | Load-balanced workers with `io.concurrent()` |
| polling_loop | `run-polling-loop` | Non-blocking `tryNextMsg()` with priority scheduling |
| select | `run-select` | Race subscription against timeout with `Io.Select` |
| batch_receiving | `run-batch-receiving` | `nextMsgBatch()` for bulk receives, stats monitoring |
| reconnection | `run-reconnection` | Auto-reconnect, backoff, buffer during disconnect |
| events | `run-events` | EventHandler callbacks with external state |
| callback | `run-callback` | `subscribe()` and `subscribeFn()` callback subscriptions |
| request_reply_callback | `run-request-reply-callback` | Service responder via callback subscription |
| graceful_shutdown | `run-graceful-shutdown` | `drain()` lifecycle, pre-shutdown health checks |
| jetstream_publish | `run-jetstream-publish` | Create a stream and publish with ack confirmation |
| jetstream_consume | `run-jetstream-consume` | Pull consumer fetch and acknowledgement |
| jetstream_push | `run-jetstream-push` | Push consumer callback delivery |
| jetstream_async_publish | `run-jetstream-async-publish` | Async JetStream publishing |
| kv | `run-kv` | Key-Value bucket operations |
| kv_watch | `run-kv-watch` | Watch Key-Value updates |
| micro_echo | `run-micro-echo` | NATS service API echo service |
================================================
FILE: src/examples/batch_receiving.zig
================================================
//! Batch Receiving Patterns
//!
//! Demonstrates efficient batch message retrieval:
//! - nextMsgBatch(): blocking batch receive (waits for at least 1 message)
//! - tryNextMsgBatch(): non-blocking batch receive for polling
//! - Stats monitoring: track messages and detect drops
//!
//! Run with: zig build run-batch-receiving
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
const Io = std.Io;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
// Connect with larger subscription queue for batch receiving
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{
.name = "batch-receiving",
.sub_queue_size = 512, // Larger queue for batch demos
},
);
defer client.deinit();
std.debug.print("Connected to NATS!\n", .{});
const sub = try client.subscribeSync("bench.>");
defer sub.deinit();
std.debug.print("Subscribed to 'bench.>'\n\n", .{});
// Publish test messages
const message_count: u32 = 100;
std.debug.print("Publishing {d} messages...\n", .{message_count});
for (0..message_count) |i| {
var buf: [64]u8 = undefined;
const payload = std.fmt.bufPrint(&buf, "Message {d}", .{i + 1}) catch "Msg";
try client.publish("bench.test", payload);
}
// BATCH RECEIVING: Receive multiple messages at once
std.debug.print("Receiving messages (batch mode)...\n", .{});
var batch_buf: [32]nats.Message = undefined;
var total_received: u32 = 0;
var batch_count: u32 = 0;
const recv_start = Io.Timestamp.now(io, .awake);
while (total_received < message_count) {
// nextBatch waits for at least 1 message, returns up to 32
const count = sub.nextMsgBatch(io, &batch_buf) catch break;
batch_count += 1;
for (batch_buf[0..count]) |*msg| {
defer msg.deinit();
total_received += 1;
}
// Check for dropped messages
const dropped = sub.dropped();
if (dropped > 0) {
std.debug.print(
" Warning: {d} messages dropped (consumer too slow)\n",
.{dropped},
);
}
}
const recv_end = Io.Timestamp.now(io, .awake);
const elapsed = recv_start.durationTo(recv_end);
const recv_ns: u64 = @intCast(elapsed.nanoseconds);
const recv_ms = @as(f64, @floatFromInt(recv_ns)) /
1_000_000.0;
std.debug.print(
"Received {d} messages in {d} batches ({d:.2}ms)\n",
.{ total_received, batch_count, recv_ms },
);
std.debug.print(
"Throughput: {d:.0} msgs/sec\n\n",
.{@as(f64, @floatFromInt(total_received)) / (recv_ms / 1000.0)},
);
// NON-BLOCKING BATCH: tryNextBatch for polling
std.debug.print("Demonstrating tryNextBatch (non-blocking)...\n", .{});
// Publish a few more messages
for (0..5) |i| {
var buf: [64]u8 = undefined;
const payload = std.fmt.bufPrint(&buf, "Extra {d}", .{i + 1}) catch "Msg";
try client.publish("bench.extra", payload);
}
// Flush to ensure messages have been delivered
try client.flush(1_000_000_000);
// Non-blocking batch receive
const available = sub.tryNextMsgBatch(&batch_buf);
std.debug.print(" tryNextBatch returned {d} messages immediately\n", .{available});
for (batch_buf[0..available]) |*msg| {
defer msg.deinit();
std.debug.print(" {s}\n", .{msg.data});
}
// STATS SUMMARY
std.debug.print("\nStats summary:\n", .{});
std.debug.print(" Messages received: {d}\n", .{sub.received_msgs});
std.debug.print(" Messages dropped: {d}\n", .{sub.dropped()});
const stats = client.stats();
std.debug.print(" Total bytes out: {d}\n", .{stats.bytes_out});
std.debug.print(" Total bytes in: {d}\n", .{stats.bytes_in});
std.debug.print("\nDone!\n", .{});
}
================================================
FILE: src/examples/callback.zig
================================================
//! Callback Subscriptions
//!
//! Demonstrates callback-style message handling using MsgHandler
//! (vtable pattern) and plain function pointers. Messages are
//! dispatched automatically -- no manual next() loop needed.
//!
//! Run with: zig build run-callback
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server -DV
const std = @import("std");
const nats = @import("nats");
// -- MsgHandler pattern: handler struct with state --
/// Application state shared with the handler.
const AppState = struct {
count: u32 = 0,
last_subject: [64]u8 = undefined,
last_subject_len: usize = 0,
};
/// Handler struct -- implements onMessage to receive callbacks.
const MyHandler = struct {
app: *AppState,
pub fn onMessage(self: *@This(), msg: *const nats.Message) void {
self.app.count += 1;
const len = @min(msg.subject.len, 64);
@memcpy(self.app.last_subject[0..len], msg.subject[0..len]);
self.app.last_subject_len = len;
std.debug.print(
" [handler] #{d} {s}: {s}\n",
.{
self.app.count,
msg.subject,
msg.data,
},
);
}
};
// -- Plain fn pattern: no struct needed --
/// Simple alert function -- stateless callback.
fn alertFn(msg: *const nats.Message) void {
std.debug.print(
" [alert] {s}: {s}\n",
.{ msg.subject, msg.data },
);
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{ .name = "callback-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
// 1. MsgHandler callback subscription
var app = AppState{};
var handler = MyHandler{ .app = &app };
const sub1 = try client.subscribe(
"demo.handler",
nats.MsgHandler.init(MyHandler, &handler),
);
defer sub1.deinit();
// 2. Plain fn callback subscription
const sub2 = try client.subscribeFn(
"demo.alert",
alertFn,
);
defer sub2.deinit();
std.debug.print("Subscribed with callbacks.\n", .{});
std.debug.print(
"Publishing messages...\n\n",
.{},
);
// Publish messages
for (0..5) |i| {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"hello {d}",
.{i + 1},
) catch "hello";
try client.publish("demo.handler", msg);
}
try client.publish("demo.alert", "fire!");
try client.publish("demo.alert", "smoke!");
// Flush to ensure messages have been delivered
try client.flush(1_000_000_000);
std.debug.print(
"\nHandler count: {d}\n",
.{app.count},
);
std.debug.print(
"Last subject: {s}\n",
.{app.last_subject[0..app.last_subject_len]},
);
// Verify all messages delivered
std.debug.assert(app.count == 5);
std.debug.print("Done!\n", .{});
}
================================================
FILE: src/examples/events.zig
================================================
//! Event Callbacks Example
//!
//! Demonstrates how to handle connection lifecycle events using the
//! EventHandler pattern. Shows how handlers can reference external state
//! without closures.
//!
//! Run with: zig build run-events
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server -DV
//!
//! Try stopping/starting nats-server to see disconnect/reconnect events.
const std = @import("std");
const nats = @import("nats");
/// Application state that callbacks will modify.
/// This pattern allows event handlers to update shared state
/// without closures.
const AppState = struct {
is_online: bool = false,
reconnect_count: u32 = 0,
last_error: ?anyerror = null,
should_shutdown: bool = false,
};
/// Event handler that references external AppState.
/// All callback methods are optional - only implement what you need.
const MyEventHandler = struct {
app: *AppState,
pub fn onConnect(self: *@This()) void {
self.app.is_online = true;
std.debug.print("[EVENT] Connected to NATS server\n", .{});
}
pub fn onDisconnect(self: *@This(), err: ?anyerror) void {
self.app.is_online = false;
self.app.last_error = err;
if (err) |e| {
std.debug.print("[EVENT] Disconnected: {s}\n", .{@errorName(e)});
} else {
std.debug.print("[EVENT] Disconnected (clean)\n", .{});
}
}
pub fn onReconnect(self: *@This()) void {
self.app.is_online = true;
self.app.reconnect_count += 1;
std.debug.print(
"[EVENT] Reconnected! (total reconnects: {})\n",
.{self.app.reconnect_count},
);
}
pub fn onClose(self: *@This()) void {
self.app.is_online = false;
self.app.should_shutdown = true;
std.debug.print("[EVENT] Connection closed permanently\n", .{});
}
pub fn onError(self: *@This(), err: anyerror) void {
self.app.last_error = err;
std.debug.print("[EVENT] Async error: {s}\n", .{@errorName(err)});
}
pub fn onLameDuck(_: *@This()) void {
std.debug.print(
"[EVENT] Server entering lame duck mode - prepare for shutdown!\n",
.{},
);
}
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
// External state that callbacks will modify
var app_state = AppState{};
// Handler with reference to external state
var handler = MyEventHandler{ .app = &app_state };
std.debug.print("Connecting to NATS with event callbacks...\n", .{});
// Connect with event handler
const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{
.event_handler = nats.EventHandler.init(MyEventHandler, &handler),
.reconnect = true,
});
defer client.deinit();
// Subscribe to test subject
const sub = try client.subscribeSync("test.>");
defer sub.deinit();
std.debug.print("\nSubscribed to test.>\n", .{});
std.debug.print("Try: nats pub test.hello 'world'\n", .{});
std.debug.print("Try stopping/starting nats-server to see events!\n", .{});
std.debug.print("Press Ctrl+C to exit.\n\n", .{});
// Main loop - processes messages and checks app_state
var msg_count: u32 = 0;
const max_msgs: u32 = 100;
while (!app_state.should_shutdown and msg_count < max_msgs) {
// Non-blocking message check with timeout
if (try sub.nextMsgTimeout(1000)) |msg| {
defer msg.deinit();
std.debug.print("Received: {s}\n", .{msg.data});
msg_count += 1;
}
// React to state changes from callbacks
if (!app_state.is_online) {
std.debug.print("(offline - waiting for reconnect...)\n", .{});
io.sleep(.fromMilliseconds(1000), .awake) catch {};
}
}
std.debug.print("\n=== Final App State ===\n", .{});
std.debug.print(" Online: {}\n", .{app_state.is_online});
std.debug.print(" Reconnects: {}\n", .{app_state.reconnect_count});
if (app_state.last_error) |err| {
std.debug.print(" Last error: {s}\n", .{@errorName(err)});
} else {
std.debug.print(" Last error: none\n", .{});
}
std.debug.print(" Messages received: {}\n", .{msg_count});
}
================================================
FILE: src/examples/graceful_shutdown.zig
================================================
//! Graceful Shutdown Pattern
//!
//! Demonstrates production-ready lifecycle management:
//! - drain() for graceful subscription cleanup
//! - Proper resource cleanup order
//! - Monitoring dropped messages before shutdown
//!
//! Run with: zig build run-graceful-shutdown
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{ .name = "graceful-shutdown-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n", .{});
// Create multiple subscriptions
const orders = try client.subscribeSync("orders.*");
defer orders.deinit();
const events = try client.subscribeSync("events.>");
defer events.deinit();
std.debug.print("Subscriptions active:\n", .{});
std.debug.print(" - orders.* (sid={d})\n", .{orders.sid});
std.debug.print(" - events.> (sid={d})\n", .{events.sid});
// Simulate some activity
std.debug.print("\nSimulating activity...\n", .{});
for (0..5) |i| {
var buf: [64]u8 = undefined;
const order = std.fmt.bufPrint(&buf, "Order #{d}", .{i + 1000}) catch "Order";
try client.publish("orders.new", order);
const event = std.fmt.bufPrint(&buf, "Event {d}", .{i + 1}) catch "Event";
try client.publish("events.user.login", event);
}
std.debug.print("Published 10 messages (5 orders, 5 events)\n", .{});
// Flush to ensure messages have been delivered
try client.flush(1_000_000_000);
var orders_count: u32 = 0;
while (orders.tryNextMsg()) |msg| {
defer msg.deinit();
orders_count += 1;
}
var events_count: u32 = 0;
while (events.tryNextMsg()) |msg| {
defer msg.deinit();
events_count += 1;
}
std.debug.print(
"Processed: {d} orders, {d} events\n",
.{ orders_count, events_count },
);
// CHECK FOR DROPPED MESSAGES before shutdown
std.debug.print("\nPre-shutdown health check:\n", .{});
const orders_dropped = orders.dropped();
const events_dropped = events.dropped();
if (orders_dropped > 0 or events_dropped > 0) {
std.debug.print(" WARNING: Messages were dropped!\n", .{});
std.debug.print(" orders: {d} dropped\n", .{orders_dropped});
std.debug.print(" events: {d} dropped\n", .{events_dropped});
} else {
std.debug.print(" No messages dropped - healthy!\n", .{});
}
// GRACEFUL SHUTDOWN with drain()
std.debug.print("\nInitiating graceful shutdown...\n", .{});
// drain() does the following:
// 1. Unsubscribes all active subscriptions
// 2. Drains any remaining messages from queues (frees memory)
// 3. Flushes pending writes to server
// 4. Closes connection and transitions to closed state
const drain_result = client.drain() catch |err| {
std.debug.print("Drain failed: {}\n", .{err});
return err;
};
std.debug.print("Drain completed:\n", .{});
if (drain_result.unsub_failures > 0) {
std.debug.print(" WARNING: {d} unsub commands failed\n", .{
drain_result.unsub_failures,
});
} else {
std.debug.print(" All subscriptions unsubscribed successfully\n", .{});
}
if (drain_result.flush_failed) {
std.debug.print(" WARNING: Final flush failed\n", .{});
} else {
std.debug.print(" Final flush succeeded\n", .{});
}
// Final stats
const stats = client.stats();
std.debug.print("\nFinal statistics:\n", .{});
std.debug.print(" Messages sent: {d}\n", .{stats.msgs_out});
std.debug.print(" Messages received: {d}\n", .{stats.msgs_in});
std.debug.print(" Bytes sent: {d}\n", .{stats.bytes_out});
std.debug.print(" Bytes received: {d}\n", .{stats.bytes_in});
std.debug.print("\nGraceful shutdown complete!\n", .{});
}
================================================
FILE: src/examples/headers.zig
================================================
//! NATS Headers
//!
//! Demonstrates publishing messages with headers and parsing
//! received header metadata.
//!
//! Run with: zig build run-headers
//! or: zig build run-headers -Dio_backend=evented
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
const io_backend = @import("io_backend");
const headers = nats.protocol.headers;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
var backend: io_backend.Backend = undefined;
try io_backend.init(&backend, allocator);
defer backend.deinit();
const io = backend.io();
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{ .name = "headers-example" },
);
defer client.deinit();
const sub = try client.subscribeSync("headers.demo");
defer sub.deinit();
try client.flush(std.time.ns_per_s);
const hdrs = [_]headers.Entry{
.{ .key = "Content-Type", .value = "application/json" },
.{ .key = "X-Request-Id", .value = "req-42" },
.{ .key = "X-Trace", .value = "alpha" },
.{ .key = "X-Trace", .value = "beta" },
};
try client.publishWithHeaders(
"headers.demo",
&hdrs,
"{\"message\":\"hello\"}",
);
if (try sub.nextMsgTimeout(1000)) |msg| {
defer msg.deinit();
std.debug.print("Received payload: {s}\n", .{msg.data});
const raw = msg.headers orelse return error.MissingHeaders;
var parsed = headers.parse(allocator, raw);
defer parsed.deinit();
if (parsed.err) |err| {
std.debug.print("Header parse error: {}\n", .{err});
return error.InvalidHeaders;
}
if (parsed.get("content-type")) |content_type| {
std.debug.print("Content-Type: {s}\n", .{content_type});
}
std.debug.print("Headers:\n", .{});
for (parsed.items()) |entry| {
std.debug.print(" {s}: {s}\n", .{
entry.key,
entry.value,
});
}
} else {
std.debug.print("Timed out waiting for message\n", .{});
}
}
================================================
FILE: src/examples/jetstream_async_publish.zig
================================================
//! JetStream Async Publish -- non-blocking publish with
//! futures.
//!
//! AsyncPublisher decouples publishing from ack waiting.
//! Messages are sent immediately and acks are correlated
//! in the background via a shared reply subscription.
//! Use this when throughput matters more than per-message
//! confirmation.
//!
//! Run with: zig build run-jetstream-async-publish
//!
//! Prerequisites: nats-server -js
const std = @import("std");
const nats = @import("nats");
const js_mod = nats.jetstream;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://127.0.0.1:4222",
.{ .name = "js-async-pub-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
var js = try js_mod.JetStream.init(client, .{});
var stream_resp = try js.createStream(.{
.name = "DEMO_ASYNC",
.subjects = &.{"perf.>"},
.storage = .memory,
});
stream_resp.deinit();
// AsyncPublisher manages a shared reply inbox and
// correlates incoming acks to pending futures.
// max_pending=64 means backpressure kicks in after
// 64 unacknowledged publishes (the caller blocks
// until acks drain below the threshold).
var ap = try js_mod.AsyncPublisher.init(
&js,
.{ .max_pending = 64 },
);
defer ap.deinit();
const msg_count: u32 = 100;
// Fire-and-forget: publish all messages without
// waiting for individual acks. The futures
// accumulate and resolve as acks arrive from the
// server in the background.
var futures: [100]*js_mod.PubAckFuture = undefined;
for (0..msg_count) |i| {
var buf: [64]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"measurement #{d}",
.{i + 1},
) catch "data";
futures[i] = try ap.publish(
"perf.metrics",
payload,
);
}
std.debug.print(
"Published {d} messages.\n",
.{msg_count},
);
std.debug.print(
"Pending acks: {d}\n\n",
.{ap.publishAsyncPending()},
);
// waitComplete blocks until all pending futures
// resolve (acks received) or the timeout expires.
// This is the batch-level sync point.
try ap.waitComplete(10000);
std.debug.print(
"All acks received (pending={d}).\n\n",
.{ap.publishAsyncPending()},
);
// Verify a few individual futures to show the
// per-message API. Each future can be checked
// independently with wait() or result().
for (0..3) |i| {
const fut = futures[i];
defer fut.deinit();
if (fut.result()) |ack| {
std.debug.print(
" Future[{d}]: seq={d}\n",
.{ i, ack.seq },
);
}
}
// Deinit remaining futures
for (3..msg_count) |i| futures[i].deinit();
// Check stream info for final message count
var info = try js.streamInfo("DEMO_ASYNC");
defer info.deinit();
if (info.value.state) |state| {
std.debug.print(
"\nStream has {d} messages.\n",
.{state.messages},
);
}
var del = try js.deleteStream("DEMO_ASYNC");
del.deinit();
std.debug.print("Done!\n", .{});
}
================================================
FILE: src/examples/jetstream_consume.zig
================================================
//! JetStream Pull Consumer -- fetch, iterate, and consume
//! patterns.
//!
//! Demonstrates three ways to receive messages from a pull
//! consumer: batch fetch, single next(), and continuous
//! MessagesContext iteration. Pull consumers are the
//! recommended pattern for most workloads -- the client
//! controls the pace.
//!
//! Run with: zig build run-jetstream-consume
//!
//! Prerequisites: nats-server -js
const std = @import("std");
const nats = @import("nats");
const js_mod = nats.jetstream;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://127.0.0.1:4222",
.{ .name = "js-consume-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
var js = try js_mod.JetStream.init(client, .{});
// Create a stream to hold our task messages
var stream_resp = try js.createStream(.{
.name = "DEMO_CONSUME",
.subjects = &.{"tasks.>"},
.storage = .memory,
});
stream_resp.deinit();
// Publish 10 task messages before consuming.
// In production, publishers and consumers run
// independently -- messages are persisted in the
// stream until acknowledged.
for (0..10) |i| {
var buf: [32]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"task {d}",
.{i + 1},
) catch "task";
var ack = try js.publish(
"tasks.work",
payload,
);
ack.deinit();
}
std.debug.print("Published 10 tasks.\n\n", .{});
// Create a durable pull consumer named "worker".
// Explicit ack means the server waits for each
// message to be acknowledged before considering
// it delivered. Unacked messages are redelivered.
var cons_resp = try js.createConsumer(
"DEMO_CONSUME",
.{
.name = "worker",
.ack_policy = .explicit,
},
);
cons_resp.deinit();
// PullSubscription is a lightweight handle that
// binds to the stream + consumer pair. No heap
// allocation -- safe to copy/move.
var pull = js_mod.PullSubscription{
.js = &js,
.stream = "DEMO_CONSUME",
};
try pull.setConsumer("worker");
// Pattern 1: Batch fetch -- get up to N messages
// in one round-trip. Efficient for bulk processing.
std.debug.print("-- Pattern 1: fetch --\n", .{});
var result = try pull.fetch(.{
.max_messages = 5,
.timeout_ms = 5000,
});
defer result.deinit();
var total: usize = 0;
for (result.messages) |*msg| {
std.debug.print(
" [{d}] {s}\n",
.{ total + 1, msg.data() },
);
// Always ack to tell the server we're done
// with this message. Without ack, the server
// will redeliver after ack_wait expires.
try msg.ack();
total += 1;
}
std.debug.print(
" Fetched {d} messages.\n\n",
.{result.count()},
);
// Pattern 2: next() -- fetch a single message.
// Good for request-at-a-time processing or when
// you need fine-grained control.
std.debug.print(
"-- Pattern 2: next --\n",
.{},
);
if (try pull.next(3000)) |*msg| {
var m = msg.*;
defer m.deinit();
std.debug.print(
" Got: {s}\n\n",
.{m.data()},
);
try m.ack();
total += 1;
}
// Pattern 3: MessagesContext -- continuous iterator
// that auto-fetches new batches as needed. Best
// for long-running workers that process messages
// in a loop.
std.debug.print(
"-- Pattern 3: messages --\n",
.{},
);
var msgs = try pull.messages(.{
.max_messages = 10,
.expires_ms = 5000,
});
defer msgs.deinit();
// Read remaining messages (4 left from our 10)
while (try msgs.next()) |*msg| {
var m = msg.*;
defer m.deinit();
std.debug.print(
" [{d}] {s}\n",
.{ total + 1, m.data() },
);
try m.ack();
total += 1;
}
std.debug.print(
"\nTotal processed: {d}\n",
.{total},
);
// Clean up stream
var del = try js.deleteStream("DEMO_CONSUME");
del.deinit();
std.debug.print("Done!\n", .{});
}
================================================
FILE: src/examples/jetstream_publish.zig
================================================
//! JetStream Publish -- stream CRUD and publish with
//! acknowledgment.
//!
//! Creates a stream, publishes messages with server-side
//! acknowledgment, demonstrates deduplication via msg-id,
//! and queries stream info.
//!
//! Run with: zig build run-jetstream-publish
//!
//! Prerequisites: nats-server -js
const std = @import("std");
const nats = @import("nats");
// JetStream is accessed through the nats.jetstream module.
const js_mod = nats.jetstream;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
// Connect to NATS with JetStream enabled on the
// server. JetStream uses the same TCP connection
// as core NATS -- no extra ports needed.
const client = try nats.Client.connect(
allocator,
io,
"nats://127.0.0.1:4222",
.{ .name = "js-publish-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
// JetStream context is stack-allocated -- it holds
// a pointer to the client plus config (no heap).
var js = try js_mod.JetStream.init(client, .{});
// Create a memory-backed stream named DEMO_PUBLISH
// that captures all subjects matching "demo.>".
// Memory storage is fast but not persistent across
// server restarts.
var create_resp = try js.createStream(.{
.name = "DEMO_PUBLISH",
.subjects = &.{"demo.>"},
.storage = .memory,
});
create_resp.deinit();
std.debug.print(
"Stream 'DEMO_PUBLISH' created.\n\n",
.{},
);
// Publish 5 messages. Each publish returns a PubAck
// from the server confirming storage. The ack
// contains the stream name and sequence number.
for (0..5) |i| {
var buf: [64]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"order #{d}",
.{i + 1},
) catch "order";
var ack = try js.publish(
"demo.orders",
payload,
);
defer ack.deinit();
std.debug.print(
"Published seq={d} stream={s}\n",
.{
ack.value.seq,
ack.value.stream orelse "?",
},
);
}
// Deduplication: publish with a msg-id header.
// If the same msg-id is sent within the stream's
// duplicate_window (default 2min), the server
// returns duplicate=true without storing again.
std.debug.print(
"\n-- Deduplication test --\n",
.{},
);
var ack1 = try js.publishWithOpts(
"demo.orders",
"unique payload",
.{ .msg_id = "order-abc-123" },
);
defer ack1.deinit();
std.debug.print("First: seq={d} dup={}\n", .{
ack1.value.seq,
ack1.value.duplicate orelse false,
});
// Same msg-id again -- server detects duplicate
var ack2 = try js.publishWithOpts(
"demo.orders",
"unique payload",
.{ .msg_id = "order-abc-123" },
);
defer ack2.deinit();
std.debug.print("Second: seq={d} dup={}\n", .{
ack2.value.seq,
ack2.value.duplicate orelse false,
});
// Query stream info to see the message count
var info = try js.streamInfo("DEMO_PUBLISH");
defer info.deinit();
if (info.value.state) |state| {
std.debug.print(
"\nStream has {d} messages" ++
" ({d} bytes)\n",
.{ state.messages, state.bytes },
);
}
// Clean up: delete the stream and all its data
var del = try js.deleteStream("DEMO_PUBLISH");
del.deinit();
std.debug.print(
"\nStream deleted. Done!\n",
.{},
);
}
================================================
FILE: src/examples/jetstream_push.zig
================================================
//! JetStream Push Consumer -- server-side message delivery.
//!
//! Push consumers have the server send messages to a
//! deliver_subject. The client subscribes to that subject
//! and processes messages via a callback handler.
//!
//! When to use push vs pull:
//! - Pull: client controls pace, best for batch/worker
//! patterns, recommended for most use cases.
//! - Push: server controls pace, good for real-time
//! fan-out, simpler for "firehose" scenarios.
//!
//! Run with: zig build run-jetstream-push
//!
//! Prerequisites: nats-server -js
const std = @import("std");
const nats = @import("nats");
const js_mod = nats.jetstream;
// Counter tracks how many messages the handler has
// processed. Must implement onMessage for the
// JsMsgHandler vtable interface. The JsMsg has
// owned=false -- its slice fields are valid only
// during this callback; do not save pointers past
// the function return.
const Counter = struct {
received: u32 = 0,
target: u32 = 0,
pub fn onMessage(
self: *Counter,
msg: *js_mod.JsMsg,
) void {
self.received += 1;
std.debug.print(
" [{d}/{d}] {s}: {s}\n",
.{
self.received,
self.target,
msg.subject(),
msg.data(),
},
);
// Push consumers with ack_policy=none don't
// need explicit acks. For explicit ack policy,
// call msg.ack() here.
}
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://127.0.0.1:4222",
.{ .name = "js-push-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
var js = try js_mod.JetStream.init(client, .{});
var stream_resp = try js.createStream(.{
.name = "DEMO_PUSH",
.subjects = &.{"events.>"},
.storage = .memory,
});
stream_resp.deinit();
const msg_count: u32 = 5;
// Set up the push subscription BEFORE creating
// the consumer. The subscription must be active
// so it catches messages the server starts
// pushing immediately after consumer creation.
var push_sub = js_mod.PushSubscription{
.js = &js,
.stream = "DEMO_PUSH",
};
try push_sub.setConsumer("push-worker");
try push_sub.setDeliverSubject(
"_DELIVER.push-example",
);
var counter = Counter{
.target = msg_count,
};
// consume() subscribes to the deliver subject
// and dispatches messages to our Counter handler
// on the IO thread.
var ctx = try push_sub.consume(
js_mod.JsMsgHandler.init(
Counter,
&counter,
),
.{},
);
defer ctx.deinit();
// Now create the push consumer on the server.
// ack_policy=none means no ack required -- good
// for monitoring/logging where loss is acceptable.
var cons_resp = try js.createPushConsumer(
"DEMO_PUSH",
.{
.name = "push-worker",
.deliver_subject = "_DELIVER.push-example",
.ack_policy = .none,
},
);
cons_resp.deinit();
// Publish messages -- server pushes them to our
// deliver subject automatically.
for (0..msg_count) |i| {
var buf: [32]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"event {d}",
.{i + 1},
) catch "event";
var ack = try js.publish(
"events.clicks",
payload,
);
ack.deinit();
}
// Flush to ensure all publishes reach the server
try client.flush(2_000_000_000);
// Wait briefly for delivery to complete
var waited: u32 = 0;
while (counter.received < msg_count and
waited < 3000)
{
var ts: std.posix.timespec = .{
.sec = 0,
.nsec = 1_000_000,
};
_ = std.posix.system.nanosleep(
&ts,
&ts,
);
waited += 1;
}
std.debug.print(
"\nReceived {d}/{d} messages.\n",
.{ counter.received, msg_count },
);
var del = try js.deleteStream("DEMO_PUSH");
del.deinit();
std.debug.print("Done!\n", .{});
}
================================================
FILE: src/examples/kv.zig
================================================
//! Key-Value Store -- CRUD, concurrency, listing.
//!
//! NATS KV is a distributed key-value store backed by
//! JetStream. Keys are NATS subjects, values are message
//! payloads. Supports history, optimistic concurrency
//! (compare-and-swap via revision numbers), and TTL.
//!
//! Run with: zig build run-kv
//!
//! Prerequisites: nats-server -js
const std = @import("std");
const nats = @import("nats");
const js_mod = nats.jetstream;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://127.0.0.1:4222",
.{ .name = "kv-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
var js = try js_mod.JetStream.init(client, .{});
// Create a KV bucket with history=5. This means
// up to 5 revisions per key are kept. Backed by
// a JetStream stream named "KV_demo-kv".
var kv = try js.createKeyValue(.{
.bucket = "demo-kv",
.history = 5,
.storage = .memory,
});
std.debug.print(
"Bucket 'demo-kv' created.\n\n",
.{},
);
// -- Basic CRUD --
// Put stores a value and returns the revision
// (stream sequence number). Each put creates a
// new revision.
const rev1 = try kv.put("user.name", "Alice");
std.debug.print(
"Put 'user.name'='Alice' rev={d}\n",
.{rev1},
);
// Get returns the latest value for a key.
// Returns null if the key was never written.
if (try kv.get("user.name")) |entry| {
var e = entry;
defer e.deinit();
std.debug.print(
"Get 'user.name'='{s}' rev={d}\n",
.{ e.value, e.revision },
);
}
// Update overwrites the value. New revision
// returned.
const rev2 = try kv.put("user.name", "Bob");
std.debug.print(
"Put 'user.name'='Bob' rev={d}\n\n",
.{rev2},
);
// -- Optimistic concurrency --
// update() takes a revision parameter: the write
// only succeeds if the key's current revision
// matches. This prevents lost updates when
// multiple clients write concurrently.
std.debug.print(
"-- Optimistic concurrency --\n",
.{},
);
const rev3 = try kv.update(
"user.name",
"Charlie",
rev2,
);
std.debug.print(
"Update with rev={d}: ok, new rev={d}\n",
.{ rev2, rev3 },
);
// Attempt to update with a stale revision.
// This simulates a concurrent writer that read
// an older value.
if (kv.update("user.name", "Dave", rev1)) |_| {
std.debug.print("Unexpected success!\n", .{});
} else |_| {
std.debug.print(
"Update with stale rev={d}: " ++
"rejected (expected)\n\n",
.{rev1},
);
}
// -- Create (if not exists) --
// create() only succeeds if the key does not yet
// exist. Useful for distributed locks or
// one-time initialization.
std.debug.print(
"-- Create if not exists --\n",
.{},
);
const email_rev = try kv.create(
"user.email",
"alice@example.com",
);
std.debug.print(
"Created 'user.email' rev={d}\n",
.{email_rev},
);
// Second create fails because key already exists
if (kv.create("user.email", "bob@example.com")) |_| {
std.debug.print("Unexpected success!\n", .{});
} else |_| {
std.debug.print(
"Create duplicate: rejected\n\n",
.{},
);
}
// -- Delete --
// Soft-delete publishes a delete marker. The key
// still appears in history but get() returns the
// delete marker with operation=.delete.
const del_rev = try kv.delete("user.email");
std.debug.print(
"Deleted 'user.email' rev={d}\n\n",
.{del_rev},
);
// -- List keys --
// keys() returns all non-deleted keys in the
// bucket. Uses an ephemeral consumer under the
// hood.
std.debug.print("-- All keys --\n", .{});
// Add a few more keys for listing
_ = try kv.put("user.age", "30");
_ = try kv.put("user.city", "Portland");
const key_list = try kv.keys(allocator);
defer {
for (key_list) |k| allocator.free(k);
allocator.free(key_list);
}
for (key_list) |key| {
std.debug.print(" {s}\n", .{key});
}
std.debug.print(
" ({d} keys total)\n\n",
.{key_list.len},
);
// -- History --
// history() returns all revisions for a key,
// including puts, updates, and deletes.
std.debug.print(
"-- History for 'user.name' --\n",
.{},
);
const hist = try kv.history(
allocator,
"user.name",
);
defer {
for (hist) |*e| {
var entry = e.*;
entry.deinit();
}
allocator.free(hist);
}
for (hist) |entry| {
const op_str: []const u8 = switch (entry.operation) {
.put => "PUT",
.delete => "DEL",
.purge => "PURGE",
};
std.debug.print(
" rev={d} op={s} val='{s}'\n",
.{ entry.revision, op_str, entry.value },
);
}
// -- Bucket status --
// status() returns the underlying stream info
// for the KV bucket.
std.debug.print("\n-- Bucket status --\n", .{});
var st = try kv.status();
defer st.deinit();
if (st.value.state) |state| {
std.debug.print(
" messages={d} bytes={d}\n",
.{ state.messages, state.bytes },
);
}
// -- Cleanup --
var del_resp = try js.deleteKeyValue("demo-kv");
del_resp.deinit();
std.debug.print(
"\nBucket deleted. Done!\n",
.{},
);
}
================================================
FILE: src/examples/kv_watch.zig
================================================
//! Key-Value Watch -- real-time change notifications.
//!
//! KV watch creates an ephemeral consumer that delivers
//! change events as they happen. The watcher first
//! delivers all existing matching keys (the "initial
//! values"), then switches to live updates. A null from
//! next() after the initial batch signals the transition
//! to live mode.
//!
//! Run with: zig build run-kv-watch
//!
//! Prerequisites: nats-server -js
const std = @import("std");
const nats = @import("nats");
const js_mod = nats.jetstream;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://127.0.0.1:4222",
.{ .name = "kv-watch-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
var js = try js_mod.JetStream.init(client, .{});
var kv = try js.createKeyValue(.{
.bucket = "demo-watch",
.storage = .memory,
});
// Seed an initial value before creating the
// watcher. This will appear as the first "initial
// value" delivered to the watcher.
_ = try kv.put("config.version", "1.0");
std.debug.print(
"Seeded 'config.version'='1.0'\n\n",
.{},
);
// Watch all keys matching "config.>". The ">"
// wildcard matches one or more tokens, so this
// catches config.version, config.debug, etc.
var watcher = try kv.watch("config.>");
defer watcher.deinit();
// Read initial values. The watcher delivers all
// existing matching keys first. A null return
// signals that all existing keys have been
// delivered and we're now in live mode.
std.debug.print(
"-- Initial values --\n",
.{},
);
while (try watcher.next(3000)) |entry| {
var e = entry;
defer e.deinit();
printEntry(&e);
}
std.debug.print(
" (initial sync complete)\n\n",
.{},
);
// Now make some live changes. These will be
// delivered to the watcher in real time.
_ = try kv.put("config.version", "2.0");
_ = try kv.put("config.debug", "true");
_ = try kv.put("config.log_level", "info");
// Flush to ensure all puts reach the server
try client.flush(2_000_000_000);
// Read live updates. Each put above generates
// one watcher event.
std.debug.print("-- Live updates --\n", .{});
var live_count: u32 = 0;
while (live_count < 3) {
if (try watcher.next(3000)) |entry| {
var e = entry;
defer e.deinit();
printEntry(&e);
live_count += 1;
} else break;
}
// Delete a key and watch the delete marker
_ = try kv.delete("config.debug");
try client.flush(2_000_000_000);
std.debug.print(
"\n-- Delete event --\n",
.{},
);
if (try watcher.next(3000)) |entry| {
var e = entry;
defer e.deinit();
printEntry(&e);
}
// Cleanup
var del = try js.deleteKeyValue("demo-watch");
del.deinit();
std.debug.print("\nBucket deleted. Done!\n", .{});
}
/// Prints a KV entry showing key, value, operation,
/// and revision. Handles all three operations: put,
/// delete, and purge.
fn printEntry(
entry: *const js_mod.KeyValueEntry,
) void {
const op_str: []const u8 = switch (entry.operation) {
.put => "PUT",
.delete => "DEL",
.purge => "PURGE",
};
if (entry.operation == .put) {
std.debug.print(
" {s} '{s}'='{s}' rev={d}\n",
.{
op_str,
entry.key,
entry.value,
entry.revision,
},
);
} else {
std.debug.print(
" {s} '{s}' rev={d}\n",
.{
op_str,
entry.key,
entry.revision,
},
);
}
}
================================================
FILE: src/examples/micro_echo.zig
================================================
const std = @import("std");
const nats = @import("nats");
const Echo = struct {
pub fn onRequest(_: *@This(), req: *nats.micro.Request) void {
req.respond(req.data()) catch {};
}
};
pub fn main(init: std.process.Init) !void {
const client = try nats.Client.connect(
init.gpa,
init.io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
var echo = Echo{};
const service = try nats.micro.addService(client, .{
.name = "echo",
.version = "1.0.0",
.endpoint = .{
.subject = "echo",
.handler = nats.micro.Handler.init(Echo, &echo),
},
});
defer service.deinit();
std.debug.print("micro echo service running on 'echo'\n", .{});
while (true) {
init.io.sleep(.fromSeconds(1), .awake) catch {};
}
}
================================================
FILE: src/examples/polling_loop.zig
================================================
//! Non-Blocking Polling Pattern
//!
//! Demonstrates non-blocking message processing with tryNextMsg():
//! - Event loop integration (check messages, do other work)
//! - Multiple subscriptions with round-robin polling
//! - Mixed workloads (NATS + other tasks)
//!
//! Use this pattern when you need to:
//! - Integrate NATS into an existing event loop
//! - Handle multiple subscriptions without threads
//! - Do other work between message processing
//!
//! Run with: zig build run-polling-loop
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{ .name = "polling-loop-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n", .{});
// Subscribe to multiple subjects
const high_priority = try client.subscribeSync("priority.high");
defer high_priority.deinit();
const normal = try client.subscribeSync("priority.normal");
defer normal.deinit();
const low_priority = try client.subscribeSync("priority.low");
defer low_priority.deinit();
std.debug.print("Subscribed to: priority.high, priority.normal, priority.low\n\n", .{});
// Publish test messages with different priorities
try client.publish("priority.high", "URGENT: System alert!");
try client.publish("priority.normal", "Info: User logged in");
try client.publish("priority.low", "Debug: Cache refreshed");
try client.publish("priority.high", "URGENT: Disk space low!");
try client.publish("priority.normal", "Info: Report generated");
try client.publish("priority.low", "Debug: Metrics collected");
std.debug.print("Published 6 messages (2 high, 2 normal, 2 low)\n\n", .{});
// Flush to ensure messages have been delivered
try client.flush(1_000_000_000);
// PRIORITY POLLING: Check high priority first, then others
std.debug.print("Priority polling (high -> normal -> low):\n", .{});
var high_count: u32 = 0;
var normal_count: u32 = 0;
var low_count: u32 = 0;
var iterations: u32 = 0;
const max_iterations: u32 = 20;
while (iterations < max_iterations) : (iterations += 1) {
var processed_any = false;
// Always check high priority first (drain completely)
while (high_priority.tryNextMsg()) |msg| {
defer msg.deinit();
high_count += 1;
processed_any = true;
std.debug.print(" [HIGH] {s}\n", .{msg.data});
}
// Then check normal priority (one at a time)
if (normal.tryNextMsg()) |msg| {
defer msg.deinit();
normal_count += 1;
processed_any = true;
std.debug.print(" [NORMAL] {s}\n", .{msg.data});
}
// Finally check low priority (one at a time)
if (low_priority.tryNextMsg()) |msg| {
defer msg.deinit();
low_count += 1;
processed_any = true;
std.debug.print(" [LOW] {s}\n", .{msg.data});
}
// Do other work if no messages
if (!processed_any) {
// In a real app, this is where you'd do other event loop work
if (iterations < 5) {
std.debug.print(" (no messages - doing other work...)\n", .{});
}
io.sleep(.fromMilliseconds(10), .awake) catch {};
}
// Exit when all messages processed
if (high_count >= 2 and normal_count >= 2 and low_count >= 2) {
break;
}
}
std.debug.print("\nProcessed: {d} high, {d} normal, {d} low\n", .{
high_count,
normal_count,
low_count,
});
// ROUND-ROBIN POLLING: Fair scheduling across subscriptions
std.debug.print("\n--- Round-Robin Polling Demo ---\n\n", .{});
// Publish more messages
for (0..3) |i| {
var buf: [64]u8 = undefined;
const high_msg = std.fmt.bufPrint(&buf, "High {d}", .{i + 1}) catch "High";
try client.publish("priority.high", high_msg);
const norm_msg = std.fmt.bufPrint(&buf, "Normal {d}", .{i + 1}) catch "Normal";
try client.publish("priority.normal", norm_msg);
const low_msg = std.fmt.bufPrint(&buf, "Low {d}", .{i + 1}) catch "Low";
try client.publish("priority.low", low_msg);
}
try client.flush(1_000_000_000);
std.debug.print("Published 9 more messages (3 each)\n", .{});
std.debug.print("Round-robin processing:\n", .{});
const subs = [_]*nats.Client.Sub{ high_priority, normal, low_priority };
const names = [_][]const u8{ "HIGH", "NORMAL", "LOW" };
var idx: usize = 0;
var total: u32 = 0;
while (total < 9) {
if (subs[idx].tryNextMsg()) |msg| {
defer msg.deinit();
total += 1;
std.debug.print(" [{s}] {s}\n", .{ names[idx], msg.data });
}
idx = (idx + 1) % 3; // Round-robin to next subscription
// Safety: prevent infinite loop if messages don't arrive
if (idx == 0) {
io.sleep(.fromMilliseconds(10), .awake) catch {};
}
}
std.debug.print("\nTotal processed: {d}\n", .{total});
std.debug.print("\nDone!\n", .{});
}
================================================
FILE: src/examples/queue_groups.zig
================================================
//! Queue Groups - Load-Balanced Workers
//!
//! Demonstrates horizontal scaling with NATS queue groups. Multiple workers
//! subscribe to the same subject with the same queue group name - NATS
//! distributes messages round-robin among them.
//!
//! This example uses io.concurrent() to run workers in parallel threads,
//! pushing results to a shared Io.Queue for the main loop to consume.
//!
//! Run with: zig build run-queue-groups
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
const Io = std.Io;
const WorkerResult = struct {
worker_id: u8,
data: []const u8,
msg: nats.Message,
fn deinit(self: WorkerResult) void {
self.msg.deinit();
}
};
fn workerTask(
io: Io,
worker_id: u8,
sub: *nats.Client.Sub,
queue: *Io.Queue(WorkerResult),
done: *std.atomic.Value(bool),
) void {
while (!done.load(.acquire)) {
const msg = sub.nextMsgTimeout(100) catch return orelse continue;
queue.putOne(io, .{
.worker_id = worker_id,
.data = msg.data,
.msg = msg,
}) catch {
msg.deinit();
return;
};
}
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{ .name = "queue-groups-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n", .{});
// Create 3 workers in queue group
const worker1 = try client.queueSubscribeSync("tasks", "workers");
defer worker1.deinit();
const worker2 = try client.queueSubscribeSync("tasks", "workers");
defer worker2.deinit();
const worker3 = try client.queueSubscribeSync("tasks", "workers");
defer worker3.deinit();
std.debug.print("Created 3 workers in queue group 'workers'\n", .{});
// Shared queue for results
var queue_buf: [32]WorkerResult = undefined;
var queue: Io.Queue(WorkerResult) = .init(&queue_buf);
var done: std.atomic.Value(bool) = .init(false);
// Launch workers in TRUE parallel threads (return void, so no catch)
var w1 = try io.concurrent(workerTask, .{
io, 1, worker1, &queue, &done,
});
defer w1.cancel(io);
var w2 = try io.concurrent(workerTask, .{
io, 2, worker2, &queue, &done,
});
defer w2.cancel(io);
var w3 = try io.concurrent(workerTask, .{
io, 3, worker3, &queue, &done,
});
defer w3.cancel(io);
// Publish messages
const message_count: u32 = 9;
std.debug.print("\nPublishing {d} messages...\n\n", .{message_count});
for (0..message_count) |i| {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "Task {d}", .{i + 1}) catch "Task";
try client.publish("tasks", msg);
}
// Consume results from queue
var counts = [3]u32{ 0, 0, 0 };
var total_received: u32 = 0;
std.debug.print("Receiving from concurrent workers:\n", .{});
while (total_received < message_count) {
const result = queue.getOne(io) catch break;
defer result.deinit();
counts[result.worker_id - 1] += 1;
total_received += 1;
std.debug.print(
" Worker {d} received: {s}\n",
.{ result.worker_id, result.data },
);
}
// Signal workers to stop
done.store(true, .release);
std.debug.print("\nDistribution summary:\n", .{});
for (counts, 0..) |count, idx| {
std.debug.print(" Worker {d}: {d} messages\n", .{ idx + 1, count });
}
std.debug.print("\nDone!\n", .{});
}
================================================
FILE: src/examples/reconnection.zig
================================================
//! Reconnection and Resilience
//!
//! Demonstrates NATS client resilience features:
//! - Reconnection configuration options
//! - Connection state monitoring
//! - Handling publish during disconnect
//!
//! Run with: zig build run-reconnection
//!
//! To test reconnection:
//! 1. Start nats-server
//! 2. Run this example
//! 3. Restart nats-server while example is running
//! 4. Watch the client reconnect automatically
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
std.debug.print("Connecting with reconnection enabled...\n", .{});
// Connect with explicit reconnection settings
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{
.name = "reconnection-example",
// Reconnection settings
.reconnect = true, // Enable auto-reconnect (default)
.max_reconnect_attempts = 10, // Max attempts (0 = infinite)
.reconnect_wait_ms = 1000, // Initial backoff: 1 second
.reconnect_wait_max_ms = 10_000, // Max backoff: 10 seconds
.reconnect_jitter_percent = 10, // Add 10% jitter to backoff
// Keepalive settings (detect stale connections)
.ping_interval_ms = 30_000, // PING every 30 seconds
.max_pings_outstanding = 2, // Disconnect after 2 missed PONGs
// Buffer publishes during reconnect (8MB default)
.pending_buffer_size = 8 * 1024 * 1024,
},
);
defer client.deinit();
std.debug.print("Connected!\n", .{});
printConnectionInfo(client);
// Subscribe
const sub = try client.subscribeSync("demo.reconnect");
defer sub.deinit();
std.debug.print("\nSubscribed to 'demo.reconnect'\n", .{});
std.debug.print("Monitoring connection for 10 seconds...\n", .{});
std.debug.print("(Try restarting nats-server to see reconnection)\n\n", .{});
// Monitor connection and publish periodically
var iteration: u32 = 0;
const max_iterations: u32 = 20;
while (iteration < max_iterations) : (iteration += 1) {
io.sleep(.fromMilliseconds(500), .awake) catch {};
// Check connection state
const connected = client.isConnected();
const state_str = if (connected) "CONNECTED" else "DISCONNECTED";
// Try to publish
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "Ping {d}", .{iteration + 1}) catch "Ping";
if (client.publish("demo.reconnect", msg)) {
std.debug.print(
"[{d:2}] {s} - Published: {s}\n",
.{ iteration + 1, state_str, msg },
);
} else |pub_err| {
std.debug.print(
"[{d:2}] {s} - Publish failed: {}\n",
.{ iteration + 1, state_str, pub_err },
);
}
// Try to receive any messages
while (sub.tryNextMsg()) |recv_msg| {
defer recv_msg.deinit();
std.debug.print(" Received: {s}\n", .{recv_msg.data});
}
// Print reconnection stats periodically
if (iteration > 0 and (iteration + 1) % 5 == 0) {
printStats(client);
}
}
std.debug.print("\nFinal connection state:\n", .{});
printConnectionInfo(client);
printStats(client);
std.debug.print("\nDone!\n", .{});
}
fn printConnectionInfo(client: *nats.Client) void {
std.debug.print("Connection info:\n", .{});
std.debug.print(" Connected: {}\n", .{client.isConnected()});
if (client.serverInfo()) |info| {
if (info.server_name.len > 0) {
std.debug.print(" Server: {s}\n", .{info.server_name});
}
if (info.version.len > 0) {
std.debug.print(" Version: {s}\n", .{info.version});
}
std.debug.print(" Max payload: {d} bytes\n", .{info.max_payload});
}
}
fn printStats(client: *nats.Client) void {
const stats = client.stats();
std.debug.print(" Stats: {d} msgs out, {d} msgs in, {d} reconnects\n", .{
stats.msgs_out,
stats.msgs_in,
stats.reconnects,
});
}
================================================
FILE: src/examples/request_reply.zig
================================================
//! Request/Reply Pattern
//!
//! Demonstrates RPC-style request/reply communication.
//! Run with: zig build run-request-reply
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
const io_backend = @import("io_backend");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
var service_backend: io_backend.Backend = undefined;
try io_backend.init(&service_backend, allocator);
defer service_backend.deinit();
const service_io = service_backend.io();
var requester_backend: io_backend.Backend = undefined;
try io_backend.init(&requester_backend, allocator);
defer requester_backend.deinit();
const requester_io = requester_backend.io();
// Service client
const service_client = try nats.Client.connect(
allocator,
service_io,
"nats://localhost:4222",
.{ .name = "service" },
);
defer service_client.deinit();
// Requester client
const requester = try nats.Client.connect(
allocator,
requester_io,
"nats://localhost:4222",
.{ .name = "requester" },
);
defer requester.deinit();
std.debug.print("Connected to NATS!\n", .{});
// Service subscribes to handle requests
const service = try service_client.subscribeSync("math.double");
defer service.deinit();
std.debug.print("Service listening on 'math.double'\n", .{});
// Run service handler in background (returns void, so no catch)
var service_future = service_io.async(handleService, .{
service_client,
service,
});
defer service_future.cancel(service_io);
// Flush to ensure server has registered the subscription
try service_client.flush(1_000_000_000);
// Send request using client.request() - handles inbox automatically
std.debug.print("\nRequester: What is 21 * 2?\n", .{});
if (try requester.request("math.double", "21", 1000)) |reply| {
defer reply.deinit();
std.debug.print("Reply: {s}\n", .{reply.data});
} else {
std.debug.print("Request timed out\n", .{});
}
std.debug.print("\nDone!\n", .{});
}
fn handleService(
client: *nats.Client,
service: *nats.Client.Sub,
) void {
const req = service.nextMsgTimeout(2000) catch return;
if (req) |r| {
defer r.deinit();
const num = std.fmt.parseInt(i32, r.data, 10) catch 0;
var buf: [32]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "{d}", .{num * 2}) catch "error";
std.debug.print("Service: {d} * 2 = {s}\n", .{ num, result });
if (r.reply_to) |reply_to| {
client.publish(reply_to, result) catch {};
}
}
}
================================================
FILE: src/examples/request_reply_callback.zig
================================================
//! Request/Reply with Callback Subscription
//!
//! Demonstrates building a service responder using callback-style
//! subscriptions. The service handler receives requests via
//! onMessage and sends replies using msg.respond().
//!
//! Run with: zig build run-request-reply-callback
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server -DV
const std = @import("std");
const nats = @import("nats");
const io_backend = @import("io_backend");
/// Doubler service -- doubles any number sent to it.
const DoublerService = struct {
client: *nats.Client,
handled: u32 = 0,
pub fn onMessage(
self: *@This(),
msg: *const nats.Message,
) void {
self.handled += 1;
const num = std.fmt.parseInt(
i32,
msg.data,
10,
) catch 0;
var buf: [32]u8 = undefined;
const result = std.fmt.bufPrint(
&buf,
"{d}",
.{num * 2},
) catch "error";
std.debug.print(
" [service] {d} * 2 = {s}\n",
.{ num, result },
);
msg.respond(self.client, result) catch |err| {
std.debug.print(
" [service] respond failed: {s}\n",
.{@errorName(err)},
);
};
}
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
var service_backend: io_backend.Backend = undefined;
try io_backend.init(&service_backend, allocator);
defer service_backend.deinit();
const service_io = service_backend.io();
var requester_backend: io_backend.Backend = undefined;
try io_backend.init(&requester_backend, allocator);
defer requester_backend.deinit();
const requester_io = requester_backend.io();
// Service client
const service_client = try nats.Client.connect(
allocator,
service_io,
"nats://localhost:4222",
.{ .name = "doubler-service" },
);
defer service_client.deinit();
// Requester client
const requester = try nats.Client.connect(
allocator,
requester_io,
"nats://localhost:4222",
.{ .name = "requester" },
);
defer requester.deinit();
std.debug.print("Connected to NATS!\n\n", .{});
// Start service with callback subscription
var svc = DoublerService{ .client = service_client };
const sub = try service_client.subscribe(
"math.double",
nats.MsgHandler.init(DoublerService, &svc),
);
defer sub.deinit();
std.debug.print("Service listening on 'math.double'\n\n", .{});
// Flush to ensure server has registered the subscription
try service_client.flush(1_000_000_000);
// Send requests
const numbers = [_][]const u8{ "21", "50", "100" };
for (numbers) |n| {
std.debug.print("Requesting: {s} * 2\n", .{n});
if (try requester.request("math.double", n, 1000)) |reply| {
defer reply.deinit();
std.debug.print(" Reply: {s}\n\n", .{reply.data});
} else {
std.debug.print(" Timed out\n\n", .{});
}
}
std.debug.print(
"Service handled {d} requests.\n",
.{svc.handled},
);
// Verify all requests were handled
std.debug.assert(svc.handled == 3);
std.debug.print("Done!\n", .{});
}
================================================
FILE: src/examples/select.zig
================================================
//! Io.Select Pattern - Subscription with Timeout
//!
//! Demonstrates Io.Select to race a subscription receive against a
//! timeout. This is the correct use case for Io.Select with NATS -
//! racing ONE subscription against a non-resource operation like sleep.
//!
//! NOTE: Do NOT use Io.Select to race multiple subscriptions -
//! cancelling a subscription task discards any message it received.
//! Use polling or io.concurrent() + Io.Queue instead (see
//! polling_loop.zig and queue_groups.zig).
//!
//! Run with: zig build run-select
//!
//! Prerequisites: nats-server running on localhost:4222
const std = @import("std");
const nats = @import("nats");
const Io = std.Io;
const Sub = nats.Client.Sub;
const Message = nats.Message;
/// Sleep function compatible with Io.Select.async()
fn sleepMs(io: Io, ms: i64) void {
io.sleep(.fromMilliseconds(ms), .awake) catch {};
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{ .name = "select-example" },
);
defer client.deinit();
std.debug.print("Connected to NATS!\n", .{});
const sub = try client.subscribeSync("demo.select");
defer sub.deinit();
std.debug.print("Subscribed to 'demo.select'\n", .{});
std.debug.print(
"\nPublishing 3 messages with 200ms gaps...\n",
.{},
);
std.debug.print(
"Using 500ms timeout - should receive all 3.\n\n",
.{},
);
// Spawn publisher in background
var publisher = io.async(publishMessages, .{ client, io });
defer publisher.cancel(io);
// Receive with timeout using Io.Select
var received: u32 = 0;
const max_attempts = 5;
const Sel = Io.Select(union(enum) {
message: anyerror!Message,
timeout: void,
});
for (0..max_attempts) |attempt| {
var buf: [2]Sel.Union = undefined;
var sel = Sel.init(io, &buf);
sel.async(.message, Sub.nextMsg, .{sub});
sel.async(.timeout, sleepMs, .{ io, 500 });
// Wait for EITHER message OR timeout
const result = sel.await() catch {
// Cancel remaining tasks, deinit any messages
while (sel.cancel()) |remaining| {
switch (remaining) {
.message => |r| {
if (r) |m| m.deinit() else |_| {}
},
.timeout => {},
}
}
break;
};
// Cancel the loser task
while (sel.cancel()) |remaining| {
switch (remaining) {
.message => |r| {
if (r) |m| m.deinit() else |_| {}
},
.timeout => {},
}
}
switch (result) {
.message => |msg_result| {
const msg = msg_result catch continue;
defer msg.deinit();
received += 1;
std.debug.print(
" [{d}] Received: {s}\n",
.{ attempt + 1, msg.data },
);
},
.timeout => {
std.debug.print(
" [{d}] Timeout - no message\n",
.{attempt + 1},
);
},
}
}
std.debug.print("\nReceived {d} messages in {d} attempts.\n", .{
received,
max_attempts,
});
std.debug.print("Done!\n", .{});
}
fn publishMessages(
client: *nats.Client,
io: Io,
) void {
io.sleep(.fromMilliseconds(100), .awake) catch {};
for (1..4) |i| {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"Message {d}",
.{i},
) catch "Msg";
client.publish("demo.select", msg) catch return;
io.sleep(.fromMilliseconds(200), .awake) catch {};
}
}
================================================
FILE: src/examples/simple.zig
================================================
//! Simple NATS Example
//!
//! Minimal "hello world" - connect, subscribe, publish, receive one message.
//! A starting point for learning the NATS Zig client.
//! Run with: zig build run-simple
//! or: zig build run-simple -Dio_backend=evented
//!
//! Prerequisites: nats-server running on localhost:4222
//! nats-server -DV
const std = @import("std");
const nats = @import("nats");
const io_backend = @import("io_backend");
/// Main entry point using Zig 0.16's std.process.Init.
/// Init provides: gpa (allocator), io (async I/O), arena, args, environ.
///
/// IMPORTANT: each Client needs its own Io. We create the backend
/// here next to the Client so it owns its own execution context.
/// We deliberately do NOT reuse `init.io` so the build option
/// `-Dio_backend=...` can pick between Threaded and Evented.
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
var backend: io_backend.Backend = undefined;
try io_backend.init(&backend, allocator);
defer backend.deinit();
const io = backend.io();
// Connect to NATS server
const client = try nats.Client.connect(
allocator,
io,
"nats://localhost:4222",
.{},
);
defer client.deinit();
std.debug.print("Connected to NATS!\n", .{});
// Subscribe to a subject
const sub = try client.subscribeSync("hello");
defer sub.deinit();
// Publish a message
try client.publish("hello", "Hello, NATS!");
// Receive the message
if (try sub.nextMsgTimeout(1000)) |msg| {
defer msg.deinit();
std.debug.print("Received: {s}\n", .{msg.data});
}
std.debug.print("Done!\n", .{});
}
================================================
FILE: src/io_backend.zig
================================================
//! Comptime selector for the std.Io backend used by entry points.
//!
//! Picks between `std.Io.Threaded` (default) and `std.Io.Evented`
//! at compile time, based on the `-Dio_backend=threaded|evented`
//! build option. The library itself is backend-agnostic and
//! accepts any `std.Io` via `Client.connect`; this module exists
//! purely so applications, examples, and integration tests can
//! flip backends without code changes.
//!
//! Usage:
//! ```
//! const io_backend = @import("io_backend");
//! var backend: io_backend.Backend = undefined;
//! try io_backend.init(&backend, gpa);
//! defer backend.deinit();
//! const io = backend.io();
//! var client = try nats.Client.connect(gpa, io, url, .{});
//! defer client.deinit();
//! ```
const std = @import("std");
const build_options = @import("build_options");
const want_evented = std.mem.eql(
u8,
build_options.io_backend,
"evented",
);
/// The selected Io backend type, chosen at compile time from the
/// `-Dio_backend=...` build option. Defaults to `std.Io.Threaded`.
pub const Backend = if (want_evented) blk: {
if (std.Io.Evented == void) @compileError(
"std.Io.Evented is not supported on this target. " ++
"Build with -Dio_backend=threaded.",
);
break :blk std.Io.Evented;
} else std.Io.Threaded;
comptime {
std.debug.assert(@sizeOf(Backend) > 0);
}
/// Initialize the selected backend in place with default options.
/// Caller owns the result and must call `Backend.deinit()`.
///
/// `out` may be undefined on entry; it is fully initialized on
/// successful return.
///
/// Threaded init cannot fail and Uring/Kqueue/Dispatch init can,
/// so the wrapper is uniformly errorable.
pub fn init(out: *Backend, gpa: std.mem.Allocator) !void {
return initWithEnviron(out, gpa, .empty);
}
/// Initialize the selected backend with a process environment.
///
/// This matters for entry points that spawn child processes: std.Io resolves
/// `argv[0]` through the environment stored in the Io backend, not through
/// later shell state.
pub fn initWithEnviron(
out: *Backend,
gpa: std.mem.Allocator,
environ: std.process.Environ,
) !void {
std.debug.assert(@sizeOf(Backend) > 0);
if (Backend == std.Io.Threaded) {
out.* = std.Io.Threaded.init(gpa, .{ .environ = environ });
} else {
try Backend.init(out, gpa, .{ .environ = environ });
}
}
test "Backend type is selectable at comptime" {
try std.testing.expect(@sizeOf(Backend) > 0);
try std.testing.expect(@hasDecl(Backend, "io"));
try std.testing.expect(@hasDecl(Backend, "deinit"));
}
================================================
FILE: src/jetstream/JetStream.zig
================================================
//! JetStream context providing stream/consumer CRUD, publish,
//! and pull subscription operations over core NATS request/reply.
const std = @import("std");
const Allocator = std.mem.Allocator;
const types = @import("types.zig");
const errors = @import("errors.zig");
const publish_headers = @import("publish_headers.zig");
const nats = @import("../nats.zig");
const Client = nats.Client;
const headers = nats.protocol.headers;
const pubsub = @import("../pubsub.zig");
pub const Response = types.Response;
pub const StreamConfig = types.StreamConfig;
pub const StreamInfo = types.StreamInfo;
pub const ConsumerConfig = types.ConsumerConfig;
pub const ConsumerInfo = types.ConsumerInfo;
pub const CreateConsumerRequest = types.CreateConsumerRequest;
pub const DeleteResponse = types.DeleteResponse;
pub const PurgeResponse = types.PurgeResponse;
pub const ConsumerPauseResponse = types.ConsumerPauseResponse;
pub const PubAck = types.PubAck;
pub const PublishOpts = types.PublishOpts;
pub const StreamNamesResponse = types.StreamNamesResponse;
pub const StreamListResponse = types.StreamListResponse;
pub const ConsumerNamesResponse = types.ConsumerNamesResponse;
pub const ConsumerListResponse = types.ConsumerListResponse;
pub const ListRequest = types.ListRequest;
pub const AccountInfo = types.AccountInfo;
const StorageType = types.StorageType;
const PushSubscription = @import(
"push.zig",
).PushSubscription;
pub const ApiError = errors.ApiError;
pub const ApiErrorJson = errors.ApiErrorJson;
const JetStream = @This();
fn returnsErrorUnion(comptime f: anytype) bool {
const ret = @typeInfo(@TypeOf(f)).@"fn".return_type orelse return false;
return switch (@typeInfo(ret)) {
.error_union => true,
else => false,
};
}
client: *Client,
allocator: Allocator,
api_prefix_buf: [128]u8 = undefined,
api_prefix_len: u8 = 0,
timeout_ms: u32 = 5000,
last_api_err: ?ApiError = null,
/// JetStream context options for API prefix, timeout, and
/// multi-tenant domain configuration.
pub const Options = struct {
api_prefix: []const u8 = "$JS.API.",
timeout_ms: u32 = 5000,
domain: ?[]const u8 = null,
};
pub fn validateName(name: []const u8) errors.Error!void {
if (name.len == 0) return errors.Error.InvalidName;
for (name) |c| {
if (c <= 0x20 or c == 0x7f or
c == '.' or c == '*' or c == '>' or
c == '/' or c == '\\')
{
return errors.Error.InvalidName;
}
}
}
pub fn validateBucketName(bucket: []const u8) errors.Error!void {
if (bucket.len == 0) return errors.Error.InvalidBucket;
if (bucket.len > 64) return errors.Error.NameTooLong;
for (bucket) |c| {
if (c <= 0x20 or c == 0x7f or
c == '.' or c == '*' or c == '>' or
c == '/' or c == '\\')
{
return errors.Error.InvalidBucket;
}
}
}
fn validateApiPrefix(prefix: []const u8) errors.Error!void {
if (prefix.len == 0) return errors.Error.InvalidApiPrefix;
if (prefix.len > 128) return errors.Error.NameTooLong;
for (prefix) |c| {
if (c <= 0x20 or c == 0x7f or c == '*' or c == '>') {
return errors.Error.InvalidApiPrefix;
}
}
}
/// Initializes a JetStream context bound to the given client.
pub fn init(client: *Client, opts: Options) !JetStream {
std.debug.assert(client.isConnected());
var js = JetStream{
.client = client,
.allocator = client.allocator,
.timeout_ms = opts.timeout_ms,
};
if (opts.domain) |d| {
try validateName(d);
// "$JS." + domain + ".API." = 9 overhead
if (d.len > 119) return errors.Error.NameTooLong;
var buf: [128]u8 = undefined;
const p = std.fmt.bufPrint(
&buf,
"$JS.{s}.API.",
.{d},
) catch unreachable;
@memcpy(
js.api_prefix_buf[0..p.len],
p,
);
js.api_prefix_len = @intCast(p.len);
} else {
const p = opts.api_prefix;
try validateApiPrefix(p);
if (p.len > js.api_prefix_buf.len) return errors.Error.NameTooLong;
@memcpy(js.api_prefix_buf[0..p.len], p);
js.api_prefix_len = @intCast(p.len);
}
return js;
}
/// Returns the last API error from the server, if any.
pub fn lastApiError(self: *const JetStream) ?ApiError {
return self.last_api_err;
}
// -- Stream CRUD --
/// Creates a stream with the given configuration.
pub fn createStream(
self: *JetStream,
config: StreamConfig,
) !Response(StreamInfo) {
try validateName(config.name);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.CREATE.{s}",
.{config.name},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(StreamInfo, subj, config);
}
/// Updates a stream with the given configuration.
pub fn updateStream(
self: *JetStream,
config: StreamConfig,
) !Response(StreamInfo) {
try validateName(config.name);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.UPDATE.{s}",
.{config.name},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(StreamInfo, subj, config);
}
/// Deletes a stream by name.
pub fn deleteStream(
self: *JetStream,
name: []const u8,
) !Response(DeleteResponse) {
try validateName(name);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.DELETE.{s}",
.{name},
) catch return errors.Error.SubjectTooLong;
return self.apiRequestNoPayload(
DeleteResponse,
subj,
);
}
/// Creates a stream or updates it if it already exists.
/// Tries update first; falls back to create on
/// stream_not_found (matches Go client behavior).
pub fn createOrUpdateStream(
self: *JetStream,
config: StreamConfig,
) !Response(StreamInfo) {
try validateName(config.name);
std.debug.assert(self.timeout_ms > 0);
return self.updateStream(config) catch |err| {
if (err == error.ApiError) {
if (self.lastApiError()) |ae| {
if (ae.err_code ==
errors.ErrCode.stream_not_found)
return self.createStream(config);
}
}
return err;
};
}
/// Gets stream info by name.
pub fn streamInfo(
self: *JetStream,
name: []const u8,
) !Response(StreamInfo) {
try validateName(name);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.INFO.{s}",
.{name},
) catch return errors.Error.SubjectTooLong;
return self.apiRequestNoPayload(StreamInfo, subj);
}
/// Purges a stream by name. Optionally filter by
/// subject to only purge matching messages.
pub fn purgeStream(
self: *JetStream,
name: []const u8,
) !Response(PurgeResponse) {
return self.purgeStreamFiltered(name, null);
}
/// Purges messages matching a specific subject.
pub fn purgeStreamSubject(
self: *JetStream,
name: []const u8,
subject: []const u8,
) !Response(PurgeResponse) {
try pubsub.validateSubscribe(subject);
return self.purgeStreamFiltered(name, subject);
}
fn purgeStreamFiltered(
self: *JetStream,
name: []const u8,
subject: ?[]const u8,
) !Response(PurgeResponse) {
try validateName(name);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.PURGE.{s}",
.{name},
) catch return errors.Error.SubjectTooLong;
if (subject) |s| {
return self.apiRequest(
PurgeResponse,
subj,
types.PurgeRequest{ .filter = s },
);
}
return self.apiRequestNoPayload(
PurgeResponse,
subj,
);
}
// -- Stream message operations --
/// Gets a raw message from a stream by sequence number.
pub fn getMsg(
self: *JetStream,
stream: []const u8,
seq: u64,
) !Response(types.MsgGetResponse) {
try validateName(stream);
std.debug.assert(seq > 0);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.MSG.GET.{s}",
.{stream},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
types.MsgGetResponse,
subj,
types.MsgGetRequest{ .seq = seq },
);
}
/// Gets the last message on a specific subject.
pub fn getLastMsgForSubject(
self: *JetStream,
stream: []const u8,
subject: []const u8,
) !Response(types.MsgGetResponse) {
try validateName(stream);
try pubsub.validateSubscribe(subject);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.MSG.GET.{s}",
.{stream},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
types.MsgGetResponse,
subj,
types.MsgGetRequest{ .last_by_subj = subject },
);
}
/// Deletes a message from a stream by sequence.
/// The message is marked as erased but not overwritten.
pub fn deleteMsg(
self: *JetStream,
stream: []const u8,
seq: u64,
) !Response(DeleteResponse) {
try validateName(stream);
std.debug.assert(seq > 0);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.MSG.DELETE.{s}",
.{stream},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
DeleteResponse,
subj,
types.MsgDeleteRequest{
.seq = seq,
.no_erase = true,
},
);
}
/// Securely deletes a message by overwriting it with
/// random data. Slower than deleteMsg.
pub fn secureDeleteMsg(
self: *JetStream,
stream: []const u8,
seq: u64,
) !Response(DeleteResponse) {
try validateName(stream);
std.debug.assert(seq > 0);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"STREAM.MSG.DELETE.{s}",
.{stream},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
DeleteResponse,
subj,
types.MsgDeleteRequest{ .seq = seq },
);
}
// -- Consumer CRUD --
/// Creates a consumer on the given stream. Returns
/// error if consumer already exists with different
/// config. The filter_subject (if any) is in the body.
pub fn createConsumer(
self: *JetStream,
stream: []const u8,
config: ConsumerConfig,
) !Response(ConsumerInfo) {
try validateName(stream);
std.debug.assert(self.timeout_ms > 0);
var buf: [512]u8 = undefined;
const name = config.name orelse
config.durable_name orelse "";
try validateName(name);
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.CREATE.{s}.{s}",
.{ stream, name },
) catch return errors.Error.SubjectTooLong;
const req = CreateConsumerRequest{
.stream_name = stream,
.config = config,
.action = "create",
};
return self.apiRequest(ConsumerInfo, subj, req);
}
/// Creates or updates a consumer on the given stream.
/// If the consumer exists, it will be updated if the
/// config change is compatible.
pub fn createOrUpdateConsumer(
self: *JetStream,
stream: []const u8,
config: ConsumerConfig,
) !Response(ConsumerInfo) {
try validateName(stream);
std.debug.assert(self.timeout_ms > 0);
var buf: [512]u8 = undefined;
const name = config.name orelse
config.durable_name orelse "";
try validateName(name);
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.CREATE.{s}.{s}",
.{ stream, name },
) catch return errors.Error.SubjectTooLong;
const req = CreateConsumerRequest{
.stream_name = stream,
.config = config,
};
return self.apiRequest(ConsumerInfo, subj, req);
}
/// Updates a consumer on the given stream.
pub fn updateConsumer(
self: *JetStream,
stream: []const u8,
config: ConsumerConfig,
) !Response(ConsumerInfo) {
try validateName(stream);
std.debug.assert(self.timeout_ms > 0);
var buf: [512]u8 = undefined;
const name = config.name orelse
config.durable_name orelse "";
try validateName(name);
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.CREATE.{s}.{s}",
.{ stream, name },
) catch return errors.Error.SubjectTooLong;
const req = CreateConsumerRequest{
.stream_name = stream,
.config = config,
.action = "update",
};
return self.apiRequest(ConsumerInfo, subj, req);
}
/// Deletes a consumer from a stream.
pub fn deleteConsumer(
self: *JetStream,
stream: []const u8,
consumer: []const u8,
) !Response(DeleteResponse) {
try validateName(stream);
try validateName(consumer);
var buf: [512]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.DELETE.{s}.{s}",
.{ stream, consumer },
) catch return errors.Error.SubjectTooLong;
return self.apiRequestNoPayload(
DeleteResponse,
subj,
);
}
/// Gets consumer info.
pub fn consumerInfo(
self: *JetStream,
stream: []const u8,
consumer: []const u8,
) !Response(ConsumerInfo) {
try validateName(stream);
try validateName(consumer);
var buf: [512]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.INFO.{s}.{s}",
.{ stream, consumer },
) catch return errors.Error.SubjectTooLong;
return self.apiRequestNoPayload(
ConsumerInfo,
subj,
);
}
/// Creates a push consumer on the given stream.
/// The config must have deliver_subject set.
pub fn createPushConsumer(
self: *JetStream,
stream: []const u8,
config: ConsumerConfig,
) !Response(ConsumerInfo) {
try validateName(stream);
std.debug.assert(config.deliver_subject != null);
std.debug.assert(self.timeout_ms > 0);
return self.createConsumer(stream, config);
}
/// Creates or updates a push consumer.
/// The config must have deliver_subject set.
pub fn createOrUpdatePushConsumer(
self: *JetStream,
stream: []const u8,
config: ConsumerConfig,
) !Response(ConsumerInfo) {
try validateName(stream);
std.debug.assert(config.deliver_subject != null);
std.debug.assert(self.timeout_ms > 0);
return self.createOrUpdateConsumer(stream, config);
}
/// Pauses a consumer until the given time (RFC 3339).
/// The consumer will not deliver messages until resumed
/// or the pause_until time is reached.
pub fn pauseConsumer(
self: *JetStream,
stream: []const u8,
consumer: []const u8,
pause_until: []const u8,
) !Response(ConsumerPauseResponse) {
try validateName(stream);
try validateName(consumer);
std.debug.assert(pause_until.len > 0);
std.debug.assert(self.timeout_ms > 0);
var buf: [512]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.PAUSE.{s}.{s}",
.{ stream, consumer },
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
ConsumerPauseResponse,
subj,
types.ConsumerPauseRequest{
.pause_until = pause_until,
},
);
}
/// Resumes a paused consumer immediately.
pub fn resumeConsumer(
self: *JetStream,
stream: []const u8,
consumer: []const u8,
) !Response(ConsumerPauseResponse) {
try validateName(stream);
try validateName(consumer);
std.debug.assert(self.timeout_ms > 0);
var buf: [512]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.PAUSE.{s}.{s}",
.{ stream, consumer },
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
ConsumerPauseResponse,
subj,
types.ConsumerPauseRequest{},
);
}
/// Updates an existing push consumer.
/// Config must have deliver_subject set.
pub fn updatePushConsumer(
self: *JetStream,
stream: []const u8,
config: ConsumerConfig,
) !Response(ConsumerInfo) {
try validateName(stream);
std.debug.assert(config.deliver_subject != null);
std.debug.assert(self.timeout_ms > 0);
return self.updateConsumer(stream, config);
}
/// Binds to an existing push consumer by name.
/// Returns a PushSubscription with deliver_subject
/// populated from the server-side config.
pub fn pushConsumer(
self: *JetStream,
stream: []const u8,
consumer_name: []const u8,
) !PushSubscription {
try validateName(stream);
try validateName(consumer_name);
var resp = try self.consumerInfo(
stream,
consumer_name,
);
defer resp.deinit();
const cfg = resp.value.config orelse
return errors.Error.ApiError;
const ds = cfg.deliver_subject orelse
return errors.Error.ApiError;
var ps = PushSubscription{
.js = self,
.stream = stream,
};
try ps.setConsumer(consumer_name);
try ps.setDeliverSubject(ds);
if (cfg.deliver_group) |dg| {
try ps.setDeliverGroup(dg);
}
return ps;
}
/// Unpins the currently pinned client for a consumer
/// in the given delivery group.
pub fn unpinConsumer(
self: *JetStream,
stream: []const u8,
consumer: []const u8,
group: []const u8,
) !Response(DeleteResponse) {
try validateName(stream);
try validateName(consumer);
std.debug.assert(group.len > 0);
std.debug.assert(self.timeout_ms > 0);
var buf: [512]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.UNPIN.{s}.{s}",
.{ stream, consumer },
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
DeleteResponse,
subj,
types.ConsumerUnpinRequest{
.group = group,
},
);
}
/// Returns the underlying client connection.
pub fn conn(self: *const JetStream) *Client {
return self.client;
}
/// Returns the JetStream context options.
pub fn options(self: *const JetStream) Options {
return .{
.api_prefix = self.apiPrefix(),
.timeout_ms = self.timeout_ms,
};
}
// -- Listing & Account Info --
/// Returns stream names. Pass offset=0 for first
/// page. Check response total/offset/limit for
/// pagination. Returns one page per call.
pub fn streamNames(
self: *JetStream,
) !Response(StreamNamesResponse) {
return self.streamNamesOffset(0);
}
/// Returns stream names starting at offset.
pub fn streamNamesOffset(
self: *JetStream,
offset: u64,
) !Response(StreamNamesResponse) {
std.debug.assert(self.timeout_ms > 0);
return self.apiRequest(
StreamNamesResponse,
"STREAM.NAMES",
ListRequest{ .offset = offset },
);
}
/// Returns all stream names across all pages.
/// Caller owns the returned slice; free each
/// string and the slice with allocator.
pub fn allStreamNames(
self: *JetStream,
allocator: Allocator,
) ![][]const u8 {
std.debug.assert(self.timeout_ms > 0);
var result: std.ArrayList([]const u8) = .empty;
errdefer {
for (result.items) |n|
allocator.free(n);
result.deinit(allocator);
}
var offset: u64 = 0;
while (true) {
var resp = try self.streamNamesOffset(
offset,
);
defer resp.deinit();
const names = resp.value.streams orelse
break;
for (names) |n| {
const owned = try allocator.dupe(u8, n);
result.append(allocator, owned) catch |e| {
allocator.free(owned);
return e;
};
}
offset += names.len;
if (offset >= resp.value.total) break;
}
return result.toOwnedSlice(allocator);
}
/// Returns stream info list (one page).
pub fn streams(
self: *JetStream,
) !Response(StreamListResponse) {
std.debug.assert(self.timeout_ms > 0);
return self.apiRequest(
StreamListResponse,
"STREAM.LIST",
ListRequest{},
);
}
/// Finds the stream name that captures a subject.
pub fn streamNameBySubject(
self: *JetStream,
subject: []const u8,
) !Response(StreamNamesResponse) {
try pubsub.validateSubscribe(subject);
std.debug.assert(self.timeout_ms > 0);
return self.apiRequest(
StreamNamesResponse,
"STREAM.NAMES",
ListRequest{ .subject = subject },
);
}
/// Returns consumer names (one page).
pub fn consumerNames(
self: *JetStream,
stream: []const u8,
) !Response(ConsumerNamesResponse) {
return self.consumerNamesOffset(stream, 0);
}
/// Returns consumer names at offset.
pub fn consumerNamesOffset(
self: *JetStream,
stream: []const u8,
offset: u64,
) !Response(ConsumerNamesResponse) {
try validateName(stream);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.NAMES.{s}",
.{stream},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
ConsumerNamesResponse,
subj,
ListRequest{ .offset = offset },
);
}
/// Returns consumer info list (one page).
pub fn consumers(
self: *JetStream,
stream: []const u8,
) !Response(ConsumerListResponse) {
try validateName(stream);
std.debug.assert(self.timeout_ms > 0);
var buf: [256]u8 = undefined;
const subj = std.fmt.bufPrint(
&buf,
"CONSUMER.LIST.{s}",
.{stream},
) catch return errors.Error.SubjectTooLong;
return self.apiRequest(
ConsumerListResponse,
subj,
ListRequest{},
);
}
/// Returns JetStream account information including
/// usage stats and limits.
pub fn accountInfo(
self: *JetStream,
) !Response(AccountInfo) {
std.debug.assert(self.timeout_ms > 0);
return self.apiRequestNoPayload(
AccountInfo,
"INFO",
);
}
// -- Key-Value Store --
const kv_mod = @import("kv.zig");
pub const KeyValue = kv_mod.KeyValue;
pub const KvWatcher = kv_mod.KvWatcher;
const KeyValueConfig = types.KeyValueConfig;
/// Creates a new key-value bucket backed by a
/// JetStream stream. Returns a KeyValue handle.
pub fn createKeyValue(
self: *JetStream,
cfg: KeyValueConfig,
) !KeyValue {
try validateBucketName(cfg.bucket);
std.debug.assert(self.timeout_ms > 0);
const sc = self.kvStreamConfig(cfg);
var subjects = [_][]const u8{sc.subject()};
var resp = try self.createStream(sc.config(
sc.stream_name(),
&subjects,
));
resp.deinit();
try self.confirmServerRoundTrip();
return try self.initKeyValue(cfg.bucket);
}
/// Updates an existing key-value bucket config.
/// Returns error if the bucket doesn't exist.
pub fn updateKeyValue(
self: *JetStream,
cfg: KeyValueConfig,
) !KeyValue {
try validateBucketName(cfg.bucket);
std.debug.assert(self.timeout_ms > 0);
const sc = self.kvStreamConfig(cfg);
var subjects = [_][]const u8{sc.subject()};
var resp = try self.updateStream(sc.config(
sc.stream_name(),
&subjects,
));
resp.deinit();
try self.confirmServerRoundTrip();
return try self.initKeyValue(cfg.bucket);
}
/// Creates or updates a key-value bucket.
pub fn createOrUpdateKeyValue(
self: *JetStream,
cfg: KeyValueConfig,
) !KeyValue {
try validateBucketName(cfg.bucket);
std.debug.assert(self.timeout_ms > 0);
const sc = self.kvStreamConfig(cfg);
var subjects = [_][]const u8{sc.subject()};
var resp = try self.createOrUpdateStream(
sc.config(sc.stream_name(), &subjects),
);
resp.deinit();
try self.confirmServerRoundTrip();
return try self.initKeyValue(cfg.bucket);
}
/// Binds to an existing key-value bucket.
/// Returns error if the stream doesn't exist.
pub fn keyValue(
self: *JetStream,
bucket_name: []const u8,
) !KeyValue {
try validateBucketName(bucket_name);
var stream_buf: [68]u8 = undefined;
const stream_name = std.fmt.bufPrint(
&stream_buf,
"KV_{s}",
.{bucket_name},
) catch return errors.Error.SubjectTooLong;
// Verify stream exists
var resp = try self.streamInfo(stream_name);
resp.deinit();
return try self.initKeyValue(bucket_name);
}
/// Deletes a key-value bucket and its backing stream.
pub fn deleteKeyValue(
self: *JetStream,
bucket_name: []const u8,
) !Response(DeleteResponse) {
try validateBucketName(bucket_name);
var stream_buf: [68]u8 = undefined;
const stream_name = std.fmt.bufPrint(
&stream_buf,
"KV_{s}",
.{bucket_name},
) catch return errors.Error.SubjectTooLong;
return self.deleteStream(stream_name);
}
fn initKeyValue(
self: *JetStream,
bucket_name: []const u8,
) !KeyValue {
try validateBucketName(bucket_name);
var kv = KeyValue{ .js = self };
@memcpy(
kv.bucket_buf[0..bucket_name.len],
bucket_name,
);
kv.bucket_len = @intCast(bucket_name.len);
var stream_buf: [68]u8 = undefined;
const sn = std.fmt.bufPrint(
&stream_buf,
"KV_{s}",
.{bucket_name},
) catch unreachable;
@memcpy(kv.stream_buf[0..sn.len], sn);
kv.stream_len = @intCast(sn.len);
return kv;
}
fn confirmServerRoundTrip(self: *JetStream) !void {
// Make newly created/updated KV streams immediately publishable on
// constrained runners where stream interest can lag the API response.
const timeout_ns = @as(u64, self.timeout_ms) *
std.time.ns_per_ms;
try self.client.flush(timeout_ns);
}
/// Builds the stream config for a KV bucket without
/// executing the API call. Shared by create/update/
/// createOrUpdate.
const KvStreamCfg = struct {
stream_name_buf: [68]u8 = undefined,
stream_name_len: u8 = 0,
subj_buf: [128]u8 = undefined,
subj_len: u8 = 0,
hist: i64 = 1,
dup_window: ?i64 = null,
max_bytes: ?i64 = null,
max_age: ?i64 = null,
max_msg_size: ?i32 = null,
storage: StorageType = .file,
replicas: ?i32 = null,
desc: ?[]const u8 = null,
fn stream_name(
self: *const KvStreamCfg,
) []const u8 {
std.debug.assert(self.stream_name_len > 0);
return self.stream_name_buf[0..self.stream_name_len];
}
fn subject(
self: *const KvStreamCfg,
) []const u8 {
std.debug.assert(self.subj_len > 0);
return self.subj_buf[0..self.subj_len];
}
fn config(
self: *const KvStreamCfg,
name: []const u8,
subjects: *const [1][]const u8,
) StreamConfig {
return .{
.name = name,
.subjects = subjects,
.max_msgs_per_subject = self.hist,
.max_bytes = self.max_bytes,
.max_age = self.max_age,
.max_msg_size = self.max_msg_size,
.storage = self.storage,
.num_replicas = self.replicas,
.discard = .new,
.duplicate_window = self.dup_window,
.max_msgs = -1,
.max_consumers = -1,
.allow_rollup_hdrs = true,
.deny_delete = true,
.deny_purge = false,
.allow_direct = true,
.mirror_direct = false,
.description = self.desc,
};
}
};
fn kvStreamConfig(
_: *const JetStream,
cfg: KeyValueConfig,
) KvStreamCfg {
std.debug.assert(cfg.bucket.len > 0);
std.debug.assert(cfg.bucket.len <= 64);
var sc = KvStreamCfg{};
const sn = std.fmt.bufPrint(
&sc.stream_name_buf,
"KV_{s}",
.{cfg.bucket},
) catch unreachable;
sc.stream_name_len = @intCast(sn.len);
const sp = std.fmt.bufPrint(
&sc.subj_buf,
"$KV.{s}.>",
.{cfg.bucket},
) catch unreachable;
sc.subj_len = @intCast(sp.len);
sc.hist = if (cfg.history) |h|
@intCast(h)
else
1;
const two_min: i64 = 120_000_000_000;
sc.dup_window = if (cfg.ttl) |ttl|
@min(ttl, two_min)
else
two_min;
sc.max_bytes = cfg.max_bytes;
sc.max_age = cfg.ttl;
sc.max_msg_size = cfg.max_value_size;
sc.storage = cfg.storage orelse .file;
sc.replicas = cfg.replicas;
sc.desc = cfg.description;
return sc;
}
/// Returns names of all key-value buckets. Queries
/// stream names with $KV subject filter, strips the
/// KV_ prefix. Caller owns the returned slice.
pub fn keyValueStoreNames(
self: *JetStream,
alloc: std.mem.Allocator,
) ![][]const u8 {
std.debug.assert(self.timeout_ms > 0);
var result: std.ArrayList([]const u8) = .empty;
errdefer {
for (result.items) |n| alloc.free(n);
result.deinit(alloc);
}
var offset: u64 = 0;
while (true) {
var resp = try self.apiRequest(
StreamNamesResponse,
"STREAM.NAMES",
ListRequest{
.offset = offset,
.subject = "$KV.*.>",
},
);
defer resp.deinit();
const names = resp.value.streams orelse break;
for (names) |n| {
// Strip "KV_" prefix
const bucket = if (n.len > 3 and
std.mem.startsWith(u8, n, "KV_"))
n[3..]
else
n;
const owned = try alloc.dupe(u8, bucket);
result.append(alloc, owned) catch |e| {
alloc.free(owned);
return e;
};
}
offset += names.len;
if (offset >= resp.value.total) break;
}
return result.toOwnedSlice(alloc);
}
/// Returns all KV buckets with status info. Caller
/// owns the returned slice.
pub fn keyValueStores(
self: *JetStream,
alloc: std.mem.Allocator,
) ![]types.KeyValueStatus {
std.debug.assert(self.timeout_ms > 0);
const names = try self.keyValueStoreNames(alloc);
defer {
for (names) |n| alloc.free(n);
alloc.free(names);
}
var result: std.ArrayList(
types.KeyValueStatus,
) = .empty;
errdefer result.deinit(alloc);
for (names) |bucket| {
var stream_buf: [68]u8 = undefined;
const sn = std.fmt.bufPrint(
&stream_buf,
"KV_{s}",
.{bucket},
) catch continue;
var resp = self.streamInfo(sn) catch continue;
defer resp.deinit();
const cfg = resp.value.config orelse continue;
const state = resp.value.state orelse
types.StreamState{};
try result.append(alloc, .{
.bucket = bucket,
.values = state.messages,
.history = cfg.max_msgs_per_subject orelse
1,
.ttl = cfg.max_age orelse 0,
.bytes = state.bytes,
.backing_store = cfg.storage orelse .file,
.is_compressed = if (cfg.compression) |c|
c == .s2
else
false,
});
}
return result.toOwnedSlice(alloc);
}
// -- JetStream Publish --
/// Publishes a message to a JetStream stream subject
/// and waits for a PubAck. Retries NoResponders within
/// the configured JetStream timeout to tolerate transient
/// stream or leadership readiness after stream creation.
pub fn publish(
self: *JetStream,
subject: []const u8,
payload: []const u8,
) !Response(PubAck) {
std.debug.assert(subject.len > 0);
std.debug.assert(payload.len <= 1048576);
return self.publishRetry(subject, payload, null);
}
pub fn publishRetry(
self: *JetStream,
subject: []const u8,
payload: []const u8,
hdrs: ?[]const headers.Entry,
) !Response(PubAck) {
const retry_wait_ms: u32 = 250;
const max_retries: u32 = @max(2, self.timeout_ms / retry_wait_ms);
const retry_wait_ns: u64 =
@as(u64, retry_wait_ms) * std.time.ns_per_ms;
var attempt: u32 = 0;
while (true) {
const resp = if (hdrs) |h|
self.client.requestWithHeaders(
subject,
h,
payload,
self.timeout_ms,
) catch |err| return err
else
self.client.request(
subject,
payload,
self.timeout_ms,
) catch |err| return err;
var msg = resp orelse
return errors.Error.Timeout;
if (msg.isNoResponders()) {
msg.deinit();
attempt += 1;
if (attempt > max_retries)
return errors.Error.NoResponders;
sleepNs(retry_wait_ns);
continue;
}
defer msg.deinit();
return self.parsePubAckResponse(&msg);
}
}
fn sleepNs(ns: u64) void {
var ts: std.posix.timespec = .{
.sec = @intCast(ns / 1_000_000_000),
.nsec = @intCast(ns % 1_000_000_000),
};
_ = std.posix.system.nanosleep(&ts, &ts);
}
/// Publishes with header-based options (msg-id, expected
/// stream/seq) and waits for a PubAck.
pub fn publishWithOpts(
self: *JetStream,
subject: []const u8,
payload: []const u8,
opts: PublishOpts,
) !Response(PubAck) {
std.debug.assert(subject.len > 0);
std.debug.assert(payload.len <= 1048576);
var hdrs: publish_headers.PublishHeaderSet = undefined;
hdrs.populate(opts);
// Pass null when opts produced no headers -- the protocol
// layer asserts entries.len > 0, and publishRetry uses the
// null/non-null distinction to select the correct publish
// path.
const slice = hdrs.slice();
const hdr_slice: ?[]const headers.Entry =
if (slice.len == 0) null else slice;
return self.publishRetry(
subject,
payload,
hdr_slice,
);
}
/// Publishes a pre-built JetStream message with user-supplied
/// headers and optional PublishOpts. User headers are merged
/// with JetStream-generated headers (msg_id, expected_stream,
/// etc.); on key collision, JetStream headers from `msg.opts`
/// override the user-supplied value (matches Go client
/// PublishMsg semantics).
///
/// Header key comparison is case-insensitive per NATS header
/// conventions.
///
/// The allocator is used only for a temporary merge buffer
/// that is freed before return. No allocations escape.
pub fn publishMsg(
self: *JetStream,
allocator: Allocator,
msg: types.JsPublishMsg,
) !Response(PubAck) {
std.debug.assert(msg.subject.len > 0);
std.debug.assert(msg.payload.len <= 1048576);
var js_hdrs: publish_headers.PublishHeaderSet = undefined;
js_hdrs.populate(msg.opts);
// Fast path: no user headers. Pass null to publishRetry
// when opts also produced no JS headers -- the protocol
// layer asserts entries.len > 0 in encodedSize() and the
// null/non-null distinction is how publishRetry chooses
// between the header and no-header publish paths.
if (msg.headers == null or msg.headers.?.len == 0) {
const js_slice = js_hdrs.slice();
const hdr_slice: ?[]const headers.Entry =
if (js_slice.len == 0) null else js_slice;
return self.publishRetry(
msg.subject,
msg.payload,
hdr_slice,
);
}
// Merge: user headers first, JS headers override on
// case-insensitive key collision.
var merged: std.ArrayList(headers.Entry) = .empty;
defer merged.deinit(allocator);
try merged.appendSlice(allocator, msg.headers.?);
for (js_hdrs.slice()) |js_entry| {
var replaced = false;
for (merged.items) |*existing| {
if (std.ascii.eqlIgnoreCase(
existing.key,
js_entry.key,
)) {
existing.value = js_entry.value;
replaced = true;
break;
}
}
if (!replaced) try merged.append(
allocator,
js_entry,
);
}
const hdr_slice: ?[]const headers.Entry =
if (merged.items.len == 0) null else merged.items;
return self.publishRetry(
msg.subject,
msg.payload,
hdr_slice,
);
}
// -- Internal helpers --
/// Builds the full API subject and sends a request with
/// JSON payload, parsing the response.
pub fn apiRequest(
self: *JetStream,
comptime T: type,
api_subject: []const u8,
payload: anytype,
) !Response(T) {
std.debug.assert(api_subject.len > 0);
const prefix = self.apiPrefix();
std.debug.assert(prefix.len > 0);
var full_buf: [512]u8 = undefined;
const full_subj = std.fmt.bufPrint(
&full_buf,
"{s}{s}",
.{ prefix, api_subject },
) catch return errors.Error.SubjectTooLong;
const json_payload = try types.jsonStringify(
self.allocator,
payload,
);
defer self.allocator.free(json_payload);
const resp = self.client.request(
full_subj,
json_payload,
self.timeout_ms,
) catch |err| return err;
var msg = resp orelse
return errors.Error.Timeout;
defer msg.deinit();
if (msg.isNoResponders())
return errors.Error.NoResponders;
return self.parseResponse(T, msg.data);
}
/// Sends a request with no payload body.
fn apiRequestNoPayload(
self: *JetStream,
comptime T: type,
api_subject: []const u8,
) !Response(T) {
std.debug.assert(api_subject.len > 0);
const prefix = self.apiPrefix();
std.debug.assert(prefix.len > 0);
var full_buf: [512]u8 = undefined;
const full_subj = std.fmt.bufPrint(
&full_buf,
"{s}{s}",
.{ prefix, api_subject },
) catch return errors.Error.SubjectTooLong;
const resp = self.client.request(
full_subj,
"",
self.timeout_ms,
) catch |err| return err;
var msg = resp orelse
return errors.Error.Timeout;
defer msg.deinit();
if (msg.isNoResponders())
return errors.Error.NoResponders;
return self.parseResponse(T, msg.data);
}
/// Parses JSON response, checks for API error envelope.
fn parseResponse(
self: *JetStream,
comptime T: type,
data: []const u8,
) !Response(T) {
std.debug.assert(data.len > 0);
var parsed = types.jsonParse(
T,
self.allocator,
data,
) catch return errors.Error.JsonParseError;
if (checkApiError(T, &parsed.value)) |api_err| {
self.last_api_err = ApiError.fromJson(api_err);
parsed.deinit();
return errors.Error.ApiError;
}
return Response(T){
.value = parsed.value,
._parsed = parsed,
};
}
/// Parses PubAck from a message response (publish goes
/// directly to stream subject, not through $JS.API).
fn parsePubAckResponse(
self: *JetStream,
msg: *Client.Message,
) !Response(PubAck) {
if (msg.isNoResponders())
return errors.Error.NoResponders;
std.debug.assert(msg.data.len > 0);
return self.parseResponse(PubAck, msg.data);
}
/// Checks if a parsed response contains an API error.
fn checkApiError(
comptime T: type,
value: *const T,
) ?ApiErrorJson {
if (@hasField(T, "error")) {
if (value.@"error") |err| {
if (err.code > 0) return err;
}
}
return null;
}
/// Returns the API prefix slice.
pub fn apiPrefix(self: *const JetStream) []const u8 {
std.debug.assert(self.api_prefix_len > 0);
return self.api_prefix_buf[0..self.api_prefix_len];
}
// -- Tests --
test "subject building" {
// Test apiPrefix format for default
var js = JetStream{
.client = undefined,
.allocator = std.testing.allocator,
};
const default_prefix = "$JS.API.";
@memcpy(
js.api_prefix_buf[0..default_prefix.len],
default_prefix,
);
js.api_prefix_len = @intCast(default_prefix.len);
try std.testing.expectEqualStrings(
"$JS.API.",
js.apiPrefix(),
);
}
test "JetStream init reports invalid prefix or domain at runtime" {
try std.testing.expect(returnsErrorUnion(JetStream.init));
}
================================================
FILE: src/jetstream/async_publish.zig
================================================
//! Async JetStream publish with PubAckFuture.
//!
//! Non-blocking publish that returns a future for the ack.
//! Uses a shared reply subscription and correlation map to
//! match incoming acks to pending futures. Supports max
//! in-flight backpressure and per-ack timeouts.
const std = @import("std");
const Allocator = std.mem.Allocator;
const nats = @import("../nats.zig");
const Client = nats.Client;
const types = @import("types.zig");
const errors = @import("errors.zig");
const JetStream = @import("JetStream.zig");
const Io = std.Io;
const publish_headers = @import("publish_headers.zig");
/// Published ack future. Returned by publishAsync().
/// Call wait() to block until the ack arrives.
pub const PubAckFuture = struct {
_done: std.atomic.Value(bool) =
std.atomic.Value(bool).init(false),
_ack: ?types.PubAck = null,
_err: ?anyerror = null,
_id_buf: [token_size]u8 = undefined,
_allocator: Allocator,
_stream_owned: ?[]u8 = null,
_domain_owned: ?[]u8 = null,
/// Blocks until the ack is received or timeout.
pub fn wait(
self: *PubAckFuture,
timeout_ms: u32,
) !types.PubAck {
std.debug.assert(timeout_ms > 0);
var elapsed: u32 = 0;
while (!self._done.load(.acquire)) {
if (elapsed >= timeout_ms)
return errors.Error.Timeout;
sleepNs(1_000_000); // 1ms poll
elapsed += 1;
}
if (self._err) |e| return e;
return self._ack orelse
errors.Error.Timeout;
}
/// Non-blocking check. Returns ack if ready.
pub fn result(
self: *const PubAckFuture,
) ?types.PubAck {
if (!self._done.load(.acquire)) return null;
return self._ack;
}
/// Returns the error if the ack failed.
pub fn err(self: *const PubAckFuture) ?anyerror {
if (!self._done.load(.acquire)) return null;
return self._err;
}
/// Frees the future.
pub fn deinit(self: *PubAckFuture) void {
if (self._stream_owned) |stream| {
self._allocator.free(stream);
self._stream_owned = null;
}
if (self._domain_owned) |domain| {
self._allocator.free(domain);
self._domain_owned = null;
}
self._allocator.destroy(self);
}
};
const token_size = 6;
const alphabet =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ++
"abcdefghijklmnopqrstuvwxyz";
/// Async publisher for JetStream. Created from a
/// JetStream context. Manages a shared reply
/// subscription and correlates incoming acks to
/// pending publish futures.
pub const AsyncPublisher = struct {
js: *JetStream,
client: *Client,
allocator: Allocator,
// Reply inbox: ".."
reply_prefix_buf: [128]u8 = undefined,
reply_prefix_len: u8 = 0,
// Callback subscription on reply_prefix + "*"
sub: ?*Client.Sub = null,
// Pending acks: id_str → *PubAckFuture
mu: Io.Mutex = .init,
pending: PendingMap = .empty,
pending_count: std.atomic.Value(u32) =
std.atomic.Value(u32).init(0),
// Config
max_pending: u32 = 256,
ack_timeout_ms: u32 = 5000,
const PendingMap = std.StringHashMapUnmanaged(
*PubAckFuture,
);
/// Options for the async publisher.
pub const Options = struct {
max_pending: u32 = 256,
ack_timeout_ms: u32 = 5000,
};
/// Creates an async publisher bound to the given
/// JetStream context. The reply subscription is
/// created lazily on first publish (the struct
/// must have a stable address by then).
pub fn init(
js: *JetStream,
opts: Options,
) !AsyncPublisher {
std.debug.assert(js.timeout_ms > 0);
const client = js.client;
const allocator = js.allocator;
var ap = AsyncPublisher{
.js = js,
.client = client,
.allocator = allocator,
.max_pending = opts.max_pending,
.ack_timeout_ms = opts.ack_timeout_ms,
};
// Build reply prefix: _INBOX..
const inbox = try client.newInbox();
defer allocator.free(inbox);
std.debug.assert(inbox.len > 0);
const plen = @min(
inbox.len,
ap.reply_prefix_buf.len - 1,
);
@memcpy(
ap.reply_prefix_buf[0..plen],
inbox[0..plen],
);
ap.reply_prefix_buf[plen] = '.';
ap.reply_prefix_len = @intCast(plen + 1);
return ap;
}
/// Ensures the reply subscription exists. Called
/// lazily on first publish when the struct has a
/// stable address.
fn ensureSubscribed(
self: *AsyncPublisher,
) !void {
if (self.sub != null) return;
var sub_buf: [130]u8 = undefined;
const sub_subj = std.fmt.bufPrint(
&sub_buf,
"{s}*",
.{self.replyPrefix()},
) catch return errors.Error.SubjectTooLong;
self.sub = try self.client.subscribe(
sub_subj,
Client.MsgHandler.init(
AsyncPublisher,
self,
),
);
}
/// Returns the reply prefix slice.
fn replyPrefix(
self: *const AsyncPublisher,
) []const u8 {
std.debug.assert(self.reply_prefix_len > 0);
return self.reply_prefix_buf[0..self.reply_prefix_len];
}
/// Publishes a message asynchronously. Returns a
/// future that resolves when the server acks.
/// Blocks if max_pending is reached (backpressure).
pub fn publish(
self: *AsyncPublisher,
subject: []const u8,
payload: []const u8,
) !*PubAckFuture {
std.debug.assert(subject.len > 0);
return self.publishWithOpts(
subject,
payload,
.{},
);
}
/// Publishes with header options (msg-id, etc).
pub fn publishWithOpts(
self: *AsyncPublisher,
subject: []const u8,
payload: []const u8,
opts: types.PublishOpts,
) !*PubAckFuture {
std.debug.assert(subject.len > 0);
try self.ensureSubscribed();
// Backpressure: wait if too many pending
var bp_elapsed: u32 = 0;
while (self.pending_count.load(.acquire) >=
self.max_pending)
{
if (bp_elapsed >= self.ack_timeout_ms)
return errors.Error.Timeout;
sleepNs(1_000_000);
bp_elapsed += 1;
}
// Generate unique token
var id: [token_size]u8 = undefined;
self.client.io.random(&id);
for (&id) |*b| {
b.* = alphabet[@mod(b.*, alphabet.len)];
}
// Build reply subject: prefix + token
var reply_buf: [140]u8 = undefined;
const prefix = self.replyPrefix();
@memcpy(
reply_buf[0..prefix.len],
prefix,
);
@memcpy(
reply_buf[prefix.len..][0..token_size],
&id,
);
const reply = reply_buf[0 .. prefix.len +
token_size];
// Create future
const fut = try self.allocator.create(
PubAckFuture,
);
fut.* = .{ ._allocator = self.allocator };
@memcpy(&fut._id_buf, &id);
// Register in pending map
const io = self.client.io;
const id_key = fut._id_buf[0..token_size];
{
try self.mu.lock(io);
defer self.mu.unlock(io);
self.pending.put(
self.allocator,
id_key,
fut,
) catch {
self.allocator.destroy(fut);
return error.OutOfMemory;
};
}
_ = self.pending_count.fetchAdd(1, .release);
errdefer {
self.mu.lock(io) catch {};
const removed = self.pending.fetchRemove(id_key);
self.mu.unlock(io);
if (removed) |entry| {
_ = self.pending_count.fetchSub(1, .release);
self.allocator.destroy(entry.value);
}
}
var hdrs: publish_headers.PublishHeaderSet = undefined;
hdrs.populate(opts);
// Publish with reply-to
if (hdrs.count > 0) {
try self.client
.publishRequestWithHeaders(
subject,
reply,
hdrs.slice(),
payload,
);
} else {
try self.client.publishRequest(
subject,
reply,
payload,
);
}
return fut;
}
/// Returns the number of pending acks.
pub fn publishAsyncPending(
self: *const AsyncPublisher,
) u32 {
return self.pending_count.load(.acquire);
}
/// Blocks until all pending acks are resolved
/// or timeout.
pub fn waitComplete(
self: *const AsyncPublisher,
timeout_ms: u32,
) !void {
std.debug.assert(timeout_ms > 0);
var elapsed: u32 = 0;
while (self.pending_count.load(.acquire) > 0) {
if (elapsed >= timeout_ms)
return errors.Error.Timeout;
sleepNs(1_000_000);
elapsed += 1;
}
}
/// Callback handler for incoming ack messages.
/// Routes acks to the correct PubAckFuture by
/// extracting the token from the reply subject.
pub fn onMessage(
self: *AsyncPublisher,
msg: *const Client.Message,
) void {
const subj = msg.subject;
const plen = self.reply_prefix_len;
if (subj.len <= plen) return;
const id = subj[plen..];
const io = self.client.io;
self.mu.lock(io) catch return;
const entry = self.pending.fetchRemove(id);
self.mu.unlock(io);
const fut = if (entry) |e| e.value else return;
defer _ = self.pending_count.fetchSub(1, .release);
// Parse PubAck from message data
if (msg.data.len == 0) {
fut._err = errors.Error.NoResponders;
fut._done.store(true, .release);
return;
}
var parsed = types.jsonParse(
types.PubAck,
self.allocator,
msg.data,
) catch {
fut._err = errors.Error.JsonParseError;
fut._done.store(true, .release);
return;
};
defer parsed.deinit();
if (parsed.value.@"error") |api_err| {
if (api_err.code > 0) {
fut._err = errors.Error.ApiError;
fut._done.store(true, .release);
return;
}
}
const stream_owned: ?[]u8 = if (parsed.value.stream) |stream|
self.allocator.dupe(u8, stream) catch {
fut._err = error.OutOfMemory;
fut._done.store(true, .release);
return;
}
else
null;
const domain_owned: ?[]u8 = if (parsed.value.domain) |domain|
self.allocator.dupe(u8, domain) catch {
if (stream_owned) |stream| self.allocator.free(stream);
fut._err = error.OutOfMemory;
fut._done.store(true, .release);
return;
}
else
null;
fut._stream_owned = stream_owned;
fut._domain_owned = domain_owned;
fut._ack = .{
.stream = if (stream_owned) |stream| stream else null,
.seq = parsed.value.seq,
.duplicate = parsed.value.duplicate,
.domain = if (domain_owned) |domain| domain else null,
};
fut._done.store(true, .release);
}
/// Cleans up the publisher. Unsubscribes and
/// fails all pending futures.
pub fn cleanup(self: *AsyncPublisher) void {
if (self.sub) |s| {
s.deinit();
self.sub = null;
}
// Fail all pending futures
const io = self.client.io;
self.mu.lock(io) catch {};
var it = self.pending.iterator();
while (it.next()) |entry| {
const fut = entry.value_ptr.*;
fut._err = errors.Error.Timeout;
fut._done.store(true, .release);
}
self.pending.clearAndFree(self.allocator);
self.mu.unlock(io);
self.pending_count.store(0, .release);
}
/// Alias for cleanup.
pub fn deinit(self: *AsyncPublisher) void {
self.cleanup();
}
};
fn sleepNs(ns: u64) void {
var ts: std.posix.timespec = .{
.sec = @intCast(ns / 1_000_000_000),
.nsec = @intCast(ns % 1_000_000_000),
};
_ = std.posix.system.nanosleep(&ts, &ts);
}
================================================
FILE: src/jetstream/consumer.zig
================================================
//! Shared consumer abstractions for JetStream pull and push.
//!
//! Types here are designed for reuse across consumption modes.
//! Pull consumers (pull.zig) use them now; push consumers
//! (push.zig) will import them in v1.1 without changes.
const std = @import("std");
const JsMsg = @import("message.zig").JsMsg;
/// Callback handler for JetStream message consumption.
/// Comptime vtable pattern matching Client.MsgHandler but
/// taking `*JsMsg` instead of `*const Message`.
pub const JsMsgHandler = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
onMessage: *const fn (
*anyopaque,
*JsMsg,
) void,
};
/// Creates a handler from a concrete type via comptime.
/// The type must have `onMessage(self, *JsMsg) void`.
pub fn init(
comptime T: type,
ptr: *T,
) JsMsgHandler {
const gen = struct {
fn onMessage(
p: *anyopaque,
msg: *JsMsg,
) void {
const self: *T = @ptrCast(
@alignCast(p),
);
self.onMessage(msg);
}
};
return .{
.ptr = ptr,
.vtable = &.{
.onMessage = gen.onMessage,
},
};
}
/// Creates a handler from a plain function pointer.
pub fn initFn(
func: *const fn (*JsMsg) void,
) JsMsgHandler {
const gen = struct {
fn onMessage(
p: *anyopaque,
msg: *JsMsg,
) void {
const f: *const fn (*JsMsg) void =
@ptrCast(@alignCast(p));
f(msg);
}
};
return .{
.ptr = @ptrCast(@constCast(func)),
.vtable = &.{
.onMessage = gen.onMessage,
},
};
}
/// Dispatches a message to the handler.
pub fn dispatch(
self: JsMsgHandler,
msg: *JsMsg,
) void {
std.debug.assert(self.ptr != undefined_ptr);
self.vtable.onMessage(self.ptr, msg);
}
const undefined_ptr: *anyopaque = @ptrFromInt(
std.math.maxInt(usize),
);
};
/// Options for continuous message consumption.
/// Shared by pull and (future) push consumers.
pub const ConsumeOpts = struct {
max_messages: u32 = 500,
max_bytes: ?u32 = null,
/// Idle heartbeat interval in ms. 0 = disabled.
/// When enabled, must be less than expires_ms.
/// Server sends status 100 at this interval when
/// idle. Client detects stale connection after 2
/// consecutive misses.
heartbeat_ms: u32 = 0,
threshold_pct: u8 = 50,
expires_ms: u32 = 30000,
err_handler: ?ErrHandler = null,
};
/// Error callback for consume operations.
pub const ErrHandler = *const fn (anyerror) void;
/// Controls an active consume() operation.
/// Standalone struct -- NOT coupled to pull or push
/// specifics. Both modes reuse the same context type.
pub const ConsumeContext = struct {
_state: std.atomic.Value(State) =
std.atomic.Value(State).init(.running),
_shared_state: ?*std.atomic.Value(State) = null,
_allocator: ?std.mem.Allocator = null,
_task: ?std.Io.Future(void) = null,
_io: std.Io = undefined,
_thread: ?std.Thread = null,
pub const State = enum(u8) {
running = 0,
draining = 1,
stopped = 2,
};
/// Reads the current state atomically.
pub fn state(self: *const ConsumeContext) State {
const state_ptr = self._shared_state orelse
@constCast(&self._state);
return state_ptr.load(.acquire);
}
/// Stops consumption immediately. Buffered messages
/// that have not been dispatched are discarded.
pub fn stop(self: *ConsumeContext) void {
std.debug.assert(self.state() != .stopped);
const state_ptr = self._shared_state orelse &self._state;
state_ptr.store(.stopped, .release);
}
/// Signals the consumer to process remaining buffered
/// messages and then stop.
pub fn drain(self: *ConsumeContext) void {
std.debug.assert(self.state() == .running);
const state_ptr = self._shared_state orelse &self._state;
state_ptr.store(.draining, .release);
}
/// Returns true if consumption has fully stopped.
pub fn closed(self: *const ConsumeContext) bool {
return self.state() == .stopped;
}
/// Stops the background task and cleans up. The
/// background task handles its own sub cleanup.
pub fn deinit(self: *ConsumeContext) void {
const state_ptr = self._shared_state orelse &self._state;
state_ptr.store(.stopped, .release);
if (self._thread) |t| {
t.join();
self._thread = null;
} else if (self._task) |*t| {
t.cancel(self._io);
self._task = null;
}
if (self._shared_state) |shared| {
if (self._allocator) |allocator| {
allocator.destroy(shared);
}
self._shared_state = null;
self._allocator = null;
}
}
};
/// Tracks idle heartbeat health for pull/push consumers.
/// The server sends status 100 heartbeats at the configured
/// interval. If we miss `max_misses` consecutive heartbeats
/// (each window = 2x interval), the connection is stale.
///
/// Usage: set receive timeout to `timeoutMs()`, call
/// `recordActivity()` on any message, `recordTimeout()`
/// on receive timeout. Reusable by pull, push, and
/// ordered consumers.
pub const HeartbeatMonitor = struct {
heartbeat_ms: u32,
misses: u32 = 0,
max_misses: u32 = 2,
/// Creates a monitor for the given heartbeat interval.
pub fn init(heartbeat_ms: u32) HeartbeatMonitor {
std.debug.assert(heartbeat_ms > 0);
return .{ .heartbeat_ms = heartbeat_ms };
}
/// Returns the timeout (ms) to use for receive ops.
/// Set to 2x heartbeat interval so one missed heartbeat
/// doesn't immediately trigger an error.
pub fn timeoutMs(
self: *const HeartbeatMonitor,
) u32 {
std.debug.assert(self.heartbeat_ms > 0);
return self.heartbeat_ms *| 2;
}
/// Call when any message or heartbeat is received.
/// Resets the miss counter.
pub fn recordActivity(
self: *HeartbeatMonitor,
) void {
self.misses = 0;
}
/// Call when a receive times out with no data.
/// Returns true if heartbeat is considered lost.
pub fn recordTimeout(
self: *HeartbeatMonitor,
) bool {
self.misses += 1;
return self.misses >= self.max_misses;
}
/// Returns true if heartbeat is currently healthy.
pub fn isHealthy(
self: *const HeartbeatMonitor,
) bool {
return self.misses < self.max_misses;
}
};
// -- Tests --
test "JsMsgHandler dispatch with struct" {
const Counter = struct {
count: u32 = 0,
pub fn onMessage(self: *@This(), _: *JsMsg) void {
self.count += 1;
}
};
var counter = Counter{};
const handler = JsMsgHandler.init(Counter, &counter);
// Create a minimal JsMsg for testing dispatch
var msg = JsMsg{
.msg = undefined,
.client = undefined,
};
handler.dispatch(&msg);
handler.dispatch(&msg);
try std.testing.expectEqual(@as(u32, 2), counter.count);
}
test "ConsumeContext state transitions" {
var ctx = ConsumeContext{};
try std.testing.expect(!ctx.closed());
try std.testing.expectEqual(
ConsumeContext.State.running,
ctx.state(),
);
ctx.drain();
try std.testing.expectEqual(
ConsumeContext.State.draining,
ctx.state(),
);
try std.testing.expect(!ctx.closed());
ctx._state.store(.running, .release);
ctx.stop();
try std.testing.expect(ctx.closed());
try std.testing.expectEqual(
ConsumeContext.State.stopped,
ctx.state(),
);
}
test "HeartbeatMonitor timeout detection" {
var hb = HeartbeatMonitor.init(5000);
try std.testing.expectEqual(
@as(u32, 10000),
hb.timeoutMs(),
);
try std.testing.expect(hb.isHealthy());
// First timeout: not yet lost
try std.testing.expect(!hb.recordTimeout());
try std.testing.expect(hb.isHealthy());
// Second timeout: heartbeat lost
try std.testing.expect(hb.recordTimeout());
try std.testing.expect(!hb.isHealthy());
// Activity resets
hb.recordActivity();
try std.testing.expect(hb.isHealthy());
try std.testing.expectEqual(@as(u32, 0), hb.misses);
}
================================================
FILE: src/jetstream/errors.zig
================================================
//! JetStream error types and error codes.
//!
//! Two-layer error handling: Zig error unions for transport/protocol
//! failures, and ApiError struct for JetStream-specific API errors
//! returned by the server.
const std = @import("std");
/// JetStream API error returned by the server.
/// Stored inline on the JetStream context (no allocation).
pub const ApiError = struct {
code: u16,
err_code: u16,
description_buf: [255]u8 = undefined,
description_len: u8 = 0,
/// Returns the error description string.
pub fn description(self: *const ApiError) []const u8 {
return self.description_buf[0..self.description_len];
}
/// Creates an ApiError from a parsed JSON error object.
pub fn fromJson(json_err: ApiErrorJson) ApiError {
std.debug.assert(json_err.code > 0);
var result = ApiError{
.code = json_err.code,
.err_code = json_err.err_code,
};
if (json_err.description) |desc| {
const len: u8 = @intCast(@min(
desc.len,
result.description_buf.len,
));
@memcpy(
result.description_buf[0..len],
desc[0..len],
);
result.description_len = len;
}
return result;
}
};
/// JSON-deserializable error object from JetStream API responses.
pub const ApiErrorJson = struct {
code: u16 = 0,
err_code: u16 = 0,
description: ?[]const u8 = null,
};
/// Well-known JetStream error codes (from Go's errors.go).
pub const ErrCode = struct {
pub const bad_request: u16 = 10003;
pub const consumer_create: u16 = 10012;
pub const consumer_not_found: u16 = 10014;
pub const max_consumers_limit: u16 = 10026;
pub const message_not_found: u16 = 10037;
pub const js_not_enabled_for_account: u16 = 10039;
pub const stream_name_in_use: u16 = 10058;
pub const stream_not_found: u16 = 10059;
pub const stream_wrong_last_seq: u16 = 10071;
pub const js_not_enabled: u16 = 10076;
pub const consumer_already_exists: u16 = 10105;
pub const duplicate_filter_subjects: u16 = 10136;
pub const overlapping_filter_subjects: u16 = 10138;
pub const consumer_empty_filter: u16 = 10139;
pub const consumer_exists: u16 = 10148;
pub const consumer_does_not_exist: u16 = 10149;
};
/// JetStream error set for Zig error unions.
pub const Error = error{
Timeout,
NoResponders,
ApiError,
JsonParseError,
SubjectTooLong,
NoHeartbeat,
ConsumerDeleted,
OrderedReset,
InvalidName,
InvalidApiPrefix,
InvalidBucket,
NameTooLong,
InvalidKey,
InvalidData,
KeyNotFound,
WrongLastRevision,
ThreadSpawnFailed,
};
test "ApiError.fromJson" {
const json_err = ApiErrorJson{
.code = 404,
.err_code = ErrCode.stream_not_found,
.description = "stream not found",
};
const api_err = ApiError.fromJson(json_err);
try std.testing.expectEqual(@as(u16, 404), api_err.code);
try std.testing.expectEqual(
ErrCode.stream_not_found,
api_err.err_code,
);
try std.testing.expectEqualStrings(
"stream not found",
api_err.description(),
);
}
test "ApiError.fromJson truncates long description" {
const long = "x" ** 300;
const json_err = ApiErrorJson{
.code = 400,
.err_code = ErrCode.bad_request,
.description = long,
};
const api_err = ApiError.fromJson(json_err);
try std.testing.expectEqual(@as(u8, 255), api_err.description_len);
try std.testing.expectEqual(@as(usize, 255), api_err.description().len);
}
test "ApiError.fromJson null description" {
const json_err = ApiErrorJson{
.code = 500,
.err_code = 0,
.description = null,
};
const api_err = ApiError.fromJson(json_err);
try std.testing.expectEqual(@as(u8, 0), api_err.description_len);
try std.testing.expectEqualStrings("", api_err.description());
}
================================================
FILE: src/jetstream/kv.zig
================================================
//! JetStream Key-Value Store.
//!
//! A key-value store backed by a JetStream stream. Keys are
//! NATS subjects under `$KV.{bucket}.{key}`, values are
//! message payloads. Supports history, delete markers, watch,
//! and optimistic concurrency via revision numbers.
const std = @import("std");
const Allocator = std.mem.Allocator;
const nats = @import("../nats.zig");
const Client = nats.Client;
const headers_mod = nats.protocol.headers;
const types = @import("types.zig");
const errors = @import("errors.zig");
const JetStream = @import("JetStream.zig");
const PullSubscription = @import(
"pull.zig",
).PullSubscription;
const JsMsg = @import("message.zig").JsMsg;
var ephemeral_counter: std.atomic.Value(u32) =
std.atomic.Value(u32).init(0);
fn validateKeyToken(token: []const u8, allow_wildcards: bool, last: bool) !void {
if (token.len == 0) return errors.Error.InvalidKey;
if (std.mem.eql(u8, token, "*")) {
if (!allow_wildcards) return errors.Error.InvalidKey;
return;
}
if (std.mem.eql(u8, token, ">")) {
if (!allow_wildcards or !last) return errors.Error.InvalidKey;
return;
}
for (token) |c| {
if (c <= 0x20 or c == 0x7f or c == '*' or c == '>') {
return errors.Error.InvalidKey;
}
}
}
fn validateKeyLike(value: []const u8, allow_wildcards: bool) !void {
if (value.len == 0) return errors.Error.InvalidKey;
var start: usize = 0;
var i: usize = 0;
while (i <= value.len) : (i += 1) {
if (i == value.len or value[i] == '.') {
try validateKeyToken(
value[start..i],
allow_wildcards,
i == value.len,
);
start = i + 1;
}
}
}
fn validateKey(key: []const u8) !void {
try validateKeyLike(key, false);
}
fn validateKeyPattern(pattern: []const u8) !void {
try validateKeyLike(pattern, true);
}
/// Key-value store bound to a specific bucket.
/// Created via `JetStream.createKeyValue()` or
/// `JetStream.keyValue()`.
pub const KeyValue = struct {
js: *JetStream,
bucket_buf: [64]u8 = undefined,
bucket_len: u8 = 0,
stream_buf: [68]u8 = undefined,
stream_len: u8 = 0,
// Stable storage for ephemeral consumer names
_eph_name_buf: [48]u8 = undefined,
_eph_name_len: u8 = 0,
/// Returns the bucket name.
pub fn bucket(self: *const KeyValue) []const u8 {
std.debug.assert(self.bucket_len > 0);
return self.bucket_buf[0..self.bucket_len];
}
/// Returns the underlying stream name.
fn streamName(
self: *const KeyValue,
) []const u8 {
std.debug.assert(self.stream_len > 0);
return self.stream_buf[0..self.stream_len];
}
/// Builds the KV subject for a key. Validates key
/// contains no wildcards or control characters.
fn kvSubject(
self: *const KeyValue,
key: []const u8,
buf: []u8,
) ![]const u8 {
try validateKey(key);
return std.fmt.bufPrint(
buf,
"$KV.{s}.{s}",
.{ self.bucket(), key },
) catch return errors.Error.SubjectTooLong;
}
// -- Get --
/// Gets the latest value for a key. Returns null
/// if the key does not exist. Returns the entry
/// even if it's a delete/purge marker (check
/// entry.operation).
pub fn get(
self: *KeyValue,
key: []const u8,
) !?types.KeyValueEntry {
std.debug.assert(key.len > 0);
std.debug.assert(self.bucket_len > 0);
return self.getBySubject(key);
}
/// Gets a specific revision of a key.
pub fn getRevision(
self: *KeyValue,
key: []const u8,
revision: u64,
) !?types.KeyValueEntry {
std.debug.assert(key.len > 0);
std.debug.assert(revision > 0);
return self.getBySeq(key, revision);
}
fn getBySubject(
self: *KeyValue,
key: []const u8,
) !?types.KeyValueEntry {
var subj_buf: [256]u8 = undefined;
const kv_subj = try self.kvSubject(
key,
&subj_buf,
);
var api_buf: [512]u8 = undefined;
const api_subj = std.fmt.bufPrint(
&api_buf,
"STREAM.MSG.GET.{s}",
.{self.streamName()},
) catch return errors.Error.SubjectTooLong;
const req = types.MsgGetRequest{
.last_by_subj = kv_subj,
};
return self.fetchAndParse(api_subj, req, key);
}
fn getBySeq(
self: *KeyValue,
key: []const u8,
seq: u64,
) !?types.KeyValueEntry {
var api_buf: [512]u8 = undefined;
const api_subj = std.fmt.bufPrint(
&api_buf,
"STREAM.MSG.GET.{s}",
.{self.streamName()},
) catch return errors.Error.SubjectTooLong;
const req = types.MsgGetRequest{ .seq = seq };
return self.fetchAndParse(api_subj, req, key);
}
fn fetchAndParse(
self: *KeyValue,
api_subj: []const u8,
req: types.MsgGetRequest,
key: []const u8,
) !?types.KeyValueEntry {
var resp = self.js.apiRequest(
types.MsgGetResponse,
api_subj,
req,
) catch |err| {
if (err == error.ApiError) {
if (self.js.lastApiError()) |ae| {
if (ae.err_code == 10037)
return null;
}
}
return err;
};
defer resp.deinit();
const msg = resp.value.message orelse
return null;
const seq = msg.seq;
// Validate subject matches expected key
if (msg.subject) |subj| {
var exp_buf: [256]u8 = undefined;
const expected = std.fmt.bufPrint(
&exp_buf,
"$KV.{s}.{s}",
.{ self.bucket(), key },
) catch return errors.Error.SubjectTooLong;
if (!std.mem.eql(u8, subj, expected))
return null;
}
// Determine operation from stored headers
var op: types.KeyValueOp = .put;
if (msg.hdrs) |hdr_b64| {
// hdrs is base64-encoded
var decode_buf: [1024]u8 = undefined;
const decoded = decodeBase64(
hdr_b64,
&decode_buf,
) orelse return types.KeyValueEntry{
.bucket = self.bucket(),
.key = key,
.value = "",
.revision = seq,
.operation = .put,
};
op = parseKvOp(decoded);
}
// Data is base64-encoded in JSON response —
// decode before returning to caller
const allocator = self.js.allocator;
var val: []const u8 = "";
var val_alloc: ?Allocator = null;
if (msg.data) |data_b64| {
if (data_b64.len > 0 and op == .put) {
// Decode base64 into allocated buffer
const decoder = std.base64.standard
.Decoder;
const dec_len = decoder
.calcSizeForSlice(data_b64) catch {
return error.InvalidData;
};
const decoded_val = try allocator.alloc(
u8,
dec_len,
);
decoder.decode(
decoded_val[0..dec_len],
data_b64,
) catch {
allocator.free(decoded_val);
return error.InvalidData;
};
val = decoded_val[0..dec_len];
val_alloc = allocator;
}
}
return types.KeyValueEntry{
.bucket = self.bucket(),
.key = key,
.value = val,
.revision = seq,
.operation = op,
.value_allocator = val_alloc,
};
}
// -- Put --
/// Puts a value for a key. Returns the revision
/// (sequence number).
pub fn put(
self: *KeyValue,
key: []const u8,
value: []const u8,
) !u64 {
std.debug.assert(key.len > 0);
std.debug.assert(self.bucket_len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
var resp = try self.js.publish(subj, value);
defer resp.deinit();
return resp.value.seq;
}
/// Puts a string value for a key. Convenience
/// wrapper around put() -- in Zig, strings are
/// already []const u8.
pub fn putString(
self: *KeyValue,
key: []const u8,
value: []const u8,
) !u64 {
std.debug.assert(key.len > 0);
return self.put(key, value);
}
/// Creates a key only if it does not already exist.
/// Returns the revision, or error.ApiError if the
/// key already exists (check lastApiError for
/// stream_wrong_last_seq).
pub fn create(
self: *KeyValue,
key: []const u8,
value: []const u8,
) !u64 {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
var resp = try self.js.publishWithOpts(
subj,
value,
.{ .expected_last_subj_seq = 0 },
);
defer resp.deinit();
return resp.value.seq;
}
/// Updates a key only if the current revision matches.
/// Returns the new revision, or error.ApiError on
/// revision mismatch.
pub fn update(
self: *KeyValue,
key: []const u8,
value: []const u8,
revision: u64,
) !u64 {
std.debug.assert(key.len > 0);
std.debug.assert(revision > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
var resp = try self.js.publishWithOpts(
subj,
value,
.{ .expected_last_subj_seq = revision },
);
defer resp.deinit();
return resp.value.seq;
}
/// Options for KV create operations.
pub const CreateOpts = struct {
/// Per-key TTL (e.g., "5s", "1m"). Requires
/// the bucket to have allow_msg_ttl enabled.
ttl: ?[]const u8 = null,
};
/// Creates a key with options (e.g., per-key TTL).
pub fn createWithOpts(
self: *KeyValue,
key: []const u8,
value: []const u8,
opts: CreateOpts,
) !u64 {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
var resp = try self.js.publishWithOpts(
subj,
value,
.{
.expected_last_subj_seq = 0,
.ttl = opts.ttl,
},
);
defer resp.deinit();
return resp.value.seq;
}
/// Options for conditional delete/purge.
pub const KvDeleteOpts = struct {
/// Only delete if latest revision matches.
last_revision: ?u64 = null,
};
// -- Delete / Purge --
/// Soft-deletes a key by publishing a delete marker.
/// Returns the revision number. The key can still
/// appear in history.
pub fn delete(
self: *KeyValue,
key: []const u8,
) !u64 {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
const hdrs = [_]nats.protocol.headers.Entry{
.{
.key = "KV-Operation",
.value = "DEL",
},
};
var resp = try self.js.publishRetry(
subj,
"",
&hdrs,
);
defer resp.deinit();
return resp.value.seq;
}
/// Purges a key and all its history.
/// Returns the revision number.
pub fn purge(
self: *KeyValue,
key: []const u8,
) !u64 {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
const hdrs = [_]nats.protocol.headers.Entry{
.{
.key = "KV-Operation",
.value = "PURGE",
},
.{
.key = "Nats-Rollup",
.value = "sub",
},
};
var resp = try self.js.publishRetry(
subj,
"",
&hdrs,
);
defer resp.deinit();
return resp.value.seq;
}
/// Deletes a key only if latest revision matches.
pub fn deleteWithOpts(
self: *KeyValue,
key: []const u8,
opts: KvDeleteOpts,
) !u64 {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
var hdr_entries: [2]nats.protocol.headers.Entry =
undefined;
hdr_entries[0] = .{
.key = "KV-Operation",
.value = "DEL",
};
var hdr_count: usize = 1;
var rev_buf: [20]u8 = undefined;
if (opts.last_revision) |rev| {
const s = std.fmt.bufPrint(
&rev_buf,
"{d}",
.{rev},
) catch unreachable;
hdr_entries[1] = .{
.key = headers_mod.HeaderName
.expected_last_subj_seq,
.value = s,
};
hdr_count = 2;
}
var resp = try self.js.publishRetry(
subj,
"",
hdr_entries[0..hdr_count],
);
defer resp.deinit();
return resp.value.seq;
}
/// Purges a key only if latest revision matches.
pub fn purgeWithOpts(
self: *KeyValue,
key: []const u8,
opts: KvDeleteOpts,
) !u64 {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const subj = try self.kvSubject(
key,
&subj_buf,
);
var hdr_entries: [3]nats.protocol.headers.Entry =
undefined;
hdr_entries[0] = .{
.key = "KV-Operation",
.value = "PURGE",
};
hdr_entries[1] = .{
.key = "Nats-Rollup",
.value = "sub",
};
var hdr_count: usize = 2;
var rev_buf: [20]u8 = undefined;
if (opts.last_revision) |rev| {
const s = std.fmt.bufPrint(
&rev_buf,
"{d}",
.{rev},
) catch unreachable;
hdr_entries[2] = .{
.key = headers_mod.HeaderName
.expected_last_subj_seq,
.value = s,
};
hdr_count = 3;
}
var resp = try self.js.publishRetry(
subj,
"",
hdr_entries[0..hdr_count],
);
defer resp.deinit();
return resp.value.seq;
}
// -- Keys --
/// Returns all current (non-deleted) keys in the
/// bucket. Creates an ephemeral consumer with
/// last_per_subject deliver policy. Caller owns
/// the slice; free each key + slice with allocator.
pub fn keys(
self: *KeyValue,
allocator: Allocator,
) ![][]const u8 {
std.debug.assert(self.bucket_len > 0);
var subj_buf: [256]u8 = undefined;
const filter = std.fmt.bufPrint(
&subj_buf,
"$KV.{s}.>",
.{self.bucket()},
) catch return errors.Error.SubjectTooLong;
var pull = try self.createEphemeralPull(
filter,
.last_per_subject,
null,
);
defer self.deleteEphemeralPull(&pull);
var result: std.ArrayList([]const u8) = .empty;
errdefer {
for (result.items) |k| allocator.free(k);
result.deinit(allocator);
}
while (true) {
var msg = (try pull.next(3000)) orelse break;
defer msg.deinit();
const subj = msg.subject();
const plen: usize = 4 +
@as(usize, self.bucket_len) + 1;
if (subj.len > plen) {
const k = subj[plen..];
if (msg.headers()) |h| {
if (isDeleteOp(h)) continue;
}
const owned = try allocator.dupe(
u8,
k,
);
result.append(allocator, owned) catch |e| {
allocator.free(owned);
return e;
};
}
}
return result.toOwnedSlice(allocator);
}
// -- History --
/// Returns all revisions for a key (up to max
/// history). Caller owns the returned slice.
pub fn history(
self: *KeyValue,
allocator: Allocator,
key: []const u8,
) ![]types.KeyValueEntry {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const filter = try self.kvSubject(
key,
&subj_buf,
);
var pull = try self.createEphemeralPull(
filter,
.all,
null,
);
defer self.deleteEphemeralPull(&pull);
var result: std.ArrayList(
types.KeyValueEntry,
) = .empty;
errdefer {
for (result.items) |*entry| entry.deinit();
result.deinit(allocator);
}
while (true) {
var msg = (try pull.next(3000)) orelse break;
defer msg.deinit();
var op: types.KeyValueOp = .put;
if (msg.headers()) |h| {
op = parseKvOp(h);
}
const md = msg.metadata();
const seq = if (md) |m|
m.stream_seq
else
0;
// Extract value before defer deinit frees msg
const data = msg.data();
var val: []const u8 = "";
var val_alloc: ?Allocator = null;
if (data.len > 0 and op == .put) {
val = try allocator.dupe(
u8,
data,
);
val_alloc = allocator;
}
result.append(allocator, .{
.bucket = self.bucket(),
.key = key,
.value = val,
.revision = seq,
.operation = op,
.value_allocator = val_alloc,
}) catch |err| {
if (val_alloc) |a| a.free(val);
return err;
};
}
return result.toOwnedSlice(allocator);
}
/// Returns all revisions with watch options.
pub fn historyWithOpts(
self: *KeyValue,
allocator: Allocator,
key: []const u8,
opts: types.WatchOpts,
) ![]types.KeyValueEntry {
std.debug.assert(key.len > 0);
var subj_buf: [256]u8 = undefined;
const filter = try self.kvSubject(
key,
&subj_buf,
);
const dp: types.DeliverPolicy = if (opts
.include_history) .all else .all;
var pull = try self.createEphemeralPull(
filter,
dp,
if (opts.resume_from_revision) |r| r else null,
);
defer self.deleteEphemeralPull(&pull);
var result: std.ArrayList(
types.KeyValueEntry,
) = .empty;
errdefer {
for (result.items) |*entry| entry.deinit();
result.deinit(allocator);
}
while (true) {
var msg = (try pull.next(3000)) orelse break;
defer msg.deinit();
var op: types.KeyValueOp = .put;
if (msg.headers()) |h| {
op = parseKvOp(h);
}
if (opts.ignore_deletes and
(op == .delete or op == .purge))
continue;
const md = msg.metadata();
const seq = if (md) |m|
m.stream_seq
else
0;
if (opts.meta_only) {
try result.append(allocator, .{
.bucket = self.bucket(),
.key = key,
.value = "",
.revision = seq,
.operation = op,
});
continue;
}
const data = msg.data();
var val: []const u8 = "";
var val_alloc: ?Allocator = null;
if (data.len > 0 and op == .put) {
val = try allocator.dupe(u8, data);
val_alloc = allocator;
}
result.append(allocator, .{
.bucket = self.bucket(),
.key = key,
.value = val,
.revision = seq,
.operation = op,
.value_allocator = val_alloc,
}) catch |err| {
if (val_alloc) |a| a.free(val);
return err;
};
}
return result.toOwnedSlice(allocator);
}
/// Returns a streaming key lister with filters.
/// Only returns keys matching the given patterns.
pub fn listKeysFiltered(
self: *KeyValue,
patterns: []const []const u8,
) !KeyLister {
std.debug.assert(self.bucket_len > 0);
std.debug.assert(patterns.len > 0);
std.debug.assert(patterns.len <= 16);
var filters: [16][]const u8 = undefined;
var filter_bufs: [16][256]u8 = undefined;
for (patterns, 0..) |p, i| {
try validateKeyPattern(p);
const f = std.fmt.bufPrint(
&filter_bufs[i],
"$KV.{s}.{s}",
.{ self.bucket(), p },
) catch return errors.Error.SubjectTooLong;
filters[i] = filter_bufs[i][0..f.len];
}
const seq = ephemeral_counter.fetchAdd(
1,
.monotonic,
);
const name = std.fmt.bufPrint(
&self._eph_name_buf,
"kv{d}x{d}",
.{ seq, @intFromPtr(self) % 99999 },
) catch unreachable;
self._eph_name_len = @intCast(name.len);
const fs = filters[0..patterns.len];
var resp = try self.js.createConsumer(
self.streamName(),
.{
.name = name,
.ack_policy = .none,
.deliver_policy = .last_per_subject,
.filter_subjects = fs,
.mem_storage = true,
.inactive_threshold = 60_000_000_000,
},
);
resp.deinit();
var pull = PullSubscription{
.js = self.js,
.stream = self.streamName(),
};
try pull.setConsumer(name);
return KeyLister{ .kv = self, .pull = pull };
}
// -- Watch --
/// Watches a key pattern for real-time updates.
/// Delivers current values (last per subject)
/// first, then continues with live updates.
pub fn watch(
self: *KeyValue,
key_pattern: []const u8,
) !KvWatcher {
return self.watchWithOpts(key_pattern, .{});
}
/// Watches with configurable options.
pub fn watchWithOpts(
self: *KeyValue,
key_pattern: []const u8,
opts: types.WatchOpts,
) !KvWatcher {
try validateKeyPattern(key_pattern);
var subj_buf: [256]u8 = undefined;
const filter = std.fmt.bufPrint(
&subj_buf,
"$KV.{s}.{s}",
.{ self.bucket(), key_pattern },
) catch return errors.Error.SubjectTooLong;
const dp = watchDeliverPolicy(opts);
const start = if (opts.resume_from_revision) |r|
r
else
null;
const pull = try self.createEphemeralPull(
filter,
dp,
start,
);
return KvWatcher{
.kv = self,
.pull = pull,
.opts = opts,
};
}
/// Watches all keys in the bucket.
pub fn watchAll(self: *KeyValue) !KvWatcher {
return self.watchAllWithOpts(.{});
}
/// Watches all keys with configurable options.
pub fn watchAllWithOpts(
self: *KeyValue,
opts: types.WatchOpts,
) !KvWatcher {
var subj_buf: [256]u8 = undefined;
const filter = std.fmt.bufPrint(
&subj_buf,
"$KV.{s}.>",
.{self.bucket()},
) catch return errors.Error.SubjectTooLong;
const dp = watchDeliverPolicy(opts);
const start = if (opts.resume_from_revision) |r|
r
else
null;
const pull = try self.createEphemeralPull(
filter,
dp,
start,
);
return KvWatcher{
.kv = self,
.pull = pull,
.opts = opts,
};
}
/// Watches multiple key patterns simultaneously.
/// Uses filter_subjects on the consumer config.
pub fn watchFiltered(
self: *KeyValue,
patterns: []const []const u8,
opts: types.WatchOpts,
) !KvWatcher {
std.debug.assert(patterns.len > 0);
std.debug.assert(patterns.len <= 16);
var filters: [16][]const u8 = undefined;
var filter_bufs: [16][256]u8 = undefined;
var filter_lens: [16]u8 = undefined;
for (patterns, 0..) |p, i| {
try validateKeyPattern(p);
const f = std.fmt.bufPrint(
&filter_bufs[i],
"$KV.{s}.{s}",
.{ self.bucket(), p },
) catch return errors.Error.SubjectTooLong;
filter_lens[i] = @intCast(f.len);
filters[i] = filter_bufs[i][0..f.len];
}
const dp = watchDeliverPolicy(opts);
const start = opts.resume_from_revision;
const seq = ephemeral_counter.fetchAdd(
1,
.monotonic,
);
const name = std.fmt.bufPrint(
&self._eph_name_buf,
"kv{d}x{d}",
.{ seq, @intFromPtr(self) % 99999 },
) catch unreachable;
self._eph_name_len = @intCast(name.len);
const fs = filters[0..patterns.len];
var resp = try self.js.createConsumer(
self.streamName(),
.{
.name = name,
.ack_policy = .none,
.deliver_policy = dp,
.opt_start_seq = start,
.filter_subjects = fs,
.mem_storage = true,
.inactive_threshold = 60_000_000_000,
.headers_only = if (opts.meta_only)
true
else
null,
},
);
resp.deinit();
var pull = PullSubscription{
.js = self.js,
.stream = self.streamName(),
};
try pull.setConsumer(name);
return KvWatcher{
.kv = self,
.pull = pull,
.opts = opts,
};
}
// -- Key listing (streaming) --
/// Streaming key lister. More memory-efficient
/// than keys() for large buckets.
pub const KeyLister = struct {
kv: *KeyValue,
pull: PullSubscription,
done: bool = false,
current_msg: ?JsMsg = null,
fn clearCurrent(self: *KeyLister) void {
if (self.current_msg) |*msg| {
msg.deinit();
self.current_msg = null;
}
}
/// Returns the next key, or null when done.
/// Caller does NOT own the returned slice --
/// it points into the message buffer and is
/// valid until the next call to next().
pub fn next(
self: *KeyLister,
) !?[]const u8 {
if (self.done) return null;
self.clearCurrent();
while (true) {
var msg = (try self.pull.next(3000)) orelse {
self.done = true;
return null;
};
const subj = msg.subject();
const plen: usize = 4 +
@as(usize, self.kv.bucket_len) + 1;
if (subj.len <= plen) {
msg.deinit();
continue;
}
if (msg.headers()) |h| {
if (KeyValue.isDeleteOp(h)) {
msg.deinit();
continue;
}
}
self.current_msg = msg;
const stored = self.current_msg.?;
return stored.subject()[plen..];
}
}
/// Cleans up the lister and its consumer.
pub fn deinit(self: *KeyLister) void {
self.clearCurrent();
self.kv.deleteEphemeralPull(&self.pull);
}
};
/// Returns a streaming key lister. Caller must
/// call deinit() when done.
pub fn listKeys(self: *KeyValue) !KeyLister {
std.debug.assert(self.bucket_len > 0);
var subj_buf: [256]u8 = undefined;
const filter = std.fmt.bufPrint(
&subj_buf,
"$KV.{s}.>",
.{self.bucket()},
) catch return errors.Error.SubjectTooLong;
const pull = try self.createEphemeralPull(
filter,
.last_per_subject,
null,
);
return KeyLister{ .kv = self, .pull = pull };
}
fn watchDeliverPolicy(
opts: types.WatchOpts,
) types.DeliverPolicy {
if (opts.resume_from_revision != null)
return .by_start_sequence;
if (opts.updates_only)
return .new;
if (opts.include_history)
return .all;
return .last_per_subject;
}
// -- Purge Deletes --
/// Options for purging delete markers.
pub const PurgeDeletesOpts = struct {
/// Only purge markers older than this (ns).
/// Default 0 = purge all markers.
older_than_ns: i64 = 0,
};
/// Removes all delete/purge markers from the
/// bucket. Optionally filters by marker age.
pub fn purgeDeletes(
self: *KeyValue,
opts: PurgeDeletesOpts,
) !u64 {
std.debug.assert(self.bucket_len > 0);
const cutoff_ns: i64 = if (opts.older_than_ns > 0) blk: {
const now = std.Io.Clock.real.now(self.js.client.io);
const now_ns: i64 = @intCast(now.nanoseconds);
break :blk if (now_ns > opts.older_than_ns)
now_ns - opts.older_than_ns
else
0;
} else 0;
var subj_buf: [256]u8 = undefined;
const filter = std.fmt.bufPrint(
&subj_buf,
"$KV.{s}.>",
.{self.bucket()},
) catch return errors.Error.SubjectTooLong;
var pull = try self.createEphemeralPull(
filter,
.last_per_subject,
null,
);
defer self.deleteEphemeralPull(&pull);
var purged: u64 = 0;
while (true) {
var msg = (try pull.next(3000)) orelse break;
defer msg.deinit();
if (msg.headers()) |h| {
if (isDeleteOp(h)) {
if (opts.older_than_ns > 0) {
const md = msg.metadata() orelse
continue;
if (md.timestamp <= 0 or
md.timestamp > cutoff_ns)
continue;
}
const subj = msg.subject();
var pr = try self.js.purgeStreamSubject(
self.streamName(),
subj,
);
pr.deinit();
purged += 1;
}
}
}
return purged;
}
/// Creates an ephemeral consumer with the given
/// deliver policy and returns a PullSubscription.
fn createEphemeralPull(
self: *KeyValue,
filter: []const u8,
deliver_policy: types.DeliverPolicy,
opt_start_seq: ?u64,
) !PullSubscription {
std.debug.assert(filter.len > 0);
// Generate unique name into stable storage
const seq = ephemeral_counter.fetchAdd(
1,
.monotonic,
);
const name = std.fmt.bufPrint(
&self._eph_name_buf,
"kv{d}x{d}",
.{ seq, @intFromPtr(self) % 99999 },
) catch unreachable;
self._eph_name_len = @intCast(name.len);
var resp = try self.js.createConsumer(
self.streamName(),
.{
.name = name,
.ack_policy = .none,
.deliver_policy = deliver_policy,
.opt_start_seq = opt_start_seq,
.filter_subject = filter,
.mem_storage = true,
.inactive_threshold = 60_000_000_000,
},
);
resp.deinit();
var pull = PullSubscription{
.js = self.js,
.stream = self.streamName(),
};
try pull.setConsumer(name);
return pull;
}
fn ephName(self: *const KeyValue) []const u8 {
std.debug.assert(self._eph_name_len > 0);
return self._eph_name_buf[0..self._eph_name_len];
}
fn deleteEphemeralPull(
self: *KeyValue,
pull: *PullSubscription,
) void {
var resp = self.js.deleteConsumer(
self.streamName(),
pull.consumerName(),
) catch return;
resp.deinit();
}
// -- Status --
/// Returns bucket status information.
pub fn status(
self: *KeyValue,
) !types.Response(types.StreamInfo) {
return self.js.streamInfo(self.streamName());
}
// -- Helpers --
fn isDeleteOp(raw_headers: []const u8) bool {
const op = parseKvOp(raw_headers);
return op == .delete or op == .purge;
}
/// Matches exact KV-Operation header values to
/// avoid substring false positives.
fn parseKvOp(raw_headers: []const u8) types.KeyValueOp {
if (std.mem.indexOf(
u8,
raw_headers,
"KV-Operation: PURGE\r\n",
) != null) return .purge;
if (std.mem.indexOf(
u8,
raw_headers,
"KV-Operation: DEL\r\n",
) != null) return .delete;
return .put;
}
fn decodeBase64(
encoded: []const u8,
buf: []u8,
) ?[]const u8 {
if (encoded.len == 0) return null;
const decoder = std.base64.standard.Decoder;
const len = decoder.calcSizeForSlice(
encoded,
) catch return null;
if (len > buf.len) return null;
decoder.decode(
buf[0..len],
encoded,
) catch return null;
return buf[0..len];
}
};
/// Watcher for real-time KV updates using an ephemeral
/// consumer with last_per_subject. Call `next()` to
/// receive entries.
pub const KvWatcher = struct {
kv: *KeyValue,
pull: PullSubscription,
opts: types.WatchOpts = .{},
initial_done: bool = false,
/// Returns the next entry update. Returns null
/// when no more updates within timeout. First
/// null after creation means all existing keys
/// have been delivered.
pub fn next(
self: *KvWatcher,
timeout_ms: u32,
) !?types.KeyValueEntry {
std.debug.assert(timeout_ms > 0);
while (true) {
var msg = (try self.pull.next(
timeout_ms,
)) orelse {
if (!self.initial_done) {
self.initial_done = true;
}
return null;
};
defer msg.deinit();
var op: types.KeyValueOp = .put;
if (msg.headers()) |h| {
op = KeyValue.parseKvOp(h);
}
// Skip deletes if configured
if (self.opts.ignore_deletes and
(op == .delete or op == .purge))
continue;
const subj = msg.subject();
const plen: usize = 4 +
@as(usize, self.kv.bucket_len) + 1;
const key = if (subj.len > plen)
subj[plen..]
else
"";
const allocator = self.kv.js.allocator;
const owned_key = try allocator.dupe(u8, key);
errdefer allocator.free(owned_key);
const md = msg.metadata();
const seq = if (md) |m|
m.stream_seq
else
0;
// meta_only: skip value extraction
if (self.opts.meta_only) {
return types.KeyValueEntry{
.bucket = self.kv.bucket(),
.key = owned_key,
.value = "",
.revision = seq,
.operation = op,
.key_allocator = allocator,
};
}
const data = msg.data();
var val: []const u8 = "";
var val_alloc: ?Allocator = null;
if (data.len > 0 and op == .put) {
val = try allocator.dupe(u8, data);
val_alloc = allocator;
}
return types.KeyValueEntry{
.bucket = self.kv.bucket(),
.key = owned_key,
.value = val,
.revision = seq,
.operation = op,
.key_allocator = allocator,
.value_allocator = val_alloc,
};
}
}
/// Cleans up the watcher and its consumer.
pub fn deinit(self: *KvWatcher) void {
self.kv.deleteEphemeralPull(&self.pull);
}
};
test "KV keys reject spaces and DEL" {
var kv = KeyValue{ .js = undefined };
@memcpy(kv.bucket_buf[0..1], "B");
kv.bucket_len = 1;
var buf: [256]u8 = undefined;
try std.testing.expectError(
errors.Error.InvalidKey,
kv.kvSubject("bad key", &buf),
);
try std.testing.expectError(
errors.Error.InvalidKey,
kv.kvSubject("bad\x7fkey", &buf),
);
}
================================================
FILE: src/jetstream/message.zig
================================================
//! JetStream message wrapper with acknowledgment protocol.
//!
//! Wraps a core NATS Message and adds JetStream ack/nak/wpi/term
//! methods that publish to the message's reply_to subject.
const std = @import("std");
const nats = @import("../nats.zig");
const Client = nats.Client;
/// JetStream message wrapper with ack protocol support.
///
/// Ownership model (mirrors Client.Message.owned):
/// - Pull/fetch path: `owned = true`. The caller receives a
/// JsMsg by value and MUST call `deinit()` when finished to
/// free the underlying backing buffer.
/// - Push callback path: `owned = false`. The subscription
/// passes a stack-local JsMsg to the handler; `deinit()`
/// is a no-op. Slice fields (subject, data, headers,
/// reply_to via the inner Client.Message) are valid ONLY
/// during the callback invocation. Do NOT copy the struct
/// out of the callback scope or save pointers past return
/// -- the backing buffer is reclaimed by the subscription
/// right after the handler returns.
///
/// This matches the existing contract for `*const
/// Client.Message` in core NATS callbacks.
pub const JsMsg = struct {
msg: Client.Message,
client: *Client,
acked: bool = false,
/// See the type-level doc comment for the lifetime
/// contract. Default is `true` (owned) so pull-path
/// constructions do not need to specify it.
owned: bool = true,
/// Acknowledges the message (+ACK).
pub fn ack(self: *JsMsg) !void {
std.debug.assert(!self.acked);
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
try self.client.publish(reply, "+ACK");
self.acked = true;
}
/// Acknowledges and waits for server confirmation.
/// Slower than ack() but guarantees the server
/// processed the acknowledgment.
pub fn doubleAck(
self: *JsMsg,
timeout_ms: u32,
) !void {
std.debug.assert(!self.acked);
std.debug.assert(timeout_ms > 0);
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
const resp = self.client.request(
reply,
"+ACK",
timeout_ms,
) catch |err| return err;
if (resp) |r| {
var m = r;
m.deinit();
}
self.acked = true;
}
/// Negatively acknowledges -- triggers redelivery (-NAK).
pub fn nak(self: *JsMsg) !void {
std.debug.assert(!self.acked);
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
try self.client.publish(reply, "-NAK");
self.acked = true;
}
/// Negatively acknowledges with a redelivery delay.
pub fn nakWithDelay(
self: *JsMsg,
delay_ns: i64,
) !void {
std.debug.assert(!self.acked);
std.debug.assert(delay_ns > 0);
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
var buf: [64]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"-NAK {{\"delay\":{d}}}",
.{delay_ns},
) catch unreachable;
try self.client.publish(reply, payload);
self.acked = true;
}
/// Signals work in progress (+WPI). Can be called
/// repeatedly to extend the ack deadline.
pub fn inProgress(self: *JsMsg) !void {
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
try self.client.publish(reply, "+WPI");
}
/// Terminates message processing (+TERM).
pub fn term(self: *JsMsg) !void {
std.debug.assert(!self.acked);
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
try self.client.publish(reply, "+TERM");
self.acked = true;
}
/// Terminates with a reason string.
pub fn termWithReason(
self: *JsMsg,
reason: []const u8,
) !void {
std.debug.assert(!self.acked);
std.debug.assert(reason.len > 0);
// "+TERM " = 6 overhead, 512 - 6 = 506 max
std.debug.assert(reason.len <= 506);
const reply = self.msg.reply_to orelse
return;
std.debug.assert(reply.len > 0);
var buf: [512]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"+TERM {s}",
.{reason},
) catch unreachable;
try self.client.publish(reply, payload);
self.acked = true;
}
/// Returns the message data payload.
pub fn data(self: *const JsMsg) []const u8 {
return self.msg.data;
}
/// Returns the message subject.
pub fn subject(self: *const JsMsg) []const u8 {
return self.msg.subject;
}
/// Returns raw headers if present.
pub fn headers(self: *const JsMsg) ?[]const u8 {
return self.msg.headers;
}
/// Returns the reply-to subject (ack subject).
pub fn replyTo(self: *const JsMsg) ?[]const u8 {
return self.msg.reply_to;
}
/// Parses JetStream metadata from the reply subject.
/// Returns null if the reply subject is missing or
/// not in the expected `$JS.ACK.*` format.
/// Returned slices point into the reply subject
/// string (owned by the underlying Message).
pub fn metadata(
self: *const JsMsg,
) ?MsgMetadata {
const reply = self.msg.reply_to orelse
return null;
std.debug.assert(reply.len > 0);
return parseMsgMetadata(reply);
}
/// Frees the underlying message. No-op when `owned` is
/// false (push-callback path -- subscription handles it).
pub fn deinit(self: *JsMsg) void {
if (!self.owned) return;
self.msg.deinit();
}
};
/// Metadata parsed from a JetStream message reply subject.
/// Format: `$JS.ACK....
/// ...`
/// With domain: `$JS..ACK..
/// .....`
pub const MsgMetadata = struct {
stream: []const u8,
consumer: []const u8,
num_delivered: u64,
stream_seq: u64,
consumer_seq: u64,
timestamp: i64,
num_pending: u64,
domain: ?[]const u8,
};
/// Parses JetStream metadata from a reply subject string.
/// Returns null if the format is invalid.
fn parseMsgMetadata(
reply: []const u8,
) ?MsgMetadata {
std.debug.assert(reply.len > 0);
var it = std.mem.splitScalar(u8, reply, '.');
// Token 0: must be "$JS"
const t0 = it.next() orelse return null;
if (!std.mem.eql(u8, t0, "$JS")) return null;
// Token 1: "ACK" or domain name
const t1 = it.next() orelse return null;
var domain: ?[]const u8 = null;
if (!std.mem.eql(u8, t1, "ACK")) {
domain = t1;
const ack_tok = it.next() orelse return null;
if (!std.mem.eql(u8, ack_tok, "ACK"))
return null;
}
const stream = it.next() orelse return null;
const consumer = it.next() orelse return null;
const n_del = it.next() orelse return null;
const s_seq = it.next() orelse return null;
const c_seq = it.next() orelse return null;
const ts = it.next() orelse return null;
const n_pend = it.next() orelse return null;
return MsgMetadata{
.stream = stream,
.consumer = consumer,
.num_delivered = parseU64(n_del) orelse
return null,
.stream_seq = parseU64(s_seq) orelse
return null,
.consumer_seq = parseU64(c_seq) orelse
return null,
.timestamp = parseI64(ts) orelse return null,
.num_pending = parseU64(n_pend) orelse
return null,
.domain = domain,
};
}
fn parseU64(s: []const u8) ?u64 {
return std.fmt.parseInt(u64, s, 10) catch null;
}
fn parseI64(s: []const u8) ?i64 {
return std.fmt.parseInt(i64, s, 10) catch null;
}
// -- Tests --
test "parse standard reply subject" {
const reply =
"$JS.ACK.ORDERS.worker.1.42.42.1710000000.5";
const md = parseMsgMetadata(reply).?;
try std.testing.expectEqualStrings(
"ORDERS",
md.stream,
);
try std.testing.expectEqualStrings(
"worker",
md.consumer,
);
try std.testing.expectEqual(@as(u64, 1), md.num_delivered);
try std.testing.expectEqual(@as(u64, 42), md.stream_seq);
try std.testing.expectEqual(@as(u64, 42), md.consumer_seq);
try std.testing.expectEqual(
@as(i64, 1710000000),
md.timestamp,
);
try std.testing.expectEqual(@as(u64, 5), md.num_pending);
try std.testing.expect(md.domain == null);
}
test "parse reply with domain" {
const reply =
"$JS.hub.ACK.ORDERS.worker.1.42.42.1710000000.5";
const md = parseMsgMetadata(reply).?;
try std.testing.expectEqualStrings("hub", md.domain.?);
try std.testing.expectEqualStrings(
"ORDERS",
md.stream,
);
try std.testing.expectEqualStrings(
"worker",
md.consumer,
);
try std.testing.expectEqual(@as(u64, 42), md.stream_seq);
}
test "invalid reply returns null" {
try std.testing.expect(
parseMsgMetadata("_INBOX.abc.def") == null,
);
try std.testing.expect(
parseMsgMetadata("$JS.ACK.STREAM") == null,
);
try std.testing.expect(
parseMsgMetadata("$JS.ACK") == null,
);
try std.testing.expect(
parseMsgMetadata("NATS.something") == null,
);
}
test "edge cases: zero values" {
const reply =
"$JS.ACK.S.C.0.0.0.0.0";
const md = parseMsgMetadata(reply).?;
try std.testing.expectEqual(@as(u64, 0), md.stream_seq);
try std.testing.expectEqual(@as(u64, 0), md.consumer_seq);
try std.testing.expectEqual(@as(u64, 0), md.num_pending);
try std.testing.expectEqual(@as(u64, 0), md.num_delivered);
try std.testing.expectEqual(@as(i64, 0), md.timestamp);
}
================================================
FILE: src/jetstream/ordered.zig
================================================
//! Ordered consumer for gap-free, in-order delivery.
//!
//! Internal type used by KV Watch. Automatically recreates
//! the ephemeral consumer on sequence gaps or heartbeat
//! failures, resuming from the last known stream position.
const std = @import("std");
const nats = @import("../nats.zig");
const Client = nats.Client;
const types = @import("types.zig");
const errors = @import("errors.zig");
const consumer_mod = @import("consumer.zig");
const msg_mod = @import("message.zig");
const JsMsg = msg_mod.JsMsg;
const MsgMetadata = msg_mod.MsgMetadata;
const JetStream = @import("JetStream.zig");
const PullSubscription = @import("pull.zig").PullSubscription;
const HeartbeatMonitor = consumer_mod.HeartbeatMonitor;
/// Auto-recreating ephemeral pull consumer ensuring
/// gap-free, in-order delivery. Not public API --
/// used internally by KV Watch.
pub const OrderedConsumer = struct {
js: *JetStream,
stream: []const u8,
config: OrderedConfig,
consumer_name_buf: [64]u8 = undefined,
consumer_name_len: u8 = 0,
stream_seq: u64 = 0,
consumer_seq: u64 = 0,
serial: u32 = 0,
pull: ?PullSubscription = null,
hb: ?HeartbeatMonitor = null,
reset_count: u32 = 0,
/// Configuration for ordered consumers.
/// Restricted subset of full ConsumerConfig.
pub const OrderedConfig = struct {
filter_subject: ?[]const u8 = null,
deliver_policy: ?types.DeliverPolicy = null,
opt_start_seq: ?u64 = null,
headers_only: ?bool = null,
heartbeat_ms: u32 = 0,
};
/// Creates an ordered consumer. Does NOT create
/// the server-side consumer yet (lazy on first
/// next() call).
pub fn init(
js: *JetStream,
stream: []const u8,
config: OrderedConfig,
) OrderedConsumer {
std.debug.assert(stream.len > 0);
std.debug.assert(js.timeout_ms > 0);
return OrderedConsumer{
.js = js,
.stream = stream,
.config = config,
.hb = if (config.heartbeat_ms > 0)
HeartbeatMonitor.init(
config.heartbeat_ms,
)
else
null,
};
}
/// Fetches the next message in order. Creates or
/// recreates the consumer as needed. Returns null
/// when no messages are available within timeout.
pub fn next(
self: *OrderedConsumer,
timeout_ms: u32,
) !?JsMsg {
std.debug.assert(timeout_ms > 0);
while (true) {
// Ensure consumer exists
if (self.pull == null) {
try self.createOrReset();
}
const recv_ms = if (self.hb) |hb|
hb.timeoutMs()
else
timeout_ms;
var pull = &self.pull.?;
const maybe = pull.next(
recv_ms,
) catch |err| {
if (err == error.Timeout or
err == error.NoResponders)
{
self.deleteConsumer();
self.pull = null;
return null;
}
return err;
};
const msg = maybe orelse {
if (self.hb) |*hb| {
if (hb.recordTimeout()) {
try self.createOrReset();
}
}
return null;
};
if (self.hb) |*hb| hb.recordActivity();
// Check for sequence gap
const md = msg.metadata();
if (md) |m| {
const expected = self.consumer_seq + 1;
if (expected > 1 and
m.consumer_seq != expected)
{
// REVIEWED(2025-03): Setting stream_seq to
// gap message's seq is correct per NATS
// ordered consumer protocol. Gaps mean
// messages were lost; restart from gap
// point is the documented recovery.
var m2 = msg;
m2.deinit();
self.stream_seq = m.stream_seq;
try self.createOrReset();
continue;
}
self.stream_seq = m.stream_seq;
self.consumer_seq = m.consumer_seq;
}
return msg;
}
}
/// Creates or recreates the server-side consumer,
/// resuming from last known stream position.
fn createOrReset(self: *OrderedConsumer) !void {
// Delete old consumer (ignore errors)
if (self.consumer_name_len > 0) {
self.deleteConsumer();
self.backoffSleep();
}
self.serial += 1;
self.consumer_seq = 0;
// Generate unique name (avoid _ prefix which
// NATS reserves for internal use)
const name = std.fmt.bufPrint(
&self.consumer_name_buf,
"oc{d}x{d}",
.{ self.serial, @intFromPtr(self) % 99999 },
) catch unreachable;
self.consumer_name_len = @intCast(name.len);
// Go's ordered consumer ALWAYS uses
// DeliverByStartSequencePolicy (ordered.go:626)
var next_seq: u64 = 1;
if (self.stream_seq > 0) {
next_seq = self.stream_seq + 1;
} else if (self.config.opt_start_seq) |s| {
next_seq = s;
}
const cfg = types.ConsumerConfig{
.name = name,
.deliver_policy = .by_start_sequence,
.opt_start_seq = next_seq,
.ack_policy = .none,
.max_deliver = 1,
.mem_storage = true,
.inactive_threshold = 300_000_000_000,
.num_replicas = 1,
.headers_only = self.config.headers_only,
.filter_subject = self.config.filter_subject,
};
var resp = try self.js.createConsumer(
self.stream,
cfg,
);
resp.deinit();
// Ensure server has processed the consumer
self.js.client.flush(5_000_000_000) catch {};
var p = PullSubscription{
.js = self.js,
.stream = self.stream,
};
try p.setConsumer(self.consumerName());
self.pull = p;
self.reset_count += 1;
}
/// Backoff sleep between resets.
fn backoffSleep(self: *const OrderedConsumer) void {
const delays = [_]u64{
250_000_000,
500_000_000,
1_000_000_000,
2_000_000_000,
5_000_000_000,
10_000_000_000,
};
const idx = @min(
self.reset_count,
delays.len - 1,
);
var ts: std.posix.timespec = .{
.sec = @intCast(delays[idx] / 1_000_000_000),
.nsec = @intCast(
delays[idx] % 1_000_000_000,
),
};
_ = std.posix.system.nanosleep(&ts, &ts);
}
/// Deletes the current server-side consumer.
fn deleteConsumer(self: *OrderedConsumer) void {
if (self.consumer_name_len == 0) return;
var resp = self.js.deleteConsumer(
self.stream,
self.consumerName(),
) catch return;
resp.deinit();
}
/// Returns the current consumer name slice.
fn consumerName(self: *const OrderedConsumer) []const u8 {
std.debug.assert(self.consumer_name_len > 0);
return self.consumer_name_buf[0..self.consumer_name_len];
}
/// Cleans up the server-side consumer.
pub fn deinit(self: *OrderedConsumer) void {
self.deleteConsumer();
self.pull = null;
self.consumer_name_len = 0;
}
};
// -- Tests --
test "OrderedConsumer config restrictions" {
// Verify the config we build matches restrictions
const cfg = types.ConsumerConfig{
.name = "_oc_test",
.ack_policy = .none,
.max_deliver = 1,
.inactive_threshold = 300_000_000_000,
.num_replicas = 1,
.filter_subject = "$KV.mybucket.>",
};
try std.testing.expectEqual(
types.AckPolicy.none,
cfg.ack_policy.?,
);
try std.testing.expectEqual(
@as(i64, 1),
cfg.max_deliver.?,
);
try std.testing.expectEqual(
@as(i64, 300_000_000_000),
cfg.inactive_threshold.?,
);
}
test "backoff delays increase" {
const delays = [_]u64{
250_000_000,
500_000_000,
1_000_000_000,
2_000_000_000,
5_000_000_000,
10_000_000_000,
};
// Verify delays are monotonically increasing
var prev: u64 = 0;
for (delays) |d| {
try std.testing.expect(d > prev);
prev = d;
}
// Verify cap at 10s
try std.testing.expectEqual(
@as(u64, 10_000_000_000),
delays[delays.len - 1],
);
}
================================================
FILE: src/jetstream/publish_headers.zig
================================================
const std = @import("std");
const nats = @import("../nats.zig");
const headers = nats.protocol.headers;
const types = @import("types.zig");
pub const PublishHeaderSet = struct {
entries: [6]headers.Entry = undefined,
count: usize = 0,
expected_last_seq_buf: [20]u8 = undefined,
expected_last_subj_seq_buf: [20]u8 = undefined,
pub fn slice(
self: *const PublishHeaderSet,
) []const headers.Entry {
return self.entries[0..self.count];
}
pub fn populate(
self: *PublishHeaderSet,
opts: types.PublishOpts,
) void {
self.* = .{};
if (opts.msg_id) |v| {
self.entries[self.count] = .{
.key = headers.HeaderName.msg_id,
.value = v,
};
self.count += 1;
}
if (opts.expected_stream) |v| {
self.entries[self.count] = .{
.key = headers.HeaderName.expected_stream,
.value = v,
};
self.count += 1;
}
if (opts.expected_last_msg_id) |v| {
self.entries[self.count] = .{
.key = headers.HeaderName.expected_last_msg_id,
.value = v,
};
self.count += 1;
}
if (opts.expected_last_seq) |v| {
const s = std.fmt.bufPrint(
&self.expected_last_seq_buf,
"{d}",
.{v},
) catch unreachable;
self.entries[self.count] = .{
.key = headers.HeaderName.expected_last_seq,
.value = s,
};
self.count += 1;
}
if (opts.expected_last_subj_seq) |v| {
const s = std.fmt.bufPrint(
&self.expected_last_subj_seq_buf,
"{d}",
.{v},
) catch unreachable;
self.entries[self.count] = .{
.key = headers.HeaderName.expected_last_subj_seq,
.value = s,
};
self.count += 1;
}
if (opts.ttl) |v| {
self.entries[self.count] = .{
.key = headers.HeaderName.msg_ttl,
.value = v,
};
self.count += 1;
}
}
};
================================================
FILE: src/jetstream/pull.zig
================================================
//! JetStream pull-based subscription.
//!
//! Implements fetch-based message consumption: subscribe to a
//! temporary inbox, publish a pull request, collect messages
//! until batch complete or timeout/status signals.
const std = @import("std");
const Allocator = std.mem.Allocator;
const nats = @import("../nats.zig");
const Client = nats.Client;
const types = @import("types.zig");
const errors = @import("errors.zig");
const consumer_mod = @import("consumer.zig");
const JsMsg = @import("message.zig").JsMsg;
const JetStream = @import("JetStream.zig");
const JsMsgHandler = consumer_mod.JsMsgHandler;
const ConsumeContext = consumer_mod.ConsumeContext;
const ConsumeOpts = consumer_mod.ConsumeOpts;
const HeartbeatMonitor = consumer_mod.HeartbeatMonitor;
fn returnsErrorUnion(comptime f: anytype) bool {
const ret = @typeInfo(@TypeOf(f)).@"fn".return_type orelse return false;
return switch (@typeInfo(ret)) {
.error_union => true,
else => false,
};
}
/// Pull-based consumer subscription.
pub const PullSubscription = struct {
js: *JetStream,
stream: []const u8,
/// Inline consumer name buffer (avoids dangling
/// slices from external buffers).
consumer_buf: [48]u8 = undefined,
consumer_len: u8 = 0,
/// Returns consumer name as a slice into the
/// inline buffer. Safe after move/copy.
pub fn consumerName(
self: *const PullSubscription,
) []const u8 {
std.debug.assert(self.consumer_len > 0);
return self.consumer_buf[0..self.consumer_len];
}
/// Sets the consumer name from a source slice.
pub fn setConsumer(
self: *PullSubscription,
name: []const u8,
) errors.Error!void {
try JetStream.validateName(name);
if (name.len > self.consumer_buf.len) {
return errors.Error.NameTooLong;
}
@memcpy(
self.consumer_buf[0..name.len],
name,
);
self.consumer_len = @intCast(name.len);
}
/// Options for pull-based message fetching.
pub const FetchOpts = struct {
max_messages: u32 = 1,
timeout_ms: u32 = 5000,
};
/// Fetches messages from the consumer. Returns a
/// FetchResult that owns the messages. Caller must
/// call `deinit()` on the result when done.
/// Auto-configures 5s heartbeat for requests > 10s
/// (matching Go client behavior).
pub fn fetch(
self: *PullSubscription,
opts: FetchOpts,
) !FetchResult {
std.debug.assert(opts.max_messages > 0);
std.debug.assert(self.stream.len > 0);
std.debug.assert(self.consumer_len > 0);
// Auto-heartbeat for long requests (Go default)
const hb: ?i64 = if (opts.timeout_ms > 10000)
5_000_000_000
else
null;
return self.fetchInternal(.{
.batch = @intCast(opts.max_messages),
.expires = msToNs(opts.timeout_ms),
.idle_heartbeat = hb,
}, opts.timeout_ms);
}
/// Fetches with no_wait: returns immediately with
/// whatever is available (may be 0 messages).
pub fn fetchNoWait(
self: *PullSubscription,
max_messages: u32,
) !FetchResult {
std.debug.assert(max_messages > 0);
std.debug.assert(self.stream.len > 0);
std.debug.assert(self.consumer_len > 0);
return self.fetchInternal(.{
.batch = @intCast(max_messages),
.no_wait = true,
}, 2000);
}
/// Fetches up to max_bytes worth of messages.
pub fn fetchBytes(
self: *PullSubscription,
max_bytes: u32,
opts: FetchOpts,
) !FetchResult {
std.debug.assert(max_bytes > 0);
std.debug.assert(self.stream.len > 0);
return self.fetchInternal(.{
.batch = @intCast(opts.max_messages),
.max_bytes = @intCast(max_bytes),
.expires = msToNs(opts.timeout_ms),
}, opts.timeout_ms);
}
/// Fetches a single message. Returns null on timeout.
pub fn next(
self: *PullSubscription,
timeout_ms: u32,
) !?JsMsg {
std.debug.assert(timeout_ms > 0);
std.debug.assert(self.stream.len > 0);
var result = try self.fetchInternal(.{
.batch = 1,
.expires = msToNs(timeout_ms),
}, timeout_ms);
if (result.messages.len == 0) {
result.deinit();
return null;
}
std.debug.assert(result.messages.len == 1);
const msg = result.messages[0];
result.allocator.free(result.messages);
return msg;
}
/// Internal fetch with arbitrary PullRequest params.
fn fetchInternal(
self: *PullSubscription,
pull_req: types.PullRequest,
timeout_ms: u32,
) !FetchResult {
const client = self.js.client;
const allocator = self.js.allocator;
const inbox = try client.newInbox();
defer allocator.free(inbox);
var sub = try client.subscribeSync(inbox);
defer sub.deinit();
var subj_buf: [512]u8 = undefined;
const prefix = self.js.apiPrefix();
const pull_subj = std.fmt.bufPrint(
&subj_buf,
"{s}CONSUMER.MSG.NEXT.{s}.{s}",
.{ prefix, self.stream, self.consumerName() },
) catch return errors.Error.SubjectTooLong;
const payload = try types.jsonStringify(
allocator,
pull_req,
);
defer allocator.free(payload);
try client.publishRequest(
pull_subj,
inbox,
payload,
);
try client.flush(5_000_000_000);
const batch: u32 = if (pull_req.batch) |b|
@intCast(b)
else
1;
var msgs: std.ArrayList(JsMsg) = .empty;
errdefer {
for (msgs.items) |*m| m.deinit();
msgs.deinit(allocator);
}
var collected: u32 = 0;
while (collected < batch) {
const maybe_msg = sub.nextMsgTimeout(
timeout_ms,
) catch |err| {
if (collected > 0) break;
return err;
};
const msg = maybe_msg orelse break;
if (msg.status()) |code| {
msg.deinit();
switch (code) {
404, 408, 409 => break,
100 => continue,
else => break,
}
}
try msgs.append(allocator, JsMsg{
.msg = msg,
.client = client,
});
collected += 1;
}
return FetchResult{
.messages = try msgs.toOwnedSlice(
allocator,
),
.allocator = allocator,
};
}
fn msToNs(ms: u32) i64 {
return @as(i64, @intCast(ms)) * 1_000_000;
}
/// Creates a message iterator for continuous pull
/// consumption. Returns a MessagesContext whose
/// `next()` method yields one JsMsg at a time.
/// Caller must call `deinit()` when done.
pub fn messages(
self: *PullSubscription,
opts: ConsumeOpts,
) !MessagesContext {
std.debug.assert(self.stream.len > 0);
std.debug.assert(self.consumer_len > 0);
std.debug.assert(opts.max_messages > 0);
std.debug.assert(opts.expires_ms > 0);
// heartbeat must be less than expires
std.debug.assert(
opts.heartbeat_ms == 0 or
opts.heartbeat_ms < opts.expires_ms,
);
const client = self.js.client;
const inbox = try client.newInbox();
defer client.allocator.free(inbox);
const sub = try client.subscribeSync(inbox);
return MessagesContext{
.pull = self,
.sub = sub,
.opts = opts,
.hb = if (opts.heartbeat_ms > 0)
HeartbeatMonitor.init(opts.heartbeat_ms)
else
null,
};
}
/// Starts continuous callback-based consumption.
/// Messages are dispatched to the handler in a
/// background task. Returns a ConsumeContext for
/// stop/drain control. Caller must call `deinit()`
/// on the returned context when done.
pub fn consume(
self: *PullSubscription,
handler: JsMsgHandler,
opts: ConsumeOpts,
) !ConsumeContext {
std.debug.assert(self.stream.len > 0);
std.debug.assert(self.consumer_len > 0);
std.debug.assert(opts.max_messages > 0);
std.debug.assert(opts.expires_ms > 0);
std.debug.assert(
opts.heartbeat_ms == 0 or
opts.heartbeat_ms < opts.expires_ms,
);
const client = self.js.client;
const inbox = try client.newInbox();
defer client.allocator.free(inbox);
const sub = try client.subscribeSync(inbox);
errdefer sub.deinit();
// Issue initial pull request
try issuePull(
self.js,
client,
sub.subject,
self.stream,
self.consumerName(),
opts,
);
const io = client.io;
const state = try client.allocator.create(
std.atomic.Value(ConsumeContext.State),
);
errdefer client.allocator.destroy(state);
state.* = std.atomic.Value(ConsumeContext.State).init(.running);
var ctx = ConsumeContext{
._io = io,
._shared_state = state,
._allocator = client.allocator,
};
ctx._task = io.async(
consumeDrainTask,
.{
self.js,
client,
sub,
handler,
opts,
state,
self.stream,
self.consumerName(),
},
);
return ctx;
}
/// Result of a fetch operation.
pub const FetchResult = struct {
messages: []JsMsg,
allocator: Allocator,
/// Returns the number of messages fetched.
pub fn count(self: *const FetchResult) usize {
return self.messages.len;
}
/// Frees all messages and the backing slice.
pub fn deinit(self: *FetchResult) void {
for (self.messages) |*m| m.deinit();
self.allocator.free(self.messages);
}
};
};
test "PullSubscription setConsumer reports invalid input at runtime" {
try std.testing.expect(returnsErrorUnion(PullSubscription.setConsumer));
}
/// Iterator for continuous pull-based consumption.
/// Each call to `next()` returns a single JsMsg.
/// Automatically issues new pull requests when the
/// current batch is exhausted. Monitors heartbeats
/// when configured (heartbeat_ms > 0 in ConsumeOpts).
pub const MessagesContext = struct {
pull: *PullSubscription,
sub: *Client.Sub,
opts: ConsumeOpts,
hb: ?HeartbeatMonitor = null,
active: bool = true,
delivered: u32 = 0,
batch_pending: bool = false,
/// Returns the next message, or null on timeout.
/// Issues pull requests automatically. Caller owns
/// the returned JsMsg and must call ack + deinit.
/// Returns error.NoHeartbeat if heartbeats stop.
pub fn next(self: *MessagesContext) !?JsMsg {
std.debug.assert(self.active);
const client = self.pull.js.client;
const recv_ms = if (self.hb) |hb|
hb.timeoutMs()
else
self.opts.expires_ms;
// Issue pull if needed
if (!self.batch_pending) {
try issuePull(
self.pull.js,
client,
self.sub.subject,
self.pull.stream,
self.pull.consumerName(),
self.opts,
);
self.batch_pending = true;
self.delivered = 0;
}
while (self.active) {
const maybe = self.sub.nextMsgTimeout(
recv_ms,
) catch |err| {
self.batch_pending = false;
return err;
};
const msg = maybe orelse {
// Receive timed out -- check heartbeat
if (self.hb) |*hb| {
if (hb.recordTimeout())
return errors.Error.NoHeartbeat;
}
self.batch_pending = false;
return null;
};
// Any message resets heartbeat monitor
if (self.hb) |*hb| hb.recordActivity();
if (msg.status()) |code| {
if (code == 100) {
if (msg.reply_to) |reply| {
client.publish(
reply,
"",
) catch {};
}
msg.deinit();
continue;
}
msg.deinit();
switch (code) {
404, 408 => {
self.batch_pending = false;
return null;
},
409 => {
self.batch_pending = false;
return null;
},
else => {
self.batch_pending = false;
return null;
},
}
}
self.delivered += 1;
const threshold = self.opts.max_messages *
self.opts.threshold_pct / 100;
if (self.delivered >= threshold) {
issuePull(
self.pull.js,
client,
self.sub.subject,
self.pull.stream,
self.pull.consumerName(),
self.opts,
) catch {};
self.delivered = 0;
}
return JsMsg{
.msg = msg,
.client = client,
};
}
return null;
}
/// Stops the iterator. No more messages after this.
pub fn stop(self: *MessagesContext) void {
std.debug.assert(self.active);
self.active = false;
}
/// Frees the underlying subscription.
pub fn deinit(self: *MessagesContext) void {
self.active = false;
self.sub.deinit();
}
};
/// Issues a pull request to the server.
fn issuePull(
js: *JetStream,
client: *Client,
inbox: []const u8,
stream: []const u8,
consumer_name: []const u8,
opts: ConsumeOpts,
) !void {
std.debug.assert(inbox.len > 0);
std.debug.assert(stream.len > 0);
std.debug.assert(
opts.heartbeat_ms == 0 or
opts.heartbeat_ms < opts.expires_ms,
);
var subj_buf: [512]u8 = undefined;
const prefix = js.apiPrefix();
const pull_subj = std.fmt.bufPrint(
&subj_buf,
"{s}CONSUMER.MSG.NEXT.{s}.{s}",
.{ prefix, stream, consumer_name },
) catch return errors.Error.SubjectTooLong;
const hb_ns: ?i64 = if (opts.heartbeat_ms > 0)
PullSubscription.msToNs(opts.heartbeat_ms)
else
null;
const pull_req = types.PullRequest{
.batch = @intCast(opts.max_messages),
.expires = PullSubscription.msToNs(
opts.expires_ms,
),
.idle_heartbeat = hb_ns,
};
const payload = try types.jsonStringify(
js.allocator,
pull_req,
);
defer js.allocator.free(payload);
try client.publishRequest(
pull_subj,
inbox,
payload,
);
try client.flush(5_000_000_000);
}
/// Background task for callback-based consume().
fn consumeDrainTask(
js: *JetStream,
client: *Client,
sub: *Client.Sub,
handler: JsMsgHandler,
opts: ConsumeOpts,
state: *std.atomic.Value(ConsumeContext.State),
stream: []const u8,
consumer_name: []const u8,
) void {
defer {
sub.deinit();
state.store(.stopped, .release);
}
var hb: ?HeartbeatMonitor = if (opts.heartbeat_ms > 0)
HeartbeatMonitor.init(opts.heartbeat_ms)
else
null;
const recv_ms = if (hb) |h|
h.timeoutMs()
else
opts.expires_ms;
var delivered: u32 = 0;
while (state.load(.acquire) == .running or
state.load(.acquire) == .draining)
{
const maybe = sub.nextMsgTimeout(
recv_ms,
) catch |err| {
if (opts.err_handler) |eh| eh(err);
if (state.load(.acquire) == .draining) break;
issuePull(
js,
client,
sub.subject,
stream,
consumer_name,
opts,
) catch break;
continue;
};
const msg = maybe orelse {
if (state.load(.acquire) == .draining) break;
// Check heartbeat
if (hb) |*h| {
if (h.recordTimeout()) {
if (opts.err_handler) |eh|
eh(errors.Error.NoHeartbeat);
break;
}
}
issuePull(
js,
client,
sub.subject,
stream,
consumer_name,
opts,
) catch break;
delivered = 0;
continue;
};
if (hb) |*h| h.recordActivity();
if (msg.status()) |code| {
if (code == 100) {
if (msg.reply_to) |reply| {
client.publish(
reply,
"",
) catch {};
}
msg.deinit();
continue;
}
msg.deinit();
switch (code) {
404, 408 => {
if (state.load(.acquire) == .draining) break;
// Re-issue pull (batch expired)
issuePull(
js,
client,
sub.subject,
stream,
consumer_name,
opts,
) catch break;
delivered = 0;
continue;
},
409 => break,
else => continue,
}
}
// REVIEWED(2025-03): Stack-local JsMsg is intentional.
// handler.dispatch() is synchronous — handler must
// process before return. Avoids allocation per msg.
var js_msg = JsMsg{
.msg = msg,
.client = client,
};
handler.dispatch(&js_msg);
delivered += 1;
const threshold = opts.max_messages *
opts.threshold_pct / 100;
if (delivered >= threshold) {
issuePull(
js,
client,
sub.subject,
stream,
consumer_name,
opts,
) catch {};
delivered = 0;
}
}
}
================================================
FILE: src/jetstream/push.zig
================================================
//! JetStream push-based consumer subscription.
//!
//! Push consumers have a deliver_subject configured and the
//! server pushes messages to that subject. The client
//! subscribes using a callback and processes messages as
//! they arrive on the IO thread.
const std = @import("std");
const Allocator = std.mem.Allocator;
const nats = @import("../nats.zig");
const Client = nats.Client;
const types = @import("types.zig");
const errors = @import("errors.zig");
const consumer_mod = @import("consumer.zig");
const JsMsg = @import("message.zig").JsMsg;
const JsMsgHandler = consumer_mod.JsMsgHandler;
const JetStream = @import("JetStream.zig");
const pubsub = @import("../pubsub.zig");
fn returnsErrorUnion(comptime f: anytype) bool {
const ret = @typeInfo(@TypeOf(f)).@"fn".return_type orelse return false;
return switch (@typeInfo(ret)) {
.error_union => true,
else => false,
};
}
/// Push-based consumer subscription. Created after a
/// push consumer exists on the server. Subscribe to
/// the deliver_subject and process messages via
/// consume().
pub const PushSubscription = struct {
js: *JetStream,
stream: []const u8,
consumer_buf: [48]u8 = undefined,
consumer_len: u8 = 0,
deliver_buf: [256]u8 = undefined,
deliver_len: u16 = 0,
deliver_group_buf: [64]u8 = undefined,
deliver_group_len: u8 = 0,
/// Returns consumer name.
pub fn consumerName(
self: *const PushSubscription,
) []const u8 {
std.debug.assert(self.consumer_len > 0);
return self.consumer_buf[0..self.consumer_len];
}
/// Returns the deliver subject.
pub fn deliverSubject(
self: *const PushSubscription,
) []const u8 {
std.debug.assert(self.deliver_len > 0);
return self.deliver_buf[0..self.deliver_len];
}
/// Sets consumer name.
pub fn setConsumer(
self: *PushSubscription,
name: []const u8,
) errors.Error!void {
try JetStream.validateName(name);
if (name.len > self.consumer_buf.len) {
return errors.Error.NameTooLong;
}
@memcpy(
self.consumer_buf[0..name.len],
name,
);
self.consumer_len = @intCast(name.len);
}
/// Sets the deliver subject.
pub fn setDeliverSubject(
self: *PushSubscription,
subj: []const u8,
) !void {
try pubsub.validatePublish(subj);
if (subj.len > self.deliver_buf.len) {
return errors.Error.SubjectTooLong;
}
@memcpy(
self.deliver_buf[0..subj.len],
subj,
);
self.deliver_len = @intCast(subj.len);
}
/// Sets the deliver group (queue group).
pub fn setDeliverGroup(
self: *PushSubscription,
group: []const u8,
) !void {
try pubsub.validateQueueGroup(group);
if (group.len > self.deliver_group_buf.len) {
return errors.Error.NameTooLong;
}
@memcpy(
self.deliver_group_buf[0..group.len],
group,
);
self.deliver_group_len = @intCast(group.len);
}
/// Options for push consumption.
pub const ConsumeOpts = struct {
/// Expected idle-heartbeat interval in ms.
/// If no message or heartbeat arrives within
/// 2x this interval, err_handler is called
/// with error.NoHeartbeat. In normal use this
/// should match the consumer's server-side
/// idle_heartbeat configuration to avoid false
/// positives during idle periods.
heartbeat_ms: u32 = 0,
err_handler: ?consumer_mod.ErrHandler = null,
};
/// Starts callback-based consumption on the
/// deliver subject. Uses the client's native
/// callback subscription (runs on IO thread).
/// To stop: call the returned context's deinit().
///
/// The handler receives `*JsMsg` with `owned = false`.
/// Slice fields (data, subject, headers, reply_to)
/// are valid ONLY during the callback; do not copy
/// the struct out or save pointers past return.
pub fn consume(
self: *PushSubscription,
handler: JsMsgHandler,
opts: ConsumeOpts,
) !PushConsumeContext {
std.debug.assert(self.deliver_len > 0);
std.debug.assert(self.consumer_len > 0);
const client = self.js.client;
const subj = self.deliverSubject();
// Allocate wrapper on heap so it outlives
// this function. Stores the JsMsgHandler
// and a pointer back to the client.
const wrapper = try client.allocator.create(
PushCallbackWrapper,
);
errdefer client.allocator.destroy(wrapper);
wrapper.* = .{
.handler = handler,
.client = client,
.allocator = client.allocator,
.err_handler = opts.err_handler,
.last_activity_ns = std.atomic.Value(u64).init(
getNowNs(client.io),
),
};
const qg: ?[]const u8 =
if (self.deliver_group_len > 0)
self.deliver_group_buf[0..self.deliver_group_len]
else
null;
// Use client.subscribe (callback mode).
// This runs callbackDrainFn on the IO thread.
// Messages are dispatched via the wrapper.
const sub = try client.queueSubscribe(
subj,
qg,
Client.MsgHandler.init(
PushCallbackWrapper,
wrapper,
),
);
errdefer sub.deinit();
// Flush to ensure SUB reaches the server
// before the caller creates the push consumer.
try client.flush(5_000_000_000);
var ctx = PushConsumeContext{
.sub = sub,
.wrapper = wrapper,
.io = client.io,
};
if (opts.heartbeat_ms > 0) {
ctx.monitor_future = client.io.concurrent(
pushHeartbeatMonitorTask,
.{ wrapper, opts.heartbeat_ms },
) catch client.io.async(
pushHeartbeatMonitorTask,
.{ wrapper, opts.heartbeat_ms },
);
}
return ctx;
}
};
test "PushSubscription setters report invalid input at runtime" {
try std.testing.expect(returnsErrorUnion(PushSubscription.setConsumer));
try std.testing.expect(returnsErrorUnion(PushSubscription.setDeliverSubject));
try std.testing.expect(returnsErrorUnion(PushSubscription.setDeliverGroup));
}
/// Heap-allocated wrapper that bridges Client.MsgHandler
/// (receives *const Message) to JsMsgHandler (receives
/// *JsMsg with owned=false). Lives on the heap because the
/// callback subscription outlives the consume() call.
const PushCallbackWrapper = struct {
handler: JsMsgHandler,
client: *Client,
allocator: Allocator,
err_handler: ?consumer_mod.ErrHandler = null,
last_activity_ns: std.atomic.Value(u64) =
std.atomic.Value(u64).init(0),
pub fn onMessage(
self: *PushCallbackWrapper,
msg: *const Client.Message,
) void {
self.last_activity_ns.store(
getNowNs(self.client.io),
.release,
);
if (msg.status()) |code| {
if (code == 100) {
if (msg.reply_to) |reply| {
self.client.publish(reply, "") catch |err| {
if (self.err_handler) |eh| eh(err);
};
}
return;
}
}
// Borrowed message. The underlying Client.Message
// backing buffer is reclaimed by callbackDrainFn
// after handler.dispatch() returns. owned=false
// makes JsMsg.deinit() a no-op.
var js_msg = JsMsg{
.msg = msg.*,
.client = self.client,
.owned = false,
};
self.handler.dispatch(&js_msg);
}
};
/// Context for controlling an active push consume.
/// Simpler than ConsumeContext -- just wraps the
/// subscription. Stopping = unsubscribing.
pub const PushConsumeContext = struct {
sub: ?*Client.Sub,
wrapper: *PushCallbackWrapper,
monitor_future: ?std.Io.Future(void) = null,
io: std.Io = undefined,
/// Stops consumption. Safe to call before deinit.
pub fn stop(self: *PushConsumeContext) void {
if (self.monitor_future) |*future| {
_ = future.cancel(self.io);
self.monitor_future = null;
}
if (self.sub) |s| {
s.deinit();
self.sub = null;
}
}
/// Stops (if not already) and frees resources.
pub fn deinit(self: *PushConsumeContext) void {
self.stop();
self.wrapper.allocator.destroy(
self.wrapper,
);
}
};
fn getNowNs(io: std.Io) u64 {
const ts = std.Io.Timestamp.now(io, .awake);
return @intCast(ts.nanoseconds);
}
fn pushHeartbeatMonitorTask(
wrapper: *PushCallbackWrapper,
heartbeat_ms: u32,
) void {
std.debug.assert(heartbeat_ms > 0);
const io = wrapper.client.io;
const timeout_ns =
@as(u64, heartbeat_ms) * 2 * std.time.ns_per_ms;
var notified = false;
while (true) {
io.sleep(
.fromMilliseconds(heartbeat_ms),
.awake,
) catch |err| {
if (err == error.Canceled) return;
return;
};
const last =
wrapper.last_activity_ns.load(.acquire);
const now = getNowNs(io);
if (now -| last >= timeout_ns) {
if (!notified) {
if (wrapper.err_handler) |eh| {
eh(errors.Error.NoHeartbeat);
}
notified = true;
}
} else {
notified = false;
}
}
}
================================================
FILE: src/jetstream/types.zig
================================================
//! JetStream type definitions for stream/consumer configuration,
//! API responses, and request payloads.
//!
//! All structs use optional fields with null defaults for
//! forward-compatible JSON serialization (omit nulls) and
//! parsing (ignore unknown fields).
const std = @import("std");
const errors = @import("errors.zig");
const headers = @import("../protocol/headers.zig");
pub const ApiErrorJson = errors.ApiErrorJson;
// -- Enums (lowercase tags match NATS JSON wire format) --
pub const RetentionPolicy = enum {
limits,
interest,
workqueue,
};
pub const StorageType = enum { file, memory };
pub const DiscardPolicy = enum { old, new };
pub const StoreCompression = enum { none, s2 };
pub const DeliverPolicy = enum {
all,
last,
new,
by_start_sequence,
by_start_time,
last_per_subject,
};
pub const AckPolicy = enum { none, all, explicit };
pub const ReplayPolicy = enum { instant, original };
pub const SubjectTransform = struct {
src: ?[]const u8 = null,
dest: ?[]const u8 = null,
};
// -- Stream types --
pub const StreamConfig = struct {
name: []const u8,
description: ?[]const u8 = null,
subjects: ?[]const []const u8 = null,
retention: ?RetentionPolicy = null,
max_consumers: ?i64 = null,
max_msgs: ?i64 = null,
max_bytes: ?i64 = null,
max_age: ?i64 = null,
max_msgs_per_subject: ?i64 = null,
max_msg_size: ?i32 = null,
storage: ?StorageType = null,
num_replicas: ?i32 = null,
no_ack: ?bool = null,
duplicate_window: ?i64 = null,
discard: ?DiscardPolicy = null,
discard_new_per_subject: ?bool = null,
sealed: ?bool = null,
deny_delete: ?bool = null,
deny_purge: ?bool = null,
allow_rollup_hdrs: ?bool = null,
allow_direct: ?bool = null,
mirror_direct: ?bool = null,
compression: ?StoreCompression = null,
first_seq: ?u64 = null,
allow_msg_ttl: ?bool = null,
metadata: ?std.json.Value = null,
subject_transform: ?SubjectTransform = null,
};
pub const StreamState = struct {
messages: u64 = 0,
bytes: u64 = 0,
first_seq: u64 = 0,
last_seq: u64 = 0,
consumer_count: i64 = 0,
num_deleted: i64 = 0,
num_subjects: u64 = 0,
};
pub const StreamInfo = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
config: ?StreamConfig = null,
state: ?StreamState = null,
created: ?[]const u8 = null,
ts: ?[]const u8 = null,
};
// -- Consumer types --
pub const ConsumerConfig = struct {
name: ?[]const u8 = null,
durable_name: ?[]const u8 = null,
description: ?[]const u8 = null,
deliver_policy: ?DeliverPolicy = null,
opt_start_seq: ?u64 = null,
ack_policy: ?AckPolicy = null,
ack_wait: ?i64 = null,
max_deliver: ?i64 = null,
filter_subject: ?[]const u8 = null,
filter_subjects: ?[]const []const u8 = null,
replay_policy: ?ReplayPolicy = null,
max_waiting: ?i64 = null,
max_ack_pending: ?i64 = null,
inactive_threshold: ?i64 = null,
num_replicas: ?i32 = null,
headers_only: ?bool = null,
mem_storage: ?bool = null,
// Push consumer fields (v1.1 ready, harmless as null)
deliver_subject: ?[]const u8 = null,
deliver_group: ?[]const u8 = null,
flow_control: ?bool = null,
idle_heartbeat: ?i64 = null,
};
pub const SequenceInfo = struct {
consumer_seq: u64 = 0,
stream_seq: u64 = 0,
};
pub const ConsumerInfo = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
stream_name: ?[]const u8 = null,
name: ?[]const u8 = null,
config: ?ConsumerConfig = null,
delivered: ?SequenceInfo = null,
ack_floor: ?SequenceInfo = null,
num_ack_pending: i64 = 0,
num_redelivered: i64 = 0,
num_waiting: i64 = 0,
num_pending: u64 = 0,
created: ?[]const u8 = null,
ts: ?[]const u8 = null,
};
pub const CreateConsumerRequest = struct {
stream_name: []const u8,
config: ConsumerConfig,
action: ?[]const u8 = null,
};
// -- Publish types --
pub const PubAck = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
stream: ?[]const u8 = null,
seq: u64 = 0,
duplicate: ?bool = null,
domain: ?[]const u8 = null,
};
pub const PublishOpts = struct {
msg_id: ?[]const u8 = null,
expected_stream: ?[]const u8 = null,
expected_last_seq: ?u64 = null,
expected_last_msg_id: ?[]const u8 = null,
expected_last_subj_seq: ?u64 = null,
ttl: ?[]const u8 = null,
};
/// Pre-built JetStream publish message with user headers.
///
/// Passed to `JetStream.publishMsg()` for publishing messages
/// with arbitrary user-supplied headers alongside JetStream-
/// specific headers from `opts`. On header-key collision
/// (case-insensitive per NATS convention), JetStream headers
/// from `opts` override the user-supplied value -- matching
/// Go client `PublishMsg` semantics.
pub const JsPublishMsg = struct {
subject: []const u8,
payload: []const u8,
headers: ?[]const headers.Entry = null,
opts: PublishOpts = .{},
};
// -- Pull types --
pub const PullRequest = struct {
batch: ?i64 = null,
expires: ?i64 = null,
no_wait: ?bool = null,
max_bytes: ?i64 = null,
idle_heartbeat: ?i64 = null,
};
// -- Delete response --
pub const DeleteResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
success: bool = false,
};
// -- Purge response --
pub const PurgeRequest = struct {
filter: ?[]const u8 = null,
seq: ?u64 = null,
keep: ?u64 = null,
};
pub const PurgeResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
success: bool = false,
purged: u64 = 0,
};
// -- Listing responses (paginated) --
pub const StreamNamesResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
total: u64 = 0,
offset: u64 = 0,
limit: u64 = 0,
streams: ?[]const []const u8 = null,
};
pub const StreamListResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
total: u64 = 0,
offset: u64 = 0,
limit: u64 = 0,
streams: ?[]const StreamInfo = null,
};
pub const ConsumerNamesResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
total: u64 = 0,
offset: u64 = 0,
limit: u64 = 0,
consumers: ?[]const []const u8 = null,
};
pub const ConsumerListResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
total: u64 = 0,
offset: u64 = 0,
limit: u64 = 0,
consumers: ?[]const ConsumerInfo = null,
};
/// Request body for paginated listing APIs.
pub const ListRequest = struct {
offset: u64 = 0,
subject: ?[]const u8 = null,
};
// -- Key-Value types --
pub const KeyValueConfig = struct {
bucket: []const u8,
description: ?[]const u8 = null,
max_value_size: ?i32 = null,
history: ?u8 = null,
ttl: ?i64 = null,
max_bytes: ?i64 = null,
storage: ?StorageType = null,
replicas: ?i32 = null,
};
pub const KeyValueOp = enum { put, delete, purge };
/// Options for KV watch operations.
pub const WatchOpts = struct {
/// Deliver all historical values, not just latest.
include_history: bool = false,
/// Skip entries with delete/purge markers.
ignore_deletes: bool = false,
/// Only deliver new updates, skip initial values.
updates_only: bool = false,
/// Only deliver metadata, not values.
meta_only: bool = false,
/// Resume watching from a specific revision.
resume_from_revision: ?u64 = null,
};
pub const KeyValueEntry = struct {
bucket: []const u8,
key: []const u8,
value: []const u8,
revision: u64,
operation: KeyValueOp,
/// Allocator used for owned key (null = not owned).
key_allocator: ?std.mem.Allocator = null,
/// Allocator used for owned value (null = not owned).
value_allocator: ?std.mem.Allocator = null,
/// Frees owned key/value buffers if allocated.
pub fn deinit(self: *KeyValueEntry) void {
if (self.key_allocator) |a| {
if (self.key.len > 0) a.free(self.key);
self.key_allocator = null;
}
if (self.value_allocator) |a| {
if (self.value.len > 0) a.free(self.value);
self.value_allocator = null;
}
}
};
// -- Stream MSG.GET types --
pub const MsgGetRequest = struct {
last_by_subj: ?[]const u8 = null,
seq: ?u64 = null,
};
pub const MsgGetResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
message: ?StoredMsg = null,
};
pub const StoredMsg = struct {
subject: ?[]const u8 = null,
seq: u64 = 0,
data: ?[]const u8 = null,
hdrs: ?[]const u8 = null,
time: ?[]const u8 = null,
};
// -- Key-Value status --
pub const KeyValueStatus = struct {
bucket: []const u8 = "",
values: u64 = 0,
history: i64 = 1,
ttl: i64 = 0,
bytes: u64 = 0,
backing_store: StorageType = .file,
is_compressed: bool = false,
};
// -- Stream MSG.DELETE types --
pub const MsgDeleteRequest = struct {
seq: u64,
no_erase: ?bool = null,
};
// -- Consumer pause types --
pub const ConsumerPauseRequest = struct {
pause_until: ?[]const u8 = null,
};
pub const ConsumerPauseResponse = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
paused: bool = false,
pause_until: ?[]const u8 = null,
pause_remaining: ?i64 = null,
};
// -- Consumer unpin types --
pub const ConsumerUnpinRequest = struct {
group: []const u8,
};
// -- Account info --
pub const AccountInfo = struct {
type: ?[]const u8 = null,
@"error": ?ApiErrorJson = null,
memory: u64 = 0,
storage: u64 = 0,
streams: u64 = 0,
consumers: u64 = 0,
limits: ?AccountLimits = null,
api: ?APIStats = null,
domain: ?[]const u8 = null,
};
pub const AccountLimits = struct {
max_memory: i64 = 0,
max_storage: i64 = 0,
max_streams: i64 = 0,
max_consumers: i64 = 0,
};
pub const APIStats = struct {
total: u64 = 0,
errors: u64 = 0,
};
// -- Generic response wrapper --
/// Wraps a parsed JSON response. All string slices in
/// `value` point into the parsed arena -- they become
/// invalid after `deinit()`. Copy any strings you need
/// to keep: `const s = try alloc.dupe(u8, val.name);`
/// Caller MUST call `deinit()`.
pub fn Response(comptime T: type) type {
return struct {
const Self = @This();
value: T,
_parsed: std.json.Parsed(T),
pub fn deinit(self: *Self) void {
self._parsed.deinit();
}
};
}
// -- JSON helpers --
const json_stringify_opts: std.json.Stringify.Options = .{
.emit_null_optional_fields = false,
};
const json_parse_opts: std.json.ParseOptions = .{
.ignore_unknown_fields = true,
.allocate = .alloc_always,
};
/// Serializes a value to JSON, omitting null optional fields.
pub fn jsonStringify(
allocator: std.mem.Allocator,
value: anytype,
) error{OutOfMemory}![]u8 {
return std.json.Stringify.valueAlloc(
allocator,
value,
json_stringify_opts,
);
}
/// Parses JSON into type T, ignoring unknown fields.
pub fn jsonParse(
comptime T: type,
allocator: std.mem.Allocator,
data: []const u8,
) std.json.ParseError(std.json.Scanner)!std.json.Parsed(T) {
return std.json.parseFromSlice(
T,
allocator,
data,
json_parse_opts,
);
}
// -- Tests --
test "StreamConfig JSON round-trip" {
const alloc = std.testing.allocator;
const config = StreamConfig{
.name = "TEST",
.subjects = &.{"test.>"},
.retention = .limits,
.storage = .file,
.max_msgs = 1000,
};
const json = try jsonStringify(alloc, config);
defer alloc.free(json);
var parsed = try jsonParse(StreamConfig, alloc, json);
defer parsed.deinit();
const v = parsed.value;
try std.testing.expectEqualStrings("TEST", v.name);
try std.testing.expectEqual(RetentionPolicy.limits, v.retention.?);
try std.testing.expectEqual(StorageType.file, v.storage.?);
try std.testing.expectEqual(@as(i64, 1000), v.max_msgs.?);
try std.testing.expect(v.subjects != null);
try std.testing.expectEqual(@as(usize, 1), v.subjects.?.len);
}
test "ConsumerConfig JSON round-trip" {
const alloc = std.testing.allocator;
const config = ConsumerConfig{
.name = "my-consumer",
.durable_name = "my-consumer",
.ack_policy = .explicit,
.deliver_policy = .all,
.max_ack_pending = 1000,
};
const json = try jsonStringify(alloc, config);
defer alloc.free(json);
var parsed = try jsonParse(ConsumerConfig, alloc, json);
defer parsed.deinit();
const v = parsed.value;
try std.testing.expectEqualStrings("my-consumer", v.name.?);
try std.testing.expectEqual(AckPolicy.explicit, v.ack_policy.?);
try std.testing.expectEqual(
DeliverPolicy.all,
v.deliver_policy.?,
);
}
test "PubAck parse with error" {
const alloc = std.testing.allocator;
const json =
\\{"type":"io.nats.jetstream.api.v1.pub_ack",
\\"error":{"code":503,"err_code":10076,
\\"description":"jetstream not enabled"}}
;
var parsed = try jsonParse(PubAck, alloc, json);
defer parsed.deinit();
const v = parsed.value;
try std.testing.expect(v.@"error" != null);
try std.testing.expectEqual(@as(u16, 503), v.@"error".?.code);
try std.testing.expectEqual(
@as(u16, 10076),
v.@"error".?.err_code,
);
}
test "DeleteResponse parse success" {
const alloc = std.testing.allocator;
const json = "{\"success\":true}";
var parsed = try jsonParse(DeleteResponse, alloc, json);
defer parsed.deinit();
try std.testing.expect(parsed.value.success);
}
test "null optional fields omitted in JSON" {
const alloc = std.testing.allocator;
const config = StreamConfig{ .name = "TEST" };
const json = try jsonStringify(alloc, config);
defer alloc.free(json);
// Should not contain "retention" since it's null
try std.testing.expect(
std.mem.indexOf(u8, json, "retention") == null,
);
// Should contain "name"
try std.testing.expect(
std.mem.indexOf(u8, json, "name") != null,
);
}
test "StreamNamesResponse JSON round-trip" {
const alloc = std.testing.allocator;
const json =
\\{"total":3,"offset":0,"limit":1024,
\\"streams":["S1","S2","S3"]}
;
var parsed = try jsonParse(
StreamNamesResponse,
alloc,
json,
);
defer parsed.deinit();
const v = parsed.value;
try std.testing.expectEqual(@as(u64, 3), v.total);
try std.testing.expectEqual(@as(u64, 0), v.offset);
try std.testing.expect(v.streams != null);
try std.testing.expectEqual(
@as(usize, 3),
v.streams.?.len,
);
try std.testing.expectEqualStrings(
"S1",
v.streams.?[0],
);
}
test "AccountInfo JSON round-trip" {
const alloc = std.testing.allocator;
const json =
\\{"memory":1024,"storage":4096,"streams":2,
\\"consumers":5,"limits":{"max_memory":-1,
\\"max_storage":-1,"max_streams":-1,
\\"max_consumers":-1},"api":{"total":42,
\\"errors":1}}
;
var parsed = try jsonParse(AccountInfo, alloc, json);
defer parsed.deinit();
const v = parsed.value;
try std.testing.expectEqual(@as(u64, 1024), v.memory);
try std.testing.expectEqual(@as(u64, 4096), v.storage);
try std.testing.expectEqual(@as(u64, 2), v.streams);
try std.testing.expectEqual(@as(u64, 5), v.consumers);
try std.testing.expect(v.limits != null);
try std.testing.expect(v.api != null);
try std.testing.expectEqual(
@as(u64, 42),
v.api.?.total,
);
}
test "ConsumerConfig with push fields serializes" {
const alloc = std.testing.allocator;
const config = ConsumerConfig{
.name = "push-test",
.deliver_subject = "deliver.test",
.deliver_group = "grp",
};
const json = try jsonStringify(alloc, config);
defer alloc.free(json);
try std.testing.expect(
std.mem.indexOf(u8, json, "deliver_subject") !=
null,
);
try std.testing.expect(
std.mem.indexOf(u8, json, "deliver_group") !=
null,
);
var parsed = try jsonParse(
ConsumerConfig,
alloc,
json,
);
defer parsed.deinit();
try std.testing.expectEqualStrings(
"deliver.test",
parsed.value.deliver_subject.?,
);
}
test "MsgDeleteRequest serialization" {
const alloc = std.testing.allocator;
// With no_erase
const json1 = try jsonStringify(alloc, MsgDeleteRequest{
.seq = 42,
.no_erase = true,
});
defer alloc.free(json1);
try std.testing.expect(
std.mem.indexOf(u8, json1, "\"seq\":42") != null,
);
try std.testing.expect(
std.mem.indexOf(u8, json1, "no_erase") != null,
);
// Without no_erase (null omitted)
const json2 = try jsonStringify(
alloc,
MsgDeleteRequest{ .seq = 7 },
);
defer alloc.free(json2);
try std.testing.expect(
std.mem.indexOf(u8, json2, "no_erase") == null,
);
try std.testing.expect(
std.mem.indexOf(u8, json2, "\"seq\":7") != null,
);
}
test "ConsumerPauseRequest serialization" {
const alloc = std.testing.allocator;
const json1 = try jsonStringify(
alloc,
ConsumerPauseRequest{
.pause_until = "2026-04-01T00:00:00Z",
},
);
defer alloc.free(json1);
try std.testing.expect(
std.mem.indexOf(u8, json1, "pause_until") !=
null,
);
const json2 = try jsonStringify(
alloc,
ConsumerPauseRequest{},
);
defer alloc.free(json2);
try std.testing.expect(
std.mem.indexOf(u8, json2, "pause_until") ==
null,
);
}
test "ConsumerPauseResponse parsing" {
const alloc = std.testing.allocator;
const json =
\\{"paused":true,
\\"pause_until":"2026-04-01T00:00:00Z",
\\"pause_remaining":3600000000000}
;
var parsed = try jsonParse(
ConsumerPauseResponse,
alloc,
json,
);
defer parsed.deinit();
try std.testing.expect(parsed.value.paused);
try std.testing.expect(
parsed.value.pause_until != null,
);
try std.testing.expectEqual(
@as(i64, 3600000000000),
parsed.value.pause_remaining.?,
);
}
test "PublishOpts TTL field" {
const alloc = std.testing.allocator;
const json = try jsonStringify(
alloc,
PublishOpts{ .ttl = "5s" },
);
defer alloc.free(json);
try std.testing.expect(
std.mem.indexOf(u8, json, "\"ttl\":\"5s\"") !=
null,
);
}
test "StreamConfig allow_msg_ttl" {
const alloc = std.testing.allocator;
const json = try jsonStringify(alloc, StreamConfig{
.name = "TTL_TEST",
.allow_msg_ttl = true,
});
defer alloc.free(json);
try std.testing.expect(
std.mem.indexOf(u8, json, "allow_msg_ttl") !=
null,
);
}
test "CreateConsumerRequest action field" {
const alloc = std.testing.allocator;
// With action = "create"
const json1 = try jsonStringify(
alloc,
CreateConsumerRequest{
.stream_name = "S",
.config = .{ .name = "C" },
.action = "create",
},
);
defer alloc.free(json1);
try std.testing.expect(
std.mem.indexOf(
u8,
json1,
"\"action\":\"create\"",
) != null,
);
// With action = null (omitted for createOrUpdate)
const json2 = try jsonStringify(
alloc,
CreateConsumerRequest{
.stream_name = "S",
.config = .{ .name = "C" },
},
);
defer alloc.free(json2);
try std.testing.expect(
std.mem.indexOf(u8, json2, "action") == null,
);
}
================================================
FILE: src/jetstream.zig
================================================
//! JetStream -- NATS persistence and streaming layer.
//!
//! Provides stream/consumer CRUD, publish with ack, pull-based
//! message consumption, and message acknowledgment protocol over
//! core NATS request/reply.
const std = @import("std");
pub const JetStream = @import("jetstream/JetStream.zig");
pub const types = @import("jetstream/types.zig");
pub const errors = @import("jetstream/errors.zig");
pub const consumer = @import("jetstream/consumer.zig");
const message = @import("jetstream/message.zig");
pub const JsMsg = message.JsMsg;
pub const MsgMetadata = message.MsgMetadata;
const pull_mod = @import("jetstream/pull.zig");
pub const PullSubscription = pull_mod.PullSubscription;
pub const MessagesContext = pull_mod.MessagesContext;
const push_mod = @import("jetstream/push.zig");
pub const PushSubscription = push_mod.PushSubscription;
const ordered_mod = @import("jetstream/ordered.zig");
pub const OrderedConsumer = ordered_mod.OrderedConsumer;
const kv_mod = @import("jetstream/kv.zig");
pub const KeyValue = kv_mod.KeyValue;
pub const KvWatcher = kv_mod.KvWatcher;
pub const KeyLister = kv_mod.KeyValue.KeyLister;
pub const KeyValueConfig = types.KeyValueConfig;
pub const KeyValueEntry = types.KeyValueEntry;
pub const KeyValueOp = types.KeyValueOp;
pub const WatchOpts = types.WatchOpts;
// Consumer abstractions
pub const JsMsgHandler = consumer.JsMsgHandler;
pub const ConsumeContext = consumer.ConsumeContext;
pub const ConsumeOpts = consumer.ConsumeOpts;
pub const HeartbeatMonitor = consumer.HeartbeatMonitor;
// Convenience re-exports
pub const StreamConfig = types.StreamConfig;
pub const ConsumerConfig = types.ConsumerConfig;
pub const StreamInfo = types.StreamInfo;
pub const ConsumerInfo = types.ConsumerInfo;
pub const PubAck = types.PubAck;
pub const ApiError = errors.ApiError;
pub const Response = types.Response;
pub const AccountInfo = types.AccountInfo;
pub const ConsumerPauseResponse = types.ConsumerPauseResponse;
pub const PublishOpts = types.PublishOpts;
pub const MsgGetResponse = types.MsgGetResponse;
pub const KeyValueStatus = types.KeyValueStatus;
const async_pub = @import("jetstream/async_publish.zig");
pub const AsyncPublisher = async_pub.AsyncPublisher;
pub const PubAckFuture = async_pub.PubAckFuture;
test {
std.testing.refAllDecls(@This());
}
================================================
FILE: src/memory/sidmap.zig
================================================
//! SidMap - Zero-Alloc Subscription ID Router
//!
//! Pre-allocated open-addressing hash map optimized for O(1) subscription
//! routing. Uses splitmix64 hash and power-of-two capacity for fast lookups.
//! Inspired by io_uring NATS client design.
//!
//! Zero allocations - caller provides pre-allocated arrays.
const std = @import("std");
const assert = std.debug.assert;
/// Sentinel values for slot state.
pub const EMPTY: u16 = 0xFFFF;
pub const TOMB: u16 = 0xFFFE;
/// Maximum valid slot index (leaves room for EMPTY/TOMB sentinels).
pub const MAX_SLOT: u16 = 0xFFFD;
/// Zero-allocation subscription ID to slot index map.
///
/// Uses open-addressing with linear probing and splitmix64 hash.
/// Caller provides pre-allocated keys/vals arrays at init.
/// Maximum 70% load factor enforced to maintain O(1) performance.
pub const SidMap = struct {
keys: []u64,
vals: []u16,
cap: u32,
len: u32,
/// Initialize SidMap with pre-allocated arrays.
/// Capacity must be power of 2 and match array lengths.
pub fn init(keys: []u64, vals: []u16) SidMap {
assert(keys.len == vals.len);
assert(keys.len > 0);
assert(isPowerOfTwo(keys.len));
assert(keys.len <= std.math.maxInt(u32));
@memset(vals, EMPTY);
return .{
.keys = keys,
.vals = vals,
.cap = @intCast(keys.len),
.len = 0,
};
}
/// O(1) lookup - returns slot index for SID or null if not found.
/// Marked inline for hot path performance.
pub inline fn get(self: *const SidMap, sid: u64) ?u16 {
assert(self.cap > 0);
assert(isPowerOfTwo(self.cap));
const mask = self.cap - 1;
var idx: u32 = @intCast(mix64(sid) & mask);
var probes: u32 = 0;
while (probes < self.cap) : (probes += 1) {
const v = self.vals[idx];
if (v == EMPTY) {
return null;
}
if (v != TOMB and self.keys[idx] == sid) {
return v;
}
idx = (idx + 1) & mask;
}
return null;
}
/// Insert or update SID -> slot mapping.
/// Returns error if map is full (load > 70%).
pub fn put(self: *SidMap, sid: u64, slot: u16) error{MapFull}!void {
assert(slot <= MAX_SLOT);
assert(self.cap > 0);
const max_load = self.cap * 7 / 10;
if (self.len >= max_load) {
return error.MapFull;
}
const mask = self.cap - 1;
var idx: u32 = @intCast(mix64(sid) & mask);
var tomb_idx: ?u32 = null;
var probes: u32 = 0;
while (probes < self.cap) : (probes += 1) {
const v = self.vals[idx];
if (v == EMPTY) {
const insert_idx = tomb_idx orelse idx;
self.keys[insert_idx] = sid;
self.vals[insert_idx] = slot;
self.len += 1;
return;
}
if (v == TOMB) {
if (tomb_idx == null) {
tomb_idx = idx;
}
} else if (self.keys[idx] == sid) {
self.vals[idx] = slot;
return;
}
idx = (idx + 1) & mask;
}
// REVIEWED(2025-03): Load factor (70%) guarantees at
// least one EMPTY slot exists within probing distance.
// Tombstones don't count toward len, so len < max_load
// ensures the loop always finds an EMPTY or matching
// slot before exhausting cap probes.
unreachable;
}
/// Remove SID from map. Returns true if found and removed.
pub fn remove(self: *SidMap, sid: u64) bool {
assert(self.cap > 0);
const mask = self.cap - 1;
var idx: u32 = @intCast(mix64(sid) & mask);
var probes: u32 = 0;
while (probes < self.cap) : (probes += 1) {
const v = self.vals[idx];
if (v == EMPTY) {
return false;
}
if (v != TOMB and self.keys[idx] == sid) {
self.vals[idx] = TOMB;
self.len -= 1;
return true;
}
idx = (idx + 1) & mask;
}
return false;
}
/// Returns current number of entries.
pub fn count(self: *const SidMap) u32 {
return self.len;
}
/// Returns true if map is empty.
pub fn isEmpty(self: *const SidMap) bool {
return self.len == 0;
}
/// Clear all entries (reset to initial state).
pub fn clear(self: *SidMap) void {
@memset(self.vals, EMPTY);
self.len = 0;
}
};
/// splitmix64 hash function - fast 64-bit mixer.
/// 5 operations, good avalanche properties.
inline fn mix64(x0: u64) u64 {
var x = x0 +% 0x9E3779B97F4A7C15;
x = (x ^ (x >> 30)) *% 0xBF58476D1CE4E5B9;
x = (x ^ (x >> 27)) *% 0x94D049BB133111EB;
return x ^ (x >> 31);
}
/// Check if n is a power of two.
inline fn isPowerOfTwo(n: usize) bool {
return n > 0 and (n & (n - 1)) == 0;
}
test {
_ = @import("sidmap_test.zig");
}
================================================
FILE: src/memory/sidmap_test.zig
================================================
//! SidMap Edge Case Tests
//!
//! - Sentinel value handling (EMPTY/TOMB corruption)
//! - Hash collision stress testing
//! - Tombstone accumulation and reuse
//! - Load factor boundary conditions
//! - Edge values (SID=0, slot=0, max values)
const std = @import("std");
const sidmap = @import("sidmap.zig");
const SidMap = sidmap.SidMap;
const EMPTY = sidmap.EMPTY;
const TOMB = sidmap.TOMB;
const MAX_SLOT = sidmap.MAX_SLOT;
test "SidMap basic operations" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try std.testing.expect(map.isEmpty());
try std.testing.expectEqual(@as(u32, 0), map.count());
try map.put(100, 0);
try map.put(200, 1);
try map.put(300, 2);
try std.testing.expectEqual(@as(u32, 3), map.count());
try std.testing.expectEqual(@as(u16, 0), map.get(100).?);
try std.testing.expectEqual(@as(u16, 1), map.get(200).?);
try std.testing.expectEqual(@as(u16, 2), map.get(300).?);
try std.testing.expect(map.get(999) == null);
try map.put(200, 42);
try std.testing.expectEqual(@as(u16, 42), map.get(200).?);
try std.testing.expectEqual(@as(u32, 3), map.count());
try std.testing.expect(map.remove(200));
try std.testing.expect(map.get(200) == null);
try std.testing.expectEqual(@as(u32, 2), map.count());
try std.testing.expect(!map.remove(999));
}
test "SidMap tombstone reuse" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try map.put(3, 2);
try std.testing.expect(map.remove(2));
try map.put(4, 3);
try std.testing.expectEqual(@as(u32, 3), map.count());
try std.testing.expectEqual(@as(u16, 0), map.get(1).?);
try std.testing.expect(map.get(2) == null);
try std.testing.expectEqual(@as(u16, 2), map.get(3).?);
try std.testing.expectEqual(@as(u16, 3), map.get(4).?);
}
test "SidMap load factor limit" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 8 = 5 entries max
try map.put(1, 0);
try map.put(2, 1);
try map.put(3, 2);
try map.put(4, 3);
try map.put(5, 4);
// 6th should fail
try std.testing.expectError(error.MapFull, map.put(6, 5));
}
test "SidMap clear" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try std.testing.expectEqual(@as(u32, 2), map.count());
map.clear();
try std.testing.expect(map.isEmpty());
try std.testing.expect(map.get(1) == null);
try std.testing.expect(map.get(2) == null);
}
test "SidMap large capacity" {
var keys: [512]u64 = undefined;
var vals: [512]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// Insert many entries
var i: u64 = 0;
while (i < 300) : (i += 1) {
try map.put(i * 1000, @intCast(i));
}
try std.testing.expectEqual(@as(u32, 300), map.count());
i = 0;
while (i < 300) : (i += 1) {
try std.testing.expectEqual(@as(u16, @intCast(i)), map.get(i * 1000).?);
}
}
test "SidMap SID zero" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(0, 42);
try std.testing.expectEqual(@as(u16, 42), map.get(0).?);
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expect(map.remove(0));
try std.testing.expect(map.get(0) == null);
}
test "SidMap SID u64 max" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
const max_sid: u64 = std.math.maxInt(u64);
try map.put(max_sid, 99);
try std.testing.expectEqual(@as(u16, 99), map.get(max_sid).?);
}
test "SidMap SID one" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try std.testing.expectEqual(@as(u16, 0), map.get(1).?);
}
test "SidMap consecutive SIDs" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try map.put(3, 2);
try map.put(4, 3);
try map.put(5, 4);
try std.testing.expectEqual(@as(u16, 0), map.get(1).?);
try std.testing.expectEqual(@as(u16, 1), map.get(2).?);
try std.testing.expectEqual(@as(u16, 2), map.get(3).?);
try std.testing.expectEqual(@as(u16, 3), map.get(4).?);
try std.testing.expectEqual(@as(u16, 4), map.get(5).?);
}
test "SidMap slot zero" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(100, 0);
try std.testing.expectEqual(@as(u16, 0), map.get(100).?);
}
test "SidMap slot MAX_SLOT" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(100, MAX_SLOT);
try std.testing.expectEqual(MAX_SLOT, map.get(100).?);
}
// Sentinel protection: put() asserts slot <= MAX_SLOT to prevent TOMB/EMPTY
// corruption. Debug builds fail assertion; release builds strip assert.
test "SidMap sentinel values documented" {
try std.testing.expectEqual(@as(u16, 0xFFFF), EMPTY);
try std.testing.expectEqual(@as(u16, 0xFFFE), TOMB);
try std.testing.expectEqual(@as(u16, 0xFFFD), MAX_SLOT);
}
test "SidMap valid slot range" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// Verify full valid range: 0 to MAX_SLOT (0xFFFD)
try map.put(1, 0); // Minimum valid
try map.put(2, MAX_SLOT); // Maximum valid
try std.testing.expectEqual(@as(u16, 0), map.get(1).?);
try std.testing.expectEqual(MAX_SLOT, map.get(2).?);
}
test "SidMap exact load factor boundary" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 8 = 5.6, truncated to 5
// So max_load = 5, inserts allowed while len < 5
try map.put(1, 0); // len=1
try map.put(2, 1); // len=2
try map.put(3, 2); // len=3
try map.put(4, 3); // len=4
try map.put(5, 4); // len=5
try std.testing.expectEqual(@as(u32, 5), map.count());
try std.testing.expectError(error.MapFull, map.put(6, 5));
}
test "SidMap load factor after removes" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try map.put(3, 2);
try map.put(4, 3);
try map.put(5, 4);
try std.testing.expect(map.remove(1));
try std.testing.expect(map.remove(2));
try std.testing.expectEqual(@as(u32, 3), map.count());
try map.put(6, 5);
try map.put(7, 6);
try std.testing.expectEqual(@as(u32, 5), map.count());
try std.testing.expectError(error.MapFull, map.put(8, 7));
}
test "SidMap load factor 16 capacity" {
var keys: [16]u64 = undefined;
var vals: [16]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 16 = 11.2, truncated to 11
var i: u64 = 0;
while (i < 11) : (i += 1) {
try map.put(i, @intCast(i));
}
try std.testing.expectEqual(@as(u32, 11), map.count());
// 12th should fail
try std.testing.expectError(error.MapFull, map.put(11, 11));
}
test "SidMap load factor 256 capacity" {
var keys: [256]u64 = undefined;
var vals: [256]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 256 = 179.2, truncated to 179
var i: u64 = 0;
while (i < 179) : (i += 1) {
try map.put(i * 7, @intCast(i)); // Spread out SIDs
}
try std.testing.expectEqual(@as(u32, 179), map.count());
// 180th should fail
try std.testing.expectError(error.MapFull, map.put(9999, 179));
}
test "SidMap tombstone lookup continues probing" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// Insert entries that will cluster (hash not controllable, insert many)
try map.put(100, 0);
try map.put(200, 1);
try map.put(300, 2);
try std.testing.expect(map.remove(200));
try std.testing.expectEqual(@as(u16, 2), map.get(300).?);
}
test "SidMap tombstone reuse on insert" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try map.put(3, 2);
try map.put(4, 3);
try map.put(5, 4);
try std.testing.expect(map.remove(1));
try std.testing.expect(map.remove(2));
try std.testing.expect(map.remove(3));
try std.testing.expect(map.remove(4));
try std.testing.expect(map.remove(5));
try std.testing.expectEqual(@as(u32, 0), map.count());
try map.put(10, 0);
try map.put(20, 1);
try map.put(30, 2);
try map.put(40, 3);
try map.put(50, 4);
try std.testing.expectEqual(@as(u32, 5), map.count());
try std.testing.expectEqual(@as(u16, 0), map.get(10).?);
try std.testing.expectEqual(@as(u16, 4), map.get(50).?);
}
test "SidMap many insert remove cycles" {
var keys: [64]u64 = undefined;
var vals: [64]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
const max_entries = 44;
var i: u64 = 0;
while (i < max_entries) : (i += 1) {
try map.put(i, @intCast(i));
}
i = 0;
while (i < max_entries) : (i += 1) {
try std.testing.expect(map.remove(i));
}
i = 1000;
while (i < 1000 + max_entries) : (i += 1) {
try map.put(i, @intCast(i - 1000));
}
try std.testing.expectEqual(@as(u32, max_entries), map.count());
try std.testing.expectEqual(@as(u16, 0), map.get(1000).?);
try std.testing.expectEqual(@as(u16, 43), map.get(1043).?);
try std.testing.expect(map.get(0) == null);
try std.testing.expect(map.get(43) == null);
}
test "SidMap tombstone does not affect count" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try std.testing.expectEqual(@as(u32, 2), map.count());
try std.testing.expect(map.remove(1));
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expect(map.get(1) == null);
try std.testing.expectEqual(@as(u16, 1), map.get(2).?);
}
test "SidMap double remove same SID" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(100, 42);
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expect(map.remove(100));
try std.testing.expectEqual(@as(u32, 0), map.count());
try std.testing.expect(!map.remove(100));
try std.testing.expectEqual(@as(u32, 0), map.count());
}
test "SidMap update existing does not change count" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(100, 0);
try std.testing.expectEqual(@as(u32, 1), map.count());
try map.put(100, 1);
try map.put(100, 2);
try map.put(100, 42);
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expectEqual(@as(u16, 42), map.get(100).?);
}
test "SidMap put after remove same SID" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(100, 0);
try std.testing.expect(map.remove(100));
try std.testing.expect(map.get(100) == null);
try map.put(100, 99);
try std.testing.expectEqual(@as(u16, 99), map.get(100).?);
try std.testing.expectEqual(@as(u32, 1), map.count());
}
test "SidMap get on empty" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try std.testing.expect(map.get(0) == null);
try std.testing.expect(map.get(1) == null);
try std.testing.expect(map.get(std.math.maxInt(u64)) == null);
}
test "SidMap remove on empty" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try std.testing.expect(!map.remove(0));
try std.testing.expect(!map.remove(100));
try std.testing.expectEqual(@as(u32, 0), map.count());
}
test "SidMap clear on empty" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
map.clear();
try std.testing.expect(map.isEmpty());
}
test "SidMap clear after operations" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try std.testing.expect(map.remove(1));
map.clear();
try std.testing.expect(map.isEmpty());
try std.testing.expect(map.get(1) == null);
try std.testing.expect(map.get(2) == null);
try map.put(10, 0);
try map.put(20, 1);
try map.put(30, 2);
try map.put(40, 3);
try map.put(50, 4);
try std.testing.expectEqual(@as(u32, 5), map.count());
}
test "SidMap remove does not break probe chain" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(10, 0);
try map.put(20, 1);
try map.put(30, 2);
try map.put(40, 3);
try map.put(50, 4);
try std.testing.expect(map.remove(20));
try std.testing.expect(map.remove(40));
try std.testing.expectEqual(@as(u16, 0), map.get(10).?);
try std.testing.expectEqual(@as(u16, 2), map.get(30).?);
try std.testing.expectEqual(@as(u16, 4), map.get(50).?);
try std.testing.expect(map.get(20) == null);
try std.testing.expect(map.get(40) == null);
}
test "SidMap insert after remove maintains integrity" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try map.put(1, 0);
try map.put(2, 1);
try map.put(3, 2);
try map.put(4, 3);
try map.put(5, 4);
try std.testing.expect(map.remove(2));
try std.testing.expect(map.remove(4));
try map.put(6, 5);
try map.put(7, 6);
try std.testing.expectEqual(@as(u16, 0), map.get(1).?);
try std.testing.expectEqual(@as(u16, 2), map.get(3).?);
try std.testing.expectEqual(@as(u16, 4), map.get(5).?);
try std.testing.expectEqual(@as(u16, 5), map.get(6).?);
try std.testing.expectEqual(@as(u16, 6), map.get(7).?);
try std.testing.expect(map.get(2) == null);
try std.testing.expect(map.get(4) == null);
}
test "SidMap minimum capacity 2" {
var keys: [2]u64 = undefined;
var vals: [2]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 2 = 1.4, truncated to 1
try map.put(100, 0);
try std.testing.expectEqual(@as(u32, 1), map.count());
// 2nd should fail
try std.testing.expectError(error.MapFull, map.put(200, 1));
}
test "SidMap capacity 4" {
var keys: [4]u64 = undefined;
var vals: [4]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 4 = 2.8, truncated to 2
try map.put(1, 0);
try map.put(2, 1);
try std.testing.expectEqual(@as(u32, 2), map.count());
// 3rd should fail
try std.testing.expectError(error.MapFull, map.put(3, 2));
}
test "SidMap capacity 1024" {
var keys: [1024]u64 = undefined;
var vals: [1024]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
// 70% of 1024 = 716.8, truncated to 716
var i: u64 = 0;
while (i < 716) : (i += 1) {
try map.put(i, @intCast(i & 0xFFFF));
}
try std.testing.expectEqual(@as(u32, 716), map.count());
// 717th should fail
try std.testing.expectError(error.MapFull, map.put(99999, 0));
}
test "SidMap lookup non-existent after many tombstones" {
var keys: [64]u64 = undefined;
var vals: [64]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
var i: u64 = 0;
while (i < 44) : (i += 1) {
try map.put(i, @intCast(i));
}
i = 0;
while (i < 44) : (i += 1) {
try std.testing.expect(map.remove(i));
}
try std.testing.expectEqual(@as(u32, 0), map.count());
try std.testing.expect(map.get(100) == null);
try std.testing.expect(map.get(999) == null);
try std.testing.expect(map.get(0) == null);
try map.put(1000, 42);
try std.testing.expectEqual(@as(u16, 42), map.get(1000).?);
}
test "SidMap alternating insert remove stress" {
var keys: [32]u64 = undefined;
var vals: [32]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
var cycle: u32 = 0;
while (cycle < 100) : (cycle += 1) {
const sid = @as(u64, cycle) * 1000;
try map.put(sid, @intCast(cycle & 0xFFFF));
try std.testing.expect(map.remove(sid));
}
try std.testing.expectEqual(@as(u32, 0), map.count());
try map.put(1, 0);
try std.testing.expectEqual(@as(u16, 0), map.get(1).?);
}
test "SidMap sequential SIDs distribute well" {
var keys: [256]u64 = undefined;
var vals: [256]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
var i: u64 = 1;
while (i <= 170) : (i += 1) {
try map.put(i, @intCast(i));
}
i = 1;
while (i <= 170) : (i += 1) {
try std.testing.expectEqual(@as(u16, @intCast(i)), map.get(i).?);
}
}
test "SidMap sparse SIDs" {
var keys: [64]u64 = undefined;
var vals: [64]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
const sids = [_]u64{
1,
1000,
1000000,
1000000000,
std.math.maxInt(u64),
std.math.maxInt(u64) - 1,
std.math.maxInt(u64) / 2,
};
for (sids, 0..) |sid, idx| {
try map.put(sid, @intCast(idx));
}
for (sids, 0..) |sid, idx| {
try std.testing.expectEqual(@as(u16, @intCast(idx)), map.get(sid).?);
}
}
test "SidMap isEmpty after fill and empty" {
var keys: [8]u64 = undefined;
var vals: [8]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try std.testing.expect(map.isEmpty());
try map.put(1, 0);
try std.testing.expect(!map.isEmpty());
try std.testing.expect(map.remove(1));
try std.testing.expect(map.isEmpty());
}
test "SidMap count accuracy through operations" {
var keys: [16]u64 = undefined;
var vals: [16]u16 = undefined;
var map: SidMap = .init(&keys, &vals);
try std.testing.expectEqual(@as(u32, 0), map.count());
try map.put(1, 0);
try std.testing.expectEqual(@as(u32, 1), map.count());
try map.put(2, 1);
try std.testing.expectEqual(@as(u32, 2), map.count());
try map.put(1, 99); // Update, not insert
try std.testing.expectEqual(@as(u32, 2), map.count());
try std.testing.expect(map.remove(1));
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expect(!map.remove(1)); // Already removed
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expect(!map.remove(999)); // Never existed
try std.testing.expectEqual(@as(u32, 1), map.count());
try std.testing.expect(map.remove(2));
try std.testing.expectEqual(@as(u32, 0), map.count());
}
================================================
FILE: src/memory/slab.zig
================================================
//! Tiered Slab Allocator
//!
//! High-performance message buffer allocator with O(1) alloc/free.
//! Uses tiered slabs with embedded free lists for zero-overhead tracking.
//! Falls back to provided allocator for oversized allocations.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const defaults = @import("../defaults.zig");
/// Configuration for tiered slab allocator (derived from defaults.Memory).
pub const Config = struct {
pub const TIER_COUNT = defaults.Memory.tier_count;
/// Slice sizes per tier (power-of-2 for efficient selection).
pub const tier_sizes = defaults.Memory.tier_sizes;
/// Slice counts per tier (derived from queue_size).
pub const tier_counts = defaults.Memory.tier_counts;
/// Maximum slice size handled by slab (larger uses fallback).
pub const max_slice_size: usize = defaults.Memory.max_slice_size;
/// Total pre-allocated memory.
pub const total_memory: usize = defaults.Memory.total_memory;
};
/// Single-tier slab with embedded free list.
///
/// Each free slice stores the index of the next free slice in its first
/// 4 bytes. This eliminates separate tracking overhead.
pub const Slab = struct {
memory: []align(4096) u8,
slice_size: u32,
slice_count: u32,
free_head: u32,
alloc_count: u32,
const NONE: u32 = 0xFFFF_FFFF;
/// Initialize slab with page-aligned memory.
pub fn init(slice_size: u32, slice_count: u32) !Slab {
assert(slice_size >= 4);
assert(slice_count > 0);
assert(slice_size <= Config.max_slice_size);
const total = @as(usize, slice_size) * slice_count;
const raw = std.heap.page_allocator.alloc(
u8,
total,
) catch return error.MmapFailed;
const memory: []align(4096) u8 = @alignCast(raw);
var slab = Slab{
.memory = @alignCast(memory),
.slice_size = slice_size,
.slice_count = slice_count,
.free_head = 0,
.alloc_count = 0,
};
var i: u32 = 0;
while (i < slice_count) : (i += 1) {
const slice = slab.getSliceByIndex(i);
const next: u32 = if (i + 1 < slice_count) i + 1 else NONE;
@as(*u32, @ptrCast(@alignCast(slice.ptr))).* = next;
}
return slab;
}
/// Release page-allocated memory.
pub fn deinit(self: *Slab) void {
std.heap.page_allocator.free(self.memory);
self.* = undefined;
}
/// O(1) allocation - pop from embedded free list.
pub inline fn alloc(self: *Slab) ?[]u8 {
if (self.free_head == NONE) return null;
const idx = self.free_head;
const slice = self.getSliceByIndex(idx);
self.free_head = @as(*u32, @ptrCast(@alignCast(slice.ptr))).*;
self.alloc_count += 1;
return slice;
}
/// O(1) deallocation - push to embedded free list.
/// Debug builds detect double-free by walking the free list.
pub inline fn free(self: *Slab, ptr: [*]u8) void {
const idx = self.ptrToIndex(ptr);
assert(idx < self.slice_count);
if (builtin.mode == .Debug) {
assert(!self.isInFreeList(idx));
}
@as(*u32, @ptrCast(@alignCast(ptr))).* = self.free_head;
self.free_head = idx;
self.alloc_count -= 1;
}
/// Debug helper: check if index is already in free list (O(n)).
fn isInFreeList(self: *Slab, target_idx: u32) bool {
var current = self.free_head;
while (current != NONE) {
if (current == target_idx) return true;
const slice = self.getSliceByIndex(current);
current = @as(*u32, @ptrCast(@alignCast(slice.ptr))).*;
}
return false;
}
inline fn getSliceByIndex(self: *Slab, idx: u32) []u8 {
const offset = @as(usize, idx) * self.slice_size;
return self.memory[offset..][0..self.slice_size];
}
inline fn ptrToIndex(self: *Slab, ptr: [*]u8) u32 {
const ptr_addr = @intFromPtr(ptr);
const mem_start = @intFromPtr(self.memory.ptr);
const mem_end = mem_start + self.memory.len;
assert(ptr_addr >= mem_start);
assert(ptr_addr < mem_end);
const offset = ptr_addr - mem_start;
assert(offset % self.slice_size == 0);
return @intCast(offset / self.slice_size);
}
/// Check if pointer belongs to this slab.
pub inline fn contains(self: *const Slab, ptr: [*]u8) bool {
const addr = @intFromPtr(ptr);
const base = @intFromPtr(self.memory.ptr);
return addr >= base and addr < base + self.memory.len;
}
/// Returns number of currently allocated slices.
pub fn getAllocCount(self: *const Slab) u32 {
return self.alloc_count;
}
/// Returns total capacity in slices.
pub fn getCapacity(self: *const Slab) u32 {
return self.slice_count;
}
};
/// Multi-tier slab allocator with fallback.
///
/// Selects appropriate tier based on requested size. Falls back to
/// provided allocator for sizes exceeding max tier.
pub const TieredSlab = struct {
tiers: [Config.TIER_COUNT]Slab,
fallback: Allocator,
fallback_count: u32,
/// Initialize all tiers with mmap'd memory.
pub fn init(fallback_allocator: Allocator) !TieredSlab {
var ts: TieredSlab = undefined;
ts.fallback = fallback_allocator;
ts.fallback_count = 0;
var initialized: usize = 0;
errdefer {
for (ts.tiers[0..initialized]) |*tier| {
tier.deinit();
}
}
for (Config.tier_sizes, Config.tier_counts, 0..) |size, count, i| {
ts.tiers[i] = try Slab.init(size, count);
initialized += 1;
}
return ts;
}
/// Release all mmap'd memory.
pub fn deinit(self: *TieredSlab) void {
for (&self.tiers) |*tier| {
tier.deinit();
}
}
/// O(1) tier selection based on size.
inline fn selectTier(size: usize) ?usize {
if (size <= 256) return 0;
if (size <= 512) return 1;
if (size <= 1024) return 2;
if (size <= 4096) return 3;
if (size <= 16384) return 4;
return null;
}
/// Allocate from appropriate tier or fallback.
pub fn alloc(self: *TieredSlab, size: usize) ?[]u8 {
assert(size > 0);
if (selectTier(size)) |tier_idx| {
if (self.tiers[tier_idx].alloc()) |slice| {
return slice[0..size];
}
}
self.fallback_count += 1;
return self.fallback.alloc(u8, size) catch null;
}
/// Free to appropriate tier or fallback.
pub fn free(self: *TieredSlab, buf: []u8) void {
assert(buf.len > 0);
const ptr = buf.ptr;
inline for (&self.tiers) |*tier| {
if (tier.contains(ptr)) {
tier.free(ptr);
return;
}
}
self.fallback_count -= 1;
self.fallback.free(buf);
}
/// Check if pointer belongs to any slab tier.
pub fn containsPtr(self: *const TieredSlab, ptr: [*]u8) bool {
inline for (&self.tiers) |*tier| {
if (tier.contains(ptr)) return true;
}
return false;
}
/// Get diagnostic statistics.
pub fn getStats(self: *const TieredSlab) Stats {
var stats = Stats{};
for (self.tiers, 0..) |tier, i| {
stats.tier_alloc_counts[i] = tier.alloc_count;
stats.tier_capacities[i] = tier.slice_count;
}
stats.fallback_count = self.fallback_count;
return stats;
}
pub const Stats = struct {
tier_alloc_counts: [Config.TIER_COUNT]u32 = .{0} ** Config.TIER_COUNT,
tier_capacities: [Config.TIER_COUNT]u32 = .{0} ** Config.TIER_COUNT,
fallback_count: u32 = 0,
/// Total allocated across all tiers.
pub fn totalAllocated(self: Stats) u32 {
var total: u32 = 0;
for (self.tier_alloc_counts) |count| {
total += count;
}
return total + self.fallback_count;
}
/// Total capacity across all tiers.
pub fn totalCapacity(self: Stats) u32 {
var total: u32 = 0;
for (self.tier_capacities) |cap| {
total += cap;
}
return total;
}
};
};
/// Wrapper that implements std.mem.Allocator interface.
///
/// This allows TieredSlab to be used transparently with code expecting
/// an Allocator, such as Message.deinit().
pub const SlabAllocator = struct {
slab: *TieredSlab,
/// Return std.mem.Allocator interface.
pub fn allocator(self: *SlabAllocator) Allocator {
return .{
.ptr = self,
.vtable = &vtable,
};
}
const vtable: Allocator.VTable = .{
.alloc = allocFn,
.resize = resizeFn,
.remap = remapFn,
.free = freeFn,
};
fn allocFn(
ctx: *anyopaque,
len: usize,
alignment: std.mem.Alignment,
ret_addr: usize,
) ?[*]u8 {
_ = alignment;
_ = ret_addr;
const self: *SlabAllocator = @ptrCast(@alignCast(ctx));
const buf = self.slab.alloc(len) orelse return null;
return buf.ptr;
}
fn resizeFn(
ctx: *anyopaque,
buf: []u8,
alignment: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) bool {
_ = ctx;
_ = alignment;
_ = ret_addr;
if (new_len <= buf.len) return true;
return false;
}
fn remapFn(
ctx: *anyopaque,
memory: []u8,
alignment: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) ?[*]u8 {
_ = ctx;
_ = memory;
_ = alignment;
_ = new_len;
_ = ret_addr;
return null;
}
fn freeFn(
ctx: *anyopaque,
buf: []u8,
alignment: std.mem.Alignment,
ret_addr: usize,
) void {
_ = alignment;
_ = ret_addr;
const self: *SlabAllocator = @ptrCast(@alignCast(ctx));
self.slab.free(buf);
}
};
test "Slab basic alloc/free" {
var slab = try Slab.init(256, 16);
defer slab.deinit();
var ptrs: [16][]u8 = undefined;
for (&ptrs) |*p| {
p.* = slab.alloc() orelse unreachable;
}
try std.testing.expect(slab.alloc() == null);
try std.testing.expectEqual(@as(u32, 16), slab.getAllocCount());
for (ptrs) |p| {
slab.free(p.ptr);
}
try std.testing.expectEqual(@as(u32, 0), slab.getAllocCount());
const p = slab.alloc() orelse unreachable;
try std.testing.expect(p.len == 256);
}
test "TieredSlab tier selection" {
const fallback = std.testing.allocator;
var ts = try TieredSlab.init(fallback);
defer ts.deinit();
const small = ts.alloc(100) orelse unreachable;
try std.testing.expect(ts.containsPtr(small.ptr));
const medium = ts.alloc(800) orelse unreachable;
try std.testing.expect(ts.containsPtr(medium.ptr));
const large = ts.alloc(20000) orelse unreachable;
try std.testing.expect(!ts.containsPtr(large.ptr));
ts.free(small);
ts.free(medium);
ts.free(large);
const stats = ts.getStats();
try std.testing.expectEqual(@as(u32, 0), stats.totalAllocated());
}
test "SlabAllocator interface" {
const fallback = std.testing.allocator;
var ts = try TieredSlab.init(fallback);
defer ts.deinit();
var sa = SlabAllocator{ .slab = &ts };
const alloc = sa.allocator();
const buf = try alloc.alloc(u8, 200);
try std.testing.expect(buf.len == 200);
alloc.free(buf);
}
================================================
FILE: src/memory.zig
================================================
//! Memory Management
//!
//! Provides SidMap for O(1) subscription routing and TieredSlab for
//! high-performance message buffer allocation.
pub const sidmap = @import("memory/sidmap.zig");
pub const SidMap = sidmap.SidMap;
pub const slab = @import("memory/slab.zig");
pub const TieredSlab = slab.TieredSlab;
pub const SlabConfig = slab.Config;
test {
_ = sidmap;
_ = slab;
}
================================================
FILE: src/micro/Service.zig
================================================
const std = @import("std");
const Client = @import("../Client.zig");
const pubsub = @import("../pubsub.zig");
const endpoint_mod = @import("endpoint.zig");
const protocol = @import("protocol.zig");
const validation = @import("validation.zig");
const json_util = @import("json_util.zig");
const timeutil = @import("timeutil.zig");
pub const Error = anyerror;
pub const Config = struct {
name: []const u8,
version: []const u8,
description: ?[]const u8 = null,
metadata: []const protocol.MetadataPair = &.{},
service_prefix: []const u8 = "$SRV",
queue_policy: endpoint_mod.QueuePolicy = .{ .queue = "q" },
endpoint: ?endpoint_mod.EndpointConfig = null,
};
pub const Service = @This();
client: *Client,
allocator: std.mem.Allocator,
name: []const u8,
version: []const u8,
description: ?[]const u8,
id: []const u8,
service_prefix: []const u8,
started: []const u8,
metadata: []protocol.MetadataPair,
queue_policy: endpoint_mod.QueuePolicy,
endpoints: std.ArrayList(*endpoint_mod.Endpoint) = .empty,
group_prefixes: std.ArrayList([]u8) = .empty,
group_queues: std.ArrayList([]u8) = .empty,
monitor_subs: std.ArrayList(*Client.Subscription) = .empty,
mutex: std.Io.Mutex = .init,
in_flight: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
stopping: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
stopped_flag: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
stop_error: ?anyerror = null,
pub fn addService(client: *Client, config: Config) !*Service {
try validation.validateName(config.name);
try validation.validateVersion(config.version);
try validation.validatePrefix(config.service_prefix);
const service = try client.allocator.create(Service);
errdefer client.allocator.destroy(service);
const name = try client.allocator.dupe(u8, config.name);
errdefer client.allocator.free(name);
const version = try client.allocator.dupe(u8, config.version);
errdefer client.allocator.free(version);
const description = if (config.description) |d|
try client.allocator.dupe(u8, d)
else
null;
errdefer if (description) |d| client.allocator.free(d);
const id = try generateId(client.allocator, client.io);
errdefer client.allocator.free(id);
const service_prefix = try client.allocator.dupe(u8, config.service_prefix);
errdefer client.allocator.free(service_prefix);
const metadata = try endpoint_mod.dupMetadata(client.allocator, config.metadata);
errdefer endpoint_mod.freeMetadata(client.allocator, metadata);
const queue_policy = try dupQueuePolicy(client.allocator, config.queue_policy);
errdefer freeQueuePolicy(client.allocator, queue_policy);
service.* = .{
.client = client,
.allocator = client.allocator,
.name = name,
.version = version,
.description = description,
.id = id,
.service_prefix = service_prefix,
.started = undefined,
.metadata = metadata,
.queue_policy = queue_policy,
};
var started_buf: [32]u8 = undefined;
const started = try timeutil.nowRfc3339(client.io, &started_buf);
service.started = try client.allocator.dupe(u8, started);
errdefer client.allocator.free(service.started);
errdefer service.cleanupRuntimeResources();
try service.initMonitorSubs();
if (config.endpoint) |ep_cfg| {
_ = try service.addEndpoint(ep_cfg);
}
// Make service discovery and endpoint subscriptions visible to the
// server before returning so immediate requests do not race setup.
try client.flush(5 * std.time.ns_per_s);
return service;
}
pub fn addEndpoint(self: *Service, cfg: endpoint_mod.EndpointConfig) !*endpoint_mod.Endpoint {
return self.addEndpointWithPrefix("", .inherit, cfg);
}
pub fn addGroup(self: *Service, prefix: []const u8) !endpoint_mod.Group {
try validation.validateGroup(prefix);
const full = try self.allocGroupPrefix("", prefix);
return .{
.service = self,
.prefix = full,
.queue_policy = .inherit,
};
}
pub fn addGroupWithQueue(
self: *Service,
prefix: []const u8,
queue: []const u8,
) !endpoint_mod.Group {
try validation.validateGroup(prefix);
try pubsub.validateQueueGroup(queue);
const full = try self.allocGroupPrefix("", prefix);
const owned_queue = try self.allocGroupQueue(queue);
return .{
.service = self,
.prefix = full,
.queue_policy = .{ .queue = owned_queue },
};
}
pub fn info(self: *Service, allocator: std.mem.Allocator) !protocol.Info {
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
const endpoints = try allocator.alloc(protocol.EndpointInfo, self.endpoints.items.len);
for (self.endpoints.items, 0..) |ep, i| {
endpoints[i] = .{
.name = ep.name,
.subject = ep.subject,
.queue_group = ep.queue_group,
.metadata = if (ep.metadata.len == 0) null else ep.metadata,
};
}
return .{
.name = self.name,
.id = self.id,
.version = self.version,
.description = self.description,
.metadata = if (self.metadata.len == 0) null else self.metadata,
.endpoints = endpoints,
};
}
pub fn stats(self: *Service, allocator: std.mem.Allocator) !protocol.StatsResponse {
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
const endpoints = try allocator.alloc(protocol.EndpointStatsJson, self.endpoints.items.len);
for (self.endpoints.items, 0..) |ep, i| {
const snap = ep.stats.snapshot();
endpoints[i] = .{
.name = ep.name,
.subject = ep.subject,
.queue_group = ep.queue_group,
.metadata = if (ep.metadata.len == 0) null else ep.metadata,
.num_requests = snap.num_requests,
.num_errors = snap.num_errors,
.last_error = snap.last_error,
.processing_time = snap.processing_time,
.average_processing_time = snap.average_processing_time,
};
}
return .{
.name = self.name,
.id = self.id,
.version = self.version,
.started = self.started,
.metadata = if (self.metadata.len == 0) null else self.metadata,
.endpoints = endpoints,
};
}
pub fn reset(self: *Service) void {
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
for (self.endpoints.items) |ep| {
ep.stats.reset();
}
}
pub fn stop(self: *Service, stop_error: ?anyerror) !void {
if (self.stopped_flag.load(.acquire)) return;
if (self.stopping.swap(true, .acq_rel)) {
return self.waitStopped();
}
self.stop_error = stop_error;
self.mutex.lockUncancelable(self.client.io);
for (self.monitor_subs.items) |sub| {
sub.deinit();
}
self.monitor_subs.clearRetainingCapacity();
for (self.endpoints.items) |ep| {
ep.sub.drain() catch {};
}
self.mutex.unlock(self.client.io);
// Ensure UNSUB frames reach the server before stop() returns so no
// new requests are accepted after shutdown completes.
self.client.flush(5 * std.time.ns_per_s) catch {};
const drain_timeout_ms = self.client.options.drain_timeout_ms;
var drain_err: ?anyerror = null;
for (self.endpoints.items) |ep| {
ep.sub.waitDrained(drain_timeout_ms) catch |err| {
if (drain_err == null) drain_err = err;
};
}
const start = std.Io.Timestamp.now(self.client.io, .awake);
const timeout_ns =
@as(i128, drain_timeout_ms) * std.time.ns_per_ms;
var spins: u32 = 0;
while (self.in_flight.load(.acquire) != 0) {
const now = std.Io.Timestamp.now(self.client.io, .awake);
if (now.nanoseconds - start.nanoseconds >= timeout_ns) {
drain_err = drain_err orelse error.Timeout;
break;
}
spins += 1;
if (spins < 100) {
std.atomic.spinLoopHint();
} else {
self.client.io.sleep(.fromNanoseconds(0), .awake) catch {};
spins = 0;
}
}
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
for (self.endpoints.items) |ep| {
ep.sub.deinit();
}
// `deinit()` sends the final unsubscribe/cancel path for callback
// subscriptions. Confirm it has reached the server before reporting
// the service as stopped.
self.client.flush(5 * std.time.ns_per_s) catch {};
self.stopped_flag.store(true, .release);
if (drain_err) |err| return err;
}
pub fn waitStopped(self: *Service) !void {
while (!self.stopped_flag.load(.acquire)) {
self.client.io.sleep(.fromNanoseconds(0), .awake) catch {};
}
if (self.stop_error) |err| return err;
}
pub fn stopped(self: *const Service) bool {
return self.stopped_flag.load(.acquire);
}
fn cleanupRuntimeResources(self: *Service) void {
for (self.monitor_subs.items) |sub| {
sub.deinit();
}
self.monitor_subs.deinit(self.allocator);
for (self.endpoints.items) |ep| {
ep.sub.deinit();
ep.deinit(self.allocator);
}
self.endpoints.deinit(self.allocator);
for (self.group_prefixes.items) |prefix| self.allocator.free(prefix);
self.group_prefixes.deinit(self.allocator);
for (self.group_queues.items) |queue| self.allocator.free(queue);
self.group_queues.deinit(self.allocator);
}
pub fn deinit(self: *Service) void {
self.stop(null) catch {};
for (self.endpoints.items) |ep| ep.deinit(self.allocator);
self.endpoints.deinit(self.allocator);
for (self.group_prefixes.items) |prefix| self.allocator.free(prefix);
self.group_prefixes.deinit(self.allocator);
for (self.group_queues.items) |queue| self.allocator.free(queue);
self.group_queues.deinit(self.allocator);
self.monitor_subs.deinit(self.allocator);
endpoint_mod.freeMetadata(self.allocator, self.metadata);
self.allocator.free(self.name);
self.allocator.free(self.version);
if (self.description) |d| self.allocator.free(d);
self.allocator.free(self.id);
self.allocator.free(self.service_prefix);
self.allocator.free(self.started);
freeQueuePolicy(self.allocator, self.queue_policy);
self.allocator.destroy(self);
}
pub fn addEndpointWithPrefix(
self: *Service,
prefix: []const u8,
inherited_policy: endpoint_mod.QueuePolicy,
cfg: endpoint_mod.EndpointConfig,
) !*endpoint_mod.Endpoint {
if (self.stopping.load(.acquire)) return error.InvalidState;
const full_subject = try joinSubject(self.allocator, prefix, cfg.subject);
errdefer self.allocator.free(full_subject);
try pubsub.validatePublish(full_subject);
const name = try self.allocator.dupe(u8, cfg.name orelse cfg.subject);
errdefer self.allocator.free(name);
const queue_group = try resolveQueuePolicy(self.allocator, cfg.queue_policy, inherited_policy, self.queue_policy);
errdefer if (queue_group) |q| self.allocator.free(q);
const metadata = try endpoint_mod.dupMetadata(self.allocator, cfg.metadata);
errdefer endpoint_mod.freeMetadata(self.allocator, metadata);
const ep = try self.allocator.create(endpoint_mod.Endpoint);
errdefer self.allocator.destroy(ep);
ep.* = .{
.service = self,
.sub = undefined,
.name = name,
.subject = full_subject,
.queue_group = queue_group,
.metadata = metadata,
.handler = cfg.handler,
};
ep.callback = .{ .endpoint = ep };
ep.sub = if (queue_group) |q|
try self.client.queueSubscribe(full_subject, q, Client.MsgHandler.init(endpoint_mod.EndpointCallback, &ep.callback))
else
try self.client.subscribe(full_subject, Client.MsgHandler.init(endpoint_mod.EndpointCallback, &ep.callback));
errdefer ep.sub.deinit();
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
try self.endpoints.append(self.allocator, ep);
// Make the new endpoint visible to the server before returning.
self.client.flush(5 * std.time.ns_per_s) catch {};
return ep;
}
pub fn allocGroupPrefix(self: *Service, base: []const u8, next: []const u8) ![]const u8 {
const full = try joinSubject(self.allocator, base, next);
errdefer self.allocator.free(full);
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
try self.group_prefixes.append(self.allocator, full);
return full;
}
pub fn allocGroupQueue(self: *Service, queue: []const u8) ![]const u8 {
try pubsub.validateQueueGroup(queue);
const owned = try self.allocator.dupe(u8, queue);
errdefer self.allocator.free(owned);
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
try self.group_queues.append(self.allocator, owned);
return owned;
}
pub fn onMessage(self: *Service, msg: *const Client.Message) void {
const reply_to = msg.reply_to orelse return;
if (self.stopped_flag.load(.acquire)) return;
var payload: ?[]u8 = null;
defer if (payload) |buf| self.allocator.free(buf);
const subject = msg.subject;
const prefix = self.service_prefix;
if (!std.mem.startsWith(u8, subject, prefix)) return;
if (subject.len <= prefix.len or subject[prefix.len] != '.') return;
const rest = subject[prefix.len + 1 ..];
if (matchVerb(rest, "PING")) {
const ping = protocol.Ping{
.name = self.name,
.id = self.id,
.version = self.version,
.metadata = if (self.metadata.len == 0) null else self.metadata,
};
payload = json_util.jsonStringify(self.allocator, ping) catch return;
} else if (matchVerb(rest, "INFO")) {
var info_resp = self.info(self.allocator) catch return;
defer info_resp.deinit(self.allocator);
payload = json_util.jsonStringify(self.allocator, info_resp) catch return;
} else if (matchVerb(rest, "STATS")) {
var stats_resp = self.stats(self.allocator) catch return;
defer stats_resp.deinit(self.allocator);
payload = json_util.jsonStringify(self.allocator, stats_resp) catch return;
} else return;
self.client.publish(reply_to, payload.?) catch {};
}
fn initMonitorSubs(self: *Service) !void {
const prefixes = [_][]const u8{
"PING", "INFO", "STATS",
};
for (prefixes) |verb| {
try self.subscribeMonitor(verb);
const name_subject = try std.fmt.allocPrint(
self.allocator,
"{s}.{s}",
.{ verb, self.name },
);
defer self.allocator.free(name_subject);
try self.subscribeMonitor(name_subject);
const id_subject = try std.fmt.allocPrint(
self.allocator,
"{s}.{s}.{s}",
.{ verb, self.name, self.id },
);
defer self.allocator.free(id_subject);
try self.subscribeMonitor(id_subject);
}
}
fn subscribeMonitor(self: *Service, suffix: []const u8) !void {
const subject = try std.fmt.allocPrint(
self.allocator,
"{s}.{s}",
.{ self.service_prefix, suffix },
);
defer self.allocator.free(subject);
const sub = try self.client.subscribe(subject, Client.MsgHandler.init(Service, self));
errdefer sub.deinit();
self.mutex.lockUncancelable(self.client.io);
defer self.mutex.unlock(self.client.io);
try self.monitor_subs.append(self.allocator, sub);
}
fn joinSubject(
allocator: std.mem.Allocator,
prefix: []const u8,
leaf: []const u8,
) ![]u8 {
if (prefix.len == 0) return allocator.dupe(u8, leaf);
return std.fmt.allocPrint(allocator, "{s}.{s}", .{ prefix, leaf });
}
fn generateId(allocator: std.mem.Allocator, io: std.Io) ![]u8 {
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const out = try allocator.alloc(u8, 16);
var random: [16]u8 = undefined;
io.random(&random);
for (out, random) |*dst, src| {
dst.* = alphabet[@mod(src, alphabet.len)];
}
return out;
}
fn dupQueuePolicy(
allocator: std.mem.Allocator,
policy: endpoint_mod.QueuePolicy,
) !endpoint_mod.QueuePolicy {
return switch (policy) {
.inherit => .inherit,
.no_queue => .no_queue,
.queue => |q| .{ .queue = try allocator.dupe(u8, q) },
};
}
fn freeQueuePolicy(
allocator: std.mem.Allocator,
policy: endpoint_mod.QueuePolicy,
) void {
switch (policy) {
.queue => |q| allocator.free(q),
else => {},
}
}
fn resolveQueuePolicy(
allocator: std.mem.Allocator,
endpoint_policy: endpoint_mod.QueuePolicy,
group_policy: endpoint_mod.QueuePolicy,
service_policy: endpoint_mod.QueuePolicy,
) !?[]u8 {
const resolved = switch (endpoint_policy) {
.inherit => switch (group_policy) {
.inherit => service_policy,
else => group_policy,
},
else => endpoint_policy,
};
return switch (resolved) {
.inherit => try allocator.dupe(u8, "q"),
.no_queue => null,
.queue => |q| try allocator.dupe(u8, q),
};
}
fn matchVerb(rest: []const u8, verb: []const u8) bool {
if (!std.mem.startsWith(u8, rest, verb)) return false;
return rest.len == verb.len or rest[verb.len] == '.';
}
================================================
FILE: src/micro/endpoint.zig
================================================
const std = @import("std");
const Client = @import("../Client.zig");
const protocol = @import("protocol.zig");
const request_mod = @import("request.zig");
const stats_mod = @import("stats.zig");
const validation = @import("validation.zig");
const pubsub = @import("../pubsub.zig");
pub const QueuePolicy = union(enum) {
inherit,
queue: []const u8,
no_queue,
};
pub const EndpointConfig = struct {
subject: []const u8,
name: ?[]const u8 = null,
handler: request_mod.Handler,
metadata: []const protocol.MetadataPair = &.{},
queue_policy: QueuePolicy = .inherit,
};
pub const Endpoint = struct {
service: *anyopaque,
sub: *Client.Subscription,
name: []const u8,
subject: []const u8,
queue_group: ?[]const u8,
metadata: []protocol.MetadataPair,
handler: request_mod.Handler,
stats: stats_mod.EndpointStats = .{},
callback: EndpointCallback = undefined,
pub fn deinit(self: *Endpoint, allocator: std.mem.Allocator) void {
allocator.free(self.name);
allocator.free(self.subject);
freeMetadata(allocator, self.metadata);
if (self.queue_group) |q| allocator.free(q);
allocator.destroy(self);
}
};
pub const Group = struct {
service: *anyopaque,
prefix: []const u8,
queue_policy: QueuePolicy,
pub fn addEndpoint(self: *Group, cfg: EndpointConfig) !*Endpoint {
const service = servicePtr(self.service);
return service.addEndpointWithPrefix(self.prefix, self.queue_policy, cfg);
}
pub fn group(self: *Group, prefix: []const u8) !Group {
const service = servicePtr(self.service);
try validation.validateGroup(prefix);
const full = try service.allocGroupPrefix(self.prefix, prefix);
return .{
.service = self.service,
.prefix = full,
.queue_policy = self.queue_policy,
};
}
pub fn groupWithQueue(self: *Group, prefix: []const u8, queue: []const u8) !Group {
const service = servicePtr(self.service);
try validation.validateGroup(prefix);
try pubsub.validateQueueGroup(queue);
const full = try service.allocGroupPrefix(self.prefix, prefix);
const owned_queue = try service.allocGroupQueue(queue);
return .{
.service = self.service,
.prefix = full,
.queue_policy = .{ .queue = owned_queue },
};
}
};
pub const EndpointCallback = struct {
endpoint: *Endpoint,
pub fn onMessage(self: *@This(), msg: *const Client.Message) void {
const service = servicePtr(self.endpoint.service);
// Belt-and-suspenders: skip dispatch once stop() has begun.
// sub.drain() + flush already prevent new messages from reaching
// here, but this guard ensures any racing in-flight delivery does
// not re-enter the user handler after stop() observed in_flight==0.
if (service.stopping.load(.acquire)) return;
const start = std.Io.Timestamp.now(service.client.io, .awake);
_ = service.in_flight.fetchAdd(1, .acq_rel);
defer _ = service.in_flight.fetchSub(1, .acq_rel);
var req = request_mod.Request{
.client = service.client,
.msg = msg,
};
self.endpoint.handler.dispatch(&req);
const end = std.Io.Timestamp.now(service.client.io, .awake);
const elapsed: u64 = @intCast(end.nanoseconds - start.nanoseconds);
if (req.errored) {
self.endpoint.stats.recordError(
elapsed,
req.error_code,
req.errorDescription(),
);
} else {
self.endpoint.stats.recordSuccess(elapsed);
}
}
};
pub fn dupMetadata(
allocator: std.mem.Allocator,
metadata: []const protocol.MetadataPair,
) ![]protocol.MetadataPair {
const out = try allocator.alloc(protocol.MetadataPair, metadata.len);
errdefer allocator.free(out);
for (metadata, 0..) |pair, i| {
out[i].key = try allocator.dupe(u8, pair.key);
errdefer allocator.free(out[i].key);
errdefer {
for (out[0..i]) |prev| {
allocator.free(prev.key);
allocator.free(prev.value);
}
}
out[i].value = try allocator.dupe(u8, pair.value);
}
return out;
}
pub fn freeMetadata(
allocator: std.mem.Allocator,
metadata: []const protocol.MetadataPair,
) void {
for (metadata) |pair| {
allocator.free(pair.key);
allocator.free(pair.value);
}
if (metadata.len > 0) allocator.free(metadata);
}
fn servicePtr(ptr: *anyopaque) *@import("Service.zig").Service {
return @ptrCast(@alignCast(ptr));
}
test "dupMetadata frees current key if value allocation fails" {
const pairs = [_]protocol.MetadataPair{
.{ .key = "role", .value = "primary" },
};
var failing = std.testing.FailingAllocator.init(
std.testing.allocator,
.{ .fail_index = 2 },
);
try std.testing.expectError(
error.OutOfMemory,
dupMetadata(failing.allocator(), &pairs),
);
}
================================================
FILE: src/micro/json_util.zig
================================================
const std = @import("std");
const json_stringify_opts: std.json.Stringify.Options = .{
.emit_null_optional_fields = false,
};
const json_parse_opts: std.json.ParseOptions = .{
.ignore_unknown_fields = true,
};
pub fn jsonStringify(
allocator: std.mem.Allocator,
value: anytype,
) error{OutOfMemory}![]u8 {
return std.json.Stringify.valueAlloc(
allocator,
value,
json_stringify_opts,
);
}
pub fn jsonParse(
comptime T: type,
allocator: std.mem.Allocator,
data: []const u8,
) std.json.ParseError(std.json.Scanner)!std.json.Parsed(T) {
return std.json.parseFromSlice(
T,
allocator,
data,
json_parse_opts,
);
}
================================================
FILE: src/micro/protocol.zig
================================================
const std = @import("std");
pub const Type = struct {
pub const ping = "io.nats.micro.v1.ping_response";
pub const info = "io.nats.micro.v1.info_response";
pub const stats = "io.nats.micro.v1.stats_response";
};
pub const MetadataPair = struct {
key: []const u8,
value: []const u8,
};
pub const Error = struct {
code: u16,
description: []const u8,
};
pub const Ping = struct {
type: []const u8 = Type.ping,
name: []const u8,
id: []const u8,
version: []const u8,
metadata: ?[]const MetadataPair = null,
};
pub const EndpointInfo = struct {
name: []const u8,
subject: []const u8,
queue_group: ?[]const u8 = null,
metadata: ?[]const MetadataPair = null,
};
pub const Info = struct {
type: []const u8 = Type.info,
name: []const u8,
id: []const u8,
version: []const u8,
description: ?[]const u8 = null,
metadata: ?[]const MetadataPair = null,
endpoints: []const EndpointInfo = &.{},
pub fn deinit(self: *Info, allocator: std.mem.Allocator) void {
if (self.endpoints.len > 0) allocator.free(self.endpoints);
self.endpoints = &.{};
}
};
pub const EndpointStatsJson = struct {
name: []const u8,
subject: []const u8,
queue_group: ?[]const u8 = null,
metadata: ?[]const MetadataPair = null,
num_requests: u64,
num_errors: u64,
last_error: ?Error = null,
processing_time: u64,
average_processing_time: u64,
};
pub const StatsResponse = struct {
type: []const u8 = Type.stats,
name: []const u8,
id: []const u8,
version: []const u8,
started: []const u8,
metadata: ?[]const MetadataPair = null,
endpoints: []const EndpointStatsJson = &.{},
pub fn deinit(self: *StatsResponse, allocator: std.mem.Allocator) void {
if (self.endpoints.len > 0) allocator.free(self.endpoints);
self.endpoints = &.{};
}
};
================================================
FILE: src/micro/request.zig
================================================
const std = @import("std");
const Client = @import("../Client.zig");
const headers_mod = @import("../protocol/headers.zig");
const json_util = @import("json_util.zig");
pub const HandlerFn = *const fn (*Request) void;
pub const Request = struct {
client: *Client,
msg: *const Client.Message,
errored: bool = false,
error_code: u16 = 0,
error_desc_len: usize = 0,
error_desc_buf: [128]u8 = undefined,
pub fn data(self: *const Request) []const u8 {
return self.msg.data;
}
pub fn subject(self: *const Request) []const u8 {
return self.msg.subject;
}
pub fn reply(self: *const Request) ?[]const u8 {
return self.msg.reply_to;
}
pub fn headers(self: *const Request) ?[]const u8 {
return self.msg.headers;
}
pub fn respond(self: *Request, payload: []const u8) !void {
const reply_to = self.msg.reply_to orelse return error.NoReplyTo;
try self.client.publish(reply_to, payload);
}
pub fn respondJson(self: *Request, value: anytype) !void {
const payload = try json_util.jsonStringify(self.client.allocator, value);
defer self.client.allocator.free(payload);
try self.respond(payload);
}
pub fn respondError(
self: *Request,
code: u16,
description: []const u8,
payload: []const u8,
) !void {
const reply_to = self.msg.reply_to orelse return error.NoReplyTo;
var code_buf: [16]u8 = undefined;
const code_str = try std.fmt.bufPrint(&code_buf, "{d}", .{code});
const hdrs = [_]headers_mod.Entry{
.{ .key = "Nats-Service-Error", .value = description },
.{ .key = "Nats-Service-Error-Code", .value = code_str },
};
try self.client.publishWithHeaders(reply_to, &hdrs, payload);
self.errored = true;
self.error_code = code;
self.error_desc_len = @min(description.len, self.error_desc_buf.len);
@memcpy(
self.error_desc_buf[0..self.error_desc_len],
description[0..self.error_desc_len],
);
}
pub fn errorDescription(self: *const Request) []const u8 {
return self.error_desc_buf[0..self.error_desc_len];
}
};
pub const Handler = struct {
impl: Impl,
pub const Impl = union(enum) {
vtable: VTableImpl,
bare_fn: HandlerFn,
};
pub const VTableImpl = struct {
ptr: *anyopaque,
call: *const fn (*anyopaque, *Request) void,
};
pub fn init(comptime T: type, ptr: *T) Handler {
const gen = struct {
fn call(p: *anyopaque, req: *Request) void {
const self: *T = @ptrCast(@alignCast(p));
self.onRequest(req);
}
};
return .{ .impl = .{ .vtable = .{
.ptr = ptr,
.call = gen.call,
} } };
}
pub fn fromFn(f: HandlerFn) Handler {
return .{ .impl = .{ .bare_fn = f } };
}
pub fn dispatch(self: Handler, req: *Request) void {
switch (self.impl) {
.vtable => |v| v.call(v.ptr, req),
.bare_fn => |f| f(req),
}
}
};
================================================
FILE: src/micro/stats.zig
================================================
const std = @import("std");
const protocol = @import("protocol.zig");
const SpinLock = @import("../sync/spin_lock.zig").SpinLock;
pub const EndpointStats = struct {
num_requests: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
num_errors: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
processing_time: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
last_error_code: std.atomic.Value(u16) = std.atomic.Value(u16).init(0),
last_error_lock: SpinLock = .{},
last_error_len: usize = 0,
last_error_desc: [128]u8 = undefined,
pub fn recordSuccess(self: *EndpointStats, elapsed_ns: u64) void {
_ = self.num_requests.fetchAdd(1, .monotonic);
_ = self.processing_time.fetchAdd(elapsed_ns, .monotonic);
}
pub fn recordError(
self: *EndpointStats,
elapsed_ns: u64,
code: u16,
description: []const u8,
) void {
_ = self.num_requests.fetchAdd(1, .monotonic);
_ = self.num_errors.fetchAdd(1, .monotonic);
_ = self.processing_time.fetchAdd(elapsed_ns, .monotonic);
self.last_error_lock.lock();
defer self.last_error_lock.unlock();
const copy_len = @min(description.len, self.last_error_desc.len);
@memcpy(self.last_error_desc[0..copy_len], description[0..copy_len]);
self.last_error_len = copy_len;
self.last_error_code.store(code, .release);
}
pub fn reset(self: *EndpointStats) void {
self.num_requests.store(0, .release);
self.num_errors.store(0, .release);
self.processing_time.store(0, .release);
self.last_error_lock.lock();
defer self.last_error_lock.unlock();
self.last_error_len = 0;
self.last_error_code.store(0, .release);
}
pub fn snapshot(
self: *EndpointStats,
) struct {
num_requests: u64,
num_errors: u64,
processing_time: u64,
average_processing_time: u64,
last_error: ?protocol.Error,
} {
const num_requests = self.num_requests.load(.acquire);
const num_errors = self.num_errors.load(.acquire);
const processing_time = self.processing_time.load(.acquire);
const average_processing_time = if (num_requests == 0)
0
else
@divTrunc(processing_time, num_requests);
self.last_error_lock.lock();
defer self.last_error_lock.unlock();
const last_error = if (self.last_error_len == 0)
null
else
protocol.Error{
.code = self.last_error_code.load(.acquire),
.description = self.last_error_desc[0..self.last_error_len],
};
return .{
.num_requests = num_requests,
.num_errors = num_errors,
.processing_time = processing_time,
.average_processing_time = average_processing_time,
.last_error = last_error,
};
}
};
test "stats basic" {
var stats: EndpointStats = .{};
stats.recordSuccess(10);
stats.recordError(20, 503, "down");
const snap = stats.snapshot();
try std.testing.expectEqual(@as(u64, 2), snap.num_requests);
try std.testing.expectEqual(@as(u64, 1), snap.num_errors);
try std.testing.expectEqual(@as(u64, 30), snap.processing_time);
try std.testing.expect(snap.last_error != null);
}
================================================
FILE: src/micro/timeutil.zig
================================================
const std = @import("std");
pub fn nowRfc3339(io: std.Io, buf: []u8) ![]const u8 {
const ts = std.Io.Timestamp.now(io, .real);
const total_ns: u64 = @intCast(ts.nanoseconds);
const epoch_secs = total_ns / std.time.ns_per_s;
const ms = @as(u32, @intCast((total_ns % std.time.ns_per_s) / std.time.ns_per_ms));
const epoch = std.time.epoch.EpochSeconds{
.secs = epoch_secs,
};
const day_secs = epoch.getDaySeconds();
const year_day = epoch.getEpochDay().calculateYearDay();
const month_day = year_day.calculateMonthDay();
return std.fmt.bufPrint(
buf,
"{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}Z",
.{
year_day.year,
@intFromEnum(month_day.month),
month_day.day_index + 1,
day_secs.getHoursIntoDay(),
day_secs.getMinutesIntoHour(),
day_secs.getSecondsIntoMinute(),
ms,
},
);
}
================================================
FILE: src/micro/validation.zig
================================================
const std = @import("std");
const pubsub = @import("../pubsub.zig");
pub const Error = error{
InvalidName,
InvalidVersion,
InvalidPrefix,
InvalidGroup,
} || pubsub.subject.ValidationError;
pub fn validateName(name: []const u8) Error!void {
if (name.len == 0) return error.InvalidName;
for (name) |c| {
switch (c) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '_' => {},
else => return error.InvalidName,
}
}
}
pub fn validateVersion(version: []const u8) Error!void {
_ = std.SemanticVersion.parse(version) catch {
return error.InvalidVersion;
};
}
pub fn validatePrefix(prefix: []const u8) Error!void {
pubsub.validatePublish(prefix) catch {
return error.InvalidPrefix;
};
}
pub fn validateGroup(prefix: []const u8) Error!void {
pubsub.validatePublish(prefix) catch {
return error.InvalidGroup;
};
}
test "validate name" {
try validateName("svc_1");
try std.testing.expectError(error.InvalidName, validateName(""));
try std.testing.expectError(error.InvalidName, validateName("bad name"));
try std.testing.expectError(error.InvalidName, validateName("bad/name"));
}
test "validate version" {
try validateVersion("1.0.0");
try validateVersion("1.0.0-rc1+build.5");
try std.testing.expectError(error.InvalidVersion, validateVersion("1.0"));
}
================================================
FILE: src/micro.zig
================================================
const std = @import("std");
pub const Service = @import("micro/Service.zig");
pub const Config = Service.Config;
pub const Error = Service.Error;
pub const addService = Service.addService;
const request_mod = @import("micro/request.zig");
pub const Request = request_mod.Request;
pub const Handler = request_mod.Handler;
pub const HandlerFn = request_mod.HandlerFn;
pub const endpoint = @import("micro/endpoint.zig");
pub const Endpoint = endpoint.Endpoint;
pub const EndpointConfig = endpoint.EndpointConfig;
pub const Group = endpoint.Group;
pub const QueuePolicy = endpoint.QueuePolicy;
pub const protocol = @import("micro/protocol.zig");
pub const Info = protocol.Info;
pub const Ping = protocol.Ping;
pub const StatsResponse = protocol.StatsResponse;
test {
std.testing.refAllDecls(@This());
}
================================================
FILE: src/nats.zig
================================================
//! NATS Client Library for Zig
//!
//! A Zig implementation of the NATS messaging protocol with native
//! async I/O using std.Io, zero external C dependencies.
//!
//! ## Quick Start
//!
//! ```zig
//! const nats = @import("nats");
//! const std = @import("std");
//!
//! pub fn main(init: std.process.Init) !void {
//! const allocator = init.gpa;
//! const io = init.io;
//!
//! const client = try nats.Client.connect(allocator, io, "nats://localhost:4222", .{});
//! defer client.deinit();
//!
//! try client.publish("hello", "world");
//! try client.flush(std.time.ns_per_s * 10);
//! }
//! ```
const std = @import("std");
// Module exports
pub const defaults = @import("defaults.zig");
pub const protocol = @import("protocol.zig");
pub const connection = @import("connection.zig");
pub const pubsub = @import("pubsub.zig");
pub const memory = @import("memory.zig");
pub const auth = @import("auth.zig");
pub const jetstream = @import("jetstream.zig");
pub const micro = @import("micro.zig");
// Configuration types
pub const QueueSize = defaults.QueueSize;
// Client module
pub const Client = @import("Client.zig");
// Primary types (nested in Client)
pub const Subscription = Client.Subscription;
pub const Message = Client.Message;
pub const Options = Client.Options;
pub const Statistics = Client.Statistics;
// Event callback types (nested in Client)
pub const Event = Client.Event;
pub const EventHandler = Client.EventHandler;
// Message callback type (nested in Client)
pub const MsgHandler = Client.MsgHandler;
// Events module exports
const events = @import("events.zig");
pub const EventError = events.Error;
pub const statusText = events.statusText;
// Convenience exports
pub const newInbox = pubsub.newInbox;
pub const validateSubject = pubsub.validatePublish;
// Protocol types
pub const ServerInfo = protocol.ServerInfo;
pub const ConnectOptions = protocol.ConnectOptions;
/// Library version
pub const version = defaults.Protocol.version;
/// Default NATS port
pub const default_port: u16 = defaults.Protocol.port;
/// Default maximum payload size (1MB)
pub const default_max_payload: u32 = defaults.Protocol.max_payload;
test {
std.testing.refAllDecls(@This());
}
================================================
FILE: src/protocol/commands.zig
================================================
//! NATS Protocol Command Definitions
//!
//! Defines the structure of all NATS protocol commands for both
//! server-to-client and client-to-server communication.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const defaults = @import("../defaults.zig");
/// Commands sent from server to client.
pub const ServerCommand = union(enum) {
info: ServerInfo,
msg: MsgArgs,
hmsg: HMsgArgs,
ping,
pong,
ok,
err: []const u8,
};
/// Commands sent from client to server.
pub const ClientCommand = union(enum) {
connect: ConnectOptions,
pub_cmd: PubArgs,
hpub: HPubArgs,
sub: SubArgs,
unsub: UnsubArgs,
ping,
pong,
};
/// Server info with owned string copies.
/// All strings are allocated and owned by this struct.
pub const ServerInfo = struct {
/// Maximum allowed max_payload value (1GB).
pub const MAX_PAYLOAD_LIMIT = 1024 * 1024 * 1024;
/// Validation errors for ServerInfo.
pub const ValidationError = error{InvalidServerInfo};
server_id: []const u8,
server_name: []const u8,
version: []const u8,
host: []const u8,
proto: u32,
port: u16,
headers: bool,
max_payload: u32,
jetstream: bool,
tls_required: bool,
tls_available: bool,
auth_required: bool,
nonce: ?[]const u8,
client_id: ?u64,
client_ip: ?[]const u8,
cluster: ?[]const u8,
/// Discovered server URLs from cluster (inline storage, no allocation).
connect_urls: [16][256]u8 = undefined,
connect_urls_lens: [16]u8 = [_]u8{0} ** 16,
connect_urls_count: u8 = 0,
/// Creates an owned copy from parsed JSON RawServerInfo.
/// Copies all strings so they outlive the JSON arena.
/// Returns InvalidServerInfo if required fields are missing or invalid.
pub fn fromParsed(
allocator: Allocator,
parsed: std.json.Parsed(RawServerInfo),
) (ValidationError || Allocator.Error)!ServerInfo {
const info = parsed.value;
// Must have at least server_id or version for identification
if (info.server_id.len == 0 and info.version.len == 0) {
return error.InvalidServerInfo;
}
// max_payload must be > 0 and <= 1GB
if (info.max_payload == 0 or info.max_payload > MAX_PAYLOAD_LIMIT) {
return error.InvalidServerInfo;
}
const sid = try allocator.dupe(u8, info.server_id);
errdefer allocator.free(sid);
const sname = try allocator.dupe(u8, info.server_name);
errdefer allocator.free(sname);
const ver = try allocator.dupe(u8, info.version);
errdefer allocator.free(ver);
const host = try allocator.dupe(u8, info.host);
errdefer allocator.free(host);
const nonce = if (info.nonce) |n|
try allocator.dupe(u8, n)
else
null;
errdefer if (nonce) |n| allocator.free(n);
const cip = if (info.client_ip) |ip|
try allocator.dupe(u8, ip)
else
null;
errdefer if (cip) |c| allocator.free(c);
const cluster = if (info.cluster) |c|
try allocator.dupe(u8, c)
else
null;
var owned = ServerInfo{
.server_id = sid,
.server_name = sname,
.version = ver,
.host = host,
.proto = info.proto,
.port = info.port,
.headers = info.headers,
.max_payload = info.max_payload,
.jetstream = info.jetstream,
.tls_required = info.tls_required,
.tls_available = info.tls_available,
.auth_required = info.auth_required,
.nonce = nonce,
.client_id = info.client_id,
.client_ip = cip,
.cluster = cluster,
};
// Copy connect_urls (inline, no allocation)
// Skip URLs > max_url_len (truncated URL would be invalid anyway)
if (info.connect_urls) |urls| {
for (urls) |url| {
if (owned.connect_urls_count >= 16) break;
if (url.len > defaults.Server.max_url_len) continue;
const len: u8 = @intCast(url.len);
const idx = owned.connect_urls_count;
@memcpy(owned.connect_urls[idx][0..len], url);
owned.connect_urls_lens[idx] = len;
owned.connect_urls_count += 1;
}
}
return owned;
}
/// Frees all owned strings.
pub fn deinit(self: *ServerInfo, allocator: Allocator) void {
assert(self.port > 0);
allocator.free(self.server_id);
allocator.free(self.server_name);
allocator.free(self.version);
allocator.free(self.host);
if (self.nonce) |n| allocator.free(n);
if (self.client_ip) |ip| allocator.free(ip);
if (self.cluster) |c| allocator.free(c);
self.* = undefined;
}
/// Get connect URL at index. Returns null if index out of bounds.
pub fn getConnectUrl(self: *const ServerInfo, idx: u8) ?[]const u8 {
if (idx >= self.connect_urls_count) return null;
const len = self.connect_urls_lens[idx];
if (len == 0) return null;
return self.connect_urls[idx][0..len];
}
};
/// Raw server INFO payload parsed from JSON.
/// Internal use only - strings borrow from JSON arena.
pub const RawServerInfo = struct {
server_id: []const u8 = "",
server_name: []const u8 = "",
version: []const u8 = "",
proto: u32 = 1,
host: []const u8 = "",
port: u16 = 4222,
headers: bool = false,
max_payload: u32 = 1048576,
jetstream: bool = false,
tls_required: bool = false,
tls_available: bool = false,
auth_required: bool = false,
connect_urls: ?[]const []const u8 = null,
nonce: ?[]const u8 = null,
client_id: ?u64 = null,
client_ip: ?[]const u8 = null,
cluster: ?[]const u8 = null,
/// Parses RawServerInfo from JSON data.
pub fn parse(
allocator: Allocator,
json_data: []const u8,
) std.json.ParseError(std.json.Scanner)!std.json.Parsed(RawServerInfo) {
return std.json.parseFromSlice(
RawServerInfo,
allocator,
json_data,
.{ .ignore_unknown_fields = true },
);
}
/// Frees a parsed RawServerInfo.
pub fn deinit(parsed: *std.json.Parsed(RawServerInfo)) void {
parsed.deinit();
}
};
/// Client CONNECT command options.
pub const ConnectOptions = struct {
verbose: bool = false,
pedantic: bool = false,
tls_required: bool = false,
auth_token: ?[]const u8 = null,
user: ?[]const u8 = null,
pass: ?[]const u8 = null,
name: ?[]const u8 = null,
lang: []const u8 = "zig",
version: []const u8 = "0.1.0",
protocol: u32 = 1,
echo: bool = true,
sig: ?[]const u8 = null,
jwt: ?[]const u8 = null,
nkey: ?[]const u8 = null,
headers: bool = true,
no_responders: bool = true,
};
/// Arguments for PUB command.
pub const PubArgs = struct {
subject: []const u8,
reply_to: ?[]const u8 = null,
payload: []const u8,
};
/// Arguments for HPUB command (publish with headers).
pub const HPubArgs = struct {
subject: []const u8,
reply_to: ?[]const u8 = null,
headers: []const u8,
payload: []const u8,
};
/// Arguments for HPUB command with structured header entries.
/// Preferred over HPubArgs for type-safe header construction.
pub const HPubWithEntriesArgs = struct {
subject: []const u8,
reply_to: ?[]const u8 = null,
headers: []const headers_mod.Entry,
payload: []const u8,
};
const headers_mod = @import("headers.zig");
/// Arguments for SUB command.
pub const SubArgs = struct {
subject: []const u8,
queue_group: ?[]const u8 = null,
sid: u64,
};
/// Arguments for UNSUB command.
pub const UnsubArgs = struct {
sid: u64,
max_msgs: ?u64 = null,
};
/// Arguments parsed from MSG command.
pub const MsgArgs = struct {
subject: []const u8,
sid: u64,
reply_to: ?[]const u8 = null,
payload_len: usize,
payload: []const u8 = "",
/// Length of header line including \r\n (for partial message parsing).
header_line_len: usize = 0,
};
/// Arguments parsed from HMSG command (message with headers).
pub const HMsgArgs = struct {
subject: []const u8,
sid: u64,
reply_to: ?[]const u8 = null,
header_len: usize,
total_len: usize,
headers: []const u8 = "",
payload: []const u8 = "",
/// Length of header line including \r\n (for partial message parsing).
header_line_len: usize = 0,
};
test "server info parse" {
const allocator = std.testing.allocator;
const json = "{\"server_id\":\"test\",\"version\":\"2.10.0\"," ++
"\"proto\":1,\"max_payload\":1048576}";
var parsed = try RawServerInfo.parse(allocator, json);
defer parsed.deinit();
try std.testing.expectEqualSlices(u8, "test", parsed.value.server_id);
try std.testing.expectEqualSlices(u8, "2.10.0", parsed.value.version);
try std.testing.expectEqual(@as(u32, 1), parsed.value.proto);
try std.testing.expectEqual(@as(u32, 1048576), parsed.value.max_payload);
}
test "server info parse with unknown fields" {
const allocator = std.testing.allocator;
const json =
\\{"server_id":"x","unknown_field":"ignored","version":"1.0"}
;
var parsed = try RawServerInfo.parse(allocator, json);
defer parsed.deinit();
try std.testing.expectEqualSlices(u8, "x", parsed.value.server_id);
}
test "connect options defaults" {
const opts: ConnectOptions = .{};
try std.testing.expect(!opts.verbose);
try std.testing.expect(opts.echo);
try std.testing.expect(opts.headers);
try std.testing.expectEqualSlices(u8, "zig", opts.lang);
}
test "pub args" {
const args: PubArgs = .{
.subject = "test.subject",
.payload = "hello",
};
try std.testing.expectEqualSlices(u8, "test.subject", args.subject);
try std.testing.expectEqual(@as(?[]const u8, null), args.reply_to);
}
test "sub args with queue" {
const args: SubArgs = .{
.subject = "orders.>",
.queue_group = "workers",
.sid = 42,
};
try std.testing.expectEqualSlices(u8, "workers", args.queue_group.?);
}
================================================
FILE: src/protocol/encoder.zig
================================================
//! NATS Protocol Encoder
//!
//! Encodes client commands into NATS wire protocol format.
//! All string fields are validated against CRLF injection and control chars.
const std = @import("std");
const assert = std.debug.assert;
const Io = std.Io;
const commands = @import("commands.zig");
const ConnectOptions = commands.ConnectOptions;
const PubArgs = commands.PubArgs;
const HPubArgs = commands.HPubArgs;
const HPubWithEntriesArgs = commands.HPubWithEntriesArgs;
const SubArgs = commands.SubArgs;
const UnsubArgs = commands.UnsubArgs;
const headers = @import("headers.zig");
const subject = @import("../pubsub/subject.zig");
const ValidationError = subject.ValidationError;
/// Fast integer-to-string conversion (avoids std.fmt overhead).
/// Writes digits directly to buffer, returns slice of written digits.
fn writeUsizeToBuffer(buf: *[20]u8, value: usize) []const u8 {
if (value == 0) {
buf[19] = '0';
return buf[19..20];
}
var v = value;
var i: usize = 20;
while (v > 0) : (v /= 10) {
i -= 1;
buf[i] = @intCast((v % 10) + '0');
}
return buf[i..20];
}
/// Protocol encoder for client commands.
pub const Encoder = struct {
/// Encoding validation errors.
/// Includes ValidationError for subject/reply-to/queue-group validation.
pub const Error = error{
EmptyHeaders,
InvalidHeader,
InvalidSid,
} || ValidationError;
/// Encodes CONNECT command with JSON options.
pub fn encodeConnect(
writer: *Io.Writer,
opts: ConnectOptions,
) Io.Writer.Error!void {
try writer.writeAll("CONNECT ");
try std.json.Stringify.value(opts, .{}, writer);
try writer.writeAll("\r\n");
}
/// Encodes PUB command.
/// Validates subject and reply_to for CRLF injection and control chars.
pub fn encodePub(
writer: *Io.Writer,
args: PubArgs,
) (Error || Io.Writer.Error)!void {
try subject.validatePublish(args.subject);
assert(args.subject.len > 0);
try writer.writeAll("PUB ");
try writer.writeAll(args.subject);
if (args.reply_to) |reply| {
if (reply.len > 0) {
try subject.validateReplyTo(reply);
try writer.writeByte(' ');
try writer.writeAll(reply);
}
}
var num_buf: [20]u8 = undefined;
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, args.payload.len));
try writer.writeAll("\r\n");
try writer.writeAll(args.payload);
try writer.writeAll("\r\n");
}
/// Encodes HPUB command (publish with headers).
/// Validates subject and reply_to for CRLF injection and control chars.
pub fn encodeHPub(
writer: *Io.Writer,
args: HPubArgs,
) (Error || Io.Writer.Error)!void {
try subject.validatePublish(args.subject);
if (args.headers.len == 0) return Error.EmptyHeaders;
assert(args.subject.len > 0);
assert(args.headers.len > 0);
try writer.writeAll("HPUB ");
try writer.writeAll(args.subject);
if (args.reply_to) |reply| {
if (reply.len > 0) {
try subject.validateReplyTo(reply);
try writer.writeByte(' ');
try writer.writeAll(reply);
}
}
const total_len = args.headers.len + args.payload.len;
var num_buf: [20]u8 = undefined;
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, args.headers.len));
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, total_len));
try writer.writeAll("\r\n");
try writer.writeAll(args.headers);
try writer.writeAll(args.payload);
try writer.writeAll("\r\n");
}
/// Encodes HPUB command with structured header entries.
/// Calculates header size and encodes headers inline.
pub fn encodeHPubWithEntries(
writer: *Io.Writer,
args: HPubWithEntriesArgs,
) (Error || Io.Writer.Error)!void {
try subject.validatePublish(args.subject);
headers.validateEntries(args.headers) catch |err| switch (err) {
error.EmptyHeaders => return Error.EmptyHeaders,
error.InvalidHeader => return Error.InvalidHeader,
};
assert(args.subject.len > 0);
assert(args.headers.len > 0);
assert(args.headers.len <= 1024);
try writer.writeAll("HPUB ");
try writer.writeAll(args.subject);
if (args.reply_to) |reply| {
if (reply.len > 0) {
try subject.validateReplyTo(reply);
try writer.writeByte(' ');
try writer.writeAll(reply);
}
}
const hdr_len = headers.encodedSize(args.headers);
const total_len = hdr_len + args.payload.len;
var num_buf: [20]u8 = undefined;
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, hdr_len));
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, total_len));
try writer.writeAll("\r\n");
try headers.encode(writer, args.headers);
try writer.writeAll(args.payload);
try writer.writeAll("\r\n");
}
/// Encodes SUB command.
/// Validates subject and queue_group for CRLF injection and control chars.
pub fn encodeSub(
writer: *Io.Writer,
args: SubArgs,
) (Error || Io.Writer.Error)!void {
try subject.validateSubscribe(args.subject);
if (args.sid == 0) return Error.InvalidSid;
assert(args.subject.len > 0);
assert(args.sid > 0);
try writer.writeAll("SUB ");
try writer.writeAll(args.subject);
if (args.queue_group) |queue| {
if (queue.len > 0) {
try subject.validateQueueGroup(queue);
try writer.writeByte(' ');
try writer.writeAll(queue);
}
}
var num_buf: [20]u8 = undefined;
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, args.sid));
try writer.writeAll("\r\n");
}
/// Encodes UNSUB command.
pub fn encodeUnsub(
writer: *Io.Writer,
args: UnsubArgs,
) (Error || Io.Writer.Error)!void {
if (args.sid == 0) return Error.InvalidSid;
assert(args.sid > 0);
var num_buf: [20]u8 = undefined;
try writer.writeAll("UNSUB ");
try writer.writeAll(writeUsizeToBuffer(&num_buf, args.sid));
if (args.max_msgs) |max| {
try writer.writeByte(' ');
try writer.writeAll(writeUsizeToBuffer(&num_buf, max));
}
try writer.writeAll("\r\n");
}
/// Encodes PING command.
pub fn encodePing(writer: *Io.Writer) Io.Writer.Error!void {
try writer.writeAll("PING\r\n");
}
/// Encodes PONG command.
pub fn encodePong(writer: *Io.Writer) Io.Writer.Error!void {
try writer.writeAll("PONG\r\n");
}
};
test {
_ = @import("encoder_test.zig");
}
================================================
FILE: src/protocol/encoder_test.zig
================================================
//! Protocol Encoder Edge Case Tests
//!
//! - Integer conversion edge cases
//! - CRLF injection attacks (SECURITY)
//! - Empty/optional field handling
//! - SID boundary values
//! - Payload size edge cases
const std = @import("std");
const Io = std.Io;
const encoder = @import("encoder.zig");
const Encoder = encoder.Encoder;
// Section 1: Existing Tests (moved from encoder.zig)
test "encode PING" {
var buf: [64]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePing(&writer);
try std.testing.expectEqualSlices(u8, "PING\r\n", writer.buffered());
}
test "encode PONG" {
var buf: [64]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePong(&writer);
try std.testing.expectEqualSlices(u8, "PONG\r\n", writer.buffered());
}
test "encode PUB" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "test.subject",
.payload = "hello",
});
try std.testing.expectEqualSlices(
u8,
"PUB test.subject 5\r\nhello\r\n",
writer.buffered(),
);
}
test "encode PUB with reply" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "request",
.reply_to = "_INBOX.123",
.payload = "data",
});
try std.testing.expectEqualSlices(
u8,
"PUB request _INBOX.123 4\r\ndata\r\n",
writer.buffered(),
);
}
test "encode SUB" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{
.subject = "events.>",
.sid = 42,
});
try std.testing.expectEqualSlices(
u8,
"SUB events.> 42\r\n",
writer.buffered(),
);
}
test "encode SUB with queue" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{
.subject = "orders.*",
.queue_group = "workers",
.sid = 1,
});
try std.testing.expectEqualSlices(
u8,
"SUB orders.* workers 1\r\n",
writer.buffered(),
);
}
test "encode UNSUB" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeUnsub(&writer, .{ .sid = 5 });
try std.testing.expectEqualSlices(u8, "UNSUB 5\r\n", writer.buffered());
}
test "encode UNSUB with max" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeUnsub(&writer, .{ .sid = 5, .max_msgs = 10 });
try std.testing.expectEqualSlices(u8, "UNSUB 5 10\r\n", writer.buffered());
}
test "encode CONNECT" {
var buf: [1024]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeConnect(&writer, .{
.verbose = false,
.name = "test-client",
});
const written = writer.buffered();
try std.testing.expect(std.mem.startsWith(u8, written, "CONNECT {"));
try std.testing.expect(std.mem.endsWith(u8, written, "}\r\n"));
try std.testing.expect(
std.mem.indexOf(u8, written, "\"name\":\"test-client\"") != null,
);
}
test "encodePub empty subject rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const result = Encoder.encodePub(&writer, .{
.subject = "",
.payload = "hello",
});
try std.testing.expectError(Encoder.Error.EmptySubject, result);
}
test "encodeHPub empty subject rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const result = Encoder.encodeHPub(&writer, .{
.subject = "",
.headers = "NATS/1.0\r\n\r\n",
.payload = "hello",
});
try std.testing.expectError(Encoder.Error.EmptySubject, result);
}
test "encodeHPub empty headers rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const result = Encoder.encodeHPub(&writer, .{
.subject = "test",
.headers = "",
.payload = "hello",
});
try std.testing.expectError(Encoder.Error.EmptyHeaders, result);
}
test "encodeSub empty subject rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const result = Encoder.encodeSub(&writer, .{
.subject = "",
.sid = 1,
});
try std.testing.expectError(Encoder.Error.EmptySubject, result);
}
test "encodeSub invalid SID rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const result = Encoder.encodeSub(&writer, .{
.subject = "test",
.sid = 0,
});
try std.testing.expectError(Encoder.Error.InvalidSid, result);
}
test "encodeUnsub invalid SID rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const result = Encoder.encodeUnsub(&writer, .{ .sid = 0 });
try std.testing.expectError(Encoder.Error.InvalidSid, result);
}
// Section 2: SID Boundary Value Tests
test "encodeSub SID one" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{ .subject = "test", .sid = 1 });
try std.testing.expectEqualSlices(u8, "SUB test 1\r\n", writer.buffered());
}
test "encodeSub SID large value" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{ .subject = "test", .sid = 999999999 });
try std.testing.expectEqualSlices(
u8,
"SUB test 999999999\r\n",
writer.buffered(),
);
}
test "encodeSub SID u64 max" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const max_sid: u64 = std.math.maxInt(u64);
try Encoder.encodeSub(&writer, .{ .subject = "t", .sid = max_sid });
const expected = "SUB t 18446744073709551615\r\n";
try std.testing.expectEqualSlices(u8, expected, writer.buffered());
}
test "encodeUnsub SID one" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeUnsub(&writer, .{ .sid = 1 });
try std.testing.expectEqualSlices(u8, "UNSUB 1\r\n", writer.buffered());
}
test "encodeUnsub SID u64 max" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const max_sid: u64 = std.math.maxInt(u64);
try Encoder.encodeUnsub(&writer, .{ .sid = max_sid });
const expected = "UNSUB 18446744073709551615\r\n";
try std.testing.expectEqualSlices(u8, expected, writer.buffered());
}
test "encodeUnsub max_msgs u64 max" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const max_val: u64 = std.math.maxInt(u64);
try Encoder.encodeUnsub(&writer, .{ .sid = 1, .max_msgs = max_val });
const expected = "UNSUB 1 18446744073709551615\r\n";
try std.testing.expectEqualSlices(u8, expected, writer.buffered());
}
// Section 3: Payload Size Edge Cases
test "encodePub empty payload" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "test",
.payload = "",
});
try std.testing.expectEqualSlices(
u8,
"PUB test 0\r\n\r\n",
writer.buffered(),
);
}
test "encodePub single byte payload" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "test",
.payload = "X",
});
try std.testing.expectEqualSlices(
u8,
"PUB test 1\r\nX\r\n",
writer.buffered(),
);
}
test "encodePub payload length 9" {
var buf: [512]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
var payload_buf: [9]u8 = undefined;
@memset(&payload_buf, 'X');
try Encoder.encodePub(&writer, .{ .subject = "s", .payload = &payload_buf });
try std.testing.expect(std.mem.startsWith(u8, writer.buffered(), "PUB s 9\r\n"));
}
test "encodePub payload length 10" {
var buf: [512]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
var payload_buf: [10]u8 = undefined;
@memset(&payload_buf, 'X');
try Encoder.encodePub(&writer, .{ .subject = "s", .payload = &payload_buf });
try std.testing.expect(std.mem.startsWith(u8, writer.buffered(), "PUB s 10\r\n"));
}
test "encodePub payload length 99" {
var buf: [512]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
var payload_buf: [99]u8 = undefined;
@memset(&payload_buf, 'X');
try Encoder.encodePub(&writer, .{ .subject = "s", .payload = &payload_buf });
try std.testing.expect(std.mem.startsWith(u8, writer.buffered(), "PUB s 99\r\n"));
}
test "encodePub payload length 100" {
var buf: [512]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
var payload_buf: [100]u8 = undefined;
@memset(&payload_buf, 'X');
try Encoder.encodePub(&writer, .{ .subject = "s", .payload = &payload_buf });
try std.testing.expect(std.mem.startsWith(u8, writer.buffered(), "PUB s 100\r\n"));
}
test "encodeHPub empty payload with headers" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeHPub(&writer, .{
.subject = "test",
.headers = "NATS/1.0\r\n\r\n",
.payload = "",
});
// headers.len = 12, total_len = 12 (headers only)
try std.testing.expectEqualSlices(
u8,
"HPUB test 12 12\r\nNATS/1.0\r\n\r\n\r\n",
writer.buffered(),
);
}
test "encodeHPub headers and payload lengths" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeHPub(&writer, .{
.subject = "test",
.headers = "NATS/1.0\r\nX:Y\r\n\r\n", // 17 bytes
.payload = "hello", // 5 bytes
});
// headers.len = 17, total_len = 22
try std.testing.expectEqualSlices(
u8,
"HPUB test 17 22\r\nNATS/1.0\r\nX:Y\r\n\r\nhello\r\n",
writer.buffered(),
);
}
// Section 4: Subject Edge Cases
test "encodePub single char subject" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{ .subject = "x", .payload = "y" });
try std.testing.expectEqualSlices(u8, "PUB x 1\r\ny\r\n", writer.buffered());
}
test "encodePub subject with dots" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "foo.bar.baz",
.payload = "",
});
try std.testing.expectEqualSlices(
u8,
"PUB foo.bar.baz 0\r\n\r\n",
writer.buffered(),
);
}
test "encodeSub subject with wildcards" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{ .subject = "foo.*.bar", .sid = 1 });
try std.testing.expectEqualSlices(
u8,
"SUB foo.*.bar 1\r\n",
writer.buffered(),
);
}
test "encodeSub subject with full wildcard" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{ .subject = "foo.>", .sid = 1 });
try std.testing.expectEqualSlices(
u8,
"SUB foo.> 1\r\n",
writer.buffered(),
);
}
test "encodeSub subject only wildcard" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{ .subject = ">", .sid = 1 });
try std.testing.expectEqualSlices(u8, "SUB > 1\r\n", writer.buffered());
}
// Section 5: CRLF Injection Tests (SECURITY) - FIXED
test "encodePub subject with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// CRLF injection attempt - must be rejected
const malicious_subject = "test\r\nUNSUB 1\r\nPUB foo";
const result = Encoder.encodePub(&writer, .{
.subject = malicious_subject,
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodePub reply_to with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const malicious_reply = "_INBOX\r\nUNSUB 1\r\nPUB foo";
const result = Encoder.encodePub(&writer, .{
.subject = "test",
.reply_to = malicious_reply,
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeSub subject with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const malicious_subject = "test\r\nUNSUB 1";
const result = Encoder.encodeSub(&writer, .{
.subject = malicious_subject,
.sid = 1,
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeSub queue_group with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const malicious_queue = "workers\r\nUNSUB 1";
const result = Encoder.encodeSub(&writer, .{
.subject = "test",
.queue_group = malicious_queue,
.sid = 1,
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeHPub subject with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const malicious_subject = "test\r\nUNSUB 1";
const result = Encoder.encodeHPub(&writer, .{
.subject = malicious_subject,
.headers = "NATS/1.0\r\n\r\n",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeHPub reply_to with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const malicious_reply = "_INBOX\r\nUNSUB 1";
const result = Encoder.encodeHPub(&writer, .{
.subject = "test",
.reply_to = malicious_reply,
.headers = "NATS/1.0\r\n\r\n",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
// Section 6: Space in Fields Tests - FIXED
test "encodePub subject with space rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Space in subject must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test subject",
.payload = "x",
});
try std.testing.expectError(error.SpaceInSubject, result);
}
test "encodeSub queue_group with space rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Space in queue_group must be rejected
const result = Encoder.encodeSub(&writer, .{
.subject = "test",
.queue_group = "worker group",
.sid = 1,
});
try std.testing.expectError(error.InvalidCharacter, result);
}
// Section 7: Empty Optional Field Tests - FIXED
test "encodePub empty reply_to treated as null" {
// Empty string reply_to is now treated as null (skipped)
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "test",
.reply_to = "", // Empty string treated as null
.payload = "x",
});
// Should produce same output as no reply_to
try std.testing.expectEqualSlices(
u8,
"PUB test 1\r\nx\r\n",
writer.buffered(),
);
}
test "encodeSub empty queue_group treated as null" {
// Empty string queue_group is now treated as null (skipped)
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeSub(&writer, .{
.subject = "test",
.queue_group = "", // Empty string treated as null
.sid = 1,
});
// Should produce same output as no queue_group
try std.testing.expectEqualSlices(
u8,
"SUB test 1\r\n",
writer.buffered(),
);
}
test "encodeHPub empty reply_to treated as null" {
// Empty string reply_to is now treated as null (skipped)
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeHPub(&writer, .{
.subject = "test",
.reply_to = "",
.headers = "NATS/1.0\r\n\r\n",
.payload = "x",
});
// Should produce same output as no reply_to
try std.testing.expectEqualSlices(
u8,
"HPUB test 12 13\r\nNATS/1.0\r\n\r\nx\r\n",
writer.buffered(),
);
}
// Section 8: Null Byte Tests - FIXED
test "encodePub subject with null byte rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Null byte in subject must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test\x00inject",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodePub payload with null byte allowed" {
// Payload CAN contain null bytes (binary data)
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodePub(&writer, .{
.subject = "test",
.payload = "hel\x00lo",
});
const written = writer.buffered();
// Null byte should be in payload
try std.testing.expectEqualSlices(
u8,
"PUB test 6\r\nhel\x00lo\r\n",
written,
);
}
// Section 9: Control Character Tests - FIXED
test "encodePub subject with tab rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Tab in subject must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test\tsubject",
.payload = "x",
});
try std.testing.expectError(error.SpaceInSubject, result);
}
test "encodePub subject with CR only rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// CR alone must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test\rsubject",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodePub subject with LF only rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// LF alone must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test\nsubject",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodePub subject with DEL char rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// DEL (0x7F) must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test\x7fsubject",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodePub reply_to with control char rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Control char in reply_to must be rejected
const result = Encoder.encodePub(&writer, .{
.subject = "test",
.reply_to = "_INBOX\x01inject",
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeSub queue_group with control char rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Control char in queue_group must be rejected
const result = Encoder.encodeSub(&writer, .{
.subject = "test",
.queue_group = "workers\x01group",
.sid = 1,
});
try std.testing.expectError(error.InvalidCharacter, result);
}
// Section 10: UNSUB max_msgs Edge Cases
test "encodeUnsub max_msgs zero" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// max_msgs = 0 might have special meaning or be invalid
try Encoder.encodeUnsub(&writer, .{ .sid = 1, .max_msgs = 0 });
try std.testing.expectEqualSlices(
u8,
"UNSUB 1 0\r\n",
writer.buffered(),
);
}
test "encodeUnsub max_msgs one" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeUnsub(&writer, .{ .sid = 1, .max_msgs = 1 });
try std.testing.expectEqualSlices(
u8,
"UNSUB 1 1\r\n",
writer.buffered(),
);
}
// Section 11: CONNECT Edge Cases
test "encodeConnect minimal options" {
var buf: [1024]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeConnect(&writer, .{});
const written = writer.buffered();
try std.testing.expect(std.mem.startsWith(u8, written, "CONNECT {"));
try std.testing.expect(std.mem.endsWith(u8, written, "}\r\n"));
}
test "encodeConnect with all options" {
var buf: [2048]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
try Encoder.encodeConnect(&writer, .{
.verbose = true,
.pedantic = true,
.name = "full-client",
.lang = "zig",
.version = "1.0.0",
.protocol = 1,
.echo = false,
.headers = true,
.no_responders = true,
});
const written = writer.buffered();
try std.testing.expect(std.mem.startsWith(u8, written, "CONNECT {"));
try std.testing.expect(std.mem.endsWith(u8, written, "}\r\n"));
try std.testing.expect(
std.mem.indexOf(u8, written, "\"verbose\":true") != null,
);
try std.testing.expect(
std.mem.indexOf(u8, written, "\"echo\":false") != null,
);
}
// Section 12: Long Subject/Payload Tests
test "encodePub long subject" {
var buf: [4096]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Create 200-char subject
var subject_buf: [200]u8 = undefined;
@memset(&subject_buf, 'x');
try Encoder.encodePub(&writer, .{
.subject = &subject_buf,
.payload = "y",
});
const written = writer.buffered();
try std.testing.expect(std.mem.startsWith(u8, written, "PUB "));
try std.testing.expect(written.len > 200);
}
test "encodeSub long queue_group" {
var buf: [4096]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
var queue_buf: [100]u8 = undefined;
@memset(&queue_buf, 'q');
try Encoder.encodeSub(&writer, .{
.subject = "test",
.queue_group = &queue_buf,
.sid = 1,
});
const written = writer.buffered();
try std.testing.expect(written.len > 100);
}
// Section 13: Binary Payload Tests
test "encodePub payload with all byte values" {
var buf: [1024]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
// Payload containing all 256 byte values
var payload: [256]u8 = undefined;
for (&payload, 0..) |*p, i| {
p.* = @intCast(i);
}
try Encoder.encodePub(&writer, .{
.subject = "binary",
.payload = &payload,
});
const written = writer.buffered();
try std.testing.expect(
std.mem.startsWith(u8, written, "PUB binary 256\r\n"),
);
// Verify payload is intact
const payload_start = std.mem.indexOf(u8, written, "\r\n").? + 2;
const payload_end = written.len - 2; // Exclude trailing \r\n
try std.testing.expectEqualSlices(
u8,
&payload,
written[payload_start..payload_end],
);
}
test "encodeHPub binary headers and payload" {
var buf: [1024]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const headers = "NATS/1.0\r\nBin: \x00\x01\x02\r\n\r\n";
const payload = "\xFF\xFE\xFD";
try Encoder.encodeHPub(&writer, .{
.subject = "binary",
.headers = headers,
.payload = payload,
});
const written = writer.buffered();
// Verify headers and payload are in output
try std.testing.expect(std.mem.indexOf(u8, written, headers) != null);
}
// Section 14: HPUB with Entries Tests
const headers_mod = @import("headers.zig");
test "encodeHPubWithEntries basic" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "Foo", .value = "bar" },
};
try Encoder.encodeHPubWithEntries(&writer, .{
.subject = "test",
.headers = &entries,
.payload = "hello",
});
// NATS/1.0\r\nFoo: bar\r\n\r\n = 22 bytes
// total = 22 + 5 = 27 bytes
try std.testing.expectEqualSlices(
u8,
"HPUB test 22 27\r\nNATS/1.0\r\nFoo: bar\r\n\r\nhello\r\n",
writer.buffered(),
);
}
test "encodeHPubWithEntries with reply_to" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "X", .value = "Y" },
};
try Encoder.encodeHPubWithEntries(&writer, .{
.subject = "request",
.reply_to = "_INBOX.123",
.headers = &entries,
.payload = "data",
});
// NATS/1.0\r\nX: Y\r\n\r\n = 18 bytes
// total = 18 + 4 = 22 bytes
try std.testing.expectEqualSlices(
u8,
"HPUB request _INBOX.123 18 22\r\nNATS/1.0\r\nX: Y\r\n\r\ndata\r\n",
writer.buffered(),
);
}
test "encodeHPubWithEntries multiple headers" {
var buf: [512]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "Content-Type", .value = "application/json" },
.{ .key = "Nats-Msg-Id", .value = "abc123" },
};
try Encoder.encodeHPubWithEntries(&writer, .{
.subject = "api.request",
.headers = &entries,
.payload = "{}",
});
const written = writer.buffered();
try std.testing.expect(std.mem.startsWith(u8, written, "HPUB api.request "));
try std.testing.expect(std.mem.indexOf(
u8,
written,
"Content-Type: application/json",
) != null);
try std.testing.expect(std.mem.indexOf(
u8,
written,
"Nats-Msg-Id: abc123",
) != null);
}
test "encodeHPubWithEntries empty payload" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "Status", .value = "100" },
};
try Encoder.encodeHPubWithEntries(&writer, .{
.subject = "notify",
.headers = &entries,
.payload = "",
});
// NATS/1.0\r\n (10) + Status: 100\r\n (13) + \r\n (2) = 25 bytes
// total = 25 + 0 = 25 bytes
try std.testing.expectEqualSlices(
u8,
"HPUB notify 25 25\r\nNATS/1.0\r\nStatus: 100\r\n\r\n\r\n",
writer.buffered(),
);
}
test "encodeHPubWithEntries empty headers rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{};
const result = Encoder.encodeHPubWithEntries(&writer, .{
.subject = "test",
.headers = &entries,
.payload = "x",
});
try std.testing.expectError(Encoder.Error.EmptyHeaders, result);
}
test "encodeHPubWithEntries empty subject rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "X", .value = "Y" },
};
const result = Encoder.encodeHPubWithEntries(&writer, .{
.subject = "",
.headers = &entries,
.payload = "x",
});
try std.testing.expectError(Encoder.Error.EmptySubject, result);
}
test "encodeHPubWithEntries subject with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "X", .value = "Y" },
};
const result = Encoder.encodeHPubWithEntries(&writer, .{
.subject = "test\r\nUNSUB 1",
.headers = &entries,
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeHPubWithEntries reply_to with CRLF rejected" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "X", .value = "Y" },
};
const result = Encoder.encodeHPubWithEntries(&writer, .{
.subject = "test",
.reply_to = "_INBOX\r\nUNSUB 1",
.headers = &entries,
.payload = "x",
});
try std.testing.expectError(error.InvalidCharacter, result);
}
test "encodeHPubWithEntries empty reply_to treated as null" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]headers_mod.Entry{
.{ .key = "X", .value = "Y" },
};
try Encoder.encodeHPubWithEntries(&writer, .{
.subject = "test",
.reply_to = "",
.headers = &entries,
.payload = "x",
});
// Should produce same as no reply_to
try std.testing.expectEqualSlices(
u8,
"HPUB test 18 19\r\nNATS/1.0\r\nX: Y\r\n\r\nx\r\n",
writer.buffered(),
);
}
================================================
FILE: src/protocol/errors.zig
================================================
//! Protocol Errors
//!
//! Error types for protocol-related failures including parsing errors,
//! server errors, and authorization violations.
const std = @import("std");
/// Protocol-related errors.
pub const Error = error{
/// Server sent an invalid protocol message.
ProtocolError,
/// Server sent an error response.
ServerError,
/// Authorization for the requested operation was denied.
AuthorizationViolation,
/// Message payload exceeds server's maximum allowed size.
MaxPayloadExceeded,
};
/// Parses a NATS server error message into a protocol Error.
/// Server errors come in the form: -ERR 'message'
pub fn parseServerError(msg: []const u8) Error {
if (std.mem.indexOf(u8, msg, "Authorization Violation")) |_| {
return error.AuthorizationViolation;
}
if (std.mem.indexOf(u8, msg, "Maximum Payload")) |_| {
return error.MaxPayloadExceeded;
}
return error.ServerError;
}
/// Returns true if the error is a permissions error.
pub fn isAuthError(err: Error) bool {
return err == error.AuthorizationViolation;
}
test "parseServerError authorization" {
const err = parseServerError("Authorization Violation");
try std.testing.expectEqual(error.AuthorizationViolation, err);
}
test "parseServerError max payload" {
const err = parseServerError("Maximum Payload Exceeded");
try std.testing.expectEqual(error.MaxPayloadExceeded, err);
}
test "parseServerError unknown" {
const err = parseServerError("Some Unknown Error");
try std.testing.expectEqual(error.ServerError, err);
}
test "isAuthError" {
try std.testing.expect(isAuthError(error.AuthorizationViolation));
try std.testing.expect(!isAuthError(error.ServerError));
try std.testing.expect(!isAuthError(error.ProtocolError));
}
================================================
FILE: src/protocol/header_map.zig
================================================
//! Header Map Builder
//!
//! Programmatic builder for NATS message headers.
//! Supports multi-value headers (same key, multiple values).
//!
//! Example:
//! ```zig
//! var headers = HeaderMap.init(allocator);
//! defer headers.deinit();
//! try headers.set("Content-Type", "application/json");
//! try headers.add("X-Tag", "important");
//! try headers.add("X-Tag", "urgent"); // Multiple values
//!
//! // Encode to NATS format
//! const encoded = try headers.encode();
//! defer allocator.free(encoded);
//! ```
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const headers = @import("headers.zig");
/// Builder for NATS message headers.
/// Supports multiple values per key.
/// Stores allocator
pub const HeaderMap = struct {
allocator: Allocator,
/// Keys (owned, case-preserved).
keys: std.ArrayList([]u8) = .empty,
/// Values (owned). Same index as key.
values: std.ArrayList([]u8) = .empty,
/// Creates a new HeaderMap with the given allocator.
pub fn init(allocator: Allocator) HeaderMap {
return .{ .allocator = allocator };
}
/// Frees all memory.
pub fn deinit(self: *HeaderMap) void {
for (self.keys.items) |key| {
self.allocator.free(key);
}
self.keys.deinit(self.allocator);
for (self.values.items) |value| {
self.allocator.free(value);
}
self.values.deinit(self.allocator);
}
/// Sets a header, replacing any existing values for this key.
/// Key comparison is case-insensitive.
pub fn set(
self: *HeaderMap,
key: []const u8,
value: []const u8,
) error{ InvalidHeader, OutOfMemory }!void {
headers.validateKeyValue(key, value) catch return error.InvalidHeader;
// Remove existing values for this key
self.deleteInternal(key);
// Add new entry
const owned_key = try self.allocator.dupe(u8, key);
errdefer self.allocator.free(owned_key);
const owned_value = try self.allocator.dupe(u8, value);
errdefer self.allocator.free(owned_value);
try self.keys.append(self.allocator, owned_key);
try self.values.append(self.allocator, owned_value);
}
/// Adds a value to a header (allows multiple values for same key).
/// Key comparison is case-insensitive for grouping.
pub fn add(
self: *HeaderMap,
key: []const u8,
value: []const u8,
) error{ InvalidHeader, OutOfMemory }!void {
headers.validateKeyValue(key, value) catch return error.InvalidHeader;
const owned_key = try self.allocator.dupe(u8, key);
errdefer self.allocator.free(owned_key);
const owned_value = try self.allocator.dupe(u8, value);
errdefer self.allocator.free(owned_value);
try self.keys.append(self.allocator, owned_key);
try self.values.append(self.allocator, owned_value);
}
/// Gets the first value for a header (case-insensitive lookup).
/// Returns null if header not found.
pub fn get(self: *const HeaderMap, key: []const u8) ?[]const u8 {
assert(key.len > 0);
for (self.keys.items, 0..) |k, i| {
if (std.ascii.eqlIgnoreCase(k, key)) {
return self.values.items[i];
}
}
return null;
}
/// Gets the last value for a header (case-insensitive lookup).
/// Useful when multiple values exist and you want the most recent.
/// Returns null if header not found.
pub fn getLast(self: *const HeaderMap, key: []const u8) ?[]const u8 {
assert(key.len > 0);
var last: ?[]const u8 = null;
for (self.keys.items, 0..) |k, i| {
if (std.ascii.eqlIgnoreCase(k, key)) {
last = self.values.items[i];
}
}
return last;
}
/// Gets all values for a header (case-insensitive lookup).
/// Caller owns returned slice, must free with allocator.
pub fn getAll(
self: *const HeaderMap,
key: []const u8,
) Allocator.Error!?[]const []const u8 {
assert(key.len > 0);
var match_count: usize = 0;
for (self.keys.items) |k| {
if (std.ascii.eqlIgnoreCase(k, key)) {
match_count += 1;
}
}
if (match_count == 0) return null;
const result = try self.allocator.alloc(
[]const u8,
match_count,
);
var idx: usize = 0;
for (self.keys.items, 0..) |k, i| {
if (std.ascii.eqlIgnoreCase(k, key)) {
result[idx] = self.values.items[i];
idx += 1;
}
}
return result;
}
/// Deletes all values for a header (case-insensitive).
pub fn delete(self: *HeaderMap, key: []const u8) void {
assert(key.len > 0);
self.deleteInternal(key);
}
fn deleteInternal(self: *HeaderMap, key: []const u8) void {
var i: usize = 0;
while (i < self.keys.items.len) {
if (std.ascii.eqlIgnoreCase(
self.keys.items[i],
key,
)) {
self.allocator.free(
self.keys.orderedRemove(i),
);
self.allocator.free(
self.values.orderedRemove(i),
);
} else {
i += 1;
}
}
}
/// Returns slice of all keys (for iteration).
/// Note: May contain duplicate keys if add() was used.
pub fn keys_slice(self: *const HeaderMap) []const []const u8 {
return @ptrCast(self.keys.items);
}
/// Returns the number of header entries.
/// Note: Same key may appear multiple times.
pub fn count(self: *const HeaderMap) usize {
return self.keys.items.len;
}
/// Returns true if the map is empty.
pub fn isEmpty(self: *const HeaderMap) bool {
return self.keys.items.len == 0;
}
/// Encodes headers to NATS/1.0 format.
/// Caller owns returned memory.
pub fn encode(self: *const HeaderMap) ![]u8 {
if (self.keys.items.len == 0) {
return error.EmptyHeaders;
}
// Calculate size
var size: usize = 10; // "NATS/1.0\r\n"
for (self.keys.items, 0..) |key, i| {
// "Key: Value\r\n"
size += key.len + 2 + self.values.items[i].len + 2;
}
size += 2; // final "\r\n"
const buf = try self.allocator.alloc(u8, size);
errdefer self.allocator.free(buf);
var pos: usize = 0;
@memcpy(buf[pos..][0..10], "NATS/1.0\r\n");
pos += 10;
for (self.keys.items, 0..) |key, i| {
const value = self.values.items[i];
@memcpy(buf[pos..][0..key.len], key);
pos += key.len;
@memcpy(buf[pos..][0..2], ": ");
pos += 2;
@memcpy(buf[pos..][0..value.len], value);
pos += value.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
}
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
assert(pos == size);
return buf;
}
/// Returns the encoded size in bytes.
pub fn encodedSize(self: *const HeaderMap) usize {
if (self.keys.items.len == 0) return 0;
var size: usize = 10; // "NATS/1.0\r\n"
for (self.keys.items, 0..) |key, i| {
size += key.len + 2 + self.values.items[i].len + 2;
}
size += 2; // final "\r\n"
return size;
}
};
// Tests
test "header map set and get" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.set("Content-Type", "application/json");
try hm.set("X-Request-Id", "abc123");
try std.testing.expectEqualStrings(
"application/json",
hm.get("Content-Type").?,
);
try std.testing.expectEqualStrings(
"abc123",
hm.get("X-Request-Id").?,
);
try std.testing.expectEqual(
@as(?[]const u8, null),
hm.get("Not-Found"),
);
}
test "header map case insensitive get" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.set("Content-Type", "text/plain");
try std.testing.expect(hm.get("content-type") != null);
try std.testing.expect(hm.get("CONTENT-TYPE") != null);
try std.testing.expect(hm.get("Content-Type") != null);
}
test "header map set replaces existing" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.set("Key", "value1");
try hm.set("Key", "value2");
try std.testing.expectEqualStrings(
"value2",
hm.get("Key").?,
);
try std.testing.expectEqual(@as(usize, 1), hm.count());
}
test "header map add multiple values" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.add("X-Tag", "important");
try hm.add("X-Tag", "urgent");
try hm.add("X-Tag", "review");
try std.testing.expectEqual(@as(usize, 3), hm.count());
const all = try hm.getAll("X-Tag");
defer allocator.free(all.?);
try std.testing.expectEqual(@as(usize, 3), all.?.len);
try std.testing.expectEqualStrings("important", all.?[0]);
try std.testing.expectEqualStrings("urgent", all.?[1]);
try std.testing.expectEqualStrings("review", all.?[2]);
}
test "header map getLast" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.add("X-Tag", "first");
try hm.add("X-Tag", "second");
try hm.add("X-Tag", "third");
// get() returns first, getLast() returns last
try std.testing.expectEqualStrings(
"first",
hm.get("X-Tag").?,
);
try std.testing.expectEqualStrings(
"third",
hm.getLast("X-Tag").?,
);
// Case insensitive
try std.testing.expectEqualStrings(
"third",
hm.getLast("x-tag").?,
);
// Non-existent key returns null
try std.testing.expectEqual(
@as(?[]const u8, null),
hm.getLast("Not-Found"),
);
}
test "header map delete" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.add("X-Tag", "value1");
try hm.add("X-Tag", "value2");
try hm.add("Other", "keep");
hm.delete("X-Tag");
try std.testing.expectEqual(
@as(?[]const u8, null),
hm.get("X-Tag"),
);
try std.testing.expectEqualStrings(
"keep",
hm.get("Other").?,
);
try std.testing.expectEqual(@as(usize, 1), hm.count());
}
test "header map encode" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.set("Foo", "bar");
try hm.set("Baz", "123");
const encoded = try hm.encode();
defer allocator.free(encoded);
try std.testing.expect(
std.mem.startsWith(u8, encoded, "NATS/1.0\r\n"),
);
try std.testing.expect(
std.mem.endsWith(u8, encoded, "\r\n\r\n"),
);
try std.testing.expect(
std.mem.indexOf(u8, encoded, "Foo: bar\r\n") != null,
);
try std.testing.expect(
std.mem.indexOf(u8, encoded, "Baz: 123\r\n") != null,
);
}
test "header map encoded size" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.set("Foo", "bar");
// "NATS/1.0\r\n" (10) + "Foo: bar\r\n" (10) + "\r\n" (2) = 22
try std.testing.expectEqual(
@as(usize, 22),
hm.encodedSize(),
);
const encoded = try hm.encode();
defer allocator.free(encoded);
try std.testing.expectEqual(
hm.encodedSize(),
encoded.len,
);
}
test "header map empty" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try std.testing.expect(hm.isEmpty());
try std.testing.expectEqual(
@as(usize, 0),
hm.count(),
);
try std.testing.expectEqual(
@as(usize, 0),
hm.encodedSize(),
);
}
test "header map with empty value" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try hm.set("Empty", "");
try std.testing.expectEqualStrings(
"",
hm.get("Empty").?,
);
const encoded = try hm.encode();
defer allocator.free(encoded);
try std.testing.expect(
std.mem.indexOf(u8, encoded, "Empty: \r\n") != null,
);
}
test "header map rejects injection-prone names and values" {
const allocator = std.testing.allocator;
var hm = HeaderMap.init(allocator);
defer hm.deinit();
try std.testing.expectError(
error.InvalidHeader,
hm.set("Bad:Name", "value"),
);
try std.testing.expectError(
error.InvalidHeader,
hm.add("Bad\x7fName", "value"),
);
try std.testing.expectError(
error.InvalidHeader,
hm.add("Good", "bad\x7fvalue"),
);
}
================================================
FILE: src/protocol/headers.zig
================================================
//! NATS Protocol Headers
//!
//! Handles NATS message headers in the NATS/1.0 format.
//! Headers are used with HPUB/HMSG commands for metadata.
//!
//! Features:
//! - Full ownership: ParseResult copies all strings, safe after source freed
//! - Case-insensitive header lookup
//! - API: parse() function, always call deinit()
//!
//! Format:
//! ```
//! NATS/1.0\r\n
//! Header-Name: value\r\n
//! Another-Header: value\r\n
//! \r\n
//! ```
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Io = std.Io;
/// Well-known NATS header names.
pub const HeaderName = struct {
pub const msg_id = "Nats-Msg-Id";
pub const expected_stream = "Nats-Expected-Stream";
pub const expected_last_msg_id = "Nats-Expected-Last-Msg-Id";
pub const expected_last_seq = "Nats-Expected-Last-Sequence";
pub const expected_last_subj_seq = "Nats-Expected-Last-Subject-Sequence";
pub const last_consumer = "Nats-Last-Consumer";
pub const last_stream = "Nats-Last-Stream";
pub const consumer_stalled = "Nats-Consumer-Stalled";
pub const rollup = "Nats-Rollup";
pub const msg_ttl = "Nats-TTL";
pub const no_responders = "Status";
pub const description = "Description";
};
/// Status codes returned in headers.
pub const Status = struct {
pub const no_responders = "503";
pub const request_timeout = "408";
pub const no_messages = "404";
pub const control_message = "100";
};
/// Extracts status code from raw header bytes without allocation.
/// Returns null if no status code present or invalid format.
/// Format expected: "NATS/1.0 503 Description\r\n..." or "NATS/1.0\r\n..."
pub fn extractStatus(header_data: []const u8) ?u16 {
// Minimum: "NATS/1.0\r\n" = 10 chars
if (header_data.len < 10) return null;
// Verify NATS/1.0 prefix
if (!std.mem.startsWith(u8, header_data, "NATS/1.0")) return null;
// Skip "NATS/1.0"
const after_version = header_data[8..];
// If next char is \r, no status code
if (after_version.len == 0 or after_version[0] == '\r') return null;
// Expect space before status code
if (after_version[0] != ' ') return null;
// Find end of status code (space or \r)
const status_start = 1; // skip space
var status_end: usize = status_start;
while (status_end < after_version.len) : (status_end += 1) {
const c = after_version[status_end];
if (c == ' ' or c == '\r') break;
}
if (status_end == status_start) return null;
const status_str = after_version[status_start..status_end];
return std.fmt.parseInt(u16, status_str, 10) catch null;
}
/// Header entry (key-value pair).
pub const Entry = struct {
key: []const u8,
value: []const u8,
};
pub const ValidationError = error{
EmptyHeaders,
InvalidHeader,
};
/// Validates a header field name.
/// Field names must be non-empty and cannot contain
/// whitespace, control characters, DEL, or ':'.
pub fn validateKey(key: []const u8) ValidationError!void {
if (key.len == 0) return error.InvalidHeader;
for (key) |c| {
if (c <= 0x20 or c == 0x7f or c == ':') {
return error.InvalidHeader;
}
}
}
/// Validates a header field value.
/// Values cannot contain control characters or DEL; this prevents
/// CRLF injection when headers are serialized to the NATS wire format.
pub fn validateValue(value: []const u8) ValidationError!void {
for (value) |c| {
if (c < 0x20 or c == 0x7f) {
return error.InvalidHeader;
}
}
}
pub fn validateKeyValue(
key: []const u8,
value: []const u8,
) ValidationError!void {
try validateKey(key);
try validateValue(value);
}
pub fn validateEntries(entries: []const Entry) ValidationError!void {
if (entries.len == 0) return error.EmptyHeaders;
for (entries) |entry| {
try validateKeyValue(entry.key, entry.value);
}
}
/// Result of header parsing.
///
/// Owns all its data - copies strings to heap. Safe to use after source
/// data is freed. Caller MUST call deinit() to free memory.
pub const ParseResult = struct {
/// Heap-allocated entry array (owns this memory).
entries: []Entry = &.{},
/// Heap-allocated string buffer (all key/value strings copied here).
string_buf: []u8 = &.{},
/// Number of valid entries.
count: usize = 0,
/// Allocator used (needed for deinit).
allocator: Allocator,
/// Status code from header line (e.g., "503") - slice into string_buf.
status: ?[]const u8 = null,
/// Description from header line - slice into string_buf.
description: ?[]const u8 = null,
/// Byte offset where headers end in original data.
header_end: usize = 0,
/// Parse error if any.
err: ?ParseError = null,
pub const ParseError = enum {
invalid_version,
invalid_header,
incomplete,
out_of_memory,
};
/// Returns all parsed entries. Empty slice if error occurred.
pub fn items(self: *const ParseResult) []const Entry {
if (self.err != null) return &.{};
assert(self.count <= self.entries.len);
return self.entries[0..self.count];
}
/// Gets first value for header name (case-insensitive).
/// Returns null if error occurred or header not found.
pub fn get(self: *const ParseResult, name: []const u8) ?[]const u8 {
if (self.err != null) return null;
for (self.items()) |entry| {
if (std.ascii.eqlIgnoreCase(entry.key, name)) {
return entry.value;
}
}
return null;
}
/// Returns true if this is a no-responders status (503).
pub fn isNoResponders(self: *const ParseResult) bool {
if (self.err != null) return false;
if (self.status) |s| {
return std.mem.eql(u8, s, Status.no_responders);
}
return false;
}
/// Frees all heap-allocated memory.
/// Safe to call multiple times. MUST be called after parse().
pub fn deinit(self: *ParseResult) void {
if (self.entries.len > 0) {
self.allocator.free(self.entries);
self.entries = &.{};
}
if (self.string_buf.len > 0) {
self.allocator.free(self.string_buf);
self.string_buf = &.{};
}
self.count = 0;
self.status = null;
self.description = null;
}
};
/// Parses headers from NATS/1.0 format.
///
/// Allocates memory and copies all header data. ParseResult owns its data
/// and is safe to use after the source data is freed.
///
/// Caller MUST call result.deinit() to free memory.
///
/// On error: result.err is set, items() returns empty, get() returns null.
pub fn parse(allocator: Allocator, data: []const u8) ParseResult {
assert(data.len > 0);
return parseImpl(allocator, data);
}
fn parseImpl(allocator: Allocator, data: []const u8) ParseResult {
var result: ParseResult = .{ .allocator = allocator };
if (!std.mem.startsWith(u8, data, "NATS/1.0")) {
result.err = .invalid_version;
return result;
}
// Pass 1: count headers and string bytes
var header_count: usize = 0;
var total_string_bytes: usize = 0;
var status_len: usize = 0;
var desc_len: usize = 0;
var pos: usize = 8;
// Parse optional status and description on first line
if (pos < data.len and data[pos] == ' ') {
pos += 1;
const status_end = std.mem.indexOfPos(u8, data, pos, " ") orelse
std.mem.indexOfPos(u8, data, pos, "\r\n") orelse {
result.err = .incomplete;
return result;
};
status_len = status_end - pos;
total_string_bytes += status_len;
pos = status_end;
// Skip description if present
if (pos < data.len and data[pos] == ' ') {
pos += 1;
const desc_end = std.mem.indexOfPos(u8, data, pos, "\r\n") orelse {
result.err = .incomplete;
return result;
};
desc_len = desc_end - pos;
total_string_bytes += desc_len;
pos = desc_end;
}
}
// Skip \r\n after version line
if (pos + 2 > data.len or !std.mem.eql(u8, data[pos..][0..2], "\r\n")) {
result.err = .incomplete;
return result;
}
pos += 2;
// Count header entries and their string sizes
while (pos < data.len) {
// Empty line marks end of headers
if (std.mem.startsWith(u8, data[pos..], "\r\n")) {
result.header_end = pos + 2;
break;
}
// Find colon separator
const colon = std.mem.indexOfPos(u8, data, pos, ":") orelse {
result.err = .invalid_header;
return result;
};
const key_len = colon - pos;
// Skip colon and optional space
var value_start = colon + 1;
if (value_start < data.len and data[value_start] == ' ') {
value_start += 1;
}
// Find end of line
const line_end = std.mem.indexOfPos(u8, data, value_start, "\r\n") orelse {
result.err = .incomplete;
return result;
};
const value_len = line_end - value_start;
header_count += 1;
// Guard against unbounded header allocation
if (header_count > 1024) {
result.err = .invalid_header;
return result;
}
total_string_bytes += key_len + value_len;
pos = line_end + 2;
} else {
// Didn't find terminating \r\n\r\n
result.err = .incomplete;
return result;
}
// Pass 2: allocate and copy
if (header_count > 0 or status_len > 0 or desc_len > 0) {
// Allocate entries array
const entries = allocator.alloc(Entry, header_count) catch {
result.err = .out_of_memory;
return result;
};
// Allocate string buffer
const string_buf = allocator.alloc(u8, total_string_bytes) catch {
allocator.free(entries);
result.err = .out_of_memory;
return result;
};
result.entries = entries;
result.string_buf = string_buf;
// Copy strings into buffer
var buf_pos: usize = 0;
var entry_idx: usize = 0;
pos = 8;
// Copy status and description
if (status_len > 0) {
pos += 1; // skip space
@memcpy(string_buf[buf_pos..][0..status_len], data[pos..][0..status_len]);
result.status = string_buf[buf_pos..][0..status_len];
buf_pos += status_len;
pos += status_len;
if (desc_len > 0) {
pos += 1; // skip space
@memcpy(
string_buf[buf_pos..][0..desc_len],
data[pos..][0..desc_len],
);
result.description = string_buf[buf_pos..][0..desc_len];
buf_pos += desc_len;
pos += desc_len;
}
}
// Skip \r\n after version line
pos = std.mem.indexOfPos(u8, data, 8, "\r\n").? + 2;
// Copy header entries
while (pos < data.len) {
if (std.mem.startsWith(u8, data[pos..], "\r\n")) {
break;
}
const colon = std.mem.indexOfPos(u8, data, pos, ":").?;
const key_len = colon - pos;
// Copy key
@memcpy(string_buf[buf_pos..][0..key_len], data[pos..][0..key_len]);
const key_slice = string_buf[buf_pos..][0..key_len];
buf_pos += key_len;
// Skip colon and optional space
var value_start = colon + 1;
if (value_start < data.len and data[value_start] == ' ') {
value_start += 1;
}
const line_end = std.mem.indexOfPos(u8, data, value_start, "\r\n").?;
const value_len = line_end - value_start;
// Copy value
@memcpy(
string_buf[buf_pos..][0..value_len],
data[value_start..][0..value_len],
);
const value_slice = string_buf[buf_pos..][0..value_len];
buf_pos += value_len;
entries[entry_idx] = .{ .key = key_slice, .value = value_slice };
entry_idx += 1;
pos = line_end + 2;
}
result.count = entry_idx;
assert(entry_idx == header_count);
assert(buf_pos == total_string_bytes);
}
return result;
}
/// Encodes headers to NATS/1.0 format.
pub fn encode(
writer: *Io.Writer,
entries: []const Entry,
) Io.Writer.Error!void {
assert(entries.len > 0);
try writer.writeAll("NATS/1.0\r\n");
for (entries) |entry| {
try writer.writeAll(entry.key);
try writer.writeAll(": ");
try writer.writeAll(entry.value);
try writer.writeAll("\r\n");
}
try writer.writeAll("\r\n");
}
/// Encodes headers directly into a pre-sized byte buffer.
/// Buffer must be exactly encodedSize(entries) bytes.
pub fn encodeToBuf(
buf: []u8,
entries: []const Entry,
) void {
assert(entries.len > 0);
var pos: usize = 0;
@memcpy(buf[pos..][0..10], "NATS/1.0\r\n");
pos += 10;
for (entries) |entry| {
@memcpy(buf[pos..][0..entry.key.len], entry.key);
pos += entry.key.len;
@memcpy(buf[pos..][0..2], ": ");
pos += 2;
@memcpy(
buf[pos..][0..entry.value.len],
entry.value,
);
pos += entry.value.len;
@memcpy(buf[pos..][0..2], "\r\n");
pos += 2;
}
@memcpy(buf[pos..][0..2], "\r\n");
}
/// Calculates the encoded size of headers.
pub fn encodedSize(entries: []const Entry) usize {
assert(entries.len > 0);
var size: usize = 10; // "NATS/1.0\r\n"
for (entries) |entry| {
size += entry.key.len + 2 + entry.value.len + 2;
}
size += 2; // final \r\n
return size;
}
// Tests
test "parse simple headers" {
const data = "NATS/1.0\r\nFoo: bar\r\nBaz: qux\r\n\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqual(@as(usize, 2), result.count);
try std.testing.expectEqualSlices(u8, "Foo", result.items()[0].key);
try std.testing.expectEqualSlices(u8, "bar", result.items()[0].value);
try std.testing.expectEqualSlices(u8, "Baz", result.items()[1].key);
try std.testing.expectEqualSlices(u8, "qux", result.items()[1].value);
}
test "parse with status" {
const data = "NATS/1.0 503 No Responders\r\n\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqualSlices(u8, "503", result.status.?);
try std.testing.expectEqualSlices(u8, "No Responders", result.description.?);
try std.testing.expect(result.isNoResponders());
}
test "parse no headers" {
const data = "NATS/1.0\r\n\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqual(@as(usize, 0), result.count);
}
test "get header case insensitive" {
const data = "NATS/1.0\r\nContent-Type: application/json\r\n\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expect(result.get("content-type") != null);
try std.testing.expect(result.get("CONTENT-TYPE") != null);
try std.testing.expect(result.get("Content-Type") != null);
}
test "parse many headers" {
// Build 100 headers dynamically
var data_buf: [4096]u8 = undefined;
var pos: usize = 0;
const prefix = "NATS/1.0\r\n";
@memcpy(data_buf[pos..][0..prefix.len], prefix);
pos += prefix.len;
for (0..100) |i| {
const written = std.fmt.bufPrint(
data_buf[pos..],
"H{d:0>3}: value{d}\r\n",
.{ i, i },
) catch unreachable;
pos += written.len;
}
@memcpy(data_buf[pos..][0..2], "\r\n");
pos += 2;
var result = parse(std.testing.allocator, data_buf[0..pos]);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqual(@as(usize, 100), result.count);
try std.testing.expect(result.get("H000") != null);
try std.testing.expect(result.get("H099") != null);
}
test "parsed data survives after source freed" {
// This test verifies ownership - parsed data is independent
const data = try std.testing.allocator.dupe(u8, "NATS/1.0\r\nKey: value\r\n\r\n");
var result = parse(std.testing.allocator, data);
defer result.deinit();
// Free source data
std.testing.allocator.free(data);
// ParseResult should still work (owns copies)
try std.testing.expectEqualSlices(u8, "Key", result.items()[0].key);
try std.testing.expectEqualSlices(u8, "value", result.items()[0].value);
}
test "error returns empty items" {
const data = "INVALID\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expect(result.err != null);
try std.testing.expectEqual(@as(usize, 0), result.items().len);
try std.testing.expect(result.get("anything") == null);
}
test "deinit is safe to call multiple times" {
const data = "NATS/1.0\r\nFoo: bar\r\n\r\n";
var result = parse(std.testing.allocator, data);
result.deinit();
result.deinit();
result.deinit();
}
test "encode headers" {
var buf: [256]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const entries = [_]Entry{
.{ .key = "Foo", .value = "bar" },
.{ .key = "Baz", .value = "123" },
};
try encode(&writer, &entries);
try std.testing.expectEqualSlices(
u8,
"NATS/1.0\r\nFoo: bar\r\nBaz: 123\r\n\r\n",
writer.buffered(),
);
}
test "encoded size" {
const entries = [_]Entry{
.{ .key = "Foo", .value = "bar" },
};
const size = encodedSize(&entries);
try std.testing.expectEqual(@as(usize, 22), size);
}
test "parse status only no description" {
const data = "NATS/1.0 503\r\n\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqualSlices(u8, "503", result.status.?);
try std.testing.expect(result.description == null);
try std.testing.expect(result.isNoResponders());
}
test "parse status with headers" {
const data = "NATS/1.0 100 Idle Heartbeat\r\nNats-Last-Consumer: 42\r\n\r\n";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqualSlices(u8, "100", result.status.?);
try std.testing.expectEqualSlices(u8, "Idle Heartbeat", result.description.?);
try std.testing.expectEqual(@as(usize, 1), result.count);
try std.testing.expectEqualSlices(u8, "42", result.get("Nats-Last-Consumer").?);
}
test "header_end is set correctly" {
// "NATS/1.0\r\nFoo: bar\r\n\r\n" = 8 + 2 + 8 + 2 + 2 = 22 bytes
const data = "NATS/1.0\r\nFoo: bar\r\n\r\npayload here";
var result = parse(std.testing.allocator, data);
defer result.deinit();
try std.testing.expectEqual(@as(?ParseResult.ParseError, null), result.err);
try std.testing.expectEqual(@as(usize, 22), result.header_end);
try std.testing.expectEqualSlices(u8, "payload here", data[result.header_end..]);
}
test "extractStatus returns 503" {
const data = "NATS/1.0 503 No Responders\r\n\r\n";
try std.testing.expectEqual(@as(?u16, 503), extractStatus(data));
}
test "extractStatus returns 408" {
const data = "NATS/1.0 408 Request Timeout\r\n\r\n";
try std.testing.expectEqual(@as(?u16, 408), extractStatus(data));
}
test "extractStatus returns 100" {
const data = "NATS/1.0 100 Idle Heartbeat\r\nHeader: value\r\n\r\n";
try std.testing.expectEqual(@as(?u16, 100), extractStatus(data));
}
test "extractStatus returns null for no status" {
const data = "NATS/1.0\r\nFoo: bar\r\n\r\n";
try std.testing.expectEqual(@as(?u16, null), extractStatus(data));
}
test "extractStatus returns null for invalid prefix" {
const data = "HTTP/1.0 200 OK\r\n\r\n";
try std.testing.expectEqual(@as(?u16, null), extractStatus(data));
}
test "extractStatus returns null for short data" {
const data = "NATS";
try std.testing.expectEqual(@as(?u16, null), extractStatus(data));
}
test "extractStatus handles status without description" {
const data = "NATS/1.0 503\r\n\r\n";
try std.testing.expectEqual(@as(?u16, 503), extractStatus(data));
}
================================================
FILE: src/protocol/parser.zig
================================================
//! NATS Protocol Parser
//!
//! Parses incoming data from NATS server into structured commands.
//! Handles streaming data that may arrive in partial chunks.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const commands = @import("commands.zig");
const ServerCommand = commands.ServerCommand;
const RawServerInfo = commands.RawServerInfo;
const ServerInfo = commands.ServerInfo;
const MsgArgs = commands.MsgArgs;
const HMsgArgs = commands.HMsgArgs;
/// Fast decimal parser for u64. Inlined for hot path performance.
/// Uses wrapping math with length guard to prevent overflow.
pub inline fn parseU64Fast(s: []const u8) error{
InvalidCharacter,
Overflow,
}!u64 {
assert(s.len > 0);
if (s.len >= 20) return error.Overflow;
var v: u64 = 0;
for (s) |c| {
if (c < '0' or c > '9') return error.InvalidCharacter;
v = v *% 10 +% @as(u64, c - '0');
}
return v;
}
/// Fast decimal parser for usize. Inlined for hot path performance.
/// Uses wrapping math with length guard to prevent overflow.
pub inline fn parseUsizeFast(s: []const u8) error{
InvalidCharacter,
Overflow,
}!usize {
assert(s.len > 0);
// REVIEWED(2025-03): Guard is correct for 64-bit targets.
// On 32-bit usize, 10-digit values could wrap, but this
// library targets 64-bit only. max_payload check catches
// any practical overflow.
if (s.len >= 20) return error.Overflow;
var v: usize = 0;
for (s) |c| {
if (c < '0' or c > '9') return error.InvalidCharacter;
v = v *% 10 +% @as(usize, c - '0');
}
return v;
}
/// Fast \r\n finder optimized for short NATS lines (~30 bytes).
/// Scans for '\r' then checks next byte, avoiding 2-byte pattern overhead.
inline fn findCRLF(data: []const u8) ?usize {
if (data.len < 2) return null;
const end = data.len - 1;
var i: usize = 0;
while (i < end) : (i += 1) {
if (data[i] == '\r' and data[i + 1] == '\n') return i;
}
return null;
}
/// Protocol parser for NATS server commands.
///
/// Stateless single-pass parser - no multi-stage state machine needed.
/// Handles partial data by returning null (need more bytes).
/// Allocates only for INFO command (ServerInfo string copies).
pub const Parser = struct {
/// Max payload size from server INFO (DoS guard).
max_payload: usize = 1048576,
/// Parse error types.
pub const Error = error{
InvalidCommand,
InvalidArguments,
PayloadTooLarge,
InvalidJson,
InvalidServerInfo,
};
/// Creates a new parser.
pub fn init() Parser {
return .{};
}
/// Resets the parser to initial state (no-op for stateless parser).
pub fn reset(self: *Parser) void {
_ = self;
}
/// Parses data and returns a command if complete.
/// Returns null if no complete command is available (need more data).
/// Sets consumed to the number of bytes consumed (0 if need more data).
pub fn parse(
self: *Parser,
allocator: Allocator,
data: []const u8,
consumed: *usize,
) (Error || Allocator.Error)!?ServerCommand {
consumed.* = 0;
if (data.len == 0) return null;
const line_end = findCRLF(data) orelse return null;
const line = data[0..line_end];
const header_len = line_end + 2;
if (line.len == 0) return Parser.Error.InvalidCommand;
// u32 word comparison for dispatch
const CMD_MSG: u32 = 0x2047534D; // "MSG " in little-endian
const CMD_PING: u32 = 0x474E4950; // "PING" in little-endian
const CMD_PONG: u32 = 0x474E4F50; // "PONG" in little-endian
const CMD_INFO: u32 = 0x4F464E49; // "INFO" in little-endian
const CMD_HMSG: u32 = 0x47534D48; // "HMSG" in little-endian
const CMD_ERR: u32 = 0x5252452D; // "-ERR" in little-endian
if (line.len >= 4) {
const cmd = std.mem.readInt(u32, line[0..4], .little);
// MSG [reply-to]
if (cmd == CMD_MSG) {
return parseFullMsgFast(data, line[4..], header_len, consumed, self.max_payload);
}
// PING (exact 4 chars)
if (cmd == CMD_PING and line.len == 4) {
consumed.* = header_len;
return .ping;
}
// PONG (exact 4 chars)
if (cmd == CMD_PONG and line.len == 4) {
consumed.* = header_len;
return .pong;
}
// INFO (need 5th char to be space)
if (cmd == CMD_INFO and line.len >= 5 and line[4] == ' ') {
const json_data = line[5..];
var parsed = std.json.parseFromSlice(
RawServerInfo,
allocator,
json_data,
.{ .ignore_unknown_fields = true },
) catch return Error.InvalidJson;
defer parsed.deinit();
const owned = ServerInfo.fromParsed(
allocator,
parsed,
) catch |err| return switch (err) {
error.OutOfMemory => error.OutOfMemory,
error.InvalidServerInfo => Error.InvalidServerInfo,
};
consumed.* = header_len;
return .{ .info = owned };
}
// HMSG [reply-to]
if (cmd == CMD_HMSG and line.len >= 5 and line[4] == ' ') {
return parseFullHMsgFast(data, line[5..], header_len, consumed, self.max_payload);
}
// -ERR
if (cmd == CMD_ERR and line.len >= 5 and line[4] == ' ') {
consumed.* = header_len;
return .{ .err = line[5..] };
}
}
if (line.len == 3 and line[0] == '+' and line[1] == 'O' and
line[2] == 'K')
{
consumed.* = header_len;
return .ok;
}
return Error.InvalidCommand;
}
};
/// Verify trailing CRLF using u16 comparison (little-endian).
inline fn verifyCRLF(data: []const u8, offset: usize) bool {
if (offset + 2 > data.len) return false;
const word = @as(u16, @bitCast([2]u8{ data[offset], data[offset + 1] }));
return word == 0x0A0D;
}
/// Parse complete MSG using manual byte scanning (no iterator allocation).
/// Returns null if payload not yet available.
inline fn parseFullMsgFast(
data: []const u8,
args_line: []const u8,
header_len: usize,
consumed: *usize,
max_payload: usize,
) Parser.Error!?ServerCommand {
if (args_line.len == 0)
return Parser.Error.InvalidArguments;
assert(header_len > 0);
var i: usize = 0;
// Parse subject (scan to first space)
const subj_start = i;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {}
if (i == subj_start or i >= args_line.len) {
return Parser.Error.InvalidArguments;
}
const subject = args_line[subj_start..i];
i += 1; // skip space
// Parse SID inline (avoids separate function call overhead)
var sid: u64 = 0;
var sid_digits: u8 = 0;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {
const c = args_line[i];
if (c < '0' or c > '9') return Parser.Error.InvalidArguments;
sid_digits += 1;
// u64 max is 20 digits; reject at >= 20 to prevent overflow
if (sid_digits >= 20) return Parser.Error.InvalidArguments;
sid = sid *% 10 +% @as(u64, c - '0');
}
if (sid_digits == 0 or sid == 0) return Parser.Error.InvalidArguments;
// Check if there's more to parse
if (i >= args_line.len) return Parser.Error.InvalidArguments;
i += 1; // skip space
// Parse third token
const t3_start = i;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {}
if (i == t3_start) return Parser.Error.InvalidArguments;
const third = args_line[t3_start..i];
// Check for optional fourth token (reply-to case)
var reply_to: ?[]const u8 = null;
var payload_len_slice: []const u8 = undefined;
if (i < args_line.len and args_line[i] == ' ') {
i += 1; // skip space
reply_to = third;
const t4_start = i;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {}
if (i == t4_start) return Parser.Error.InvalidArguments;
payload_len_slice = args_line[t4_start..i];
} else {
payload_len_slice = third;
}
// Parse payload length inline (with overflow guard)
if (payload_len_slice.len >= 20) return Parser.Error.InvalidArguments;
var payload_len: usize = 0;
for (payload_len_slice) |c| {
if (c < '0' or c > '9') return Parser.Error.InvalidArguments;
payload_len = payload_len *% 10 +% @as(usize, c - '0');
}
if (payload_len > max_payload)
return Parser.Error.PayloadTooLarge;
// Calculate total message size: header + payload + trailing \r\n
const total_len = header_len + payload_len + 2;
// Check for complete message
if (data.len < total_len) return null;
// Verify trailing CRLF with u16 comparison
if (!verifyCRLF(data, header_len + payload_len)) {
return Parser.Error.InvalidArguments;
}
// Extract payload - it's right after the header
const payload = data[header_len..][0..payload_len];
consumed.* = total_len;
assert(consumed.* <= data.len);
assert(subject.len > 0);
assert(sid > 0);
return .{ .msg = .{
.subject = subject,
.sid = sid,
.reply_to = reply_to,
.payload_len = payload_len,
.payload = payload,
} };
}
/// Parse complete MSG in single pass (legacy, kept for test compatibility).
/// Returns null if payload not yet available.
inline fn parseFullMsg(
data: []const u8,
args_line: []const u8,
header_len: usize,
consumed: *usize,
) Parser.Error!?ServerCommand {
return parseFullMsgFast(data, args_line, header_len, consumed, 1048576);
}
/// Parse complete HMSG using manual byte scanning (no iterator allocation).
/// Returns null if headers/payload not yet available.
inline fn parseFullHMsgFast(
data: []const u8,
args_line: []const u8,
header_len: usize,
consumed: *usize,
max_payload: usize,
) Parser.Error!?ServerCommand {
if (args_line.len == 0)
return Parser.Error.InvalidArguments;
assert(header_len > 0);
var i: usize = 0;
// Parse subject (scan to first space)
const subj_start = i;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {}
if (i == subj_start or i >= args_line.len) {
return Parser.Error.InvalidArguments;
}
const subject = args_line[subj_start..i];
i += 1; // skip space
// Parse SID inline (with overflow guard)
var sid: u64 = 0;
var sid_digits: u8 = 0;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {
const c = args_line[i];
if (c < '0' or c > '9') return Parser.Error.InvalidArguments;
sid_digits += 1;
// u64 max is 20 digits; reject at >= 20 to prevent overflow
if (sid_digits >= 20) return Parser.Error.InvalidArguments;
sid = sid *% 10 +% @as(u64, c - '0');
}
if (sid_digits == 0 or sid == 0 or i >= args_line.len)
return Parser.Error.InvalidArguments;
i += 1; // skip space
// Collect remaining tokens (2 or 3: [reply-to] hdr_len total_len)
var tokens: [3][]const u8 = undefined;
var token_count: usize = 0;
while (i < args_line.len and token_count < 3) {
const t_start = i;
while (i < args_line.len and args_line[i] != ' ') : (i += 1) {}
if (i == t_start) break;
tokens[token_count] = args_line[t_start..i];
token_count += 1;
if (i < args_line.len and args_line[i] == ' ') i += 1;
}
if (token_count < 2) return Parser.Error.InvalidArguments;
var reply_to: ?[]const u8 = null;
var hdr_len_slice: []const u8 = undefined;
var total_len_slice: []const u8 = undefined;
if (token_count == 3) {
reply_to = tokens[0];
hdr_len_slice = tokens[1];
total_len_slice = tokens[2];
} else {
hdr_len_slice = tokens[0];
total_len_slice = tokens[1];
}
// Parse hdr_len inline (with overflow guard)
if (hdr_len_slice.len >= 20) return Parser.Error.InvalidArguments;
var hdr_len: usize = 0;
for (hdr_len_slice) |c| {
if (c < '0' or c > '9') return Parser.Error.InvalidArguments;
hdr_len = hdr_len *% 10 +% @as(usize, c - '0');
}
// Parse total_content_len inline (with overflow guard)
if (total_len_slice.len >= 20) return Parser.Error.InvalidArguments;
var total_content_len: usize = 0;
for (total_len_slice) |c| {
if (c < '0' or c > '9') return Parser.Error.InvalidArguments;
total_content_len = total_content_len *% 10 +% @as(usize, c - '0');
}
if (hdr_len > total_content_len) return Parser.Error.InvalidArguments;
if (total_content_len > max_payload)
return Parser.Error.PayloadTooLarge;
// Calculate total message size: header line + content + trailing \r\n
const total_len = header_len + total_content_len + 2;
// Check for complete message
if (data.len < total_len) return null;
// Verify trailing CRLF with u16 comparison
if (!verifyCRLF(data, header_len + total_content_len)) {
return Parser.Error.InvalidArguments;
}
// Extract headers and payload - they're right after the header line
const headers = data[header_len..][0..hdr_len];
const payload_len = total_content_len - hdr_len;
const payload = data[header_len + hdr_len ..][0..payload_len];
consumed.* = total_len;
assert(consumed.* <= data.len);
assert(subject.len > 0);
assert(sid > 0);
assert(hdr_len <= total_content_len);
return .{ .hmsg = .{
.subject = subject,
.sid = sid,
.reply_to = reply_to,
.header_len = hdr_len,
.total_len = total_content_len,
.headers = headers,
.payload = payload,
} };
}
/// Parse complete HMSG in single pass (legacy, kept for test compatibility).
/// Returns null if headers/payload not yet available.
inline fn parseFullHMsg(
data: []const u8,
args_line: []const u8,
header_len: usize,
consumed: *usize,
) Parser.Error!?ServerCommand {
return parseFullHMsgFast(data, args_line, header_len, consumed, 1048576);
}
test {
_ = @import("parser_test.zig");
}
================================================
FILE: src/protocol/parser_test.zig
================================================
//! Parser Edge Case Tests
//!
//! - Integer parsing edge cases (overflow, boundaries, invalid chars)
//! - MSG/HMSG parsing (truncated, malformed, edge values)
//! - INFO JSON parsing (invalid JSON, type mismatches, overflow)
//! - Command dispatch (case sensitivity, prefix validation)
//! - CRLF verification and buffer boundaries
const std = @import("std");
const parser = @import("parser.zig");
const Parser = parser.Parser;
const parseU64Fast = parser.parseU64Fast;
const parseUsizeFast = parser.parseUsizeFast;
const ServerCommand = @import("commands.zig").ServerCommand;
// Section 1: Existing Tests (moved from parser.zig)
test "parse PING" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"PING\r\n",
&consumed,
);
try std.testing.expectEqual(@as(usize, 6), consumed);
try std.testing.expectEqual(ServerCommand.ping, result.?);
}
test "parse PONG" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"PONG\r\n",
&consumed,
);
try std.testing.expectEqual(@as(usize, 6), consumed);
try std.testing.expectEqual(ServerCommand.pong, result.?);
}
test "parse +OK" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"+OK\r\n",
&consumed,
);
try std.testing.expectEqual(ServerCommand.ok, result.?);
}
test "parse -ERR" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"-ERR 'Authorization Violation'\r\n",
&consumed,
);
try std.testing.expectEqualSlices(
u8,
"'Authorization Violation'",
result.?.err,
);
}
test "parse MSG without payload" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"MSG test.subject 1 0\r\n\r\n",
&consumed,
);
const msg = result.?.msg;
try std.testing.expectEqualSlices(u8, "test.subject", msg.subject);
try std.testing.expectEqual(@as(u64, 1), msg.sid);
try std.testing.expectEqual(@as(usize, 0), msg.payload_len);
}
test "parse MSG with payload" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "MSG test.subject 42 5\r\nhello\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
const msg = result.?.msg;
try std.testing.expectEqualSlices(u8, "test.subject", msg.subject);
try std.testing.expectEqual(@as(u64, 42), msg.sid);
try std.testing.expectEqualSlices(u8, "hello", msg.payload);
try std.testing.expectEqual(@as(usize, 30), consumed);
}
test "parse MSG with payload - partial data" {
var p: Parser = .{};
var consumed: usize = 0;
const partial = "MSG test.subject 42 5\r\nhel";
var result = try p.parse(std.testing.allocator, partial, &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
const full = "MSG test.subject 42 5\r\nhello\r\n";
result = try p.parse(std.testing.allocator, full, &consumed);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "hello", result.?.msg.payload);
}
test "parse MSG with reply-to" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "MSG test.subject 1 _INBOX.123 5\r\nworld\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
const msg = result.?.msg;
try std.testing.expectEqualSlices(u8, "_INBOX.123", msg.reply_to.?);
try std.testing.expectEqualSlices(u8, "world", msg.payload);
}
test "parse incomplete data returns null" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(std.testing.allocator, "PIN", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
}
test "parse INFO" {
var p: Parser = .{};
var consumed: usize = 0;
const info_json = "INFO {\"server_id\":\"test\"," ++
"\"version\":\"2.10.0\",\"proto\":1,\"max_payload\":1048576}\r\n";
const alloc = std.testing.allocator;
const result = try p.parse(alloc, info_json, &consumed);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
const info = result.?.info;
try std.testing.expectEqualSlices(u8, "test", info.server_id);
try std.testing.expectEqualSlices(u8, "2.10.0", info.version);
try std.testing.expectEqual(@as(u32, 1048576), info.max_payload);
}
test "parse HMSG without payload" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "HMSG test.subject 1 12 12\r\nNATS/1.0\r\n\r\n\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
const hmsg = result.?.hmsg;
try std.testing.expectEqualSlices(u8, "test.subject", hmsg.subject);
try std.testing.expectEqual(@as(u64, 1), hmsg.sid);
try std.testing.expectEqual(@as(usize, 12), hmsg.header_len);
try std.testing.expectEqual(@as(usize, 12), hmsg.total_len);
}
test "parse HMSG with payload" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "HMSG test.subject 42 12 17\r\nNATS/1.0\r\n\r\nhello\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
const hmsg = result.?.hmsg;
try std.testing.expectEqualSlices(u8, "test.subject", hmsg.subject);
try std.testing.expectEqual(@as(u64, 42), hmsg.sid);
try std.testing.expectEqualSlices(u8, "NATS/1.0\r\n\r\n", hmsg.headers);
try std.testing.expectEqualSlices(u8, "hello", hmsg.payload);
}
test "parse invalid command" {
var p: Parser = .{};
var consumed: usize = 0;
const alloc = std.testing.allocator;
const result = p.parse(alloc, "INVALID\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parseU64Fast valid numbers" {
try std.testing.expectEqual(@as(u64, 0), try parseU64Fast("0"));
try std.testing.expectEqual(@as(u64, 1), try parseU64Fast("1"));
try std.testing.expectEqual(@as(u64, 123), try parseU64Fast("123"));
try std.testing.expectEqual(@as(u64, 999999), try parseU64Fast("999999"));
}
test "parseU64Fast invalid input" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("abc"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12a3"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("-1"));
}
test "parseUsizeFast valid numbers" {
try std.testing.expectEqual(@as(usize, 0), try parseUsizeFast("0"));
try std.testing.expectEqual(@as(usize, 42), try parseUsizeFast("42"));
const large: usize = 1048576;
try std.testing.expectEqual(large, try parseUsizeFast("1048576"));
}
test "parseUsizeFast invalid input" {
try std.testing.expectError(error.InvalidCharacter, parseUsizeFast("xyz"));
try std.testing.expectError(error.InvalidCharacter, parseUsizeFast("1 2"));
}
test "parseU64Fast overflow protection" {
// 21 digits - guaranteed overflow, should be rejected
try std.testing.expectError(
error.Overflow,
parseU64Fast("123456789012345678901"),
);
// 20 digits now rejected (overflow guard)
try std.testing.expectError(
error.Overflow,
parseU64Fast("18446744073709551615"),
);
// 20 zeros also rejected
try std.testing.expectError(
error.Overflow,
parseU64Fast("00000000000000000000"),
);
}
test "parseUsizeFast overflow protection" {
// 21 digits - guaranteed overflow, should be rejected
try std.testing.expectError(
error.Overflow,
parseUsizeFast("123456789012345678901"),
);
// 20 digits now rejected (overflow guard)
try std.testing.expectError(
error.Overflow,
parseUsizeFast("18446744073709551615"),
);
}
test "parse MSG with SID=0 rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG test.subject 0 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "parse HMSG with SID=0 rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"HMSG test.subject 0 12 12\r\nNATS/1.0\r\n\r\n\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
// Section 2: Integer Parsing Edge Cases (parseU64Fast / parseUsizeFast)
test "parseU64Fast u64 max value rejected" {
// u64 max = 18446744073709551615 (20 digits, now rejected)
try std.testing.expectError(
error.Overflow,
parseU64Fast("18446744073709551615"),
);
}
test "parseU64Fast u64 max plus one rejected" {
// 20 digits rejected by guard
try std.testing.expectError(
error.Overflow,
parseU64Fast("18446744073709551616"),
);
}
test "parseU64Fast 19 digit large value" {
const result = try parseU64Fast("9999999999999999999");
try std.testing.expectEqual(@as(u64, 9999999999999999999), result);
}
test "parseU64Fast leading zeros preserved value" {
// Leading zeros should parse correctly
try std.testing.expectEqual(@as(u64, 1), try parseU64Fast("0000000000000000001"));
try std.testing.expectEqual(@as(u64, 42), try parseU64Fast("0000000000000000042"));
try std.testing.expectEqual(@as(u64, 0), try parseU64Fast("0000000000000000000"));
}
test "parseU64Fast 20 chars overflow" {
// 20+ chars always rejected (prevents wrapping)
try std.testing.expectError(
error.Overflow,
parseU64Fast("00000000000000000000"),
);
}
test "parseU64Fast 21 zeros overflow" {
// 21 characters should error regardless of value
try std.testing.expectError(
error.Overflow,
parseU64Fast("000000000000000000000"),
);
}
test "parseU64Fast invalid first char" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("a123"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("x999"));
}
test "parseU64Fast invalid middle char" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12a34"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("99x99"));
}
test "parseU64Fast invalid last char" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("1234a"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("9999z"));
}
test "parseU64Fast space in middle" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12 34"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("1 2"));
}
test "parseU64Fast tab character" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12\t34"));
}
test "parseU64Fast newline character" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12\n34"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("123\r\n"));
}
test "parseU64Fast null byte" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12\x0034"));
}
test "parseU64Fast negative sign" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("-123"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("-1"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("-0"));
}
test "parseU64Fast positive sign" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("+123"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("+1"));
}
test "parseU64Fast minus in middle" {
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("12-34"));
}
test "parseU64Fast exactly 20 chars boundary" {
// 20-char numbers now rejected (overflow guard)
try std.testing.expectError(
error.Overflow,
parseU64Fast("10000000000000000000"),
);
try std.testing.expectError(
error.Overflow,
parseU64Fast("12345678901234567890"),
);
try std.testing.expectError(
error.Overflow,
parseU64Fast("99999999999999999999"),
);
}
test "parseU64Fast exactly 21 chars overflow" {
try std.testing.expectError(
error.Overflow,
parseU64Fast("100000000000000000000"),
);
try std.testing.expectError(
error.Overflow,
parseU64Fast("999999999999999999999"),
);
}
test "parseU64Fast single digit all values" {
try std.testing.expectEqual(@as(u64, 0), try parseU64Fast("0"));
try std.testing.expectEqual(@as(u64, 1), try parseU64Fast("1"));
try std.testing.expectEqual(@as(u64, 5), try parseU64Fast("5"));
try std.testing.expectEqual(@as(u64, 9), try parseU64Fast("9"));
}
test "parseUsizeFast boundaries" {
// 20-char numbers now rejected (overflow guard)
try std.testing.expectError(
error.Overflow,
parseUsizeFast("18446744073709551615"),
);
try std.testing.expectEqual(
@as(usize, 0),
try parseUsizeFast("0"),
);
try std.testing.expectEqual(
@as(usize, 1),
try parseUsizeFast("1"),
);
}
test "parseU64Fast special ASCII near digits" {
// Characters just before '0' (ASCII 48) and after '9' (ASCII 57)
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("/"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast(":"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("1/2"));
try std.testing.expectError(error.InvalidCharacter, parseU64Fast("1:2"));
}
// Section 3: MSG Parsing Edge Cases
test "MSG header only no CRLF returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// No \r\n means incomplete - should return null
const result = try p.parse(std.testing.allocator, "MSG subject 1 5", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
}
test "MSG header with CRLF but no payload returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// Header complete but payload not yet arrived
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\r\n",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
}
test "MSG partial payload returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// Only 3 of 5 payload bytes
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\r\nhel",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
}
test "MSG payload without trailing CRLF returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// Payload complete but missing trailing \r\n
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\r\nhello",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "MSG one byte short of complete returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// Missing final \n
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\r\nhello\r",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "MSG empty subject rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// Double space = empty subject
const result = p.parse(
std.testing.allocator,
"MSG 1 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG non-numeric SID rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject abc 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG non-numeric payload length rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject 1 abc\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG float SID rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject 1.5 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG float payload length rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject 1 5.5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG negative SID rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject -1 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG negative payload length rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject 1 -5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG SID one is valid" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(u64, 1), result.?.msg.sid);
}
test "MSG SID large value" {
var p: Parser = .{};
var consumed: usize = 0;
// Large but valid SID
const result = try p.parse(
std.testing.allocator,
"MSG subject 999999999999 5\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(u64, 999999999999), result.?.msg.sid);
}
test "MSG SID overflow 21 digits rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject 123456789012345678901 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG zero payload valid" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 0\r\n\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 0), result.?.msg.payload_len);
try std.testing.expectEqualSlices(u8, "", result.?.msg.payload);
}
test "MSG payload length overflow 21 digits rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"MSG subject 1 123456789012345678901\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "MSG subject with dots" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"MSG foo.bar.baz 1 5\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "foo.bar.baz", result.?.msg.subject);
}
test "MSG single char subject" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"MSG x 1 5\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "x", result.?.msg.subject);
}
test "MSG long subject" {
var p: Parser = .{};
var consumed: usize = 0;
// 256 character subject
const long_subject = "a" ** 256;
const data = "MSG " ++ long_subject ++ " 1 5\r\nhello\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 256), result.?.msg.subject.len);
}
test "MSG reply-to with dots" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 _INBOX.abc.def 5\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(
u8,
"_INBOX.abc.def",
result.?.msg.reply_to.?,
);
}
test "MSG tab instead of space rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// Tab is not a valid delimiter
const result = p.parse(
std.testing.allocator,
"MSG\tsubject\t1\t5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "MSG missing SID field rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// Only subject and length, no SID
const result = p.parse(
std.testing.allocator,
"MSG subject 5\r\nhello\r\n",
&consumed,
);
// This parses "5" as SID and then fails with missing length
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
// Section 4: HMSG Parsing Edge Cases
test "HMSG header length zero" {
var p: Parser = .{};
var consumed: usize = 0;
// hdr_len=0 means no headers, only payload
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 0 5\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 0), result.?.hmsg.header_len);
try std.testing.expectEqualSlices(u8, "", result.?.hmsg.headers);
try std.testing.expectEqualSlices(u8, "hello", result.?.hmsg.payload);
}
test "HMSG header length exceeds total rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// hdr_len > total_len is invalid
const result = p.parse(
std.testing.allocator,
"HMSG subject 1 100 50\r\n" ++ "x" ** 50 ++ "\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "HMSG header length equals total" {
var p: Parser = .{};
var consumed: usize = 0;
// hdr_len == total_len means headers only, no payload
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 12 12\r\nNATS/1.0\r\n\r\n\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 12), result.?.hmsg.header_len);
try std.testing.expectEqual(@as(usize, 12), result.?.hmsg.total_len);
try std.testing.expectEqualSlices(u8, "", result.?.hmsg.payload);
}
test "HMSG header length one less than total" {
var p: Parser = .{};
var consumed: usize = 0;
// 1 byte payload
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 12 13\r\nNATS/1.0\r\n\r\nX\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "X", result.?.hmsg.payload);
}
test "HMSG zero total length" {
var p: Parser = .{};
var consumed: usize = 0;
// Both hdr_len and total_len are 0
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 0 0\r\n\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 0), result.?.hmsg.header_len);
try std.testing.expectEqual(@as(usize, 0), result.?.hmsg.total_len);
}
test "HMSG total length overflow rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"HMSG subject 1 0 123456789012345678901\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "HMSG header length overflow rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"HMSG subject 1 123456789012345678901 100\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "HMSG partial headers returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// Need 12 bytes of headers but only have 5
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 12 12\r\nNATS/",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "HMSG headers complete payload partial returns null" {
var p: Parser = .{};
var consumed: usize = 0;
// Headers complete (12 bytes) but payload (5 bytes) incomplete
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 12 17\r\nNATS/1.0\r\n\r\nhel",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "HMSG with reply-to" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"HMSG subject 1 _INBOX.reply 12 17\r\nNATS/1.0\r\n\r\nhello\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "_INBOX.reply", result.?.hmsg.reply_to.?);
}
test "HMSG SID overflow rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"HMSG subject 123456789012345678901 12 12\r\nNATS/1.0\r\n\r\n\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "HMSG non-numeric header length rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"HMSG subject 1 abc 12\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
test "HMSG non-numeric total length rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"HMSG subject 1 12 abc\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidArguments, result);
}
// Section 5: INFO Parsing Edge Cases
test "INFO minimal valid json" {
var p: Parser = .{};
var consumed: usize = 0;
// Minimal valid JSON - needs at least server_id or version
const result = try p.parse(
std.testing.allocator,
"INFO {\"server_id\":\"test\"}\r\n",
&consumed,
);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "test", result.?.info.server_id);
}
test "INFO malformed json rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "INFO {invalid}\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO truncated json rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"INFO {\"server_id\":\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO array instead of object rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "INFO []\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO null rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "INFO null\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO string instead of object rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "INFO \"test\"\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO max_payload zero rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// max_payload = 0 is invalid
const result = p.parse(
std.testing.allocator,
"INFO {\"server_id\":\"test\",\"max_payload\":0}\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidServerInfo, result);
}
test "INFO max_payload exceeds limit rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// max_payload > 1GB is invalid
const result = p.parse(
std.testing.allocator,
"INFO {\"server_id\":\"test\",\"max_payload\":2000000000}\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidServerInfo, result);
}
test "INFO empty json rejected" {
var p: Parser = .{};
var consumed: usize = 0;
// Empty JSON has no server_id or version
const result = p.parse(std.testing.allocator, "INFO {}\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidServerInfo, result);
}
test "INFO empty server_id with version valid" {
var p: Parser = .{};
var consumed: usize = 0;
// Empty server_id is valid if version is present
const result = try p.parse(
std.testing.allocator,
"INFO {\"server_id\":\"\",\"version\":\"2.10.0\"}\r\n",
&consumed,
);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "", result.?.info.server_id);
try std.testing.expectEqualSlices(u8, "2.10.0", result.?.info.version);
}
test "INFO with unknown fields ignored" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(
std.testing.allocator,
"INFO {\"server_id\":\"test\",\"unknown_field\":\"value\"," ++
"\"another_unknown\":123}\r\n",
&consumed,
);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "test", result.?.info.server_id);
}
test "INFO boolean as string type mismatch" {
var p: Parser = .{};
var consumed: usize = 0;
// headers should be bool, not string
const result = p.parse(
std.testing.allocator,
"INFO {\"headers\":\"true\"}\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO number as string type mismatch" {
var p: Parser = .{};
var consumed: usize = 0;
// server_id should be string, not number
const result = p.parse(
std.testing.allocator,
"INFO {\"server_id\":123}\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidJson, result);
}
test "INFO string as number coerced" {
var p: Parser = .{};
var consumed: usize = 0;
// Zig JSON parser coerces string "1000" to number 1000
const result = try p.parse(
std.testing.allocator,
"INFO {\"server_id\":\"test\",\"max_payload\":\"1000\"}\r\n",
&consumed,
);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(u32, 1000), result.?.info.max_payload);
}
test "INFO with all valid fields" {
var p: Parser = .{};
var consumed: usize = 0;
const json = "INFO {" ++
"\"server_id\":\"NATS123\"," ++
"\"server_name\":\"my-nats\"," ++
"\"version\":\"2.10.0\"," ++
"\"proto\":1," ++
"\"host\":\"localhost\"," ++
"\"port\":4222," ++
"\"max_payload\":1048576," ++
"\"headers\":true," ++
"\"jetstream\":true" ++
"}\r\n";
const result = try p.parse(std.testing.allocator, json, &consumed);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
const info = result.?.info;
try std.testing.expectEqualSlices(u8, "NATS123", info.server_id);
try std.testing.expectEqualSlices(u8, "my-nats", info.server_name);
try std.testing.expectEqual(@as(u16, 4222), info.port);
try std.testing.expectEqual(true, info.headers);
try std.testing.expectEqual(true, info.jetstream);
}
// Section 6: Command Dispatch Edge Cases
test "parse MSG without space rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "MSG123\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse HMSG without space rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "HMSG123\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse INFO without space rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "INFO{}\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse lowercase msg rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(
std.testing.allocator,
"msg subject 1 5\r\nhello\r\n",
&consumed,
);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse lowercase ping rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "ping\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse mixed case Ping rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "Ping\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse PING with trailing data rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "PING extra\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse PONG with trailing data rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "PONG extra\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse +OK with trailing data rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "+OK extra\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse -ERR empty message" {
var p: Parser = .{};
var consumed: usize = 0;
// -ERR with just a space and nothing after
const result = try p.parse(std.testing.allocator, "-ERR \r\n", &consumed);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "", result.?.err);
}
test "parse -ERR no space rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "-ERRmessage\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse -ERR long message" {
var p: Parser = .{};
var consumed: usize = 0;
const long_msg = "x" ** 1000;
const data = "-ERR " ++ long_msg ++ "\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 1000), result.?.err.len);
}
test "parse empty buffer returns null" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(std.testing.allocator, "", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
}
test "parse single byte returns null" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(std.testing.allocator, "M", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "parse CR only returns null" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(std.testing.allocator, "\r", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "parse LF only returns null" {
var p: Parser = .{};
var consumed: usize = 0;
const result = try p.parse(std.testing.allocator, "\n", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "parse unknown command rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "UNKNOWN\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse binary garbage rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "\x00\x01\x02\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
test "parse high ASCII rejected" {
var p: Parser = .{};
var consumed: usize = 0;
const result = p.parse(std.testing.allocator, "\xFF\xFE\r\n", &consumed);
try std.testing.expectError(Parser.Error.InvalidCommand, result);
}
// Section 7: CRLF Verification Edge Cases
test "MSG with LF only line ending incomplete" {
var p: Parser = .{};
var consumed: usize = 0;
// \n alone is not recognized as line ending, returns null
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\nhello\n",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "MSG with CR only line ending incomplete" {
var p: Parser = .{};
var consumed: usize = 0;
// \r alone is not recognized as line ending
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 5\rhello\r",
&consumed,
);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
}
test "MSG payload contains CRLF" {
var p: Parser = .{};
var consumed: usize = 0;
// CRLF in payload is valid - payload is binary
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 7\r\nhel\r\nlo\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "hel\r\nlo", result.?.msg.payload);
}
test "MSG payload is all CRLF" {
var p: Parser = .{};
var consumed: usize = 0;
// Payload of just \r\n\r\n (4 bytes)
const result = try p.parse(
std.testing.allocator,
"MSG subject 1 4\r\n\r\n\r\n\r\n",
&consumed,
);
try std.testing.expect(result != null);
try std.testing.expectEqualSlices(u8, "\r\n\r\n", result.?.msg.payload);
}
test "MSG wrong CRLF order in payload trailing" {
var p: Parser = .{};
var consumed: usize = 0;
// \n\r instead of \r\n at end - should fail verification
const result = p.parse(
std.testing.allocator,
"MSG subject 1 5\r\nhello\n\r",
&consumed,
);
// Either returns null (incomplete) or InvalidArguments
if (result) |r| {
try std.testing.expectEqual(@as(?ServerCommand, null), r);
} else |err| {
try std.testing.expectEqual(Parser.Error.InvalidArguments, err);
}
}
// Section 8: Buffer Boundary Edge Cases
test "MSG exactly fills buffer" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "MSG subject 1 5\r\nhello\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
try std.testing.expectEqual(data.len, consumed);
}
test "multiple commands in buffer first only" {
var p: Parser = .{};
var consumed: usize = 0;
// Two commands: MSG then PING
const data = "MSG subject 1 5\r\nhello\r\nPING\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
// Should only consume the MSG, not the PING
try std.testing.expectEqual(@as(usize, 24), consumed);
try std.testing.expectEqualSlices(u8, "hello", result.?.msg.payload);
// Parse again for PING
const remaining = data[consumed..];
var consumed2: usize = 0;
const result2 = try p.parse(std.testing.allocator, remaining, &consumed2);
try std.testing.expect(result2 != null);
try std.testing.expectEqual(ServerCommand.ping, result2.?);
}
test "partial parse consumed is zero" {
var p: Parser = .{};
var consumed: usize = 0;
// Incomplete command
const result = try p.parse(std.testing.allocator, "MSG subject 1 5\r\nhel", &consumed);
try std.testing.expectEqual(@as(?ServerCommand, null), result);
try std.testing.expectEqual(@as(usize, 0), consumed);
}
test "consumed never exceeds data length" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "PING\r\n";
_ = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(consumed <= data.len);
}
test "MSG with extra data after" {
var p: Parser = .{};
var consumed: usize = 0;
// MSG followed by garbage
const data = "MSG subject 1 5\r\nhello\r\ngarbage";
const result = try p.parse(std.testing.allocator, data, &consumed);
try std.testing.expect(result != null);
try std.testing.expectEqual(@as(usize, 24), consumed);
// Garbage should remain unparsed
try std.testing.expectEqualSlices(u8, "garbage", data[consumed..]);
}
test "INFO consumed includes CRLF" {
var p: Parser = .{};
var consumed: usize = 0;
const data = "INFO {\"server_id\":\"test\"}\r\n";
const result = try p.parse(std.testing.allocator, data, &consumed);
defer {
if (result) |cmd| {
switch (cmd) {
.info => |*info| {
var info_mut = info.*;
info_mut.deinit(std.testing.allocator);
},
else => {},
}
}
}
try std.testing.expect(result != null);
try std.testing.expectEqual(data.len, consumed);
}
================================================
FILE: src/protocol.zig
================================================
//! NATS Protocol Implementation
//!
//! This module handles the NATS wire protocol including parsing server
//! commands and encoding client commands.
//!
//! - Server commands: INFO, MSG, HMSG, PING, PONG, +OK, -ERR
//! - Client commands: CONNECT, PUB, HPUB, SUB, UNSUB, PING, PONG
const std = @import("std");
pub const commands = @import("protocol/commands.zig");
pub const parser = @import("protocol/parser.zig");
pub const encoder = @import("protocol/encoder.zig");
pub const headers = @import("protocol/headers.zig");
pub const header_map = @import("protocol/header_map.zig");
pub const errors = @import("protocol/errors.zig");
// Re-export common types
pub const ServerInfo = commands.ServerInfo;
pub const RawServerInfo = commands.RawServerInfo;
pub const ConnectOptions = commands.ConnectOptions;
pub const ServerCommand = commands.ServerCommand;
pub const ClientCommand = commands.ClientCommand;
pub const MsgArgs = commands.MsgArgs;
pub const HMsgArgs = commands.HMsgArgs;
pub const PubArgs = commands.PubArgs;
pub const SubArgs = commands.SubArgs;
pub const Parser = parser.Parser;
pub const Encoder = encoder.Encoder;
pub const HeaderMap = header_map.HeaderMap;
// Protocol errors
pub const Error = errors.Error;
pub const parseServerError = errors.parseServerError;
pub const isAuthError = errors.isAuthError;
test {
std.testing.refAllDecls(@This());
}
================================================
FILE: src/pubsub/inbox.zig
================================================
//! Inbox Generation
//!
//! Generates unique inbox subjects for request/reply patterns.
//! Inbox format: _INBOX.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Io = std.Io;
/// Inbox prefix used by NATS.
pub const prefix = "_INBOX.";
/// Length of the random portion of inbox.
pub const random_len = 22;
/// Total length of a generated inbox.
pub const total_len = prefix.len + random_len;
/// Characters used in inbox generation (base62).
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ++
"abcdefghijklmnopqrstuvwxyz";
/// Generates a new unique inbox subject.
/// Caller owns returned memory.
pub fn newInbox(allocator: Allocator, io: Io) Allocator.Error![]u8 {
const result = try allocator.alloc(u8, total_len);
@memcpy(result[0..prefix.len], prefix);
fillRandom(io, result[prefix.len..]);
assert(result.len == total_len);
return result;
}
/// Generates inbox into provided buffer.
/// Buffer must be at least total_len bytes.
pub fn newInboxBuf(io: Io, buf: []u8) error{BufferTooSmall}![]u8 {
if (buf.len < total_len) return error.BufferTooSmall;
assert(buf.len >= total_len);
@memcpy(buf[0..prefix.len], prefix);
fillRandom(io, buf[prefix.len..][0..random_len]);
return buf[0..total_len];
}
/// Fills buffer with random base62 characters.
fn fillRandom(io: Io, buf: []u8) void {
assert(buf.len > 0);
io.random(buf);
for (buf) |*b| {
b.* = alphabet[@mod(b.*, alphabet.len)];
}
}
/// Checks if a subject is an inbox.
pub fn isInbox(subject: []const u8) bool {
return std.mem.startsWith(u8, subject, prefix);
}
/// Generates inbox with custom prefix for wildcards.
/// Format: _INBOX..
/// Caller owns returned memory.
pub fn newInboxWithPrefix(
allocator: Allocator,
io: Io,
custom_prefix: []const u8,
) Allocator.Error![]u8 {
assert(custom_prefix.len > 0);
const len = prefix.len + custom_prefix.len + 1 + random_len;
const result = try allocator.alloc(u8, len);
var pos: usize = 0;
@memcpy(result[pos..][0..prefix.len], prefix);
pos += prefix.len;
@memcpy(result[pos..][0..custom_prefix.len], custom_prefix);
pos += custom_prefix.len;
result[pos] = '.';
pos += 1;
fillRandom(io, result[pos..][0..random_len]);
return result;
}
test "new inbox" {
const allocator = std.testing.allocator;
var io: Io.Threaded = .init(allocator, .{ .environ = .empty });
defer io.deinit();
const inbox = try newInbox(allocator, io.io());
defer allocator.free(inbox);
try std.testing.expectEqual(total_len, inbox.len);
try std.testing.expect(std.mem.startsWith(u8, inbox, prefix));
try std.testing.expect(isInbox(inbox));
}
test "new inbox buf" {
const allocator = std.testing.allocator;
var io: Io.Threaded = .init(allocator, .{ .environ = .empty });
defer io.deinit();
var buf: [64]u8 = undefined;
const inbox = try newInboxBuf(io.io(), &buf);
try std.testing.expectEqual(total_len, inbox.len);
try std.testing.expect(std.mem.startsWith(u8, inbox, prefix));
}
test "new inbox buf too small" {
const allocator = std.testing.allocator;
var io: Io.Threaded = .init(allocator, .{ .environ = .empty });
defer io.deinit();
var buf: [10]u8 = undefined;
try std.testing.expectError(
error.BufferTooSmall,
newInboxBuf(io.io(), &buf),
);
}
test "inbox uniqueness" {
const allocator = std.testing.allocator;
var io: Io.Threaded = .init(allocator, .{ .environ = .empty });
defer io.deinit();
const inbox1 = try newInbox(allocator, io.io());
defer allocator.free(inbox1);
const inbox2 = try newInbox(allocator, io.io());
defer allocator.free(inbox2);
try std.testing.expect(!std.mem.eql(u8, inbox1, inbox2));
}
test "is inbox" {
try std.testing.expect(isInbox("_INBOX.abc123"));
try std.testing.expect(isInbox("_INBOX."));
try std.testing.expect(!isInbox("foo.bar"));
try std.testing.expect(!isInbox("_INBOX"));
}
test "inbox with prefix" {
const allocator = std.testing.allocator;
var io: Io.Threaded = .init(allocator, .{ .environ = .empty });
defer io.deinit();
const inbox = try newInboxWithPrefix(allocator, io.io(), "myprefix");
defer allocator.free(inbox);
try std.testing.expect(std.mem.startsWith(u8, inbox, "_INBOX.myprefix."));
try std.testing.expect(isInbox(inbox));
}
================================================
FILE: src/pubsub/subject.zig
================================================
//! Subject Validation and Matching
//!
//! NATS subjects are dot-separated tokens. Wildcards:
//! - `*` matches exactly one token
//! - `>` matches one or more tokens (must be last)
const std = @import("std");
const assert = std.debug.assert;
/// Errors during subject validation.
pub const ValidationError = error{
EmptySubject,
EmptyToken,
InvalidCharacter,
WildcardNotLast,
SpaceInSubject,
};
/// Validates a subject for publishing (no wildcards allowed).
pub fn validatePublish(subject: []const u8) ValidationError!void {
if (subject.len == 0) return error.EmptySubject;
var token_start: usize = 0;
for (subject, 0..) |c, i| {
if (c == '.') {
if (i == token_start) return error.EmptyToken;
token_start = i + 1;
} else if (c == ' ' or c == '\t') {
return error.SpaceInSubject;
} else if (c == '*' or c == '>' or c < 0x20 or c == 0x7f) {
// Wildcards and control chars (includes CR/LF/null)
return error.InvalidCharacter;
}
}
// Check last token isn't empty
if (token_start >= subject.len) return error.EmptyToken;
}
/// Validates a subject for subscribing (wildcards allowed).
pub fn validateSubscribe(subject: []const u8) ValidationError!void {
if (subject.len == 0) return error.EmptySubject;
var token_start: usize = 0;
var has_full_wildcard = false;
for (subject, 0..) |c, i| {
if (c == '.') {
if (i == token_start) return error.EmptyToken;
if (has_full_wildcard) return error.WildcardNotLast;
token_start = i + 1;
} else if (c == ' ' or c == '\t') {
return error.SpaceInSubject;
} else if (c == '>') {
// > must be alone in its token (at start AND next is . or end)
if (i != token_start) return error.InvalidCharacter;
if (i + 1 < subject.len and subject[i + 1] != '.') {
return error.InvalidCharacter;
}
has_full_wildcard = true;
} else if (c == '*') {
// * must be alone in its token (at start AND next is . or end)
if (i != token_start) return error.InvalidCharacter;
if (i + 1 < subject.len and subject[i + 1] != '.') {
return error.InvalidCharacter;
}
} else if (c < 0x20 or c == 0x7f) {
// Control chars (includes CR/LF/null)
return error.InvalidCharacter;
}
}
// Check last token isn't empty
if (token_start >= subject.len) return error.EmptyToken;
}
/// Validates a reply-to address for protocol safety.
pub fn validateReplyTo(reply_to: []const u8) ValidationError!void {
if (reply_to.len == 0) return error.EmptySubject;
for (reply_to) |c| {
if (c <= 0x20 or c == 0x7f) return error.InvalidCharacter;
}
}
/// Validates a queue group name for protocol safety.
pub fn validateQueueGroup(queue: []const u8) ValidationError!void {
if (queue.len == 0) return error.EmptySubject;
for (queue) |c| {
if (c <= 0x20 or c == 0x7f) return error.InvalidCharacter;
}
}
/// Checks if a subject matches a pattern (with wildcards).
pub fn matches(pattern: []const u8, subject: []const u8) bool {
// Empty inputs are invalid subjects - return false
if (pattern.len == 0 or subject.len == 0) return false;
var pat_iter = std.mem.tokenizeScalar(u8, pattern, '.');
var subj_iter = std.mem.tokenizeScalar(u8, subject, '.');
while (pat_iter.next()) |pat_token| {
if (std.mem.eql(u8, pat_token, ">")) {
// > matches rest of subject (one or more tokens)
return subj_iter.next() != null;
}
const subj_token = subj_iter.next() orelse return false;
if (std.mem.eql(u8, pat_token, "*")) {
// * matches any single token
continue;
}
if (!std.mem.eql(u8, pat_token, subj_token)) {
return false;
}
}
// Both must be exhausted for exact match
return subj_iter.next() == null;
}
/// Counts the number of tokens in a subject.
pub fn tokenCount(subject: []const u8) usize {
if (subject.len == 0) return 0;
var count: usize = 1;
for (subject) |c| {
if (c == '.') count += 1;
}
return count;
}
/// Extracts a specific token from a subject (0-indexed).
pub fn getToken(subject: []const u8, index: usize) ?[]const u8 {
var iter = std.mem.tokenizeScalar(u8, subject, '.');
var i: usize = 0;
while (iter.next()) |token| {
if (i == index) return token;
i += 1;
}
return null;
}
test {
_ = @import("subject_test.zig");
}
================================================
FILE: src/pubsub/subject_test.zig
================================================
//! Subject Validation Edge Case Tests
//!
//! - Empty/boundary inputs
//! - Null bytes and control characters
//! - Wildcard position validation
//! - Pattern matching edge cases
//! - Token counting edge cases
const std = @import("std");
const subject = @import("subject.zig");
const validatePublish = subject.validatePublish;
const validateSubscribe = subject.validateSubscribe;
const validateReplyTo = subject.validateReplyTo;
const validateQueueGroup = subject.validateQueueGroup;
const matches = subject.matches;
const tokenCount = subject.tokenCount;
const getToken = subject.getToken;
const ValidationError = subject.ValidationError;
// Section 1: Existing Tests (moved from subject.zig)
test "validate publish subject" {
try validatePublish("foo");
try validatePublish("foo.bar");
try validatePublish("foo.bar.baz");
try validatePublish("_INBOX.abc123");
try std.testing.expectError(error.EmptySubject, validatePublish(""));
try std.testing.expectError(error.EmptyToken, validatePublish("foo."));
try std.testing.expectError(error.EmptyToken, validatePublish(".foo"));
try std.testing.expectError(error.EmptyToken, validatePublish("foo..bar"));
const inv_char = error.InvalidCharacter;
try std.testing.expectError(inv_char, validatePublish("foo.*"));
try std.testing.expectError(inv_char, validatePublish("foo.>"));
const space_err = error.SpaceInSubject;
try std.testing.expectError(space_err, validatePublish("foo bar"));
// CR/LF injection protection
try std.testing.expectError(inv_char, validatePublish("test\r\nINFO"));
try std.testing.expectError(inv_char, validatePublish("test\nfoo"));
try std.testing.expectError(inv_char, validatePublish("test\rfoo"));
}
test "validate subscribe subject" {
try validateSubscribe("foo");
try validateSubscribe("foo.bar");
try validateSubscribe("foo.*");
try validateSubscribe("*.bar");
try validateSubscribe("foo.>");
try validateSubscribe(">");
try std.testing.expectError(error.EmptySubject, validateSubscribe(""));
try std.testing.expectError(error.EmptyToken, validateSubscribe("foo."));
const wc_err = error.WildcardNotLast;
try std.testing.expectError(wc_err, validateSubscribe("foo.>.bar"));
const inv_char = error.InvalidCharacter;
try std.testing.expectError(inv_char, validateSubscribe("foo.bar>"));
try std.testing.expectError(inv_char, validateSubscribe("foo.bar*"));
// CR/LF injection protection
try std.testing.expectError(inv_char, validateSubscribe("test\r\nUNSUB"));
try std.testing.expectError(inv_char, validateSubscribe("test\nfoo"));
}
test "subject matching" {
// Exact match
try std.testing.expect(matches("foo.bar", "foo.bar"));
try std.testing.expect(!matches("foo.bar", "foo.baz"));
// Single token wildcard
try std.testing.expect(matches("foo.*", "foo.bar"));
try std.testing.expect(matches("foo.*", "foo.baz"));
try std.testing.expect(!matches("foo.*", "foo.bar.baz"));
try std.testing.expect(matches("*.bar", "foo.bar"));
// Full wildcard
try std.testing.expect(matches("foo.>", "foo.bar"));
try std.testing.expect(matches("foo.>", "foo.bar.baz"));
try std.testing.expect(!matches("foo.>", "foo"));
try std.testing.expect(matches(">", "foo"));
try std.testing.expect(matches(">", "foo.bar.baz"));
}
test "token count" {
try std.testing.expectEqual(@as(usize, 0), tokenCount(""));
try std.testing.expectEqual(@as(usize, 1), tokenCount("foo"));
try std.testing.expectEqual(@as(usize, 2), tokenCount("foo.bar"));
try std.testing.expectEqual(@as(usize, 3), tokenCount("foo.bar.baz"));
}
test "get token" {
try std.testing.expectEqualSlices(u8, "foo", getToken("foo.bar.baz", 0).?);
try std.testing.expectEqualSlices(u8, "bar", getToken("foo.bar.baz", 1).?);
try std.testing.expectEqualSlices(u8, "baz", getToken("foo.bar.baz", 2).?);
try std.testing.expect(getToken("foo.bar.baz", 3) == null);
}
test "validateReplyTo rejects injection" {
// Valid reply-to addresses
try validateReplyTo("_INBOX.abc123");
try validateReplyTo("reply.to.subject");
// CR/LF injection
const inv_char = error.InvalidCharacter;
try std.testing.expectError(inv_char, validateReplyTo("inbox\r\nUNSUB"));
try std.testing.expectError(inv_char, validateReplyTo("inbox\nfoo"));
try std.testing.expectError(inv_char, validateReplyTo("inbox\rfoo"));
// Spaces and tabs
try std.testing.expectError(inv_char, validateReplyTo("inbox foo"));
try std.testing.expectError(inv_char, validateReplyTo("inbox\tfoo"));
}
test "validateQueueGroup rejects injection" {
// Valid queue groups
try validateQueueGroup("workers");
try validateQueueGroup("queue-1");
// CR/LF injection
const inv_char = error.InvalidCharacter;
try std.testing.expectError(inv_char, validateQueueGroup("workers\r\n"));
try std.testing.expectError(inv_char, validateQueueGroup("workers\nfoo"));
try std.testing.expectError(inv_char, validateQueueGroup("workers\rfoo"));
// Spaces and tabs
try std.testing.expectError(inv_char, validateQueueGroup("workers foo"));
try std.testing.expectError(inv_char, validateQueueGroup("workers\tfoo"));
}
// Section 2: validatePublish Edge Cases
test "validatePublish single character subject" {
try validatePublish("a");
try validatePublish("x");
try validatePublish("1");
try validatePublish("_");
}
test "validatePublish single dot rejected" {
// Single dot = two empty tokens
try std.testing.expectError(error.EmptyToken, validatePublish("."));
}
test "validatePublish multiple consecutive dots rejected" {
try std.testing.expectError(error.EmptyToken, validatePublish(".."));
try std.testing.expectError(error.EmptyToken, validatePublish("..."));
try std.testing.expectError(error.EmptyToken, validatePublish("foo...bar"));
}
test "validatePublish null byte rejected" {
// Null bytes could be used for injection - should be rejected
const result = validatePublish("foo\x00bar");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validatePublish control characters rejected" {
// Various control characters that should be rejected
try std.testing.expectError(error.InvalidCharacter, validatePublish("foo\x01bar"));
try std.testing.expectError(error.InvalidCharacter, validatePublish("foo\x7fbar"));
}
test "validatePublish unicode characters" {
// Unicode should probably be allowed (common in international use)
try validatePublish("日本語");
try validatePublish("foo.émoji.bar");
}
test "validatePublish very long subject" {
// Very long subject - should there be a limit?
const long_subject = "a" ** 10000;
try validatePublish(long_subject);
}
test "validatePublish subject with numbers and special chars" {
try validatePublish("foo-bar");
try validatePublish("foo_bar");
try validatePublish("foo123");
try validatePublish("123");
try validatePublish("foo-bar_baz.123");
}
test "validatePublish leading/trailing dots" {
try std.testing.expectError(error.EmptyToken, validatePublish(".foo.bar"));
try std.testing.expectError(error.EmptyToken, validatePublish("foo.bar."));
try std.testing.expectError(error.EmptyToken, validatePublish("."));
}
// Section 3: validateSubscribe Edge Cases
test "validateSubscribe single wildcard tokens" {
try validateSubscribe("*");
try validateSubscribe(">");
}
test "validateSubscribe multiple single wildcards" {
try validateSubscribe("*.*");
try validateSubscribe("*.*.*");
try validateSubscribe("foo.*.*");
try validateSubscribe("*.*.bar");
}
test "validateSubscribe wildcard in middle of token rejected" {
// "*abc" or "abc*" should be rejected
try std.testing.expectError(error.InvalidCharacter, validateSubscribe("*abc"));
try std.testing.expectError(error.InvalidCharacter, validateSubscribe("abc*"));
try std.testing.expectError(error.InvalidCharacter, validateSubscribe("a*c"));
}
test "validateSubscribe > in middle of token rejected" {
try std.testing.expectError(error.InvalidCharacter, validateSubscribe(">abc"));
try std.testing.expectError(error.InvalidCharacter, validateSubscribe("abc>"));
try std.testing.expectError(error.InvalidCharacter, validateSubscribe("a>c"));
}
test "validateSubscribe > not at end rejected" {
try std.testing.expectError(error.WildcardNotLast, validateSubscribe(">.bar"));
try std.testing.expectError(error.WildcardNotLast, validateSubscribe("foo.>.bar"));
try std.testing.expectError(error.WildcardNotLast, validateSubscribe(">.*"));
}
test "validateSubscribe null byte rejected" {
const result = validateSubscribe("foo\x00bar");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateSubscribe single dot rejected" {
try std.testing.expectError(error.EmptyToken, validateSubscribe("."));
}
test "validateSubscribe empty token before wildcard" {
try std.testing.expectError(error.EmptyToken, validateSubscribe(".*"));
try std.testing.expectError(error.EmptyToken, validateSubscribe(".>"));
}
// Section 4: validateReplyTo Edge Cases
test "validateReplyTo empty string" {
const result = validateReplyTo("");
try std.testing.expectError(error.EmptySubject, result);
}
test "validateReplyTo null byte rejected" {
const result = validateReplyTo("inbox\x00inject");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo allows dots and special chars" {
// Reply-to can contain dots (for inbox subjects)
try validateReplyTo("_INBOX.abc.123.def");
try validateReplyTo("reply-to");
try validateReplyTo("reply_to");
}
test "validateReplyTo tab rejected" {
const result = validateReplyTo("inbox\treply");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo CR only rejected" {
const result = validateReplyTo("inbox\rreply");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo LF only rejected" {
const result = validateReplyTo("inbox\nreply");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo space rejected" {
const result = validateReplyTo("inbox reply");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo DEL char rejected" {
const result = validateReplyTo("inbox\x7freply");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo control char 0x01 rejected" {
const result = validateReplyTo("inbox\x01reply");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateReplyTo very long string" {
// 10000 character reply-to should be valid (no length limit)
const long_reply = "a" ** 10000;
try validateReplyTo(long_reply);
}
// Section 5: validateQueueGroup Edge Cases
test "validateQueueGroup empty string" {
const result = validateQueueGroup("");
try std.testing.expectError(error.EmptySubject, result);
}
test "validateQueueGroup null byte rejected" {
const result = validateQueueGroup("workers\x00inject");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup allows dots" {
// Queue groups can contain dots
try validateQueueGroup("worker.group.1");
}
test "validateQueueGroup tab rejected" {
const result = validateQueueGroup("workers\tgroup");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup CR only rejected" {
const result = validateQueueGroup("workers\rgroup");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup LF only rejected" {
const result = validateQueueGroup("workers\ngroup");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup space rejected" {
const result = validateQueueGroup("workers group");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup DEL char rejected" {
const result = validateQueueGroup("workers\x7fgroup");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup control char 0x01 rejected" {
const result = validateQueueGroup("workers\x01group");
try std.testing.expectError(error.InvalidCharacter, result);
}
test "validateQueueGroup very long string" {
// 10000 character queue group should be valid (no length limit)
const long_qg = "w" ** 10000;
try validateQueueGroup(long_qg);
}
test "validateQueueGroup allows special chars" {
try validateQueueGroup("worker-pool_1");
try validateQueueGroup("worker.pool.1");
try validateQueueGroup("WORKERS");
}
// Section 6: matches() Edge Cases
test "matches empty pattern" {
const result = matches("", "foo");
try std.testing.expect(!result);
}
test "matches empty subject" {
const result = matches("foo", "");
try std.testing.expect(!result);
}
test "matches both empty" {
// Both empty = both invalid, return false
const result = matches("", "");
try std.testing.expect(!result);
}
test "matches single token exact" {
try std.testing.expect(matches("foo", "foo"));
try std.testing.expect(!matches("foo", "bar"));
}
test "matches pattern longer than subject" {
try std.testing.expect(!matches("foo.bar.baz", "foo.bar"));
try std.testing.expect(!matches("foo.bar", "foo"));
}
test "matches subject longer than pattern" {
try std.testing.expect(!matches("foo", "foo.bar"));
try std.testing.expect(!matches("foo.bar", "foo.bar.baz"));
}
test "matches * matches empty token behavior" {
// What happens with "foo.*" matching "foo." (empty last token)?
// tokenizeScalar skips empty tokens, so this might have unexpected behavior
try std.testing.expect(!matches("foo.*", "foo."));
}
test "matches > requires at least one token" {
// ">" should require at least one token to match
try std.testing.expect(matches(">", "a"));
try std.testing.expect(matches(">", "a.b.c"));
// "foo.>" requires at least one token after foo
try std.testing.expect(!matches("foo.>", "foo"));
try std.testing.expect(matches("foo.>", "foo.x"));
}
test "matches multiple * wildcards" {
try std.testing.expect(matches("*.*", "a.b"));
try std.testing.expect(!matches("*.*", "a"));
try std.testing.expect(!matches("*.*", "a.b.c"));
try std.testing.expect(matches("*.*.*", "a.b.c"));
}
test "matches * and > combination" {
try std.testing.expect(matches("*.>", "a.b"));
try std.testing.expect(matches("*.>", "a.b.c"));
try std.testing.expect(!matches("*.>", "a"));
}
test "matches with dots in pattern edge cases" {
// Pattern and subject with trailing/leading dots
// tokenizeScalar skips empty tokens, so "foo." -> ["foo"] and ".foo" -> ["foo"]
// Both become equivalent, so they match (garbage in, garbage out for invalid subjects)
try std.testing.expect(matches("foo.", "foo."));
try std.testing.expect(matches(".foo", ".foo"));
}
// Section 7: tokenCount Edge Cases
test "tokenCount single dot" {
// "." has two empty tokens
const count = tokenCount(".");
try std.testing.expectEqual(@as(usize, 2), count);
}
test "tokenCount multiple dots" {
// ".." has three empty tokens
try std.testing.expectEqual(@as(usize, 3), tokenCount(".."));
try std.testing.expectEqual(@as(usize, 4), tokenCount("..."));
}
test "tokenCount trailing dot" {
// "foo." counts as 2 tokens even though second is empty
try std.testing.expectEqual(@as(usize, 2), tokenCount("foo."));
}
test "tokenCount leading dot" {
// ".foo" counts as 2 tokens even though first is empty
try std.testing.expectEqual(@as(usize, 2), tokenCount(".foo"));
}
test "tokenCount very long subject" {
const long_subject = "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z";
try std.testing.expectEqual(@as(usize, 26), tokenCount(long_subject));
}
// Section 8: getToken Edge Cases
test "getToken empty subject" {
// Empty subject should return null for any index
try std.testing.expect(getToken("", 0) == null);
try std.testing.expect(getToken("", 1) == null);
}
test "getToken single dot" {
// "." splits into empty tokens - tokenizeScalar skips them
try std.testing.expect(getToken(".", 0) == null);
}
test "getToken trailing dot" {
// "foo." - what does getToken return for index 1?
const token0 = getToken("foo.", 0);
try std.testing.expectEqualSlices(u8, "foo", token0.?);
// Index 1 should be null (empty token skipped by tokenizeScalar)
try std.testing.expect(getToken("foo.", 1) == null);
}
test "getToken leading dot" {
// ".foo" - what does getToken return for index 0?
const token0 = getToken(".foo", 0);
try std.testing.expectEqualSlices(u8, "foo", token0.?);
try std.testing.expect(getToken(".foo", 1) == null);
}
test "getToken very large index" {
try std.testing.expect(getToken("foo.bar", 1000) == null);
}
================================================
FILE: src/pubsub/subscription.zig
================================================
//! Subscription Types (for embedded/zero-allocation use)
//!
//! This module contains types for embedded/no-alloc scenarios.
//! For normal use, see client.zig Subscription type.
const std = @import("std");
const assert = std.debug.assert;
const subject_mod = @import("subject.zig");
/// Subscription state.
pub const State = enum {
active,
draining,
unsubscribed,
};
/// Subscription-related errors.
pub const Error = error{
InvalidSubject,
InvalidSubscription,
SubscriptionClosed,
};
/// Fixed-size ring buffer queue (zero allocations).
/// For embedded use cases where dynamic allocation is not allowed.
pub fn FixedQueue(comptime T: type, comptime capacity: u16) type {
return struct {
items: [capacity]T = undefined,
head: u16 = 0,
tail: u16 = 0,
count: u16 = 0,
const Self = @This();
pub fn push(self: *Self, item: T) !void {
if (self.count >= capacity) return error.QueueFull;
self.items[self.tail] = item;
self.tail = (self.tail + 1) % capacity;
self.count += 1;
}
pub fn tryPop(self: *Self) ?T {
if (self.count == 0) return null;
const item = self.items[self.head];
self.head = (self.head + 1) % capacity;
self.count -= 1;
return item;
}
pub fn clear(self: *Self) void {
self.head = 0;
self.tail = 0;
self.count = 0;
}
};
}
/// FixedSubscription slot configuration.
pub const FixedSubConfig = struct {
max_subject_len: u16 = 256,
max_queue_group_len: u16 = 64,
queue_capacity: u16 = 256,
};
/// Zero-allocation subscription slot (for embedded use).
/// Uses fixed buffers for subject, queue_group, and message queue.
/// Designed for embedding in fixed arrays (no heap allocation).
/// Message type must be provided by user as it's defined in client.zig.
pub fn FixedSubscription(
comptime ClientType: type,
comptime MessageType: type,
comptime config: FixedSubConfig,
) type {
return struct {
client: *ClientType,
sid: u64,
subject_buf: [config.max_subject_len]u8,
subject_len: u16,
queue_group_buf: [config.max_queue_group_len]u8,
queue_group_len: u16,
messages: FixedQueue(MessageType, config.queue_capacity),
state: State,
max_msgs: u64,
received_msgs: u64,
active: bool,
const Self = @This();
/// Initialize an inactive slot.
pub fn initEmpty() Self {
return .{
.client = undefined,
.sid = 0,
.subject_buf = undefined,
.subject_len = 0,
.queue_group_buf = undefined,
.queue_group_len = 0,
.messages = .{},
.state = .unsubscribed,
.max_msgs = 0,
.received_msgs = 0,
.active = false,
};
}
/// Activation errors.
pub const ActivateError = error{
EmptySubject,
SubjectTooLong,
QueueGroupTooLong,
};
/// Activate slot with subscription data.
pub fn activate(
self: *Self,
client_ptr: *ClientType,
sid_val: u64,
subj: []const u8,
queue_grp: ?[]const u8,
) ActivateError!void {
if (subj.len == 0) return error.EmptySubject;
if (subj.len >= config.max_subject_len) {
return error.SubjectTooLong;
}
assert(subj.len > 0);
assert(subj.len <= config.max_subject_len);
self.client = client_ptr;
self.sid = sid_val;
self.subject_len = @intCast(subj.len);
@memcpy(self.subject_buf[0..subj.len], subj);
if (queue_grp) |qg| {
if (qg.len > config.max_queue_group_len) {
return error.QueueGroupTooLong;
}
self.queue_group_len = @intCast(qg.len);
@memcpy(self.queue_group_buf[0..qg.len], qg);
} else {
self.queue_group_len = 0;
}
self.messages.clear();
self.state = .active;
self.max_msgs = 0;
self.received_msgs = 0;
self.active = true;
}
/// Deactivate slot (returns to pool).
pub fn deactivate(self: *Self) void {
self.active = false;
self.state = .unsubscribed;
self.sid = 0;
}
/// Get subject slice.
pub fn subject(self: *const Self) []const u8 {
return self.subject_buf[0..self.subject_len];
}
/// Get queue group slice (null if not set).
pub fn queueGroup(self: *const Self) ?[]const u8 {
if (self.queue_group_len == 0) return null;
return self.queue_group_buf[0..self.queue_group_len];
}
/// Returns pending message count.
pub fn pending(self: *const Self) u16 {
return self.messages.count;
}
/// Start draining.
pub fn drain(self: *Self) void {
if (self.state == .active) {
self.state = .draining;
}
}
/// Check if active.
pub fn isActive(self: *const Self) bool {
return self.state == .active and self.active;
}
/// Match subject pattern.
pub fn matches(self: *const Self, msg_subject: []const u8) bool {
return subject_mod.matches(self.subject(), msg_subject);
}
};
}
test {
_ = @import("subscription_test.zig");
}
================================================
FILE: src/pubsub/subscription_test.zig
================================================
//! Subscription Module Tests
//!
//! Tests ring buffer wraparound, capacity limits, state transitions,
//! and subject validation edge cases.
const std = @import("std");
const testing = std.testing;
const subscription = @import("subscription.zig");
const FixedQueue = subscription.FixedQueue;
const FixedSubscription = subscription.FixedSubscription;
const State = subscription.State;
// Test types for FixedSubscription testing
const TestClient = struct {
dummy: u32 = 0,
};
const TestMessage = struct {
data: u32,
};
const TestConfig = subscription.FixedSubConfig{
.max_subject_len = 64,
.max_queue_group_len = 32,
.queue_capacity = 8,
};
const TestSub = FixedSubscription(TestClient, TestMessage, TestConfig);
// Section 1: FixedQueue Basic Operations
test "FixedQueue push and pop basic" {
var q: FixedQueue(u32, 4) = .{};
try testing.expectEqual(@as(u16, 0), q.count);
try q.push(1);
try q.push(2);
try q.push(3);
try testing.expectEqual(@as(u16, 3), q.count);
try testing.expectEqual(@as(?u32, 1), q.tryPop());
try testing.expectEqual(@as(?u32, 2), q.tryPop());
try testing.expectEqual(@as(?u32, 3), q.tryPop());
try testing.expectEqual(@as(u16, 0), q.count);
}
test "FixedQueue full returns error" {
var q: FixedQueue(u32, 2) = .{};
try q.push(1);
try q.push(2);
try testing.expectError(error.QueueFull, q.push(3));
try testing.expectEqual(@as(u16, 2), q.count);
}
test "FixedQueue pop from empty returns null" {
var q: FixedQueue(u32, 4) = .{};
try testing.expectEqual(@as(?u32, null), q.tryPop());
try testing.expectEqual(@as(?u32, null), q.tryPop());
try testing.expectEqual(@as(u16, 0), q.count);
}
// Section 2: FixedQueue Wraparound Behavior
test "FixedQueue wraparound single cycle" {
// Capacity 4: fill, empty, refill to test wraparound
var q: FixedQueue(u32, 4) = .{};
// Fill completely
try q.push(1);
try q.push(2);
try q.push(3);
try q.push(4);
try testing.expectError(error.QueueFull, q.push(5));
// Empty completely
try testing.expectEqual(@as(?u32, 1), q.tryPop());
try testing.expectEqual(@as(?u32, 2), q.tryPop());
try testing.expectEqual(@as(?u32, 3), q.tryPop());
try testing.expectEqual(@as(?u32, 4), q.tryPop());
try testing.expectEqual(@as(?u32, null), q.tryPop());
// Refill - now head/tail have wrapped
try q.push(10);
try q.push(20);
try q.push(30);
try q.push(40);
// Verify FIFO order maintained
try testing.expectEqual(@as(?u32, 10), q.tryPop());
try testing.expectEqual(@as(?u32, 20), q.tryPop());
try testing.expectEqual(@as(?u32, 30), q.tryPop());
try testing.expectEqual(@as(?u32, 40), q.tryPop());
}
test "FixedQueue wraparound interleaved push pop" {
var q: FixedQueue(u32, 4) = .{};
// Push 2, pop 1, repeat - causes gradual wraparound
try q.push(1);
try q.push(2);
try testing.expectEqual(@as(?u32, 1), q.tryPop());
try q.push(3);
try q.push(4);
try testing.expectEqual(@as(?u32, 2), q.tryPop());
try q.push(5);
try q.push(6);
try testing.expectEqual(@as(?u32, 3), q.tryPop());
// Continue until wrapped multiple times
try q.push(7);
try testing.expectEqual(@as(?u32, 4), q.tryPop());
try q.push(8);
try testing.expectEqual(@as(?u32, 5), q.tryPop());
// Verify remaining
try testing.expectEqual(@as(?u32, 6), q.tryPop());
try testing.expectEqual(@as(?u32, 7), q.tryPop());
try testing.expectEqual(@as(?u32, 8), q.tryPop());
try testing.expectEqual(@as(?u32, null), q.tryPop());
}
test "FixedQueue wraparound stress" {
var q: FixedQueue(u32, 8) = .{};
// 100 push/pop cycles to stress wraparound
var i: u32 = 0;
while (i < 100) : (i += 1) {
try q.push(i);
const val = q.tryPop();
try testing.expectEqual(@as(?u32, i), val);
}
try testing.expectEqual(@as(u16, 0), q.count);
}
// Section 3: FixedQueue Clear Operations
test "FixedQueue clear non-empty" {
var q: FixedQueue(u32, 4) = .{};
try q.push(1);
try q.push(2);
try q.push(3);
try testing.expectEqual(@as(u16, 3), q.count);
q.clear();
try testing.expectEqual(@as(u16, 0), q.count);
try testing.expectEqual(@as(?u32, null), q.tryPop());
// Should be able to push again
try q.push(100);
try testing.expectEqual(@as(?u32, 100), q.tryPop());
}
test "FixedQueue clear empty" {
var q: FixedQueue(u32, 4) = .{};
q.clear();
q.clear(); // Double clear
q.clear();
try testing.expectEqual(@as(u16, 0), q.count);
try testing.expectEqual(@as(?u32, null), q.tryPop());
}
test "FixedQueue clear after wraparound" {
var q: FixedQueue(u32, 4) = .{};
// Cause wraparound
try q.push(1);
try q.push(2);
_ = q.tryPop();
_ = q.tryPop();
try q.push(3);
try q.push(4);
q.clear();
try testing.expectEqual(@as(u16, 0), q.count);
try testing.expectEqual(@as(u16, 0), q.head);
try testing.expectEqual(@as(u16, 0), q.tail);
}
// Section 4: FixedQueue Edge Capacities
test "FixedQueue capacity 1" {
var q: FixedQueue(u32, 1) = .{};
try q.push(42);
try testing.expectError(error.QueueFull, q.push(43));
try testing.expectEqual(@as(?u32, 42), q.tryPop());
try testing.expectEqual(@as(?u32, null), q.tryPop());
// Can push again
try q.push(100);
try testing.expectEqual(@as(?u32, 100), q.tryPop());
}
test "FixedQueue capacity 2" {
var q: FixedQueue(u32, 2) = .{};
try q.push(1);
try q.push(2);
try testing.expectError(error.QueueFull, q.push(3));
try testing.expectEqual(@as(?u32, 1), q.tryPop());
try q.push(3);
try testing.expectEqual(@as(?u32, 2), q.tryPop());
try testing.expectEqual(@as(?u32, 3), q.tryPop());
}
test "FixedQueue capacity 256" {
var q: FixedQueue(u32, 256) = .{};
// Fill to capacity
var i: u32 = 0;
while (i < 256) : (i += 1) {
try q.push(i);
}
try testing.expectError(error.QueueFull, q.push(256));
try testing.expectEqual(@as(u16, 256), q.count);
// Verify FIFO order
i = 0;
while (i < 256) : (i += 1) {
try testing.expectEqual(@as(?u32, i), q.tryPop());
}
}
// Section 5: FixedQueue Count Accuracy
test "FixedQueue count accuracy through operations" {
var q: FixedQueue(u32, 8) = .{};
try testing.expectEqual(@as(u16, 0), q.count);
try q.push(1);
try testing.expectEqual(@as(u16, 1), q.count);
try q.push(2);
try q.push(3);
try testing.expectEqual(@as(u16, 3), q.count);
_ = q.tryPop();
try testing.expectEqual(@as(u16, 2), q.count);
_ = q.tryPop();
_ = q.tryPop();
try testing.expectEqual(@as(u16, 0), q.count);
// Pop from empty doesn't affect count
_ = q.tryPop();
try testing.expectEqual(@as(u16, 0), q.count);
}
test "FixedQueue count with failed push" {
var q: FixedQueue(u32, 2) = .{};
try q.push(1);
try q.push(2);
try testing.expectEqual(@as(u16, 2), q.count);
// Failed push shouldn't change count
try testing.expectError(error.QueueFull, q.push(3));
try testing.expectEqual(@as(u16, 2), q.count);
}
// Section 6: FixedQueue FIFO Order
test "FixedQueue strict FIFO order" {
var q: FixedQueue(u32, 16) = .{};
const values = [_]u32{ 100, 200, 300, 400, 500, 600, 700, 800 };
for (values) |v| {
try q.push(v);
}
for (values) |expected| {
try testing.expectEqual(@as(?u32, expected), q.tryPop());
}
}
test "FixedQueue FIFO with interleaved operations" {
var q: FixedQueue(u32, 4) = .{};
try q.push(1);
try q.push(2);
try testing.expectEqual(@as(?u32, 1), q.tryPop());
try q.push(3);
try testing.expectEqual(@as(?u32, 2), q.tryPop());
try q.push(4);
try q.push(5);
try testing.expectEqual(@as(?u32, 3), q.tryPop());
try testing.expectEqual(@as(?u32, 4), q.tryPop());
try testing.expectEqual(@as(?u32, 5), q.tryPop());
}
// Section 7: FixedQueue Different Types
test "FixedQueue with struct type" {
const Item = struct { id: u32, value: u64 };
var q: FixedQueue(Item, 4) = .{};
try q.push(.{ .id = 1, .value = 100 });
try q.push(.{ .id = 2, .value = 200 });
const item1 = q.tryPop().?;
try testing.expectEqual(@as(u32, 1), item1.id);
try testing.expectEqual(@as(u64, 100), item1.value);
const item2 = q.tryPop().?;
try testing.expectEqual(@as(u32, 2), item2.id);
try testing.expectEqual(@as(u64, 200), item2.value);
}
test "FixedQueue with pointer type" {
var values = [_]u32{ 10, 20, 30 };
var q: FixedQueue(*u32, 4) = .{};
try q.push(&values[0]);
try q.push(&values[1]);
try q.push(&values[2]);
const p1 = q.tryPop().?;
try testing.expectEqual(@as(u32, 10), p1.*);
const p2 = q.tryPop().?;
try testing.expectEqual(@as(u32, 20), p2.*);
}
// Section 8: FixedSubscription initEmpty
test "FixedSubscription initEmpty defaults" {
const sub = TestSub.initEmpty();
try testing.expectEqual(@as(u64, 0), sub.sid);
try testing.expectEqual(@as(u8, 0), sub.subject_len);
try testing.expectEqual(@as(u8, 0), sub.queue_group_len);
try testing.expectEqual(State.unsubscribed, sub.state);
try testing.expectEqual(@as(u64, 0), sub.max_msgs);
try testing.expectEqual(@as(u64, 0), sub.received_msgs);
try testing.expectEqual(false, sub.active);
}
test "FixedSubscription initEmpty subject accessor" {
const sub = TestSub.initEmpty();
// subject() returns empty slice when inactive
const s = sub.subject();
try testing.expectEqual(@as(usize, 0), s.len);
}
test "FixedSubscription initEmpty queueGroup accessor" {
const sub = TestSub.initEmpty();
// queueGroup() returns null when not set
try testing.expect(sub.queueGroup() == null);
}
// Section 9: FixedSubscription activate Valid Cases
test "FixedSubscription activate basic" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 123, "test.subject", null);
try testing.expectEqual(@as(u64, 123), sub.sid);
try testing.expectEqualStrings("test.subject", sub.subject());
try testing.expect(sub.queueGroup() == null);
try testing.expectEqual(State.active, sub.state);
try testing.expectEqual(true, sub.active);
try testing.expectEqual(true, sub.isActive());
}
test "FixedSubscription activate with queue group" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 456, "orders", "workers");
try testing.expectEqualStrings("orders", sub.subject());
try testing.expectEqualStrings("workers", sub.queueGroup().?);
try testing.expectEqual(true, sub.isActive());
}
test "FixedSubscription activate subject at max length" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// max_subject_len is exclusive (>= rejects it)
const max_subject = "a" ** 63;
try sub.activate(&client, 1, max_subject, null);
try testing.expectEqual(@as(usize, 63), sub.subject().len);
try testing.expectEqualStrings(max_subject, sub.subject());
}
test "FixedSubscription activate queue_group at max length" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// Config has max_queue_group_len = 32
const max_qg = "q" ** 32;
try sub.activate(&client, 1, "test", max_qg);
try testing.expectEqual(@as(usize, 32), sub.queueGroup().?.len);
try testing.expectEqualStrings(max_qg, sub.queueGroup().?);
}
// Section 10: FixedSubscription activate Error Cases
test "FixedSubscription activate subject too long" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// Config has max_subject_len = 64
const long_subject = "a" ** 65;
try testing.expectError(
error.SubjectTooLong,
sub.activate(&client, 1, long_subject, null),
);
// Should remain inactive
try testing.expectEqual(false, sub.active);
}
test "FixedSubscription activate queue_group too long" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// Config has max_queue_group_len = 32
const long_qg = "q" ** 33;
try testing.expectError(
error.QueueGroupTooLong,
sub.activate(&client, 1, "test", long_qg),
);
try testing.expectEqual(false, sub.active);
}
test "FixedSubscription activate empty subject returns error" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try testing.expectError(
error.EmptySubject,
sub.activate(&client, 1, "", null),
);
try testing.expectEqual(false, sub.active);
}
// Section 11: FixedSubscription deactivate
test "FixedSubscription deactivate resets state" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 123, "test", null);
try testing.expectEqual(true, sub.isActive());
sub.deactivate();
try testing.expectEqual(false, sub.active);
try testing.expectEqual(State.unsubscribed, sub.state);
try testing.expectEqual(@as(u64, 0), sub.sid);
try testing.expectEqual(false, sub.isActive());
}
test "FixedSubscription deactivate on inactive" {
var sub = TestSub.initEmpty();
// Should be safe to deactivate an inactive slot
sub.deactivate();
sub.deactivate();
try testing.expectEqual(false, sub.active);
}
test "FixedSubscription reactivate after deactivate" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "first.subject", null);
sub.deactivate();
try sub.activate(&client, 2, "second.subject", "queue");
try testing.expectEqual(@as(u64, 2), sub.sid);
try testing.expectEqualStrings("second.subject", sub.subject());
try testing.expectEqualStrings("queue", sub.queueGroup().?);
try testing.expectEqual(true, sub.isActive());
}
// Section 12: FixedSubscription State Transitions
test "FixedSubscription drain from active" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "test", null);
try testing.expectEqual(State.active, sub.state);
sub.drain();
try testing.expectEqual(State.draining, sub.state);
// Note: isActive() returns false when draining
try testing.expectEqual(false, sub.isActive());
}
test "FixedSubscription drain from non-active is noop" {
var sub = TestSub.initEmpty();
sub.drain();
try testing.expectEqual(State.unsubscribed, sub.state);
// Set to draining, drain again should be noop
var client = TestClient{};
try sub.activate(&client, 1, "test", null);
sub.drain();
try testing.expectEqual(State.draining, sub.state);
sub.drain(); // Should not change state
try testing.expectEqual(State.draining, sub.state);
}
test "FixedSubscription isActive requires both flags" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// Initially: state=unsubscribed, active=false -> isActive=false
try testing.expectEqual(false, sub.isActive());
// After activate: state=active, active=true -> isActive=true
try sub.activate(&client, 1, "test", null);
try testing.expectEqual(true, sub.isActive());
// After drain: state=draining, active=true -> isActive=false
sub.drain();
try testing.expectEqual(false, sub.isActive());
// After deactivate: state=unsubscribed, active=false -> isActive=false
sub.deactivate();
try testing.expectEqual(false, sub.isActive());
}
// Section 13: FixedSubscription pending count
test "FixedSubscription pending initial" {
const sub = TestSub.initEmpty();
try testing.expectEqual(@as(u16, 0), sub.pending());
}
test "FixedSubscription pending after activate" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "test", null);
try testing.expectEqual(@as(u16, 0), sub.pending());
}
test "FixedSubscription pending after message push" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "test", null);
try sub.messages.push(.{ .data = 1 });
try testing.expectEqual(@as(u16, 1), sub.pending());
try sub.messages.push(.{ .data = 2 });
try testing.expectEqual(@as(u16, 2), sub.pending());
_ = sub.messages.tryPop();
try testing.expectEqual(@as(u16, 1), sub.pending());
}
// Section 14: FixedSubscription matches
test "FixedSubscription matches exact" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "foo.bar", null);
try testing.expect(sub.matches("foo.bar"));
try testing.expect(!sub.matches("foo.baz"));
try testing.expect(!sub.matches("foo"));
try testing.expect(!sub.matches("foo.bar.baz"));
}
test "FixedSubscription matches wildcard single" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "foo.*", null);
try testing.expect(sub.matches("foo.bar"));
try testing.expect(sub.matches("foo.baz"));
try testing.expect(!sub.matches("foo.bar.baz"));
try testing.expect(!sub.matches("foo"));
}
test "FixedSubscription matches wildcard full" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "foo.>", null);
try testing.expect(sub.matches("foo.bar"));
try testing.expect(sub.matches("foo.bar.baz"));
try testing.expect(sub.matches("foo.a.b.c.d"));
try testing.expect(!sub.matches("foo"));
try testing.expect(!sub.matches("bar.foo"));
}
// Section 15: FixedSubscription Multiple Cycles
test "FixedSubscription multiple activate deactivate cycles" {
var client = TestClient{};
var sub = TestSub.initEmpty();
var i: u64 = 0;
while (i < 10) : (i += 1) {
try sub.activate(&client, i, "test", null);
try testing.expectEqual(i, sub.sid);
try testing.expectEqual(true, sub.isActive());
sub.deactivate();
try testing.expectEqual(false, sub.isActive());
}
}
test "FixedSubscription activate clears message queue" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "test", null);
// Push some messages
try sub.messages.push(.{ .data = 1 });
try sub.messages.push(.{ .data = 2 });
try testing.expectEqual(@as(u16, 2), sub.pending());
// Deactivate and reactivate
sub.deactivate();
try sub.activate(&client, 2, "other", null);
// Queue should be cleared
try testing.expectEqual(@as(u16, 0), sub.pending());
}
// Section 16: State Enum
test "State enum values distinct" {
try testing.expect(State.active != State.draining);
try testing.expect(State.active != State.unsubscribed);
try testing.expect(State.draining != State.unsubscribed);
}
// Section 17: Edge Case Configs
test "FixedSubscription minimal config" {
const MinConfig = subscription.FixedSubConfig{
.max_subject_len = 2,
.max_queue_group_len = 2,
.queue_capacity = 1,
};
const MinSub = FixedSubscription(TestClient, TestMessage, MinConfig);
var client = TestClient{};
var sub = MinSub.initEmpty();
try sub.activate(&client, 1, "a", "b");
try testing.expectEqualStrings("a", sub.subject());
try testing.expectEqualStrings("b", sub.queueGroup().?);
// Queue capacity 1
try sub.messages.push(.{ .data = 1 });
try testing.expectError(error.QueueFull, sub.messages.push(.{ .data = 2 }));
}
test "FixedSubscription large config" {
const LargeConfig = subscription.FixedSubConfig{
.max_subject_len = 1024,
.max_queue_group_len = 512,
.queue_capacity = 1024,
};
const LargeSub = FixedSubscription(TestClient, TestMessage, LargeConfig);
var client = TestClient{};
var sub = LargeSub.initEmpty();
const long_subject = "a" ** 1023;
try sub.activate(&client, 1, long_subject, null);
try testing.expectEqual(@as(usize, 1023), sub.subject().len);
}
test "FixedSubscription large queue group" {
const LargeConfig = subscription.FixedSubConfig{
.max_subject_len = 256,
.max_queue_group_len = 512,
.queue_capacity = 256,
};
const LargeSub = FixedSubscription(TestClient, TestMessage, LargeConfig);
var client = TestClient{};
var sub = LargeSub.initEmpty();
const long_qg = "q" ** 512;
try sub.activate(&client, 1, "test", long_qg);
try testing.expectEqual(@as(usize, 512), sub.queueGroup().?.len);
}
// Section 18: SID Edge Values
test "FixedSubscription SID zero" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 0, "test", null);
try testing.expectEqual(@as(u64, 0), sub.sid);
}
test "FixedSubscription SID max u64" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, std.math.maxInt(u64), "test", null);
try testing.expectEqual(std.math.maxInt(u64), sub.sid);
}
// Section 19: Received/Max Messages
test "FixedSubscription received msgs initial" {
const sub = TestSub.initEmpty();
try testing.expectEqual(@as(u64, 0), sub.received_msgs);
}
test "FixedSubscription max msgs initial" {
const sub = TestSub.initEmpty();
try testing.expectEqual(@as(u64, 0), sub.max_msgs);
}
test "FixedSubscription activate resets counters" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// Manually set counters
sub.received_msgs = 100;
sub.max_msgs = 50;
try sub.activate(&client, 1, "test", null);
try testing.expectEqual(@as(u64, 0), sub.received_msgs);
try testing.expectEqual(@as(u64, 0), sub.max_msgs);
}
// Section 20: Subject/QueueGroup with Dots
test "FixedSubscription subject with dots" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "a.b.c.d.e", null);
try testing.expectEqualStrings("a.b.c.d.e", sub.subject());
}
test "FixedSubscription subject single char" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "x", null);
try testing.expectEqualStrings("x", sub.subject());
}
test "FixedSubscription queue group with dots" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// NATS queue groups typically don't have dots but are accepted
try sub.activate(&client, 1, "test", "group.name");
try testing.expectEqualStrings("group.name", sub.queueGroup().?);
}
// Section 21: Subject/QueueGroup Special Characters
test "FixedSubscription subject with hyphens and underscores" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "my-subject_name", null);
try testing.expectEqualStrings("my-subject_name", sub.subject());
}
test "FixedSubscription subject with numbers" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "topic123.sub456", null);
try testing.expectEqualStrings("topic123.sub456", sub.subject());
}
test "FixedSubscription queue group with hyphens" {
var client = TestClient{};
var sub = TestSub.initEmpty();
try sub.activate(&client, 1, "test", "worker-pool-1");
try testing.expectEqualStrings("worker-pool-1", sub.queueGroup().?);
}
test "FixedSubscription subject all printable ASCII" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// Most printable ASCII chars (excluding space and control chars)
try sub.activate(&client, 1, "!#$%&'()+,-./0123456789:;<=>?@ABC", null);
try testing.expectEqualStrings("!#$%&'()+,-./0123456789:;<=>?@ABC", sub.subject());
}
// Section 22: Subject/QueueGroup Unicode
// NOTE: FixedSubscription stores raw bytes - it does NOT validate subject content.
// Unicode validation (if needed) should be done at the NATS protocol encoder level.
// These tests verify that arbitrary bytes are stored/retrieved correctly.
test "FixedSubscription subject with unicode bytes" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// UTF-8 encoded Japanese: "テスト" (tesuto = test)
const unicode_subject = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88";
try sub.activate(&client, 1, unicode_subject, null);
try testing.expectEqualStrings(unicode_subject, sub.subject());
}
test "FixedSubscription subject with emoji bytes" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// UTF-8 encoded emoji: (rocket)
const emoji_subject = "\xf0\x9f\x9a\x80.launch";
try sub.activate(&client, 1, emoji_subject, null);
try testing.expectEqualStrings(emoji_subject, sub.subject());
}
test "FixedSubscription queue group with unicode bytes" {
var client = TestClient{};
var sub = TestSub.initEmpty();
// UTF-8 encoded: "队列" (duìliè = queue in Chinese)
const unicode_qg = "\xe9\x98\x9f\xe5\x88\x97";
try sub.activate(&client, 1, "test", unicode_qg);
try testing.expectEqualStrings(unicode_qg, sub.queueGroup().?);
}
================================================
FILE: src/pubsub.zig
================================================
//! Pub/Sub Module
//!
//! Provides publish/subscribe utilities including inbox generation
//! and subject validation. For Message and Subscription types, see client.zig.
const std = @import("std");
pub const inbox = @import("pubsub/inbox.zig");
pub const subject = @import("pubsub/subject.zig");
pub const subscription = @import("pubsub/subscription.zig");
// Subscription state enum (for embedded/fixed use)
pub const SubscriptionState = subscription.State;
pub const SubscriptionError = subscription.Error;
// Fixed types for embedded use (no allocations)
pub const FixedQueue = subscription.FixedQueue;
pub const FixedSubscription = subscription.FixedSubscription;
pub const FixedSubConfig = subscription.FixedSubConfig;
pub const newInbox = inbox.newInbox;
pub const newInboxBuf = inbox.newInboxBuf;
pub const isInbox = inbox.isInbox;
pub const validatePublish = subject.validatePublish;
pub const validateSubscribe = subject.validateSubscribe;
pub const validateQueueGroup = subject.validateQueueGroup;
pub const validateReplyTo = subject.validateReplyTo;
pub const subjectMatches = subject.matches;
test {
std.testing.refAllDecls(@This());
}
================================================
FILE: src/sync/byte_ring.zig
================================================
//! Lock-free SPSC Byte Ring Buffer
//!
//! Variable-length message passing between producer and consumer threads.
//! Each entry is: [4-byte little-endian length][payload bytes].
//! Length=0 is a padding sentinel meaning "skip to ring start".
//!
//! ## Memory Ordering
//!
//! Same release-acquire pattern as SpscQueue (see spsc_queue.zig):
//! - `head`: written by producer (.release), read by consumer (.acquire)
//! - `tail`: written by consumer (.release), read by producer (.acquire)
//! - Producer's buffer writes are visible to consumer via head release-acquire.
//! - Consumer's consumption is visible to producer via tail release-acquire.
const std = @import("std");
const assert = std.debug.assert;
/// Length header size (4 bytes, little-endian u32).
pub const HDR_SIZE: usize = 4;
/// Lock-free SPSC ring buffer for variable-length byte messages.
/// Producer reserves space, writes data, commits. Consumer peeks, reads, advances.
pub const ByteRing = struct {
buffer: []u8,
capacity: usize,
head: std.atomic.Value(usize),
tail: std.atomic.Value(usize),
const Self = @This();
/// Initialize with pre-allocated buffer.
/// Capacity must be a power of 2 and >= 64.
pub fn init(buffer: []u8) Self {
assert(buffer.len >= 64);
assert(std.math.isPowerOfTwo(buffer.len));
return .{
.buffer = buffer,
.capacity = buffer.len,
.head = std.atomic.Value(usize).init(0),
.tail = std.atomic.Value(usize).init(0),
};
}
/// Returns available space in the ring (approximate).
pub fn available(self: *const Self) usize {
const head = self.head.load(.monotonic);
const tail = self.tail.load(.acquire);
return self.capacity -% (head -% tail);
}
/// Producer: calculate encoded size for a message.
/// Returns total ring bytes needed (header + payload).
pub fn entrySize(data_len: usize) usize {
return HDR_SIZE + data_len;
}
/// Producer: reserve contiguous space for `data_len` bytes.
/// Returns writable slice (header + payload area) or null if full.
/// If the entry doesn't fit before wrap, inserts a padding
/// sentinel and wraps to ring start.
pub fn reserve(
self: *Self,
data_len: usize,
) ?[]u8 {
const total = entrySize(data_len);
if (total > self.capacity / 2) return null;
const head = self.head.load(.monotonic);
const tail = self.tail.load(.acquire);
const used = head -% tail;
if (used + total > self.capacity) return null;
const offset = head % self.capacity;
const remaining = self.capacity - offset;
if (remaining >= total) {
// Fits before wrap
return self.buffer[offset .. offset + total];
}
// Doesn't fit — insert padding sentinel at wrap point
if (remaining >= HDR_SIZE) {
// Write zero-length marker
const pad = self.buffer[offset..][0..HDR_SIZE];
std.mem.writeInt(u32, pad, 0, .little);
}
// Check if wrapping still fits
const new_used = used + remaining + total;
if (new_used > self.capacity) return null;
// REVIEWED(2025-03): Head advance here is safe.
// Consumer sees padding sentinel (zero-length header)
// and skips to offset 0. Actual data at offset 0 is
// only visible after commit() does a second .release
// store. The release-acquire chain prevents reading
// uncommitted data.
self.head.store(head +% remaining, .release);
// Now at ring start
return self.buffer[0..total];
}
/// Producer: commit an entry. Writes the length header and
/// advances head atomically (.release).
pub fn commit(self: *Self, entry: []u8, data_len: usize) void {
assert(entry.len >= HDR_SIZE + data_len);
assert(data_len > 0);
// Write length header
const hdr = entry[0..HDR_SIZE];
std.mem.writeInt(u32, hdr, @intCast(data_len), .little);
// Advance head past this entry
const head = self.head.load(.monotonic);
self.head.store(
head +% entrySize(data_len),
.release,
);
}
/// Consumer: peek at the next entry's data.
/// Returns the data slice (after the 4-byte header).
/// Skips padding sentinels (length=0) automatically.
/// Returns null if ring is empty.
pub fn peek(self: *Self) ?[]const u8 {
var tail = self.tail.load(.monotonic);
const head = self.head.load(.acquire);
while (tail != head) {
const offset = tail % self.capacity;
// Need at least HDR_SIZE bytes to read length
if (self.capacity - offset < HDR_SIZE) {
// Not enough space for a header — skip to start
tail = tail +% (self.capacity - offset);
self.tail.store(tail, .release);
continue;
}
const hdr = self.buffer[offset..][0..HDR_SIZE];
const entry_len = std.mem.readInt(
u32,
hdr,
.little,
);
if (entry_len == 0) {
// Padding sentinel — skip to wrap
const remaining = self.capacity - offset;
tail = tail +% remaining;
self.tail.store(tail, .release);
continue;
}
const data_start = offset + HDR_SIZE;
return self.buffer[data_start .. data_start + entry_len];
}
return null;
}
/// Consumer: advance past the current entry.
/// Must be called after peek() returned non-null.
pub fn advance(self: *Self) void {
const tail = self.tail.load(.monotonic);
const offset = tail % self.capacity;
const hdr = self.buffer[offset..][0..HDR_SIZE];
const entry_len = std.mem.readInt(
u32,
hdr,
.little,
);
assert(entry_len > 0);
self.tail.store(
tail +% entrySize(entry_len),
.release,
);
}
/// Consumer: drain all entries, writing each to the writer.
/// Returns number of entries drained.
pub fn drainToWriter(
self: *Self,
writer: anytype,
) !usize {
var count: usize = 0;
while (self.peek()) |data| {
try writer.writeAll(data);
self.advance();
count += 1;
}
return count;
}
/// Returns true if ring appears empty.
pub fn isEmpty(self: *const Self) bool {
const head = self.head.load(.acquire);
const tail = self.tail.load(.acquire);
return head == tail;
}
/// Returns approximate number of bytes used.
pub fn len(self: *const Self) usize {
const head = self.head.load(.acquire);
const tail = self.tail.load(.acquire);
return head -% tail;
}
/// Reset ring to empty state (not thread-safe).
pub fn clear(self: *Self) void {
self.head.store(0, .release);
self.tail.store(0, .release);
}
};
// --- Tests ---
test "ByteRing basic write/read" {
var buf: [256]u8 = undefined;
var ring = ByteRing.init(&buf);
// Reserve and write
const entry = ring.reserve(5).?;
@memcpy(entry[HDR_SIZE..][0..5], "hello");
ring.commit(entry, 5);
// Read back
const data = ring.peek().?;
try std.testing.expectEqualStrings("hello", data);
ring.advance();
// Now empty
try std.testing.expect(ring.peek() == null);
try std.testing.expect(ring.isEmpty());
}
test "ByteRing multiple entries" {
var buf: [1024]u8 = undefined;
var ring = ByteRing.init(&buf);
const messages = [_][]const u8{
"one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten",
};
for (messages) |msg| {
const entry = ring.reserve(msg.len).?;
@memcpy(entry[HDR_SIZE..][0..msg.len], msg);
ring.commit(entry, msg.len);
}
for (messages) |msg| {
const data = ring.peek().?;
try std.testing.expectEqualStrings(msg, data);
ring.advance();
}
try std.testing.expect(ring.peek() == null);
}
test "ByteRing wraparound" {
var buf: [128]u8 = undefined;
var ring = ByteRing.init(&buf);
// Fill to ~90% (each entry = HDR + 20 = 24 bytes, ~5 fit)
for (0..5) |i| {
const entry = ring.reserve(20).?;
@memset(entry[HDR_SIZE..][0..20], @intCast(i + 1));
ring.commit(entry, 20);
}
// Drain all
for (0..5) |i| {
const data = ring.peek().?;
try std.testing.expectEqual(
@as(u8, @intCast(i + 1)),
data[0],
);
ring.advance();
}
// Fill again — wraps around
for (0..5) |i| {
const entry = ring.reserve(20).?;
@memset(entry[HDR_SIZE..][0..20], @intCast(i + 10));
ring.commit(entry, 20);
}
for (0..5) |i| {
const data = ring.peek().?;
try std.testing.expectEqual(
@as(u8, @intCast(i + 10)),
data[0],
);
ring.advance();
}
try std.testing.expect(ring.isEmpty());
}
test "ByteRing padding sentinel" {
// 64-byte ring. Write entries until one forces a wrap.
var buf: [64]u8 = undefined;
var ring = ByteRing.init(&buf);
// Entry of 20 bytes = 24 with header. Fits twice (48/64 used).
const e1 = ring.reserve(20).?;
@memset(e1[HDR_SIZE..][0..20], 'A');
ring.commit(e1, 20);
const e2 = ring.reserve(20).?;
@memset(e2[HDR_SIZE..][0..20], 'B');
ring.commit(e2, 20);
// Consume first to free space
const d1 = ring.peek().?;
try std.testing.expectEqual(@as(u8, 'A'), d1[0]);
ring.advance();
// Now 24 bytes free at start, 16 bytes free at end.
// An entry of 20 (24 total) won't fit in the 16 bytes at end.
// Should pad and wrap to start.
const e3 = ring.reserve(20).?;
@memset(e3[HDR_SIZE..][0..20], 'C');
ring.commit(e3, 20);
// Read B then C
const d2 = ring.peek().?;
try std.testing.expectEqual(@as(u8, 'B'), d2[0]);
ring.advance();
const d3 = ring.peek().?;
try std.testing.expectEqual(@as(u8, 'C'), d3[0]);
ring.advance();
try std.testing.expect(ring.isEmpty());
}
test "ByteRing full returns null" {
var buf: [64]u8 = undefined;
var ring = ByteRing.init(&buf);
// Fill: 2 entries of 20 = 48 bytes used
const e1 = ring.reserve(20).?;
ring.commit(e1, 20);
const e2 = ring.reserve(20).?;
ring.commit(e2, 20);
// Third should fail (48 + 24 = 72 > 64)
try std.testing.expect(ring.reserve(20) == null);
// Drain one, now should fit
_ = ring.peek().?;
ring.advance();
try std.testing.expect(ring.reserve(20) != null);
}
test "ByteRing empty returns null" {
var buf: [64]u8 = undefined;
var ring = ByteRing.init(&buf);
try std.testing.expect(ring.peek() == null);
try std.testing.expect(ring.isEmpty());
}
test "ByteRing concurrent stress" {
const NUM_MSGS = 100_000;
const allocator = std.testing.allocator;
const ring_buf = try allocator.alloc(u8, 65536);
defer allocator.free(ring_buf);
var ring = ByteRing.init(ring_buf);
var received: usize = 0;
var corrupt: bool = false;
const consumer = try std.Thread.spawn(.{}, struct {
fn run(
r: *ByteRing,
recv: *usize,
bad: *bool,
) void {
var count: usize = 0;
while (count < NUM_MSGS) {
if (r.peek()) |data| {
// Verify pattern: first byte = count & 0xFF
const expected: u8 = @truncate(count);
if (data.len < 1 or data[0] != expected) {
bad.* = true;
}
r.advance();
count += 1;
} else {
std.atomic.spinLoopHint();
}
}
recv.* = count;
}
}.run, .{ &ring, &received, &corrupt });
// Producer
for (0..NUM_MSGS) |i| {
while (true) {
if (ring.reserve(16)) |entry| {
const pattern: u8 = @truncate(i);
@memset(entry[HDR_SIZE..][0..16], pattern);
ring.commit(entry, 16);
break;
} else {
std.atomic.spinLoopHint();
}
}
}
consumer.join();
try std.testing.expectEqual(NUM_MSGS, received);
try std.testing.expect(!corrupt);
}
test "ByteRing max message size" {
const allocator = std.testing.allocator;
// Ring of 4096, message of ~2000 bytes (< capacity/2)
const ring_buf = try allocator.alloc(u8, 4096);
defer allocator.free(ring_buf);
var ring = ByteRing.init(ring_buf);
const msg_len = 2000;
const entry = ring.reserve(msg_len).?;
for (0..msg_len) |j| {
entry[HDR_SIZE + j] = @truncate(j);
}
ring.commit(entry, msg_len);
const data = ring.peek().?;
try std.testing.expectEqual(msg_len, data.len);
for (0..msg_len) |j| {
const expected: u8 = @truncate(j);
try std.testing.expectEqual(expected, data[j]);
}
ring.advance();
try std.testing.expect(ring.isEmpty());
}
test "ByteRing alternating sizes" {
const allocator = std.testing.allocator;
const ring_buf = try allocator.alloc(u8, 131072);
defer allocator.free(ring_buf);
var ring = ByteRing.init(ring_buf);
// Alternate tiny (10B) and large (1000B) entries
const sizes = [_]usize{ 10, 1000 };
const COUNT = 200;
for (0..COUNT) |i| {
const sz = sizes[i % 2];
const entry = ring.reserve(sz).?;
@memset(entry[HDR_SIZE..][0..sz], @truncate(i));
ring.commit(entry, sz);
}
for (0..COUNT) |i| {
const sz = sizes[i % 2];
const data = ring.peek().?;
try std.testing.expectEqual(sz, data.len);
try std.testing.expectEqual(@as(u8, @truncate(i)), data[0]);
ring.advance();
}
try std.testing.expect(ring.isEmpty());
}
test "ByteRing drainToWriter" {
var buf: [256]u8 = undefined;
var ring = ByteRing.init(&buf);
const e1 = ring.reserve(3).?;
@memcpy(e1[HDR_SIZE..][0..3], "abc");
ring.commit(e1, 3);
const e2 = ring.reserve(3).?;
@memcpy(e2[HDR_SIZE..][0..3], "def");
ring.commit(e2, 3);
var out_buf: [64]u8 = undefined;
var writer = std.Io.Writer.fixed(&out_buf);
const count = try ring.drainToWriter(&writer);
try std.testing.expectEqual(@as(usize, 2), count);
try std.testing.expectEqualStrings(
"abcdef",
out_buf[0..writer.end],
);
try std.testing.expect(ring.isEmpty());
}
================================================
FILE: src/sync/spin_lock.zig
================================================
//! Lightweight atomic spinlock for short critical sections.
//!
//! Used where Io.Mutex is unavailable (e.g., Message.deinit()
//! which has no io parameter). Lock hold time must be very
//! short (nanoseconds) — only for single slot writes.
const std = @import("std");
const assert = std.debug.assert;
/// Atomic spinlock using compare-and-swap.
pub const SpinLock = struct {
locked: std.atomic.Value(u8) =
std.atomic.Value(u8).init(0),
/// Acquire the lock. Spins until successful.
pub fn lock(self: *SpinLock) void {
while (self.locked.cmpxchgWeak(
0,
1,
.acquire,
.monotonic,
) != null) {
std.atomic.spinLoopHint();
}
}
/// Release the lock.
pub fn unlock(self: *SpinLock) void {
assert(self.locked.load(.monotonic) == 1);
self.locked.store(0, .release);
}
};
test "SpinLock basic" {
var sl: SpinLock = .{};
sl.lock();
sl.unlock();
}
test "SpinLock concurrent" {
const NUM_THREADS = 4;
const ITERS = 100_000;
var sl: SpinLock = .{};
var counter: usize = 0;
const threads = blk: {
var t: [NUM_THREADS]std.Thread = undefined;
for (&t) |*thread| {
thread.* = try std.Thread.spawn(.{}, struct {
fn run(
s: *SpinLock,
c: *usize,
) void {
for (0..ITERS) |_| {
s.lock();
c.* += 1;
s.unlock();
}
}
}.run, .{ &sl, &counter });
}
break :blk t;
};
for (&threads) |*t| t.join();
try std.testing.expectEqual(
NUM_THREADS * ITERS,
counter,
);
}
================================================
FILE: src/sync/spsc_queue.zig
================================================
//! Lock-free Single Producer Single Consumer Queue
//!
//! Zero syscalls, zero mutex, maximum throughput.
//! Designed for cross-thread message passing between io_task and subscriber.
//!
//! ## Memory Ordering Rationale
//!
//! This SPSC queue uses a carefully chosen memory ordering strategy that is
//! both correct on weakly-ordered architectures (ARM) and optimal for
//! strongly-ordered architectures (x86_64).
//!
//! **Key insight**: Each index (head/tail) has exactly ONE writer thread.
//! - `head`: written only by producer, read by both
//! - `tail`: written only by consumer, read by both
//!
//! **Ordering rules applied**:
//! - Reading your OWN index: `.monotonic` (you're the only writer, no sync needed)
//! - Reading OTHER's index: `.acquire` (must see their prior writes to buffer)
//! - Writing your index: `.release` (your buffer writes must be visible first)
//!
//! **The release-acquire pairing**:
//! ```
//! Producer: Consumer:
//! buffer[head] = item; head = head.load(.acquire); // sees data
//! head.store(new, .release); ----> item = buffer[tail];
//! tail.store(new, .release);
//! <---- tail.load(.acquire); // sees consumption
//! ```
//!
//! This ensures: when consumer sees updated head, the data write is visible.
//! When producer sees updated tail, the slot is safe to reuse.
const std = @import("std");
const assert = std.debug.assert;
/// Lock-free SPSC queue with runtime-sized buffer.
/// Producer (io_task) and consumer (subscriber) can run on different threads.
/// See module doc comment for memory ordering rationale.
pub fn SpscQueue(comptime T: type) type {
return struct {
buffer: []T,
capacity: usize,
head: std.atomic.Value(usize),
tail: std.atomic.Value(usize),
closed: std.atomic.Value(bool),
const Self = @This();
/// Initialize queue with pre-allocated buffer.
/// Buffer length MUST be a power of 2 for bitwise AND optimization.
pub fn init(buffer: []T) Self {
assert(buffer.len > 0);
assert(std.math.isPowerOfTwo(buffer.len));
return .{
.buffer = buffer,
.capacity = buffer.len,
.head = std.atomic.Value(usize).init(0),
.tail = std.atomic.Value(usize).init(0),
.closed = std.atomic.Value(bool).init(false),
};
}
/// Push item (producer only). Returns false if full.
/// O(1), lock-free, never blocks.
pub fn push(self: *Self, item: T) bool {
if (self.closed.load(.acquire)) return false;
// .monotonic: single head writer, no sync needed for own read
const head = self.head.load(.monotonic);
// .acquire: must see consumer's tail updates to know slots are free
const tail = self.tail.load(.acquire);
if (head -% tail >= self.capacity) return false;
const mask = self.capacity - 1;
self.buffer[head & mask] = item;
// .release: ensures item write is visible BEFORE head increment
// Consumer's .acquire on head will see this data
self.head.store(head +% 1, .release);
return true;
}
/// Pop item (consumer only). Returns null if empty.
/// O(1), lock-free, never blocks.
pub fn pop(self: *Self) ?T {
// .monotonic: single tail writer, no sync needed for own read
const tail = self.tail.load(.monotonic);
// .acquire: must see producer's buffer writes that happened before
// their .release store to head
const head = self.head.load(.acquire);
if (tail == head) return null;
const mask = self.capacity - 1;
const item = self.buffer[tail & mask];
// .release: ensures item read completes BEFORE tail increment
// Producer's .acquire on tail will see slot is now free
self.tail.store(tail +% 1, .release);
return item;
}
/// Pop multiple items into output buffer. Returns count popped.
/// O(n), lock-free, never blocks.
/// Same memory ordering rationale as pop() - see module doc.
pub fn popBatch(self: *Self, out: []T) usize {
// .monotonic: single tail writer
const tail = self.tail.load(.monotonic);
// .acquire: must see producer's buffer writes
const head = self.head.load(.acquire);
const available = head -% tail;
if (available == 0) return 0;
const mask = self.capacity - 1;
const count = @min(available, out.len);
for (0..count) |i| {
out[i] = self.buffer[(tail +% i) & mask];
}
// .release: ensures all reads complete before tail update
self.tail.store(tail +% count, .release);
return count;
}
/// Number of items in queue (approximate, may be stale).
pub fn len(self: *const Self) usize {
const head = self.head.load(.acquire);
const tail = self.tail.load(.acquire);
return head -% tail;
}
/// True if queue appears empty (may be stale).
pub fn isEmpty(self: *const Self) bool {
return self.len() == 0;
}
/// Returns true once the queue has been closed.
pub fn isClosed(self: *const Self) bool {
return self.closed.load(.acquire);
}
/// Close queue and wake receivers polling for shutdown.
pub fn close(self: *Self, io: anytype) void {
_ = io;
self.closed.store(true, .release);
}
};
}
test "SpscQueue push/pop" {
var buffer: [4]u32 = undefined;
var q = SpscQueue(u32).init(&buffer);
try std.testing.expect(q.isEmpty());
try std.testing.expectEqual(@as(usize, 0), q.len());
try std.testing.expect(q.push(1));
try std.testing.expect(q.push(2));
try std.testing.expect(q.push(3));
try std.testing.expect(q.push(4));
try std.testing.expect(!q.push(5));
try std.testing.expectEqual(@as(usize, 4), q.len());
try std.testing.expectEqual(@as(?u32, 1), q.pop());
try std.testing.expectEqual(@as(?u32, 2), q.pop());
try std.testing.expectEqual(@as(?u32, 3), q.pop());
try std.testing.expectEqual(@as(?u32, 4), q.pop());
try std.testing.expectEqual(@as(?u32, null), q.pop());
try std.testing.expect(q.isEmpty());
}
test "SpscQueue popBatch" {
var buffer: [8]u32 = undefined;
var q = SpscQueue(u32).init(&buffer);
_ = q.push(1);
_ = q.push(2);
_ = q.push(3);
_ = q.push(4);
_ = q.push(5);
var out: [3]u32 = undefined;
const count = q.popBatch(&out);
try std.testing.expectEqual(@as(usize, 3), count);
try std.testing.expectEqual(@as(u32, 1), out[0]);
try std.testing.expectEqual(@as(u32, 2), out[1]);
try std.testing.expectEqual(@as(u32, 3), out[2]);
try std.testing.expectEqual(@as(usize, 2), q.len());
}
test "SpscQueue wraparound" {
var buffer: [4]u32 = undefined;
var q = SpscQueue(u32).init(&buffer);
for (0..10) |cycle| {
const base: u32 = @intCast(cycle * 4);
try std.testing.expect(q.push(base + 1));
try std.testing.expect(q.push(base + 2));
try std.testing.expect(q.push(base + 3));
try std.testing.expect(q.push(base + 4));
try std.testing.expect(!q.push(base + 5));
try std.testing.expectEqual(@as(?u32, base + 1), q.pop());
try std.testing.expectEqual(@as(?u32, base + 2), q.pop());
try std.testing.expectEqual(@as(?u32, base + 3), q.pop());
try std.testing.expectEqual(@as(?u32, base + 4), q.pop());
try std.testing.expectEqual(@as(?u32, null), q.pop());
}
}
================================================
FILE: src/testing/README.md
================================================
# Integration Tests
This directory contains the integration test harness and fixtures for the
NATS Zig client. These tests exercise the client against real `nats-server`
processes instead of mocks.
## Layout
- `integration_test.zig` runs the main end-to-end integration suite.
- `micro_integration_test.zig` runs the micro service integration suite.
- `tls_integration_test.zig` runs the focused JWT/TLS integration suite.
- `client/` contains the grouped test cases used by the runners.
- `configs/` contains NATS server configurations used by the harness.
- `certs/` contains TLS certificates used only by the test servers.
- `server_manager.zig` and `test_utils.zig` provide shared test
infrastructure.
## Commands
Run from the repository root:
```sh
zig build test-integration
zig build test-integration-micro
zig build test-integration-tls
```
The tests require `nats-server` to be available on `PATH`. The main
integration suite also uses the `nats` CLI for JetStream/KV
cross-verification tests.
## Scope
This directory is for correctness and interoperability tests. Performance
benchmarking is intentionally kept out of the main client repository; use the
separate benchmark repository for cross-client performance comparisons.
================================================
FILE: src/testing/certs/client-all.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCznnzpbfhgAUBe
NtyUVTdHJZ93vWx7WeuO3q8zMePpxFhmCGmdtaO/rkdunQk5WGZpnO6wB3Ricn/O
k9CZs+6RU3X4b17mUjljO+k/CHjXHbiZsh3KzE9bHVlcMIBru5dv6gKhbp5NsYaC
5iOjpUtiFY2fkYdMbgtWlCEkvQJboQUwtKuNl5rbYX6blIYGjwq8w8ZqyGsiTzqs
Tw5mNVh4q0rW963LGjGJzsUBfw6YQ2bKEzysatDJMRRk2jJHrCQOzWdrxEh+wxle
Y4HTg2pdK/E0THhoIpFza4lAC05Ql5SQlzzLyWa9CJwUa+eDMUqJVbpMt2kyq0Bd
9txX54YxAgMBAAECggEAQ9rdqXmH2Qzf+jeTgN3ochI+egevUbIYkPKDET4Jsagh
FPqcm52g7Kq0BY+Bio5gsgk9CnbmesJykeG5bjdRKslyyZWZLj1lvJ1Hci6LKAjs
UfO92XzxhaRCu9b+zLQjc33d3Ipjd0pXXGAAmrO5FKa7x8o8aJ0x31U6aByXJXG+
CtMJXuCdk4a7FnsiBIQxhC2Ihb+1whBH1mdv14+FqWPo9Vb+ioSIEAVO4thbD4jy
sKmDSnjrEDtjzKAEIrvZf9Hq56HJdu0RrpREIv9lmUOKze8GkAzIkDj55R2G10db
vRsWsjB/iqI5an3+LDfUtNadR0h21udCRaZOJX+/GQKBgQDAy34fMvIQhRl63f9A
GOT/bQ5HjOrXpmYFZw6062pGgfywj4ln5BYH+3s3orPeiExHjF5Lw4SG6u44nNmA
6RhPRsLfIfhg7OHfa8VYhQP+hexz0JlMwGMmid30aySnyIrXn/WsDpICCbPZR4Lz
OUIr23p/20WqbmPYdf2cW+PvfwKBgQDugTPwxyfcubVddZfVFIpxzGuZGLgL8s0K
7lxZE5B4d0FTMDUX9MHq7j9hMiHhSuyn0SeHyuP03FqZWzoSjv1r+keCwCmoPWAC
ocJC9SoGz7AdBnRmqlbqEHpY+3RvLtVYkwDcmfxVYq5I8gQu5O6X4PbQf+cFb/xI
hmC8Vh5iTwKBgQC6pYbxf2nX0nOLftY5YKB6JEM5w9QreH22Z0JWpr6ZighvilaV
TLyDd9SfVRXbr4phjiRQJvXrhA+ioT70zTVqsm/Ag2upskst+HDytLvcMh1rNhzj
sDGNQtWtZfjzsnOwMr0tmGGENY53IQNGoz1Lpkze8RJt4DcrfXdMY6200wKBgHv5
aRhVTWEsnxuvjnbSMIyqp5ty/+gmE3MFJ7edtdEInEozmsWTEmGd6hAJ0RacrZsl
2xh43DlheS6R/wO6k/xWomlSndS34no7vxCzA197AZ50xni/PmJ4okAypPlOLNPX
xfDlkgaIPvPn6Ui+8067P1Bty5ZF+atxPkNnuG99AoGAO++bI6twiF2yFrqp8bJ5
6zjkwVYVDds/acnierTnAVF3yM+eqyq1BYjSCTSupqnd7KT0EMX77BBbGHAQuUZM
fO3L7vgo10S18VWNiAt5yyZUKhN0lLwQeQeP0mIDcRKiLn7BwnNz8Wxo4p0KoKLS
QflLqbILbuspjBXQvQ1Asb0=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIEnjCCAwagAwIBAgIQciuy77HHsdMG7UD/WPT1gzANBgkqhkiG9w0BAQsFADCB
gzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSwwKgYDVQQLDCNzdGpl
cGFuQGxvY2FsaG9zdCAoU3RqZXBhbiBHbGF2aW5hKTEzMDEGA1UEAwwqbWtjZXJ0
IHN0amVwYW5AbG9jYWxob3N0IChTdGplcGFuIEdsYXZpbmEpMB4XDTE5MDYwMTAw
MDAwMFoXDTMwMTEwNTEyMDk1N1owYTEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3Bt
ZW50IGNlcnRpZmljYXRlMTYwNAYDVQQLDC1zdGplcGFuQE1hY0Jvb2stUHJvLTIu
bG9jYWwgKFN0amVwYW4gR2xhdmluYSkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCznnzpbfhgAUBeNtyUVTdHJZ93vWx7WeuO3q8zMePpxFhmCGmdtaO/
rkdunQk5WGZpnO6wB3Ricn/Ok9CZs+6RU3X4b17mUjljO+k/CHjXHbiZsh3KzE9b
HVlcMIBru5dv6gKhbp5NsYaC5iOjpUtiFY2fkYdMbgtWlCEkvQJboQUwtKuNl5rb
YX6blIYGjwq8w8ZqyGsiTzqsTw5mNVh4q0rW963LGjGJzsUBfw6YQ2bKEzysatDJ
MRRk2jJHrCQOzWdrxEh+wxleY4HTg2pdK/E0THhoIpFza4lAC05Ql5SQlzzLyWa9
CJwUa+eDMUqJVbpMt2kyq0Bd9txX54YxAgMBAAGjga4wgaswDgYDVR0PAQH/BAQD
AgWgMDEGA1UdJQQqMCgGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsG
AQUFBwMEMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaDvaviAqw6RnyuBf3BI7
S2uUmfIwNwYDVR0RBDAwLoIJbG9jYWxob3N0gQ9lbWFpbEBsb2NhbGhvc3SHEAAA
AAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAFc5p6LYY6zIBvJbVNXL
pofEM/ky999eHmLacKAAs08bBEpRLvho7Maf/YsRfFEB9tzE/Nhrc/9yC2hM1Jsy
9I3jM5OPErEHAqzCpi5L0ykuKcmvLCAVLrBiEmBcAvY4snirIGZ0YMW5fqcrcy4B
84Ptg9efUnV/XC3VQTLRX0pWp8t1T98P1ZFYK7dK1ejkC61/kO5WjTlIOkT+oVXf
A/juQubq2QKkROTUze5pmA0jBTgFtszXfryuh++NsBb9vHWHV3JFgW2k5brw0ozs
0F3snFOafLrw3xJcI8j2gYi+4sI6mmZTRWKhWF6TbWPl4Ysk+K/BkcEcDebYFCJz
BNmqVjzKLv3qEDV+XaTYa96qlEJRTI96rofL3zzXC381XYhNRLFS5ExHchuETUKW
S70tKNsyzpd3V0X4RTgopFuXzSfvaQycu3OwM74Ks0xmTgZL396ZlbtJfuT8v/2d
c+t8Ei6H4a0r/nyip1df3keRL/otvkP4sby7D4xi0CJ2dw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIE2DCCA0CgAwIBAgIRAIW/i8Ryvk+oZGg+/FvDpW8wDQYJKoZIhvcNAQELBQAw
gYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEsMCoGA1UECwwjc3Rq
ZXBhbkBsb2NhbGhvc3QgKFN0amVwYW4gR2xhdmluYSkxMzAxBgNVBAMMKm1rY2Vy
dCBzdGplcGFuQGxvY2FsaG9zdCAoU3RqZXBhbiBHbGF2aW5hKTAeFw0yMDA3MDcx
NTU4NTlaFw0zMDA3MDcxNTU4NTlaMIGDMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxv
cG1lbnQgQ0ExLDAqBgNVBAsMI3N0amVwYW5AbG9jYWxob3N0IChTdGplcGFuIEds
YXZpbmEpMTMwMQYDVQQDDCpta2NlcnQgc3RqZXBhbkBsb2NhbGhvc3QgKFN0amVw
YW4gR2xhdmluYSkwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCyRbze
lNj5QkxgMxYIr4yEqLtTz/jSiX1W6uUymI+e1GQzl3BpP3fS/SyB9HZv1bn4PoiW
B+BggsFrOsmVF4aXbikRz74DQ7FCqVMU/QGvlIbHkH5TShcLGzngG8DR3+Url3S8
rlvYQBf2AXYXWcmSl1bFYzVxWSt67NzS29mvus40aAPXTpB7vq6nNs6F+7sARhDi
OPRqniPqV29khMmxndwExAEMJBVZbeTNuwfehCud8dOj0kzk5ESX+3upIwOrnoye
2tRNW34WWaxQrV65KOeGxdgIN+PeO7WL1jbCitCaGnGitTbtPGMCdf6LmRhA30Wy
IZ3ZkxOmvXGkpAR6mxz3pqQWTZYieTA92s63LeVSNeYUof0tNu0SMTYWuAas0Ob3
A/lu7PTCjTag5vVU5RwkfBmcNrbNNy9NbKgQB7TafZn7sfPpZT4EpAcFMgRb4KfR
HfPiaxlDu2LKmBS9+i7x79nYAlB5IGgLyQ1cldwjDYqAAizBoigM2PI3tEMCAwEA
AaNFMEMwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0O
BBYEFGg72r4gKsOkZ8rgX9wSO0trlJnyMA0GCSqGSIb3DQEBCwUAA4IBgQBzxbH6
s7dhvyOGK5HnQmc/vPiTbOk4QhXX12PPekL+UrhMyiMzWET2wkpHJXxtes0dGNU3
tmQwVQTvg/BhyxxGcafvQe/o/5rS9lrrpdQriZg3gqqeUoi8UX+5/ef9EB7Z32Ho
qUSOd6hPLLI+3UrnmWP+u5PRDmsHQYKc2YqyqQisjRVwLtGtCmGYfuBncP145Yyi
qNlwI6jeZTAtRSkcKy6fnyJcjOCYKFWHpTGmBTMtO4LiTGadxnmbAq9mRBiKJJp6
wrSz1JvbVXVY4caxpbDfkaT8RiP+k1Fbd6uMWnZTJLHPTNbzCl4aXcuHgoRhCLeq
SdF3L7m0tM7lsTP3tddRY6zb+1u0II0Gu6umDsdyL6JOV4vv9Qb7xdy2jTU231+o
TXLHaypw4Amp267EyvvWmU3VOl8BeUkJ/7LOqzZfKxTECwnxWywx6NV9ONQt8mNC
ATAQAyYXklJsZkX6VLMPE0Lv4Qbt/GnGUejER09zQi433e9jUF+vwQGwj/g=
-----END CERTIFICATE-----
================================================
FILE: src/testing/certs/client-cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEnjCCAwagAwIBAgIQciuy77HHsdMG7UD/WPT1gzANBgkqhkiG9w0BAQsFADCB
gzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSwwKgYDVQQLDCNzdGpl
cGFuQGxvY2FsaG9zdCAoU3RqZXBhbiBHbGF2aW5hKTEzMDEGA1UEAwwqbWtjZXJ0
IHN0amVwYW5AbG9jYWxob3N0IChTdGplcGFuIEdsYXZpbmEpMB4XDTE5MDYwMTAw
MDAwMFoXDTMwMTEwNTEyMDk1N1owYTEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3Bt
ZW50IGNlcnRpZmljYXRlMTYwNAYDVQQLDC1zdGplcGFuQE1hY0Jvb2stUHJvLTIu
bG9jYWwgKFN0amVwYW4gR2xhdmluYSkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCznnzpbfhgAUBeNtyUVTdHJZ93vWx7WeuO3q8zMePpxFhmCGmdtaO/
rkdunQk5WGZpnO6wB3Ricn/Ok9CZs+6RU3X4b17mUjljO+k/CHjXHbiZsh3KzE9b
HVlcMIBru5dv6gKhbp5NsYaC5iOjpUtiFY2fkYdMbgtWlCEkvQJboQUwtKuNl5rb
YX6blIYGjwq8w8ZqyGsiTzqsTw5mNVh4q0rW963LGjGJzsUBfw6YQ2bKEzysatDJ
MRRk2jJHrCQOzWdrxEh+wxleY4HTg2pdK/E0THhoIpFza4lAC05Ql5SQlzzLyWa9
CJwUa+eDMUqJVbpMt2kyq0Bd9txX54YxAgMBAAGjga4wgaswDgYDVR0PAQH/BAQD
AgWgMDEGA1UdJQQqMCgGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsG
AQUFBwMEMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaDvaviAqw6RnyuBf3BI7
S2uUmfIwNwYDVR0RBDAwLoIJbG9jYWxob3N0gQ9lbWFpbEBsb2NhbGhvc3SHEAAA
AAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAFc5p6LYY6zIBvJbVNXL
pofEM/ky999eHmLacKAAs08bBEpRLvho7Maf/YsRfFEB9tzE/Nhrc/9yC2hM1Jsy
9I3jM5OPErEHAqzCpi5L0ykuKcmvLCAVLrBiEmBcAvY4snirIGZ0YMW5fqcrcy4B
84Ptg9efUnV/XC3VQTLRX0pWp8t1T98P1ZFYK7dK1ejkC61/kO5WjTlIOkT+oVXf
A/juQubq2QKkROTUze5pmA0jBTgFtszXfryuh++NsBb9vHWHV3JFgW2k5brw0ozs
0F3snFOafLrw3xJcI8j2gYi+4sI6mmZTRWKhWF6TbWPl4Ysk+K/BkcEcDebYFCJz
BNmqVjzKLv3qEDV+XaTYa96qlEJRTI96rofL3zzXC381XYhNRLFS5ExHchuETUKW
S70tKNsyzpd3V0X4RTgopFuXzSfvaQycu3OwM74Ks0xmTgZL396ZlbtJfuT8v/2d
c+t8Ei6H4a0r/nyip1df3keRL/otvkP4sby7D4xi0CJ2dw==
-----END CERTIFICATE-----
================================================
FILE: src/testing/certs/client-key.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCznnzpbfhgAUBe
NtyUVTdHJZ93vWx7WeuO3q8zMePpxFhmCGmdtaO/rkdunQk5WGZpnO6wB3Ricn/O
k9CZs+6RU3X4b17mUjljO+k/CHjXHbiZsh3KzE9bHVlcMIBru5dv6gKhbp5NsYaC
5iOjpUtiFY2fkYdMbgtWlCEkvQJboQUwtKuNl5rbYX6blIYGjwq8w8ZqyGsiTzqs
Tw5mNVh4q0rW963LGjGJzsUBfw6YQ2bKEzysatDJMRRk2jJHrCQOzWdrxEh+wxle
Y4HTg2pdK/E0THhoIpFza4lAC05Ql5SQlzzLyWa9CJwUa+eDMUqJVbpMt2kyq0Bd
9txX54YxAgMBAAECggEAQ9rdqXmH2Qzf+jeTgN3ochI+egevUbIYkPKDET4Jsagh
FPqcm52g7Kq0BY+Bio5gsgk9CnbmesJykeG5bjdRKslyyZWZLj1lvJ1Hci6LKAjs
UfO92XzxhaRCu9b+zLQjc33d3Ipjd0pXXGAAmrO5FKa7x8o8aJ0x31U6aByXJXG+
CtMJXuCdk4a7FnsiBIQxhC2Ihb+1whBH1mdv14+FqWPo9Vb+ioSIEAVO4thbD4jy
sKmDSnjrEDtjzKAEIrvZf9Hq56HJdu0RrpREIv9lmUOKze8GkAzIkDj55R2G10db
vRsWsjB/iqI5an3+LDfUtNadR0h21udCRaZOJX+/GQKBgQDAy34fMvIQhRl63f9A
GOT/bQ5HjOrXpmYFZw6062pGgfywj4ln5BYH+3s3orPeiExHjF5Lw4SG6u44nNmA
6RhPRsLfIfhg7OHfa8VYhQP+hexz0JlMwGMmid30aySnyIrXn/WsDpICCbPZR4Lz
OUIr23p/20WqbmPYdf2cW+PvfwKBgQDugTPwxyfcubVddZfVFIpxzGuZGLgL8s0K
7lxZE5B4d0FTMDUX9MHq7j9hMiHhSuyn0SeHyuP03FqZWzoSjv1r+keCwCmoPWAC
ocJC9SoGz7AdBnRmqlbqEHpY+3RvLtVYkwDcmfxVYq5I8gQu5O6X4PbQf+cFb/xI
hmC8Vh5iTwKBgQC6pYbxf2nX0nOLftY5YKB6JEM5w9QreH22Z0JWpr6ZighvilaV
TLyDd9SfVRXbr4phjiRQJvXrhA+ioT70zTVqsm/Ag2upskst+HDytLvcMh1rNhzj
sDGNQtWtZfjzsnOwMr0tmGGENY53IQNGoz1Lpkze8RJt4DcrfXdMY6200wKBgHv5
aRhVTWEsnxuvjnbSMIyqp5ty/+gmE3MFJ7edtdEInEozmsWTEmGd6hAJ0RacrZsl
2xh43DlheS6R/wO6k/xWomlSndS34no7vxCzA197AZ50xni/PmJ4okAypPlOLNPX
xfDlkgaIPvPn6Ui+8067P1Bty5ZF+atxPkNnuG99AoGAO++bI6twiF2yFrqp8bJ5
6zjkwVYVDds/acnierTnAVF3yM+eqyq1BYjSCTSupqnd7KT0EMX77BBbGHAQuUZM
fO3L7vgo10S18VWNiAt5yyZUKhN0lLwQeQeP0mIDcRKiLn7BwnNz8Wxo4p0KoKLS
QflLqbILbuspjBXQvQ1Asb0=
-----END PRIVATE KEY-----
================================================
FILE: src/testing/certs/ip-ca.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEkDCCA3igAwIBAgIUSZwW7btc9EUbrMWtjHpbM0C2bSEwDQYJKoZIhvcNAQEL
BQAwcTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoM
B1N5bmFkaWExEDAOBgNVBAsMB25hdHMuaW8xKTAnBgNVBAMMIENlcnRpZmljYXRl
IEF1dGhvcml0eSAyMDIyLTA4LTI3MB4XDTIyMDgyNzIwMjMwMloXDTMyMDgyNDIw
MjMwMlowcTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNV
BAoMB1N5bmFkaWExEDAOBgNVBAsMB25hdHMuaW8xKTAnBgNVBAMMIENlcnRpZmlj
YXRlIEF1dGhvcml0eSAyMDIyLTA4LTI3MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAqilVqyY8rmCpTwAsLF7DEtWEq37KbljBWVjmlp2Wo6TgMd3b537t
6iO8+SbI8KH75i63RcxV3Uzt1/L9Yb6enDXF52A/U5ugmDhaa+Vsoo2HBTbCczmp
qndp7znllQqn7wNLv6aGSvaeIUeYS5Dmlh3kt7Vqbn4YRANkOUTDYGSpMv7jYKSu
1ee05Rco3H674zdwToYto8L8V7nVMrky42qZnGrJTaze+Cm9tmaIyHCwUq362CxS
dkmaEuWx11MOIFZvL80n7ci6pveDxe5MIfwMC3/oGn7mbsSqidPMcTtjw6ey5NEu
Z0UrC/2lL1FtF4gnVMKUSaEhU2oKjj0ZAQIDAQABo4IBHjCCARowHQYDVR0OBBYE
FP7Pfz4u7sSt6ltviEVsx4hIFIs6MIGuBgNVHSMEgaYwgaOAFP7Pfz4u7sSt6ltv
iEVsx4hIFIs6oXWkczBxMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p
YTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwHbmF0cy5pbzEpMCcGA1UEAwwg
Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMjItMDgtMjeCFEmcFu27XPRFG6zFrYx6
WzNAtm0hMAwGA1UdEwQFMAMBAf8wOgYJYIZIAYb4QgENBC0WK25hdHMuaW8gbmF0
cy1zZXJ2ZXIgdGVzdC1zdWl0ZSB0cmFuc2llbnQgQ0EwDQYJKoZIhvcNAQELBQAD
ggEBAHDCHLQklYZlnzHDaSwxgGSiPUrCf2zhk2DNIYSDyBgdzrIapmaVYQRrCBtA
j/4jVFesgw5WDoe4TKsyha0QeVwJDIN8qg2pvpbmD8nOtLApfl0P966vcucxDwqO
dQWrIgNsaUdHdwdo0OfvAlTfG0v/y2X0kbL7h/el5W9kWpxM/rfbX4IHseZL2sLq
FH69SN3FhMbdIm1ldrcLBQVz8vJAGI+6B9hSSFQWljssE0JfAX+8VW/foJgMSx7A
vBTq58rLkAko56Jlzqh/4QT+ckayg9I73v1Q5/44jP1mHw35s5ZrzpDQt2sVv4l5
lwRPJFXMwe64flUs9sM+/vqJaIY=
-----END CERTIFICATE-----
================================================
FILE: src/testing/certs/ip-cert.pem
================================================
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
1d:d9:1f:06:dd:fd:90:26:4e:27:ea:2e:01:4b:31:e6:d2:49:31:1f
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=California, O=Synadia, OU=nats.io, CN=Certificate Authority 2022-08-27
Validity
Not Before: Aug 27 20:23:02 2022 GMT
Not After : Aug 24 20:23:02 2032 GMT
Subject: C=US, ST=California, O=Synadia, OU=nats.io, CN=localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:e6:fb:47:65:cd:c9:a2:2d:af:8b:cd:d5:6a:79:
54:3c:07:5f:eb:5a:71:2b:2b:e5:6f:be:31:fb:16:
65:68:76:0e:59:e7:e4:57:ca:88:e9:77:d6:41:ad:
57:7a:42:b2:d2:54:c4:0f:7c:5b:c1:bc:61:97:e3:
22:3a:3e:1e:4a:5d:47:9f:6b:7d:6f:34:e3:8c:86:
9d:85:19:29:9a:11:58:44:4c:a1:90:d3:14:61:e1:
57:da:01:ea:ce:3f:90:ae:9e:5d:13:6d:2c:89:ca:
39:15:6b:b6:9e:32:d7:2a:4c:48:85:2f:b0:1e:d8:
4b:62:32:14:eb:32:b6:29:04:34:3c:af:39:b6:8b:
52:32:4d:bf:43:5f:9b:fb:0d:43:a6:ad:2c:a7:41:
29:55:c9:70:b3:b5:15:46:34:bf:e4:1e:52:2d:a4:
49:2e:d5:21:ed:fc:00:f7:a2:0b:bc:12:0a:90:64:
50:7c:c5:14:70:f5:fb:9b:62:08:78:43:49:31:f3:
47:b8:93:d4:2d:4c:a9:dc:17:70:76:34:66:ff:65:
c1:39:67:e9:a6:1c:80:6a:f0:9d:b3:28:c8:a3:3a:
b7:5d:de:6e:53:6d:09:b3:0d:b1:13:10:e8:ec:e0:
bd:5e:a1:94:4b:70:bf:dc:bd:8b:b9:82:65:dd:af:
81:7b
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
Netscape Comment:
nats.io nats-server test-suite certificate
X509v3 Subject Key Identifier:
2B:8C:A3:8B:DB:DB:5C:CE:18:DB:F6:A8:31:4E:C2:3E:EE:D3:40:7E
X509v3 Authority Key Identifier:
keyid:FE:CF:7F:3E:2E:EE:C4:AD:EA:5B:6F:88:45:6C:C7:88:48:14:8B:3A
DirName:/C=US/ST=California/O=Synadia/OU=nats.io/CN=Certificate Authority 2022-08-27
serial:49:9C:16:ED:BB:5C:F4:45:1B:AC:C5:AD:8C:7A:5B:33:40:B6:6D:21
X509v3 Subject Alternative Name:
DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
Netscape Cert Type:
SSL Client, SSL Server
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, Netscape Server Gated Crypto, Microsoft Server Gated Crypto, TLS Web Client Authentication
Signature Algorithm: sha256WithRSAEncryption
54:49:34:2b:38:d1:aa:3b:43:60:4c:3f:6a:f8:74:ca:49:53:
a1:af:12:d3:a8:17:90:7b:9d:a3:69:13:6e:da:2c:b7:61:31:
ac:eb:00:93:92:fc:0c:10:d4:18:a0:16:61:94:4b:42:cb:eb:
7a:f6:80:c6:45:c0:9c:09:aa:a9:48:e8:36:e3:c5:be:36:e0:
e9:78:2a:bb:ab:64:9b:20:eb:e6:0f:63:2b:59:c3:58:0b:3a:
84:15:04:c1:7e:12:03:1b:09:25:8d:4c:03:e8:18:26:c0:6c:
b7:90:b1:fd:bc:f1:cf:d0:d5:4a:03:15:71:0c:7d:c1:76:87:
92:f1:3e:bc:75:51:5a:c4:36:a4:ff:91:98:df:33:5d:a7:38:
de:50:29:fd:0f:c8:55:e6:8f:24:c2:2e:98:ab:d9:5d:65:2f:
50:cc:25:f6:84:f2:21:2e:5e:76:d0:86:1e:69:8b:cb:8a:3a:
2d:79:21:5e:e7:f7:2d:06:18:a1:13:cb:01:c3:46:91:2a:de:
b4:82:d7:c3:62:6f:08:a1:d5:90:19:30:9d:64:8e:e4:f8:ba:
4f:2f:ba:13:b4:a3:9f:d1:d5:77:64:8a:3e:eb:53:c5:47:ac:
ab:3e:0e:7a:9b:a6:f4:48:25:66:eb:c7:4c:f9:50:24:eb:71:
e0:75:ae:e6
-----BEGIN CERTIFICATE-----
MIIE+TCCA+GgAwIBAgIUHdkfBt39kCZOJ+ouAUsx5tJJMR8wDQYJKoZIhvcNAQEL
BQAwcTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoM
B1N5bmFkaWExEDAOBgNVBAsMB25hdHMuaW8xKTAnBgNVBAMMIENlcnRpZmljYXRl
IEF1dGhvcml0eSAyMDIyLTA4LTI3MB4XDTIyMDgyNzIwMjMwMloXDTMyMDgyNDIw
MjMwMlowWjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNV
BAoMB1N5bmFkaWExEDAOBgNVBAsMB25hdHMuaW8xEjAQBgNVBAMMCWxvY2FsaG9z
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOb7R2XNyaItr4vN1Wp5
VDwHX+tacSsr5W++MfsWZWh2Dlnn5FfKiOl31kGtV3pCstJUxA98W8G8YZfjIjo+
HkpdR59rfW8044yGnYUZKZoRWERMoZDTFGHhV9oB6s4/kK6eXRNtLInKORVrtp4y
1ypMSIUvsB7YS2IyFOsytikENDyvObaLUjJNv0Nfm/sNQ6atLKdBKVXJcLO1FUY0
v+QeUi2kSS7VIe38APeiC7wSCpBkUHzFFHD1+5tiCHhDSTHzR7iT1C1MqdwXcHY0
Zv9lwTln6aYcgGrwnbMoyKM6t13eblNtCbMNsRMQ6OzgvV6hlEtwv9y9i7mCZd2v
gXsCAwEAAaOCAZ4wggGaMAkGA1UdEwQCMAAwOQYJYIZIAYb4QgENBCwWKm5hdHMu
aW8gbmF0cy1zZXJ2ZXIgdGVzdC1zdWl0ZSBjZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU
K4yji9vbXM4Y2/aoMU7CPu7TQH4wga4GA1UdIwSBpjCBo4AU/s9/Pi7uxK3qW2+I
RWzHiEgUizqhdaRzMHExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRAwDgYDVQQKDAdTeW5hZGlhMRAwDgYDVQQLDAduYXRzLmlvMSkwJwYDVQQDDCBD
ZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAyMi0wOC0yN4IUSZwW7btc9EUbrMWtjHpb
M0C2bSEwLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAA
AAABMBEGCWCGSAGG+EIBAQQEAwIGwDALBgNVHQ8EBAMCBaAwNAYDVR0lBC0wKwYI
KwYBBQUHAwEGCWCGSAGG+EIEAQYKKwYBBAGCNwoDAwYIKwYBBQUHAwIwDQYJKoZI
hvcNAQELBQADggEBAFRJNCs40ao7Q2BMP2r4dMpJU6GvEtOoF5B7naNpE27aLLdh
MazrAJOS/AwQ1BigFmGUS0LL63r2gMZFwJwJqqlI6Dbjxb424Ol4KrurZJsg6+YP
YytZw1gLOoQVBMF+EgMbCSWNTAPoGCbAbLeQsf288c/Q1UoDFXEMfcF2h5LxPrx1
UVrENqT/kZjfM12nON5QKf0PyFXmjyTCLpir2V1lL1DMJfaE8iEuXnbQhh5pi8uK
Oi15IV7n9y0GGKETywHDRpEq3rSC18Nibwih1ZAZMJ1kjuT4uk8vuhO0o5/R1Xdk
ij7rU8VHrKs+DnqbpvRIJWbrx0z5UCTrceB1ruY=
-----END CERTIFICATE-----
================================================
FILE: src/testing/certs/ip-key.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDm+0dlzcmiLa+L
zdVqeVQ8B1/rWnErK+VvvjH7FmVodg5Z5+RXyojpd9ZBrVd6QrLSVMQPfFvBvGGX
4yI6Ph5KXUefa31vNOOMhp2FGSmaEVhETKGQ0xRh4VfaAerOP5Cunl0TbSyJyjkV
a7aeMtcqTEiFL7Ae2EtiMhTrMrYpBDQ8rzm2i1IyTb9DX5v7DUOmrSynQSlVyXCz
tRVGNL/kHlItpEku1SHt/AD3ogu8EgqQZFB8xRRw9fubYgh4Q0kx80e4k9QtTKnc
F3B2NGb/ZcE5Z+mmHIBq8J2zKMijOrdd3m5TbQmzDbETEOjs4L1eoZRLcL/cvYu5
gmXdr4F7AgMBAAECggEBAK4sr3MiEbjcsHJAvXyzjwRRH1Bu+8VtLW7swe2vvrpd
w4aiKXrV/BXpSsRtvPgxkXyvdMSkpuBZeFI7cVTwAJFc86RQPt77x9bwr5ltFwTZ
rXCbRH3b3ZPNhByds3zhS+2Q92itu5cPyanQdn2mor9/lHPyOOGZgobCcynELL6R
wRElkeDyf5ODuWEd7ADC5IFyZuwb3azNVexIK+0yqnMmv+QzEW3hsycFmFGAeB7v
MIMjb2BhLrRr6Y5Nh+k58yM5DCf9h/OJhDpeXwLkxyK4BFg+aZffEbUX0wHDMR7f
/nMv1g6cKvDWiLU8xLzez4t2qNIBNdxw5ZSLyQRRolECgYEA+ySTKrBAqI0Uwn8H
sUFH95WhWUXryeRyGyQsnWAjZGF1+d67sSY2un2W6gfZrxRgiNLWEFq9AaUs0MuH
6syF4Xwx/aZgU/gvsGtkgzuKw1bgvekT9pS/+opmHRCZyQAFEHj0IEpzyB6rW1u/
LdlR3ShEENnmXilFv/uF/uXP5tMCgYEA63LiT0w46aGPA/E+aLRWU10c1eZ7KdhR
c3En6zfgIxgFs8J38oLdkOR0CF6T53DSuvGR/OprVKdlnUhhDxBgT1oQjK2GlhPx
JV5uMvarJDJxAwsF+7T4H2QtZ00BtEfpyp790+TlypSG1jo/BnSMmX2uEbV722lY
hzINLY49obkCgYBEpN2YyG4T4+PtuXznxRkfogVk+kiVeVx68KtFJLbnw//UGT4i
EHjbBmLOevDT+vTb0QzzkWmh3nzeYRM4aUiatjCPzP79VJPsW54whIDMHZ32KpPr
TQMgPt3kSdpO5zN7KiRIAzGcXE2n/e7GYGUQ1uWr2XMu/4byD5SzdCscQwJ/Ymii
LoKtRvk/zWYHr7uwWSeR5dVvpQ3E/XtONAImrIRd3cRqXfJUqTrTRKxDJXkCmyBc
5FkWg0t0LUkTSDiQCJqcUDA3EINFR1kwthxja72pfpwc5Be/nV9BmuuUysVD8myB
qw8A/KsXsHKn5QrRuVXOa5hvLEXbuqYw29mX6QKBgDGDzIzpR9uPtBCqzWJmc+IJ
z4m/1NFlEz0N0QNwZ/TlhyT60ytJNcmW8qkgOSTHG7RDueEIzjQ8LKJYH7kXjfcF
6AJczUG5PQo9cdJKo9JP3e1037P/58JpLcLe8xxQ4ce03zZpzhsxR2G/tz8DstJs
b8jpnLyqfGrcV2feUtIZ
-----END PRIVATE KEY-----
================================================
FILE: src/testing/certs/rootCA-key.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIG/gIBADANBgkqhkiG9w0BAQEFAASCBugwggbkAgEAAoIBgQCyRbzelNj5Qkxg
MxYIr4yEqLtTz/jSiX1W6uUymI+e1GQzl3BpP3fS/SyB9HZv1bn4PoiWB+BggsFr
OsmVF4aXbikRz74DQ7FCqVMU/QGvlIbHkH5TShcLGzngG8DR3+Url3S8rlvYQBf2
AXYXWcmSl1bFYzVxWSt67NzS29mvus40aAPXTpB7vq6nNs6F+7sARhDiOPRqniPq
V29khMmxndwExAEMJBVZbeTNuwfehCud8dOj0kzk5ESX+3upIwOrnoye2tRNW34W
WaxQrV65KOeGxdgIN+PeO7WL1jbCitCaGnGitTbtPGMCdf6LmRhA30WyIZ3ZkxOm
vXGkpAR6mxz3pqQWTZYieTA92s63LeVSNeYUof0tNu0SMTYWuAas0Ob3A/lu7PTC
jTag5vVU5RwkfBmcNrbNNy9NbKgQB7TafZn7sfPpZT4EpAcFMgRb4KfRHfPiaxlD
u2LKmBS9+i7x79nYAlB5IGgLyQ1cldwjDYqAAizBoigM2PI3tEMCAwEAAQKCAYBp
K5kj2r4yNrmmGx1JnH8SmBSDenL5ieEm0MbMVZKNChHfGd1YSfgfwfpq5FSm33iq
CgI8OINXjGwdHX5k9Y8ScQvLlTos5NeDUy9Pd39yHPZybz0HV/NGOxamrtjPN/4T
/HMDCP3oEs/P8sa/OdogICYxpriVmRx8lZYk00yWTmduJVr2v0OfrTuOLFgkVQDa
RXuaai1PZOIdUt3FeE0g+tcc/KD9j6AEtT9BW7BlxqWQtWS9BckVU9FftB4dBykc
EIqo4wqWAVNulV6m6zSlzG1YioN5ZYvW3T5IMsRf2s0t6hmkRU56t5Uqp1MYSzZY
biDHyAkHLPFvmnJIIbcj7oBVdE0iM8VEmlYChiPHsEP3WhQKyN+AELTx+pr3DTuR
4y7r6eAH7kJldo09xanRBE9aBDmblEBIvPCV0SKapWe41y2NGUfrDVvc8lVVXLe8
1DVo+VOJGMXS9fSx+KOFDho7gNyQYs7K+9lhXa9nRov44+GXL4VdzAEs8msi0yEC
gcEA2Yu56/fYZ1bvkw+bxE8cafs7Q0e8vpf/GRubaCgK0kRHh8Ssf+reqN1laCOj
KgZd7b6V0UJsH62tLPzoqccWK3JHLRTUfSCsfR/uygp5oc6azpasyEzqWFLoUnkB
A4q3aABau0s9At8OJs//lAXyI1QE4bjwgOeDRPp/ynPfYVcGHDoyyd8lAyDUG7cc
f8G+o64oPxMEnIYNmLkag36k7fP0szx6kEbrp3H+5h/c7uy0JFW9PwZAHp+fgRWU
allxAoHBANHI1OadRzqbEB5+iyxXFZok/BqMHE7QyKxNMTtKqaWUS8RH9f+vPSBK
hK7I8kiTXaloLQ4VW54qjJ4NCd3eR6PGlGLecTHj4F3wu+84jncHnSaV5f1akvD2
teC61Gmd8oeje/9G5xdVJpDEl9hfXqtTSPiy96IsXhFNr97q0sm35Alc6ZYeICXA
J0i9wdk3vtimELH/ul9daI25asFi4kA36ymawqNXk4b3/bOaBXFqVpECpV3YfXMR
sJQpYxqu8wKBwQCdtNOFotj4oWdwPwJ3H7rDgeOGdLz5lorSEtdofI7Lu7/3Rrae
zQ+5bzaSdjNUxeTV8zH8z6A+ntNKJ9YrLi5+NIwwvEcGpucklj+vrERc7r//P+/m
DQxeF0xgbWQ0wx0OgiNEX9jM+hLyRBtNnbnZrpETadTAPhVFrityAupPUJ0XXYFw
Ixpb2DKsHOTGIRgo5Jo8j3bqWawFqTr1VJwP/KjKPu/DJAa2Dsfw3+x0MJivNpDI
3akiCinBlHlRV6ECgcEApG3tsfSE6AKyV7SIEXEQlYl3sLcxWPV81NCMThTvc8EQ
wgBFaOtJ1g2Sgg0vGoOnXikxZ2CGNyrSnO9LVIPtUwlLNVN1Fc2vBvKx24dQ4yss
mhnT8wkTM5usY0ENTNtoRbh2cFh6uWccm0v8WLQn19Gn2IcuYga0lIt31hnorgNc
0Znp3KgwOmaqY/GYB1ISXG2NmHcA9c6ZLLywWHPRMtShljKfbLgwAhJO4H9Q1Nys
jWytgSk26wJqjTcDXt7RAoHAd0geRKlPbumvnHqs6e9Aq+0q63ofskEoCDDmE1nT
GTlcjLfEe3PB6KBlU/f2UGT5JwAeBMxlB0+XzS9CQnHKU2f6N4qn4nXGK0ZhXzu9
+WqQhKzfQb3D1x0BTpRfVNtFSHvCjWjhZgXgAZDCtArVZ1dnZZPiOpnym+CrBun+
cw4IOH2QbK6TNGpFW2Rtcs/CginusGcXUIAjQZGjrEIkkX73QjLQ5hU39CCe/PvI
4ptoUsi0eFG0TZN3kB99ERCJ
-----END PRIVATE KEY-----
================================================
FILE: src/testing/certs/rootCA.pem
================================================
-----BEGIN CERTIFICATE-----
MIIE2DCCA0CgAwIBAgIRAIW/i8Ryvk+oZGg+/FvDpW8wDQYJKoZIhvcNAQELBQAw
gYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEsMCoGA1UECwwjc3Rq
ZXBhbkBsb2NhbGhvc3QgKFN0amVwYW4gR2xhdmluYSkxMzAxBgNVBAMMKm1rY2Vy
dCBzdGplcGFuQGxvY2FsaG9zdCAoU3RqZXBhbiBHbGF2aW5hKTAeFw0yMDA3MDcx
NTU4NTlaFw0zMDA3MDcxNTU4NTlaMIGDMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxv
cG1lbnQgQ0ExLDAqBgNVBAsMI3N0amVwYW5AbG9jYWxob3N0IChTdGplcGFuIEds
YXZpbmEpMTMwMQYDVQQDDCpta2NlcnQgc3RqZXBhbkBsb2NhbGhvc3QgKFN0amVw
YW4gR2xhdmluYSkwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCyRbze
lNj5QkxgMxYIr4yEqLtTz/jSiX1W6uUymI+e1GQzl3BpP3fS/SyB9HZv1bn4PoiW
B+BggsFrOsmVF4aXbikRz74DQ7FCqVMU/QGvlIbHkH5TShcLGzngG8DR3+Url3S8
rlvYQBf2AXYXWcmSl1bFYzVxWSt67NzS29mvus40aAPXTpB7vq6nNs6F+7sARhDi
OPRqniPqV29khMmxndwExAEMJBVZbeTNuwfehCud8dOj0kzk5ESX+3upIwOrnoye
2tRNW34WWaxQrV65KOeGxdgIN+PeO7WL1jbCitCaGnGitTbtPGMCdf6LmRhA30Wy
IZ3ZkxOmvXGkpAR6mxz3pqQWTZYieTA92s63LeVSNeYUof0tNu0SMTYWuAas0Ob3
A/lu7PTCjTag5vVU5RwkfBmcNrbNNy9NbKgQB7TafZn7sfPpZT4EpAcFMgRb4KfR
HfPiaxlDu2LKmBS9+i7x79nYAlB5IGgLyQ1cldwjDYqAAizBoigM2PI3tEMCAwEA
AaNFMEMwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0O
BBYEFGg72r4gKsOkZ8rgX9wSO0trlJnyMA0GCSqGSIb3DQEBCwUAA4IBgQBzxbH6
s7dhvyOGK5HnQmc/vPiTbOk4QhXX12PPekL+UrhMyiMzWET2wkpHJXxtes0dGNU3
tmQwVQTvg/BhyxxGcafvQe/o/5rS9lrrpdQriZg3gqqeUoi8UX+5/ef9EB7Z32Ho
qUSOd6hPLLI+3UrnmWP+u5PRDmsHQYKc2YqyqQisjRVwLtGtCmGYfuBncP145Yyi
qNlwI6jeZTAtRSkcKy6fnyJcjOCYKFWHpTGmBTMtO4LiTGadxnmbAq9mRBiKJJp6
wrSz1JvbVXVY4caxpbDfkaT8RiP+k1Fbd6uMWnZTJLHPTNbzCl4aXcuHgoRhCLeq
SdF3L7m0tM7lsTP3tddRY6zb+1u0II0Gu6umDsdyL6JOV4vv9Qb7xdy2jTU231+o
TXLHaypw4Amp267EyvvWmU3VOl8BeUkJ/7LOqzZfKxTECwnxWywx6NV9ONQt8mNC
ATAQAyYXklJsZkX6VLMPE0Lv4Qbt/GnGUejER09zQi433e9jUF+vwQGwj/g=
-----END CERTIFICATE-----
================================================
FILE: src/testing/certs/server-cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEbjCCAtagAwIBAgIRAI5awGA99MSpuYlAyXOE32AwDQYJKoZIhvcNAQELBQAw
gYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEsMCoGA1UECwwjc3Rq
ZXBhbkBsb2NhbGhvc3QgKFN0amVwYW4gR2xhdmluYSkxMzAxBgNVBAMMKm1rY2Vy
dCBzdGplcGFuQGxvY2FsaG9zdCAoU3RqZXBhbiBHbGF2aW5hKTAeFw0xOTA2MDEw
MDAwMDBaFw0zMDExMDUxMjA5NDRaMGExJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9w
bWVudCBjZXJ0aWZpY2F0ZTE2MDQGA1UECwwtc3RqZXBhbkBNYWNCb29rLVByby0y
LmxvY2FsIChTdGplcGFuIEdsYXZpbmEpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAvH8A6SClj/Vwhp3QImnI5mUg5cQKZy6/2Gz+5abTRNW4Je4RKK66
3zfIDBdDerXBtArRjMLrou1K6XxB0/yz8fQAkvgRR+xpWap30GjMgd7qIewIex79
yW2U3BrKLa0FZC7b5H3NK07Bwz/RVlILycZA/hJ1/ApBDJ+mA30D7jMHMopFMSvG
FHuAY6TZvdQyDdin3Yw4ciIsDHf8JUNYrMsqGjYukj49B3g0n7161lLn/Fo6s0Pv
ltqUfhwXyXdGU+2OELTvVCLlPLj8CBxTcfpENnXQYGUI1l/B+TnKlu+Iz3D/k3Nm
vmSJMGPnL37B9X3RqNnkw7TDMsmRk8WduwIDAQABo34wfDAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAW
gBRoO9q+ICrDpGfK4F/cEjtLa5SZ8jAmBgNVHREEHzAdgglsb2NhbGhvc3SHEAAA
AAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAG6T+h8wRElEzt8NOC7A
SFUTT60RaImz6/nu1LYG0uxY5hSITUSEAUUXpeeP+o133CWrhIpNkUr/bmN5tpQX
vyCv33zJHuaRZ+icwrNu0MmSZ2QVc2ovW0lDr6rWJHrsnH4vk7YBnSN8u2J2Q5u3
nJLX7NkU0KPoDyCpolci0wgrCEgwR1CSak0M25cch2X2TQylsoeROfsIpWLT1VKf
H9cZzasj1TzSoo02YuyHhuZP28NGFZvZZy0/aVHmwo6s2fbHVBvaAyDmjRIJI3Ep
0OVwv/FFmubygoLdI0EU40bfkxNoCpf+MR5rwu1lJFfbNuY2ojjeHWjXxj48Y6fN
wc6OztjNO9Wjj+l4XKP/tUhuCX/aXVVZP6DVB74pAlOODoEu/4XKCyJSrmRIuzfA
xPyNDb/ABXF2JMV9zbVsDmp5/QrBIovvM25pWS4oG8L9wBgKF7fbenCR3iy+rgUe
wN0bqTGWrB9k0mFtBbDDg+VbMPXo7mOcCmNYz8uJMPnrFA==
-----END CERTIFICATE-----
================================================
FILE: src/testing/certs/server-key.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8fwDpIKWP9XCG
ndAiacjmZSDlxApnLr/YbP7lptNE1bgl7hEorrrfN8gMF0N6tcG0CtGMwuui7Urp
fEHT/LPx9ACS+BFH7GlZqnfQaMyB3uoh7Ah7Hv3JbZTcGsotrQVkLtvkfc0rTsHD
P9FWUgvJxkD+EnX8CkEMn6YDfQPuMwcyikUxK8YUe4BjpNm91DIN2KfdjDhyIiwM
d/wlQ1isyyoaNi6SPj0HeDSfvXrWUuf8WjqzQ++W2pR+HBfJd0ZT7Y4QtO9UIuU8
uPwIHFNx+kQ2ddBgZQjWX8H5OcqW74jPcP+Tc2a+ZIkwY+cvfsH1fdGo2eTDtMMy
yZGTxZ27AgMBAAECggEAXJZJnTlC+Y5Ggmj79htd6gVcfl+n+HzXEPigz6788T/F
HyRr2z7QXZppsb6vj5O9nLD/sxN/aN0DweId94GV5c/DhG1DF8ABE2EPTxha86PJ
/3WPyOI1KH6h8udZzcvB7S6zJe3BHHen5z7ulWbhkW/HNsVcnLtwrkGw6t+6UYJ3
a1yceXFAKFlv8f9M8g/PaP0zKyuprKtGupdTUhoekZ4M87jjsKQQ33oNdF5GBl56
nIEAXK+rrR3ep2xeFtQ2/tdzAGfwB0NfrX4q7BEjPwpnt7lzLx/pIY4+ZSZc4GsZ
foxzTsAZ+5SYkhu2m5hLGUPrD4FIDedv2UEEMv74oQKBgQD6bDSuNqzRZNQC/8Yk
6BLpiMTJVqHQi8etGsFy/608ahRz6BHdmtf3Jz63OYdxOL3nSa4Eh0lKzCyU9Ryw
qvQIGdMnT6Bqpp7gNI596jzlLDhw1XD16XS+lFCvJ4DFjn9ZqxGo7dw62RA3LV/L
C75zdrUqyO5XlfxmKghEC2YnawKBgQDAsbibqYBl/TPfn9J3x87m97t90j5OatG5
nHAFdRmp7+QfxH5cCkHESPfNbiJL1tIDxIuNNvon3ffa7WPpKlf3fybE0WQq2jKi
R0VD+U56OUZA0hpHOhG2aKZIix02oniVLQZ0Qq59jAiiiJWkvu4FLSirC+EGnL/h
ah54nIoG8QKBgQCMNv/8N8Ll75XCJCJ20badKiY9MZOi6FEiPKPqVvxRonfXOi6e
rS+VRFUaVEzg+UtjcF7OTE2eYtnngaLRzLacvpD7JtuEO80jbmoGWJxGGU905h28
oz3p47OVjwHMG/B0bZOSybQRAy7QJkjHsMivb90amqzRP7q2HXzJVLSbBwKBgBXv
5alrCaASzGYIBuj2CVsIFwNC/S7mQEwWQDaO10YedmUbdJs727Lh77wmbqcdpLkj
FhQUjzQctAvrfLVdybf2dM5xXCr4vkz1OjB74HBPtuzIPo+fT8bpcQzPMZs3seyh
vJtdwAmw+IawcADab7SNKJUYfBzJmZqq/x8SCzCxAoGARA5w5I7xVJpseMmBit7y
GsID7iFQoYCScQI3vYpDyS52hYK5F0NetUVqd5Ph3/uwHK5cCPK6k2n38zrXUxE3
Y406bA1cwl6+M0npkJnBt3c8IV50fJwoYtUP5Yr/JogSQJadIQhZg2cch3yiZbc9
DW01ooYHlL6XeSk5nn1Jc7s=
-----END PRIVATE KEY-----
================================================
FILE: src/testing/client/advanced.zig
================================================
//! Tests for checkCompatibility, publishMsg, requestMsg, and message status.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
/// Test checkCompatibility returns true for current server.
pub fn testCheckCompatibility(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("check_compatibility", false, "connect failed");
return;
};
defer client.deinit();
// NATS server should be at least 2.0.0
if (client.checkCompatibility(2, 0, 0)) {
// Also verify it fails for unreasonably high version
if (!client.checkCompatibility(99, 0, 0)) {
reportResult("check_compatibility", true, "");
} else {
reportResult("check_compatibility", false, "99.0.0 should fail");
}
} else {
reportResult("check_compatibility", false, "2.0.0 should pass");
}
}
/// Test publishMsg republishes a message correctly.
pub fn testPublishMsg(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("publish_msg", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("test.publishmsg") catch {
reportResult("publish_msg", false, "subscribe failed");
return;
};
defer sub.deinit();
// Create a message to republish
const original = nats.Client.Message{
.subject = "test.publishmsg",
.sid = 0,
.reply_to = null,
.data = "republished-data",
.headers = null,
.owned = false,
};
client.publishMsg(&original) catch {
reportResult("publish_msg", false, "publishMsg failed");
return;
};
if (sub.nextMsgTimeout(500) catch null) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "republished-data")) {
reportResult("publish_msg", true, "");
} else {
reportResult("publish_msg", false, "wrong data");
}
} else {
reportResult("publish_msg", false, "no message received");
}
}
/// Test Message.getStatus and isNoResponders with actual no-responders.
pub fn testNoRespondersStatus(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.no_responders = true,
}) catch {
reportResult("no_responders_status", false, "connect failed");
return;
};
defer client.deinit();
// Request to a subject with no subscribers - should get 503
const reply = client.request(
"nonexistent.subject.xyz",
"test",
500,
) catch {
reportResult("no_responders_status", false, "request failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
// Check status via getStatus()
const status = msg.status();
if (status == 503 and msg.isNoResponders()) {
reportResult("no_responders_status", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "status={?}", .{status}) catch "e";
reportResult("no_responders_status", false, detail);
}
} else {
// Timeout - server might not support no_responders or timing issue
reportResult("no_responders_status", true, "");
}
}
/// Test requestMsg forwards a message as request.
pub fn testRequestMsg(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(allocator, io_req.io(), url, .{
.reconnect = false,
}) catch {
reportResult("request_msg", false, "requester connect failed");
return;
};
defer requester.deinit();
const io_resp = utils.newIo(allocator);
defer io_resp.deinit();
const responder = nats.Client.connect(allocator, io_resp.io(), url, .{
.reconnect = false,
}) catch {
reportResult("request_msg", false, "responder connect failed");
return;
};
defer responder.deinit();
// Set up a responder
var sub = responder.subscribeSync("test.requestmsg") catch {
reportResult("request_msg", false, "subscribe failed");
return;
};
defer sub.deinit();
responder.flush(1_000_000_000) catch {
reportResult("request_msg", false, "flush failed");
return;
};
// Spawn responder task
var responder_future = io_resp.io().async(
responderTask,
.{ &sub, responder },
);
defer responder_future.cancel(io_resp.io());
// Create message to send as request
const request_msg = nats.Client.Message{
.subject = "test.requestmsg",
.sid = 0,
.reply_to = null,
.data = "request-data",
.headers = null,
.owned = false,
};
const reply = requester.requestMsg(&request_msg, 1000) catch {
reportResult("request_msg", false, "requestMsg failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "response-data")) {
reportResult("request_msg", true, "");
} else {
reportResult("request_msg", false, "wrong response");
}
} else {
reportResult("request_msg", false, "timeout");
}
}
fn responderTask(
sub: **nats.Subscription,
client: *nats.Client,
) void {
if (sub.*.nextMsgTimeout(2000) catch null) |msg| {
defer msg.deinit();
if (msg.reply_to) |reply_to| {
client.publish(reply_to, "response-data") catch {};
}
}
}
fn requestMsgOrderingResponder(
sub: **nats.Subscription,
client: *nats.Client,
) void {
var saw_state = false;
var handled: usize = 0;
while (handled < 2) {
const maybe_msg = sub.*.nextMsgTimeout(1000) catch return;
const msg = maybe_msg orelse return;
defer msg.deinit();
handled += 1;
if (std.mem.eql(u8, msg.subject, "test.requestmsg.ordering.state")) {
saw_state = true;
continue;
}
if (std.mem.eql(u8, msg.subject, "test.requestmsg.ordering.service")) {
const reply_to = msg.reply_to orelse return;
client.publish(
reply_to,
if (saw_state) "fresh" else "stale",
) catch {};
return;
}
}
}
pub fn testRequestMsgOrdering(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(
allocator,
io_req.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_msg_ordering", false, "requester connect failed");
return;
};
defer requester.deinit();
const io_resp = utils.newIo(allocator);
defer io_resp.deinit();
const responder = nats.Client.connect(
allocator,
io_resp.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_msg_ordering", false, "responder connect failed");
return;
};
defer responder.deinit();
var sub = responder.subscribeSync("test.requestmsg.ordering.>") catch {
reportResult("request_msg_ordering", false, "subscribe failed");
return;
};
defer sub.deinit();
responder.flush(1_000_000_000) catch {
reportResult("request_msg_ordering", false, "flush failed");
return;
};
var handler = io_resp.io().async(
requestMsgOrderingResponder,
.{ &sub, responder },
);
defer handler.cancel(io_resp.io());
requester.publish("test.requestmsg.ordering.state", "state-update") catch {
reportResult("request_msg_ordering", false, "publish failed");
return;
};
const request_msg = nats.Client.Message{
.subject = "test.requestmsg.ordering.service",
.sid = 0,
.reply_to = null,
.data = "request-data",
.headers = null,
.owned = false,
};
const reply = requester.requestMsg(&request_msg, 1000) catch {
reportResult("request_msg_ordering", false, "requestMsg failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "fresh")) {
reportResult("request_msg_ordering", true, "");
} else {
reportResult("request_msg_ordering", false, "request overtook publish");
}
} else {
reportResult("request_msg_ordering", false, "timeout");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testCheckCompatibility(allocator);
testPublishMsg(allocator);
testNoRespondersStatus(allocator);
testRequestMsg(allocator);
testRequestMsgOrdering(allocator);
}
================================================
FILE: src/testing/client/async_patterns.zig
================================================
//! Async Patterns Integration Tests
//!
//! Tests std.Io async patterns with NATS client including:
//! - Io.Select for racing operations
//! - io.concurrent() + Io.Queue for workers
//! - io.async() for parallel receives
//! - Cancellation and cleanup patterns
//! - Batch receiving
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
const ServerManager = utils.ServerManager;
const Io = std.Io;
const Allocator = std.mem.Allocator;
const Sub = nats.Client.Sub;
const Message = nats.Message;
/// Sleep function compatible with Io.Select.async()
fn sleepMs(io: Io, ms: i64) void {
io.sleep(.fromMilliseconds(ms), .awake) catch {};
}
const MsgSel = Io.Select(union(enum) {
message: anyerror!Message,
timeout: void,
});
/// Cancel a MsgSel, deiniting any completed messages.
fn cancelMsgSel(sel: *MsgSel) void {
while (sel.cancel()) |remaining| {
switch (remaining) {
.message => |r| {
if (r) |m| m.deinit() else |_| {}
},
.timeout => {},
}
}
}
/// Test 1: Race subscription receive against timeout - timeout wins.
fn testAsyncSelectTimeout(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_select_timeout",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"async.timeout.test",
) catch {
reportResult(
"async_select_timeout",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Do NOT publish - we want timeout to win
var buf: [2]MsgSel.Union = undefined;
var sel = MsgSel.init(io, &buf);
sel.async(.message, Sub.nextMsg, .{sub});
sel.async(.timeout, sleepMs, .{ io, 50 });
const result = sel.await() catch {
cancelMsgSel(&sel);
reportResult(
"async_select_timeout",
false,
"select failed",
);
return;
};
cancelMsgSel(&sel);
switch (result) {
.message => {
reportResult(
"async_select_timeout",
false,
"expected timeout",
);
},
.timeout => {
reportResult("async_select_timeout", true, "");
},
}
}
/// Test 2: Race subscription receive against timeout - message wins.
fn testAsyncSelectMessage(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_select_message",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"async.message.test",
) catch {
reportResult(
"async_select_message",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Publish message immediately
client.publish(
"async.message.test",
"select-test-msg",
) catch {
reportResult(
"async_select_message",
false,
"publish failed",
);
return;
};
var buf: [2]MsgSel.Union = undefined;
var sel = MsgSel.init(io, &buf);
sel.async(.message, Sub.nextMsg, .{sub});
sel.async(.timeout, sleepMs, .{ io, 500 });
const result = sel.await() catch {
cancelMsgSel(&sel);
reportResult(
"async_select_message",
false,
"select failed",
);
return;
};
cancelMsgSel(&sel);
switch (result) {
.message => |msg_result| {
const msg = msg_result catch {
reportResult(
"async_select_message",
false,
"msg error",
);
return;
};
defer msg.deinit();
if (std.mem.eql(
u8,
msg.data,
"select-test-msg",
)) {
reportResult(
"async_select_message",
true,
"",
);
} else {
reportResult(
"async_select_message",
false,
"wrong data",
);
}
},
.timeout => {
reportResult(
"async_select_message",
false,
"unexpected timeout",
);
},
}
}
/// Worker result for concurrent workers test.
const WorkerResult = struct {
worker_id: u8,
msg: nats.Message,
fn deinit(self: WorkerResult) void {
self.msg.deinit();
}
};
/// Worker task for concurrent test.
fn workerTask(
io: Io,
worker_id: u8,
sub: *Sub,
queue: *Io.Queue(WorkerResult),
done: *std.atomic.Value(bool),
) void {
while (!done.load(.acquire)) {
const msg = sub.nextMsgTimeout(
100,
) catch return orelse continue;
queue.putOne(io, .{
.worker_id = worker_id,
.msg = msg,
}) catch {
msg.deinit();
return;
};
}
}
/// Test 3: Multiple workers with io.concurrent() + Io.Queue.
fn testAsyncConcurrentWorkers(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_concurrent_workers",
false,
"connect failed",
);
return;
};
defer client.deinit();
// Create 3 workers in queue group
const w1_sub = client.queueSubscribeSync(
"async.workers",
"workers",
) catch {
reportResult(
"async_concurrent_workers",
false,
"sub1 failed",
);
return;
};
defer w1_sub.deinit();
const w2_sub = client.queueSubscribeSync(
"async.workers",
"workers",
) catch {
reportResult(
"async_concurrent_workers",
false,
"sub2 failed",
);
return;
};
defer w2_sub.deinit();
const w3_sub = client.queueSubscribeSync(
"async.workers",
"workers",
) catch {
reportResult(
"async_concurrent_workers",
false,
"sub3 failed",
);
return;
};
defer w3_sub.deinit();
// Shared queue for results
var queue_buf: [64]WorkerResult = undefined;
var queue: Io.Queue(WorkerResult) = .init(&queue_buf);
var done: std.atomic.Value(bool) = .init(false);
// Launch workers
var w1 = io.concurrent(workerTask, .{
io, 1, w1_sub, &queue, &done,
}) catch {
reportResult(
"async_concurrent_workers",
false,
"w1 launch failed",
);
return;
};
defer w1.cancel(io);
var w2 = io.concurrent(workerTask, .{
io, 2, w2_sub, &queue, &done,
}) catch {
reportResult(
"async_concurrent_workers",
false,
"w2 launch failed",
);
return;
};
defer w2.cancel(io);
var w3 = io.concurrent(workerTask, .{
io, 3, w3_sub, &queue, &done,
}) catch {
reportResult(
"async_concurrent_workers",
false,
"w3 launch failed",
);
return;
};
defer w3.cancel(io);
// Publish messages
const message_count: u32 = 30;
var i: u32 = 0;
while (i < message_count) : (i += 1) {
client.publish("async.workers", "work-item") catch {};
}
// Consume results
var total_received: u32 = 0;
var timeout_count: u32 = 0;
const max_timeouts: u32 = 10;
const QSel = Io.Select(union(enum) {
result: anyerror!WorkerResult,
timeout: void,
});
while (total_received < message_count and
timeout_count < max_timeouts)
{
var sel_buf: [2]QSel.Union = undefined;
var sel = QSel.init(io, &sel_buf);
sel.async(
.result,
Io.Queue(WorkerResult).getOne,
.{ &queue, io },
);
sel.async(.timeout, sleepMs, .{ io, 200 });
const s = sel.await() catch {
while (sel.cancel()) |remaining| {
switch (remaining) {
.result => |r| {
if (r) |wr| wr.deinit() else |_| {}
},
.timeout => {},
}
}
break;
};
while (sel.cancel()) |remaining| {
switch (remaining) {
.result => |r| {
if (r) |wr| wr.deinit() else |_| {}
},
.timeout => {},
}
}
switch (s) {
.result => |res| {
const r = res catch break;
r.deinit();
total_received += 1;
},
.timeout => {
timeout_count += 1;
},
}
}
// Signal workers to stop
done.store(true, .release);
if (total_received >= message_count * 9 / 10) {
reportResult("async_concurrent_workers", true, "");
} else {
var result_buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&result_buf,
"{d}/{d} received",
.{ total_received, message_count },
) catch "partial";
reportResult(
"async_concurrent_workers",
false,
detail,
);
}
}
/// Test 4: Multiple parallel subscriptions with io.async().
fn testAsyncParallelSubscriptions(
allocator: Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_parallel_subs",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub_a = client.subscribeSync(
"async.parallel.a",
) catch {
reportResult(
"async_parallel_subs",
false,
"sub_a failed",
);
return;
};
defer sub_a.deinit();
const sub_b = client.subscribeSync(
"async.parallel.b",
) catch {
reportResult(
"async_parallel_subs",
false,
"sub_b failed",
);
return;
};
defer sub_b.deinit();
const sub_c = client.subscribeSync(
"async.parallel.c",
) catch {
reportResult(
"async_parallel_subs",
false,
"sub_c failed",
);
return;
};
defer sub_c.deinit();
// Publish to all three
client.publish("async.parallel.a", "msg-a") catch {};
client.publish("async.parallel.b", "msg-b") catch {};
client.publish("async.parallel.c", "msg-c") catch {};
// Launch parallel receives
var future_a = io.async(Sub.nextMsg, .{sub_a});
defer if (future_a.cancel(io)) |m|
m.deinit()
else |_| {};
var future_b = io.async(Sub.nextMsg, .{sub_b});
defer if (future_b.cancel(io)) |m|
m.deinit()
else |_| {};
var future_c = io.async(Sub.nextMsg, .{sub_c});
defer if (future_c.cancel(io)) |m|
m.deinit()
else |_| {};
var received: u8 = 0;
if (future_a.await(io)) |_| {
received += 1;
} else |_| {}
if (future_b.await(io)) |_| {
received += 1;
} else |_| {}
if (future_c.await(io)) |_| {
received += 1;
} else |_| {}
if (received == 3) {
reportResult("async_parallel_subs", true, "");
} else {
var result_buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&result_buf,
"{d}/3 received",
.{received},
) catch "partial";
reportResult("async_parallel_subs", false, detail);
}
}
/// Test 5: Cancellation of pending receive.
fn testAsyncCancellation(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_cancellation",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"async.cancel.test",
) catch {
reportResult(
"async_cancellation",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Start async receive but cancel immediately
var future = io.async(Sub.nextMsg, .{sub});
// Small delay then cancel
io.sleep(.fromMilliseconds(10), .awake) catch {};
if (future.cancel(io)) |msg| {
msg.deinit();
reportResult("async_cancellation", true, "");
} else |_| {
reportResult("async_cancellation", true, "");
}
}
/// Test 6: Cancel after message already received.
fn testAsyncCancelWithPendingMessage(
allocator: Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_cancel_with_msg",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"async.cancel.msg",
) catch {
reportResult(
"async_cancel_with_msg",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Publish message first
client.publish(
"async.cancel.msg",
"pending-msg",
) catch {
reportResult(
"async_cancel_with_msg",
false,
"publish failed",
);
return;
};
// Let message arrive
io.sleep(.fromMilliseconds(50), .awake) catch {};
var future = io.async(Sub.nextMsg, .{sub});
defer if (future.cancel(io)) |m|
m.deinit()
else |_| {};
if (future.await(io)) |msg| {
if (std.mem.eql(u8, msg.data, "pending-msg")) {
reportResult(
"async_cancel_with_msg",
true,
"",
);
} else {
reportResult(
"async_cancel_with_msg",
false,
"wrong data",
);
}
} else |_| {
reportResult(
"async_cancel_with_msg",
false,
"await failed",
);
}
}
/// Test 7: nextBatch() receives multiple messages.
fn testBatchReceive(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.sub_queue_size = 64,
}) catch {
reportResult("batch_receive", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("async.batch") catch {
reportResult(
"batch_receive",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Publish 20 messages rapidly
var ii: u8 = 0;
while (ii < 20) : (ii += 1) {
client.publish("async.batch", "batch-data") catch {};
}
// Let messages arrive
io.sleep(.fromMilliseconds(100), .awake) catch {};
// Batch receive
var batch_buf: [32]nats.Message = undefined;
const count = sub.nextMsgBatch(io, &batch_buf) catch {
reportResult(
"batch_receive",
false,
"nextBatch failed",
);
return;
};
for (batch_buf[0..count]) |*msg| {
msg.deinit();
}
// Drain any remaining
const remaining = sub.tryNextMsgBatch(&batch_buf);
for (batch_buf[0..remaining]) |*msg| {
msg.deinit();
}
const total = count + remaining;
if (total >= 15) {
reportResult("batch_receive", true, "");
} else {
var result_buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&result_buf,
"{d}/20 received",
.{total},
) catch "partial";
reportResult("batch_receive", false, detail);
}
}
/// Test 8: tryNextBatch() non-blocking.
fn testTryNextBatch(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"try_next_batch",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"async.trybatch",
) catch {
reportResult(
"try_next_batch",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
var batch_buf: [32]nats.Message = undefined;
// Empty queue should return 0
const empty_count = sub.tryNextMsgBatch(&batch_buf);
if (empty_count != 0) {
reportResult(
"try_next_batch",
false,
"expected 0 on empty",
);
return;
}
// Publish messages
var ii: u8 = 0;
while (ii < 10) : (ii += 1) {
client.publish("async.trybatch", "data") catch {};
}
// Let messages arrive
io.sleep(.fromMilliseconds(50), .awake) catch {};
// Should get some messages
const first_count = sub.tryNextMsgBatch(&batch_buf);
for (batch_buf[0..first_count]) |*msg| {
msg.deinit();
}
// Drain remaining
const second_count = sub.tryNextMsgBatch(&batch_buf);
for (batch_buf[0..second_count]) |*msg| {
msg.deinit();
}
// Call again on drained queue
const third_count = sub.tryNextMsgBatch(&batch_buf);
if (first_count > 0 and third_count == 0) {
reportResult("try_next_batch", true, "");
} else {
var result_buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&result_buf,
"first={d} third={d}",
.{ first_count, third_count },
) catch "error";
reportResult("try_next_batch", false, detail);
}
}
/// Inner function that returns early to test defer cleanup.
fn innerAsyncWithEarlyReturn(
io: Io,
sub: *Sub,
) bool {
var future = io.async(Sub.nextMsg, .{sub});
defer if (future.cancel(io)) |m|
m.deinit()
else |_| {};
// Simulate early return (e.g., error condition)
return true; // defer should clean up the future
}
/// Test 9: Defer pattern cleans up on early return.
fn testAsyncDeferCleanup(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"async_defer_cleanup",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"async.defer.test",
) catch {
reportResult(
"async_defer_cleanup",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
const result = innerAsyncWithEarlyReturn(io, sub);
if (result) {
reportResult("async_defer_cleanup", true, "");
} else {
reportResult(
"async_defer_cleanup",
false,
"unexpected result",
);
}
}
/// Test 10: Race multiple subscriptions with Io.Select.
fn testSelectMultipleSubs(allocator: Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult(
"select_multiple_subs",
false,
"connect failed",
);
return;
};
defer client.deinit();
const fast_sub = client.subscribeSync(
"async.fast",
) catch {
reportResult(
"select_multiple_subs",
false,
"fast_sub failed",
);
return;
};
defer fast_sub.deinit();
const slow_sub = client.subscribeSync(
"async.slow",
) catch {
reportResult(
"select_multiple_subs",
false,
"slow_sub failed",
);
return;
};
defer slow_sub.deinit();
// Only publish to "fast"
client.publish("async.fast", "fast-msg") catch {
reportResult(
"select_multiple_subs",
false,
"publish failed",
);
return;
};
const SubSel = Io.Select(union(enum) {
fast: anyerror!Message,
slow: anyerror!Message,
});
var sel_buf: [2]SubSel.Union = undefined;
var sel = SubSel.init(io, &sel_buf);
sel.async(.fast, Sub.nextMsg, .{fast_sub});
sel.async(.slow, Sub.nextMsg, .{slow_sub});
const result = sel.await() catch {
while (sel.cancel()) |remaining| {
switch (remaining) {
.fast => |r| {
if (r) |m| m.deinit() else |_| {}
},
.slow => |r| {
if (r) |m| m.deinit() else |_| {}
},
}
}
reportResult(
"select_multiple_subs",
false,
"select failed",
);
return;
};
while (sel.cancel()) |remaining| {
switch (remaining) {
.fast => |r| {
if (r) |m| m.deinit() else |_| {}
},
.slow => |r| {
if (r) |m| m.deinit() else |_| {}
},
}
}
switch (result) {
.fast => |msg_result| {
const msg = msg_result catch {
reportResult(
"select_multiple_subs",
false,
"fast msg error",
);
return;
};
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "fast-msg")) {
reportResult(
"select_multiple_subs",
true,
"",
);
} else {
reportResult(
"select_multiple_subs",
false,
"wrong data",
);
}
},
.slow => {
reportResult(
"select_multiple_subs",
false,
"slow won unexpectedly",
);
},
}
}
pub fn runAll(allocator: Allocator, _: *ServerManager) void {
testAsyncSelectTimeout(allocator);
testAsyncSelectMessage(allocator);
testAsyncConcurrentWorkers(allocator);
testAsyncParallelSubscriptions(allocator);
testAsyncCancellation(allocator);
testAsyncCancelWithPendingMessage(allocator);
testBatchReceive(allocator);
testTryNextBatch(allocator);
testAsyncDeferCleanup(allocator);
testSelectMultipleSubs(allocator);
}
================================================
FILE: src/testing/client/auth.zig
================================================
//! Auth Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testAuthentication(allocator: std.mem.Allocator) void {
var url_buf: [128]u8 = undefined;
const url = formatAuthUrl(&url_buf, auth_port, test_token);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("authentication", false, "auth connect failed");
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("authentication", true, "");
} else {
reportResult("authentication", false, "not connected");
}
}
pub fn testAuthenticationFailure(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, auth_port); // No token!
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false });
if (result) |client| {
client.deinit();
reportResult("auth_failure", false, "should have failed");
} else |_| {
reportResult("auth_failure", true, "");
}
}
pub fn testAuthenticatedPubSub(allocator: std.mem.Allocator) void {
var url_buf: [128]u8 = undefined;
const url = formatAuthUrl(&url_buf, auth_port, test_token);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("auth_pubsub", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("auth.test.subject") catch {
reportResult("auth_pubsub", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("auth.test.subject", "auth message") catch {
reportResult("auth_pubsub", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("auth_pubsub", true, "");
} else {
reportResult("auth_pubsub", false, "no message");
}
}
pub fn testEmptyToken(allocator: std.mem.Allocator) void {
var url_buf: [128]u8 = undefined;
const url = formatAuthUrl(&url_buf, auth_port, "");
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false });
if (result) |client| {
if (client.isConnected()) {
client.deinit();
reportResult("empty_token", false, "should fail auth");
return;
}
client.deinit();
} else |_| {
// Expected - auth failed
}
reportResult("empty_token", true, "");
}
pub fn testTokenSpecialChars(allocator: std.mem.Allocator) void {
var url_buf: [128]u8 = undefined;
const url = formatAuthUrl(&url_buf, auth_port, test_token);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("token_special_chars", false, "connect failed");
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("token_special_chars", true, "");
} else {
reportResult("token_special_chars", false, "not connected");
}
}
pub fn testAuthRejectionRecovery(allocator: std.mem.Allocator) void {
const io = utils.newIo(allocator);
defer io.deinit();
var bad_url_buf: [128]u8 = undefined;
const bad_url = formatAuthUrl(&bad_url_buf, auth_port, "wrong-token");
const bad_result = nats.Client.connect(allocator, io.io(), bad_url, .{ .reconnect = false });
if (bad_result) |client| {
client.deinit();
// If it connected, that's unexpected but not a failure of this test
} else |_| {
// Expected - auth failed
}
// Second: succeed with correct token
var good_url_buf: [128]u8 = undefined;
const good_url = formatAuthUrl(&good_url_buf, auth_port, test_token);
const good_result = nats.Client.connect(allocator, io.io(), good_url, .{ .reconnect = false });
if (good_result) |client| {
defer client.deinit();
if (client.isConnected()) {
reportResult("auth_rejection_recovery", true, "");
return;
}
} else |_| {
reportResult("auth_rejection_recovery", false, "good connect failed");
return;
}
reportResult("auth_rejection_recovery", false, "not connected");
}
pub fn testMultipleAuthAttempts(allocator: std.mem.Allocator) void {
const io = utils.newIo(allocator);
defer io.deinit();
var url_buf: [128]u8 = undefined;
const bad_url = formatAuthUrl(&url_buf, auth_port, "wrong");
var failures: u32 = 0;
for (0..5) |_| {
const result = nats.Client.connect(allocator, io.io(), bad_url, .{ .reconnect = false });
if (result) |client| {
client.deinit();
} else |_| {
failures += 1;
}
}
if (failures == 5) {
reportResult("multiple_auth_attempts", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "{d}/5 failed", .{failures}) catch "e";
reportResult("multiple_auth_attempts", false, detail);
}
}
pub fn testAuthRequiredDetection(allocator: std.mem.Allocator) void {
var url_buf: [128]u8 = undefined;
const url = formatAuthUrl(&url_buf, auth_port, test_token);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("auth_required_detect", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("auth_required_detect", false, "no server info");
return;
}
// Server should have auth_required = true
if (info.?.auth_required) {
reportResult("auth_required_detect", true, "");
} else {
reportResult("auth_required_detect", false, "auth not required");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testAuthentication(allocator);
testAuthenticationFailure(allocator);
testAuthenticatedPubSub(allocator);
testEmptyToken(allocator);
testTokenSpecialChars(allocator);
testAuthRejectionRecovery(allocator);
testMultipleAuthAttempts(allocator);
testAuthRequiredDetection(allocator);
}
================================================
FILE: src/testing/client/autoflush.zig
================================================
//! Autoflush Integration Tests
//!
//! Tests automatic buffer flushing for ALL code paths that set
//! flush_requested, including:
//! - publish, publishRequest, publishWithHeaders
//! - publishRequestWithHeaders, publishWithHeaderMap, publishMsg
//! - subscribe, autoUnsubscribe, drain, unsubscribe
//! - High throughput, latency, TLS, disconnect safety
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const headers = nats.protocol.headers;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatTlsUrl = utils.formatTlsUrl;
const test_port = utils.test_port;
const tls_port = utils.tls_port;
const ServerManager = utils.ServerManager;
const Dir = std.Io.Dir;
const autoflush_port: u16 = 14240;
/// Returns absolute path to CA file. Caller owns returned memory.
fn getCaFilePath(
allocator: std.mem.Allocator,
io: std.Io,
) ?[:0]const u8 {
return Dir.realPathFileAlloc(
.cwd(),
io,
utils.tls_ca_file,
allocator,
) catch null;
}
/// Test 1: Verify messages are delivered without explicit flush.
fn testAutoflushBasicDelivery(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch |err| {
var err_buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&err_buf,
"connect failed: {}",
.{err},
) catch "connect error";
reportResult(
"autoflush_basic_delivery",
false,
msg,
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.basic",
) catch {
reportResult(
"autoflush_basic_delivery",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
client.publish(
"autoflush.basic",
"autoflush-test-msg",
) catch {
reportResult(
"autoflush_basic_delivery",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
100,
) catch null) |msg| {
defer msg.deinit();
if (std.mem.eql(
u8,
msg.data,
"autoflush-test-msg",
)) {
reportResult(
"autoflush_basic_delivery",
true,
"",
);
} else {
reportResult(
"autoflush_basic_delivery",
false,
"wrong data",
);
}
} else {
reportResult(
"autoflush_basic_delivery",
false,
"no message received",
);
}
}
/// Test 2: Verify multiple messages batch and deliver together.
fn testAutoflushMultipleMessages(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_multiple_msgs",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.multi",
) catch {
reportResult(
"autoflush_multiple_msgs",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
const msg_count: u8 = 10;
var i: u8 = 0;
while (i < msg_count) : (i += 1) {
var buf: [32]u8 = undefined;
const payload = std.fmt.bufPrint(
&buf,
"msg-{d}",
.{i},
) catch "msg";
client.publish(
"autoflush.multi",
payload,
) catch {
reportResult(
"autoflush_multiple_msgs",
false,
"publish failed",
);
return;
};
}
var received: u8 = 0;
while (received < msg_count) {
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
received += 1;
} else {
break;
}
}
if (received == msg_count) {
reportResult(
"autoflush_multiple_msgs",
true,
"",
);
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"{d}/{d} received",
.{ received, msg_count },
) catch "partial";
reportResult(
"autoflush_multiple_msgs",
false,
detail,
);
}
}
/// Test 3: Verify autoflush delivers all messages under high
/// publish rate. NATS over TCP on localhost must be lossless.
fn testAutoflushHighThroughput(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_high_throughput",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.throughput",
) catch {
reportResult(
"autoflush_high_throughput",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Publish 1000 messages - every publish must succeed
const msg_count: u32 = 1000;
var i: u32 = 0;
while (i < msg_count) : (i += 1) {
client.publish(
"autoflush.throughput",
"data",
) catch {
reportResult(
"autoflush_high_throughput",
false,
"publish failed",
);
return;
};
}
// Receive all - TCP on localhost must be lossless
var received: u32 = 0;
while (received < msg_count) {
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
received += 1;
} else {
break;
}
}
if (received == msg_count) {
reportResult(
"autoflush_high_throughput",
true,
"",
);
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"{d}/{d} received",
.{ received, msg_count },
) catch "partial";
reportResult(
"autoflush_high_throughput",
false,
detail,
);
}
}
/// Test 4: Double-check pattern prevents BADF panic during
/// disconnect.
fn testAutoflushDuringDisconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, autoflush_port);
const io = utils.newIo(allocator);
defer io.deinit();
const server = manager.startServer(
allocator,
io.io(),
.{ .port = autoflush_port },
) catch {
reportResult(
"autoflush_during_disconnect",
false,
"server start failed",
);
return;
};
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = true,
.max_reconnect_attempts = 5,
.reconnect_wait_ms = 100,
},
) catch {
server.stop(io.io());
reportResult(
"autoflush_during_disconnect",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.disconnect",
) catch {
server.stop(io.io());
reportResult(
"autoflush_during_disconnect",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
var i: u8 = 0;
while (i < 10) : (i += 1) {
client.publish(
"autoflush.disconnect",
"before",
) catch {};
}
// Stop server mid-operation (must NOT panic with BADF)
server.stop(io.io());
i = 0;
while (i < 5) : (i += 1) {
client.publish(
"autoflush.disconnect",
"during",
) catch {};
io.io().sleep(
.fromMilliseconds(10),
.awake,
) catch {};
}
const server2 = manager.startServer(
allocator,
io.io(),
.{ .port = autoflush_port },
) catch {
reportResult(
"autoflush_during_disconnect",
false,
"restart failed",
);
return;
};
defer server2.stop(io.io());
io.io().sleep(
.fromMilliseconds(500),
.awake,
) catch {};
// Reaching here without panic is the success criterion
reportResult(
"autoflush_during_disconnect",
true,
"",
);
}
/// Test 5: Verify TLS double-flush works correctly.
fn testAutoflushTLS(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const io = utils.newIo(allocator);
defer io.deinit();
const ca_path = getCaFilePath(
allocator,
io.io(),
) orelse {
reportResult(
"autoflush_tls",
false,
"CA file not found",
);
return;
};
defer allocator.free(ca_path);
const tls_server = manager.startServer(
allocator,
io.io(),
.{
.port = tls_port,
.config_file = utils.tls_config_file,
},
) catch {
reportResult(
"autoflush_tls",
false,
"TLS server start failed",
);
return;
};
defer tls_server.stop(io.io());
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = false,
.tls_ca_file = ca_path,
},
) catch |err| {
var err_buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&err_buf,
"connect failed: {}",
.{err},
) catch "connect error";
reportResult("autoflush_tls", false, msg);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.tls",
) catch {
reportResult(
"autoflush_tls",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
client.publish(
"autoflush.tls",
"tls-autoflush-msg",
) catch {
reportResult(
"autoflush_tls",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
defer msg.deinit();
if (std.mem.eql(
u8,
msg.data,
"tls-autoflush-msg",
)) {
reportResult("autoflush_tls", true, "");
} else {
reportResult(
"autoflush_tls",
false,
"wrong data",
);
}
} else {
reportResult(
"autoflush_tls",
false,
"no message received",
);
}
}
/// Test 6: Verify reasonable latency (message within 50ms).
fn testAutoflushLatencyBound(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_latency",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.latency",
) catch {
reportResult(
"autoflush_latency",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
client.publish(
"autoflush.latency",
"latency-test",
) catch {
reportResult(
"autoflush_latency",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
50,
) catch null) |msg| {
msg.deinit();
reportResult("autoflush_latency", true, "");
} else {
reportResult(
"autoflush_latency",
false,
"timeout (>50ms)",
);
}
}
/// Test 7: Verify subscribe also triggers autoflush.
fn testAutoflushWithSubscribe(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const client1 = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_subscribe",
false,
"client1 connect failed",
);
return;
};
defer client1.deinit();
const client2 = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_subscribe",
false,
"client2 connect failed",
);
return;
};
defer client2.deinit();
var sub = client2.subscribeSync(
"autoflush.sub.test",
) catch {
reportResult(
"autoflush_subscribe",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
io1.io().sleep(
.fromMilliseconds(20),
.awake,
) catch {};
client1.publish(
"autoflush.sub.test",
"sub-test-msg",
) catch {
reportResult(
"autoflush_subscribe",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
reportResult(
"autoflush_subscribe",
true,
"",
);
} else {
reportResult(
"autoflush_subscribe",
false,
"no message received",
);
}
}
/// Test 8: Verify single message doesn't get stuck in buffer.
fn testAutoflushNoBatching(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_no_batching",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.single",
) catch {
reportResult(
"autoflush_no_batching",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
client.publish(
"autoflush.single",
"single-msg",
) catch {
reportResult(
"autoflush_no_batching",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
30,
) catch null) |msg| {
msg.deinit();
reportResult(
"autoflush_no_batching",
true,
"",
);
} else {
reportResult(
"autoflush_no_batching",
false,
"message stuck in buffer",
);
}
}
/// Test 9: Verify autoflush works with multiple clients.
fn testAutoflushMultiClient(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const io3 = utils.newIo(allocator);
defer io3.deinit();
const client1 = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_multi_client",
false,
"client1 connect failed",
);
return;
};
defer client1.deinit();
const client2 = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_multi_client",
false,
"client2 connect failed",
);
return;
};
defer client2.deinit();
const client3 = nats.Client.connect(
allocator,
io3.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_multi_client",
false,
"client3 connect failed",
);
return;
};
defer client3.deinit();
var sub1 = client1.subscribeSync(
"autoflush.mc.to1",
) catch {
reportResult(
"autoflush_multi_client",
false,
"sub1 failed",
);
return;
};
defer sub1.deinit();
var sub2 = client2.subscribeSync(
"autoflush.mc.to2",
) catch {
reportResult(
"autoflush_multi_client",
false,
"sub2 failed",
);
return;
};
defer sub2.deinit();
var sub3 = client3.subscribeSync(
"autoflush.mc.to3",
) catch {
reportResult(
"autoflush_multi_client",
false,
"sub3 failed",
);
return;
};
defer sub3.deinit();
io1.io().sleep(
.fromMilliseconds(20),
.awake,
) catch {};
client1.publish(
"autoflush.mc.to2",
"from1",
) catch {};
client2.publish(
"autoflush.mc.to3",
"from2",
) catch {};
client3.publish(
"autoflush.mc.to1",
"from3",
) catch {};
var received: u8 = 0;
if (sub1.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
received += 1;
}
if (sub2.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
received += 1;
}
if (sub3.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
received += 1;
}
if (received == 3) {
reportResult(
"autoflush_multi_client",
true,
"",
);
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"{d}/3 received",
.{received},
) catch "partial";
reportResult(
"autoflush_multi_client",
false,
detail,
);
}
}
// -- New tests for uncovered code paths --
/// Test 10: publishRequest autoflush - request-reply pattern
/// where a service publishes expecting a reply on a given inbox.
fn testAutoflushPublishRequest(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const pub_client = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_publish_request",
false,
"pub connect failed",
);
return;
};
defer pub_client.deinit();
const sub_client = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_publish_request",
false,
"sub connect failed",
);
return;
};
defer sub_client.deinit();
var sub = sub_client.subscribeSync(
"autoflush.pubreq",
) catch {
reportResult(
"autoflush_publish_request",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
io1.io().sleep(
.fromMilliseconds(20),
.awake,
) catch {};
// publishRequest: no explicit flush
pub_client.publishRequest(
"autoflush.pubreq",
"reply.inbox.1",
"request-payload",
) catch {
reportResult(
"autoflush_publish_request",
false,
"publishRequest failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
defer msg.deinit();
const data_ok = std.mem.eql(
u8,
msg.data,
"request-payload",
);
const reply_ok = if (msg.reply_to) |rt|
std.mem.eql(u8, rt, "reply.inbox.1")
else
false;
if (data_ok and reply_ok) {
reportResult(
"autoflush_publish_request",
true,
"",
);
} else if (!reply_ok) {
reportResult(
"autoflush_publish_request",
false,
"wrong reply_to",
);
} else {
reportResult(
"autoflush_publish_request",
false,
"wrong data",
);
}
} else {
reportResult(
"autoflush_publish_request",
false,
"no message received",
);
}
}
/// Test 11: publishWithHeaders autoflush - publishing messages
/// with metadata headers (tracing IDs, content types).
fn testAutoflushPublishWithHeaders(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_pub_headers",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.headers",
) catch {
reportResult(
"autoflush_pub_headers",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
const hdrs = [_]headers.Entry{
.{
.key = "X-Trace-Id",
.value = "af-trace-001",
},
};
// publishWithHeaders: no explicit flush
client.publishWithHeaders(
"autoflush.headers",
&hdrs,
"hdr-payload",
) catch {
reportResult(
"autoflush_pub_headers",
false,
"publishWithHeaders failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
defer msg.deinit();
if (msg.headers == null) {
reportResult(
"autoflush_pub_headers",
false,
"no headers received",
);
return;
}
var parsed = headers.parse(
allocator,
msg.headers.?,
);
defer parsed.deinit();
if (parsed.err != null) {
reportResult(
"autoflush_pub_headers",
false,
"header parse error",
);
return;
}
if (parsed.get("X-Trace-Id")) |val| {
if (std.mem.eql(
u8,
val,
"af-trace-001",
)) {
reportResult(
"autoflush_pub_headers",
true,
"",
);
} else {
reportResult(
"autoflush_pub_headers",
false,
"wrong header value",
);
}
} else {
reportResult(
"autoflush_pub_headers",
false,
"header key not found",
);
}
} else {
reportResult(
"autoflush_pub_headers",
false,
"no message received",
);
}
}
/// Test 12: publishRequestWithHeaders autoflush - request-reply
/// with metadata (correlation IDs, auth tokens).
fn testAutoflushPubReqWithHeaders(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_pubreq_headers",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.hdr.req",
) catch {
reportResult(
"autoflush_pubreq_headers",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
const hdrs = [_]headers.Entry{
.{
.key = "X-Correlation-Id",
.value = "corr-42",
},
};
// publishRequestWithHeaders: no explicit flush
client.publishRequestWithHeaders(
"autoflush.hdr.req",
"reply.hdr.inbox",
&hdrs,
"hdr-req-payload",
) catch {
reportResult(
"autoflush_pubreq_headers",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
defer msg.deinit();
const reply_ok = if (msg.reply_to) |rt|
std.mem.eql(u8, rt, "reply.hdr.inbox")
else
false;
if (!reply_ok) {
reportResult(
"autoflush_pubreq_headers",
false,
"wrong reply_to",
);
return;
}
if (msg.headers == null) {
reportResult(
"autoflush_pubreq_headers",
false,
"no headers",
);
return;
}
var parsed = headers.parse(
allocator,
msg.headers.?,
);
defer parsed.deinit();
if (parsed.get("X-Correlation-Id")) |val| {
if (std.mem.eql(u8, val, "corr-42")) {
reportResult(
"autoflush_pubreq_headers",
true,
"",
);
} else {
reportResult(
"autoflush_pubreq_headers",
false,
"wrong header value",
);
}
} else {
reportResult(
"autoflush_pubreq_headers",
false,
"header not found",
);
}
} else {
reportResult(
"autoflush_pubreq_headers",
false,
"no message received",
);
}
}
/// Test 13: publishWithHeaderMap autoflush - dynamically-built
/// headers via HeaderMap API (middleware, routing code).
fn testAutoflushPubWithHeaderMap(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_pub_headermap",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub = client.subscribeSync(
"autoflush.hdrmap",
) catch {
reportResult(
"autoflush_pub_headermap",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
var hdr_map = nats.Client.HeaderMap.init(allocator);
defer hdr_map.deinit();
hdr_map.set(
"X-Route",
"autoflush-map",
) catch {
reportResult(
"autoflush_pub_headermap",
false,
"headermap set failed",
);
return;
};
// publishWithHeaderMap: no explicit flush
client.publishWithHeaderMap(
"autoflush.hdrmap",
&hdr_map,
"map-payload",
) catch {
reportResult(
"autoflush_pub_headermap",
false,
"publish failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
defer msg.deinit();
if (msg.headers == null) {
reportResult(
"autoflush_pub_headermap",
false,
"no headers",
);
return;
}
var parsed = headers.parse(
allocator,
msg.headers.?,
);
defer parsed.deinit();
if (parsed.get("X-Route")) |val| {
if (std.mem.eql(
u8,
val,
"autoflush-map",
)) {
reportResult(
"autoflush_pub_headermap",
true,
"",
);
} else {
reportResult(
"autoflush_pub_headermap",
false,
"wrong header value",
);
}
} else {
reportResult(
"autoflush_pub_headermap",
false,
"header not found",
);
}
} else {
reportResult(
"autoflush_pub_headermap",
false,
"no message received",
);
}
}
/// Test 14: publishMsg autoflush - message forwarding pattern.
/// Receive a message and republish it to another subject.
fn testAutoflushPublishMsg(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_publish_msg",
false,
"connect failed",
);
return;
};
defer client.deinit();
var sub_dst = client.subscribeSync(
"autoflush.msg.dst",
) catch {
reportResult(
"autoflush_publish_msg",
false,
"subscribe dst failed",
);
return;
};
defer sub_dst.deinit();
// Construct a Message to forward via publishMsg
const fwd_msg = nats.Client.Message{
.subject = "autoflush.msg.dst",
.sid = 0,
.reply_to = null,
.data = "forwarded-payload",
.headers = null,
.owned = false,
};
// publishMsg: no explicit flush
client.publishMsg(&fwd_msg) catch {
reportResult(
"autoflush_publish_msg",
false,
"publishMsg failed",
);
return;
};
if (sub_dst.nextMsgTimeout(
200,
) catch null) |msg| {
defer msg.deinit();
if (std.mem.eql(
u8,
msg.data,
"forwarded-payload",
)) {
reportResult(
"autoflush_publish_msg",
true,
"",
);
} else {
reportResult(
"autoflush_publish_msg",
false,
"wrong data",
);
}
} else {
reportResult(
"autoflush_publish_msg",
false,
"no message received",
);
}
}
/// Test 15: autoUnsubscribe autoflush - server enforces message
/// limit after UNSUB is auto-flushed.
fn testAutoflushAutoUnsubscribe(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const pub_client = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_auto_unsub",
false,
"pub connect failed",
);
return;
};
defer pub_client.deinit();
const sub_client = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_auto_unsub",
false,
"sub connect failed",
);
return;
};
defer sub_client.deinit();
var sub = sub_client.subscribeSync(
"autoflush.autounsub",
) catch {
reportResult(
"autoflush_auto_unsub",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// autoUnsubscribe(3): UNSUB sent via autoflush
sub.autoUnsubscribe(3) catch {
reportResult(
"autoflush_auto_unsub",
false,
"autoUnsubscribe failed",
);
return;
};
// Wait for UNSUB to reach server via autoflush
io1.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
// Publish 5 messages - server should only deliver 3
var i: u8 = 0;
while (i < 5) : (i += 1) {
pub_client.publish(
"autoflush.autounsub",
"msg",
) catch {
reportResult(
"autoflush_auto_unsub",
false,
"publish failed",
);
return;
};
}
var received: u8 = 0;
while (received < 5) {
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
received += 1;
} else {
break;
}
}
if (received == 3) {
reportResult(
"autoflush_auto_unsub",
true,
"",
);
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"{d}/3 received",
.{received},
) catch "count mismatch";
reportResult(
"autoflush_auto_unsub",
false,
detail,
);
}
}
/// Test 16: drain autoflush - graceful shutdown stops new
/// messages after UNSUB is auto-flushed.
fn testAutoflushDrain(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const pub_client = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_drain",
false,
"pub connect failed",
);
return;
};
defer pub_client.deinit();
const sub_client = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_drain",
false,
"sub connect failed",
);
return;
};
defer sub_client.deinit();
var sub = sub_client.subscribeSync(
"autoflush.drain",
) catch {
reportResult(
"autoflush_drain",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
io1.io().sleep(
.fromMilliseconds(20),
.awake,
) catch {};
// Publish 3 messages before drain
var i: u8 = 0;
while (i < 3) : (i += 1) {
pub_client.publish(
"autoflush.drain",
"before-drain",
) catch {
reportResult(
"autoflush_drain",
false,
"publish before failed",
);
return;
};
}
// Receive the 3 pre-drain messages
var pre_drain: u8 = 0;
while (pre_drain < 3) {
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
pre_drain += 1;
} else {
break;
}
}
if (pre_drain != 3) {
reportResult(
"autoflush_drain",
false,
"pre-drain msgs missing",
);
return;
}
// Drain: sends UNSUB via autoflush
sub.drain() catch {
reportResult(
"autoflush_drain",
false,
"drain failed",
);
return;
};
// Wait for UNSUB to reach server via autoflush
io1.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
// Publish after drain - should not be delivered
pub_client.publish(
"autoflush.drain",
"after-drain",
) catch {
reportResult(
"autoflush_drain",
false,
"publish after failed",
);
return;
};
// Verify no new messages arrive
if (sub.nextMsgTimeout(
100,
) catch null) |msg| {
msg.deinit();
reportResult(
"autoflush_drain",
false,
"got msg after drain",
);
} else {
reportResult("autoflush_drain", true, "");
}
}
/// Test 17: unsubscribe autoflush - explicit mid-session unsub
/// stops delivery after UNSUB is auto-flushed. Uses a control
/// subscription to verify (unsubscribed subs cannot receive).
fn testAutoflushUnsubscribe(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const pub_client = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"pub connect failed",
);
return;
};
defer pub_client.deinit();
const sub_client = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"sub connect failed",
);
return;
};
defer sub_client.deinit();
var sub = sub_client.subscribeSync(
"autoflush.unsub",
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Control sub to verify connection still works
var ctrl = sub_client.subscribeSync(
"autoflush.unsub.ctrl",
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"ctrl subscribeSync failed",
);
return;
};
defer ctrl.deinit();
io1.io().sleep(
.fromMilliseconds(20),
.awake,
) catch {};
// Verify subscription works first
pub_client.publish(
"autoflush.unsub",
"before-unsub",
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"publish before failed",
);
return;
};
if (sub.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
} else {
reportResult(
"autoflush_unsubscribe",
false,
"pre-unsub msg missing",
);
return;
}
// Unsubscribe: sends UNSUB via autoflush
sub.unsubscribe() catch {
reportResult(
"autoflush_unsubscribe",
false,
"unsubscribe failed",
);
return;
};
// Wait for UNSUB to reach server via autoflush
io1.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
// Publish to both subjects after unsub
pub_client.publish(
"autoflush.unsub",
"after-unsub",
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"publish after failed",
);
return;
};
pub_client.publish(
"autoflush.unsub.ctrl",
"ctrl-msg",
) catch {
reportResult(
"autoflush_unsubscribe",
false,
"publish ctrl failed",
);
return;
};
// Control sub must receive (proves msgs are flowing)
if (ctrl.nextMsgTimeout(
200,
) catch null) |msg| {
msg.deinit();
} else {
reportResult(
"autoflush_unsubscribe",
false,
"ctrl msg missing",
);
return;
}
// The unsubscribed sub's state is .unsubscribed,
// meaning server honored the auto-flushed UNSUB.
// Control sub received its msg proving the
// connection works and the UNSUB was delivered.
reportResult(
"autoflush_unsubscribe",
true,
"",
);
}
pub fn runAll(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
// Original tests (1-9)
testAutoflushBasicDelivery(allocator);
testAutoflushMultipleMessages(allocator);
testAutoflushHighThroughput(allocator);
testAutoflushNoBatching(allocator);
testAutoflushLatencyBound(allocator);
testAutoflushWithSubscribe(allocator);
testAutoflushMultiClient(allocator);
testAutoflushTLS(allocator, manager);
testAutoflushDuringDisconnect(allocator, manager);
// Publish variant tests (10-14)
testAutoflushPublishRequest(allocator);
testAutoflushPublishWithHeaders(allocator);
testAutoflushPubReqWithHeaders(allocator);
testAutoflushPubWithHeaderMap(allocator);
testAutoflushPublishMsg(allocator);
// Subscription control tests (15-17)
testAutoflushAutoUnsubscribe(allocator);
testAutoflushDrain(allocator);
testAutoflushUnsubscribe(allocator);
}
================================================
FILE: src/testing/client/basic.zig
================================================
//! Basic Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testClientBasic(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.name = "client-test",
.sub_queue_size = 64,
.reconnect = false,
}) catch |err| {
var err_buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&err_buf,
"connect failed: {}",
.{err},
) catch "error";
reportResult("client_basic", false, msg);
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("client_basic", false, "not connected");
return;
}
const sub = client.subscribeSync("basic") catch {
reportResult("client_basic", false, "subscribe failed");
return;
};
defer sub.deinit();
reportResult("client_basic", true, "");
}
pub fn testClientTryNext(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_try_next", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("trynext") catch {
reportResult("client_try_next", false, "sub failed");
return;
};
defer sub.deinit();
if (sub.tryNextMsg() != null) {
reportResult("client_try_next", false, "expected null");
return;
}
reportResult("client_try_next", true, "");
}
pub fn testClientServerInfo(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_server_info", false, "connect failed");
return;
};
defer client.deinit();
if (client.serverInfo()) |info| {
if (info.port == test_port) {
reportResult("client_server_info", true, "");
return;
}
}
reportResult("client_server_info", false, "no server info");
}
pub fn testClientRapidSubUnsub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_rapid_sub", false, "connect failed");
return;
};
defer client.deinit();
for (0..20) |i| {
var buf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(&buf, "rapid.{d}", .{i}) catch "e";
const sub = client.subscribeSync(subj) catch {
reportResult("client_rapid_sub", false, "sub failed");
return;
};
sub.deinit();
}
const sub = client.subscribeSync("rapid.final") catch {
reportResult("client_rapid_sub", false, "final sub failed");
return;
};
defer sub.deinit();
if (client.isConnected()) {
reportResult("client_rapid_sub", true, "");
} else {
reportResult("client_rapid_sub", false, "disconnected");
}
}
pub fn testClientName(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.name = "test-client-name",
.reconnect = false,
}) catch {
reportResult("client_name_opt", false, "connect failed");
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("client_name_opt", true, "");
} else {
reportResult("client_name_opt", false, "not connected");
}
}
pub fn testClientVerbose(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.verbose = true,
.reconnect = false,
}) catch {
reportResult("client_verbose", false, "connect failed");
return;
};
defer client.deinit();
client.publish("verbose.test", "data") catch {
reportResult("client_verbose", false, "publish failed");
return;
};
reportResult("client_verbose", true, "");
}
pub fn testMultipleConnectDisconnect(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
for (0..5) |_| {
const io = utils.newIo(allocator);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
io.deinit();
reportResult("multi_connect_disconnect", false, "connect failed");
return;
};
client.deinit();
io.deinit();
}
reportResult("multi_connect_disconnect", true, "");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testClientBasic(allocator);
testClientTryNext(allocator);
testClientServerInfo(allocator);
testClientRapidSubUnsub(allocator);
testClientName(allocator);
testClientVerbose(allocator);
testMultipleConnectDisconnect(allocator);
}
================================================
FILE: src/testing/client/callback.zig
================================================
//! Callback Subscription Tests
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
// -- MsgHandler delivery test --
const CountHandler = struct {
count: *u32,
pub fn onMessage(self: *@This(), _: *const nats.Message) void {
self.count.* += 1;
}
};
pub fn testCallbackMsgHandler(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_msg_handler",
false,
"connect failed",
);
return;
};
defer client.deinit();
var count: u32 = 0;
var handler = CountHandler{ .count = &count };
const sub = client.subscribe(
"cb.handler.test",
nats.MsgHandler.init(CountHandler, &handler),
) catch {
reportResult(
"callback_msg_handler",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
for (0..5) |_| {
client.publish("cb.handler.test", "x") catch {
reportResult(
"callback_msg_handler",
false,
"publish failed",
);
return;
};
}
// Wait for callbacks to fire
io.io().sleep(
.fromMilliseconds(300),
.awake,
) catch {};
if (count == 5) {
reportResult("callback_msg_handler", true, "");
} else {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"expected 5, got {d}",
.{count},
) catch "count mismatch";
reportResult("callback_msg_handler", false, msg);
}
}
// -- Plain fn delivery test --
var plain_fn_count: u32 = 0;
fn plainCallback(_: *const nats.Message) void {
plain_fn_count += 1;
}
pub fn testCallbackPlainFn(
allocator: std.mem.Allocator,
) void {
plain_fn_count = 0;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_plain_fn",
false,
"connect failed",
);
return;
};
defer client.deinit();
const sub = client.subscribeFn(
"cb.plainfn.test",
plainCallback,
) catch {
reportResult(
"callback_plain_fn",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
for (0..3) |_| {
client.publish("cb.plainfn.test", "y") catch {
reportResult(
"callback_plain_fn",
false,
"publish failed",
);
return;
};
}
io.io().sleep(
.fromMilliseconds(300),
.awake,
) catch {};
if (plain_fn_count == 3) {
reportResult("callback_plain_fn", true, "");
} else {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"expected 3, got {d}",
.{plain_fn_count},
) catch "count mismatch";
reportResult("callback_plain_fn", false, msg);
}
}
// -- Queue group test --
const QueueHandler = struct {
count: *u32,
pub fn onMessage(self: *@This(), _: *const nats.Message) void {
self.count.* += 1;
}
};
pub fn testCallbackQueueGroup(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_queue_group",
false,
"connect failed",
);
return;
};
defer client.deinit();
var count1: u32 = 0;
var count2: u32 = 0;
var h1 = QueueHandler{ .count = &count1 };
var h2 = QueueHandler{ .count = &count2 };
const sub1 = client.queueSubscribe(
"cb.queue.test",
"workers",
nats.MsgHandler.init(QueueHandler, &h1),
) catch {
reportResult(
"callback_queue_group",
false,
"sub1 failed",
);
return;
};
defer sub1.deinit();
const sub2 = client.queueSubscribe(
"cb.queue.test",
"workers",
nats.MsgHandler.init(QueueHandler, &h2),
) catch {
reportResult(
"callback_queue_group",
false,
"sub2 failed",
);
return;
};
defer sub2.deinit();
for (0..10) |_| {
client.publish("cb.queue.test", "z") catch {
reportResult(
"callback_queue_group",
false,
"publish failed",
);
return;
};
}
io.io().sleep(
.fromMilliseconds(300),
.awake,
) catch {};
const total = count1 + count2;
if (total >= 9) {
reportResult("callback_queue_group", true, "");
} else {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"expected >=9, got {d}",
.{total},
) catch "total mismatch";
reportResult("callback_queue_group", false, msg);
}
}
// -- Deinit cleanup test (no hang) --
pub fn testCallbackDeinitCleanup(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_deinit_cleanup",
false,
"connect failed",
);
return;
};
defer client.deinit();
var count: u32 = 0;
var handler = CountHandler{ .count = &count };
const sub = client.subscribe(
"cb.deinit.test",
nats.MsgHandler.init(CountHandler, &handler),
) catch {
reportResult(
"callback_deinit_cleanup",
false,
"subscribe failed",
);
return;
};
// Immediately deinit -- must not hang
sub.deinit();
reportResult("callback_deinit_cleanup", true, "");
}
// -- Mode field test --
pub fn testCallbackModeField(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_mode_field",
false,
"connect failed",
);
return;
};
defer client.deinit();
// Manual sub should have .manual mode
const manual_sub = client.subscribeSync(
"cb.mode.manual",
) catch {
reportResult(
"callback_mode_field",
false,
"manual sub failed",
);
return;
};
defer manual_sub.deinit();
var count: u32 = 0;
var handler = CountHandler{ .count = &count };
// Callback sub should have .callback mode
const cb_sub = client.subscribe(
"cb.mode.callback",
nats.MsgHandler.init(CountHandler, &handler),
) catch {
reportResult(
"callback_mode_field",
false,
"callback sub failed",
);
return;
};
defer cb_sub.deinit();
if (manual_sub.mode != .manual) {
reportResult(
"callback_mode_field",
false,
"manual sub mode wrong",
);
return;
}
if (cb_sub.mode != .callback) {
reportResult(
"callback_mode_field",
false,
"callback sub mode wrong",
);
return;
}
reportResult("callback_mode_field", true, "");
}
// -- High volume delivery test --
pub fn testCallbackHighVolume(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_high_volume",
false,
"connect failed",
);
return;
};
defer client.deinit();
var count: u32 = 0;
var handler = CountHandler{ .count = &count };
const sub = client.subscribe(
"cb.volume.test",
nats.MsgHandler.init(CountHandler, &handler),
) catch {
reportResult(
"callback_high_volume",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
for (0..100) |_| {
client.publish("cb.volume.test", "payload") catch {
reportResult(
"callback_high_volume",
false,
"publish failed",
);
return;
};
}
io.io().sleep(
.fromMilliseconds(500),
.awake,
) catch {};
if (count == 100) {
reportResult("callback_high_volume", true, "");
} else {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"expected 100, got {d}",
.{count},
) catch "count mismatch";
reportResult(
"callback_high_volume",
false,
msg,
);
}
}
// -- Data integrity test --
const IntegrityHandler = struct {
/// Tracks which payload indices were received.
seen: *[100]bool,
count: *u32,
pub fn onMessage(
self: *@This(),
msg: *const nats.Message,
) void {
const idx = std.fmt.parseInt(
usize,
msg.data,
10,
) catch return;
if (idx < 100) {
self.seen.*[idx] = true;
}
self.count.* += 1;
}
};
pub fn testCallbackDataIntegrity(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_data_integrity",
false,
"connect failed",
);
return;
};
defer client.deinit();
var seen: [100]bool = .{false} ** 100;
var count: u32 = 0;
var handler = IntegrityHandler{
.seen = &seen,
.count = &count,
};
const sub = client.subscribe(
"cb.integrity.test",
nats.MsgHandler.init(
IntegrityHandler,
&handler,
),
) catch {
reportResult(
"callback_data_integrity",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
var pbuf: [8]u8 = undefined;
for (0..100) |i| {
const payload = std.fmt.bufPrint(
&pbuf,
"{d}",
.{i},
) catch "0";
client.publish(
"cb.integrity.test",
payload,
) catch {
reportResult(
"callback_data_integrity",
false,
"publish failed",
);
return;
};
}
io.io().sleep(
.fromMilliseconds(500),
.awake,
) catch {};
if (count != 100) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"expected 100, got {d}",
.{count},
) catch "count mismatch";
reportResult(
"callback_data_integrity",
false,
msg,
);
return;
}
for (0..100) |i| {
if (!seen[i]) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"missing payload {d}",
.{i},
) catch "missing payload";
reportResult(
"callback_data_integrity",
false,
msg,
);
return;
}
}
reportResult("callback_data_integrity", true, "");
}
// -- Mixed manual + callback test --
pub fn testCallbackMixedModes(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_mixed_modes",
false,
"connect failed",
);
return;
};
defer client.deinit();
// Callback sub on one subject
var cb_count: u32 = 0;
var handler = CountHandler{ .count = &cb_count };
const cb_sub = client.subscribe(
"cb.mixed.auto",
nats.MsgHandler.init(CountHandler, &handler),
) catch {
reportResult(
"callback_mixed_modes",
false,
"callback sub failed",
);
return;
};
defer cb_sub.deinit();
// Manual sub on different subject
const man_sub = client.subscribeSync(
"cb.mixed.manual",
) catch {
reportResult(
"callback_mixed_modes",
false,
"manual sub failed",
);
return;
};
defer man_sub.deinit();
// Publish to both subjects
for (0..5) |_| {
client.publish("cb.mixed.auto", "a") catch {
reportResult(
"callback_mixed_modes",
false,
"publish auto failed",
);
return;
};
client.publish("cb.mixed.manual", "m") catch {
reportResult(
"callback_mixed_modes",
false,
"publish manual failed",
);
return;
};
}
io.io().sleep(
.fromMilliseconds(300),
.awake,
) catch {};
// Drain manual sub
var manual_count: u32 = 0;
while (man_sub.tryNextMsg()) |_| {
manual_count += 1;
}
if (cb_count != 5 or manual_count != 5) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"cb={d} man={d}, expected 5/5",
.{ cb_count, manual_count },
) catch "count mismatch";
reportResult(
"callback_mixed_modes",
false,
msg,
);
return;
}
reportResult("callback_mixed_modes", true, "");
}
// -- Callback request/reply test --
const EchoHandler = struct {
client: *nats.Client,
handled: *u32,
pub fn onMessage(
self: *@This(),
msg: *const nats.Message,
) void {
self.handled.* += 1;
msg.respond(self.client, msg.data) catch {};
}
};
pub fn testCallbackRequestReply(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const svc_io = utils.newIo(allocator);
defer svc_io.deinit();
const svc_client = nats.Client.connect(
allocator,
svc_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_request_reply",
false,
"svc connect failed",
);
return;
};
defer svc_client.deinit();
const req_io = utils.newIo(allocator);
defer req_io.deinit();
const req_client = nats.Client.connect(
allocator,
req_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"callback_request_reply",
false,
"req connect failed",
);
return;
};
defer req_client.deinit();
var handled: u32 = 0;
var handler = EchoHandler{
.client = svc_client,
.handled = &handled,
};
const sub = svc_client.subscribe(
"cb.echo.test",
nats.MsgHandler.init(EchoHandler, &handler),
) catch {
reportResult(
"callback_request_reply",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Wait for sub to propagate
svc_io.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
var replies: u32 = 0;
const payloads = [_][]const u8{ "a", "b", "c" };
for (payloads) |payload| {
if (req_client.request(
"cb.echo.test",
payload,
1000,
)) |maybe_reply| {
if (maybe_reply) |reply| {
defer reply.deinit();
if (std.mem.eql(
u8,
reply.data,
payload,
)) {
replies += 1;
}
}
} else |_| {}
}
if (handled != 3 or replies != 3) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"handled={d} replies={d}",
.{ handled, replies },
) catch "mismatch";
reportResult(
"callback_request_reply",
false,
msg,
);
return;
}
reportResult("callback_request_reply", true, "");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testCallbackMsgHandler(allocator);
testCallbackPlainFn(allocator);
testCallbackQueueGroup(allocator);
testCallbackDeinitCleanup(allocator);
testCallbackModeField(allocator);
testCallbackHighVolume(allocator);
testCallbackDataIntegrity(allocator);
testCallbackMixedModes(allocator);
testCallbackRequestReply(allocator);
}
================================================
FILE: src/testing/client/concurrency.zig
================================================
//! Concurrency Tests for NATS Client
//!
//! Tests for race conditions, concurrent operations, and thread safety.
//! These tests verify the client behaves correctly under concurrent access.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
fn sleepMs(io: std.Io, ms: i64) void {
io.sleep(.fromMilliseconds(ms), .awake) catch {};
}
pub fn testConcurrentSubscribe(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("concurrent_subscribe", false, "connect failed");
return;
};
defer client.deinit();
const NUM_SUBS = 10;
var subs: [NUM_SUBS]?*nats.Subscription =
[_]?*nats.Subscription{null} ** NUM_SUBS;
var created: u32 = 0;
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..NUM_SUBS) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"concurrent.sub.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subject) catch {
continue;
};
created += 1;
}
if (created != NUM_SUBS) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/10",
.{created},
) catch "e";
reportResult("concurrent_subscribe", false, detail);
return;
}
var sids: [NUM_SUBS]u64 = undefined;
for (0..NUM_SUBS) |i| {
if (subs[i]) |sub| {
sids[i] = sub.sid;
for (0..i) |j| {
if (sids[j] == sids[i]) {
reportResult(
"concurrent_subscribe",
false,
"duplicate SID",
);
return;
}
}
}
}
if (client.isConnected()) {
reportResult("concurrent_subscribe", true, "");
} else {
reportResult("concurrent_subscribe", false, "disconnected");
}
}
pub fn testRapidPublish(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("rapid_publish", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("rapid.publish") catch {
reportResult("rapid_publish", false, "subscribe failed");
return;
};
defer sub.deinit();
const NUM_MSGS = 100;
var published: u32 = 0;
for (0..NUM_MSGS) |_| {
client.publish("rapid.publish", "data") catch {
continue;
};
published += 1;
}
client.flush(500_000_000) catch {};
if (published != NUM_MSGS) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"pub {d}/100",
.{published},
) catch "e";
reportResult("rapid_publish", false, detail);
return;
}
var received: u32 = 0;
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(100) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
if (received == NUM_MSGS) {
reportResult("rapid_publish", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/100",
.{received},
) catch "e";
reportResult("rapid_publish", false, detail);
}
}
pub fn testConcurrentSubUnsub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("concurrent_sub_unsub", false, "connect failed");
return;
};
defer client.deinit();
const CYCLES = 20;
var current_sub: ?*nats.Subscription = null;
for (0..CYCLES) |i| {
if (current_sub) |sub| {
sub.unsubscribe() catch {};
sub.deinit();
current_sub = null;
}
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"cycle.{d}",
.{i},
) catch continue;
current_sub = client.subscribeSync(subject) catch {
reportResult("concurrent_sub_unsub", false, "subscribe failed");
return;
};
}
if (current_sub) |sub| {
sub.deinit();
}
if (client.isConnected()) {
reportResult("concurrent_sub_unsub", true, "");
} else {
reportResult("concurrent_sub_unsub", false, "disconnected");
}
}
pub fn testRaceSubscribeVsDelivery(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("race_sub_delivery", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const subscriber = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("race_sub_delivery", false, "sub connect failed");
return;
};
defer subscriber.deinit();
const sub = subscriber.subscribeSync("race.delivery") catch {
reportResult("race_sub_delivery", false, "subscribe failed");
return;
};
defer sub.deinit();
publisher.publish("race.delivery", "race-msg-1") catch {
reportResult("race_sub_delivery", false, "publish1 failed");
return;
};
sub_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
publisher.publish("race.delivery", "race-msg-2") catch {
reportResult("race_sub_delivery", false, "publish2 failed");
return;
};
var received: u32 = 0;
for (0..2) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received >= 1) {
reportResult("race_sub_delivery", true, "");
} else {
reportResult("race_sub_delivery", false, "no messages received");
}
}
pub fn testRaceUnsubscribeVsDelivery(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.sub_queue_size = 64,
.reconnect = false,
}) catch {
reportResult("race_unsub_delivery", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("race.unsub") catch {
reportResult("race_unsub_delivery", false, "subscribe failed");
return;
};
defer sub.deinit();
for (0..50) |_| {
client.publish("race.unsub", "msg") catch {};
}
sub.unsubscribe() catch {};
for (0..50) |_| {
client.publish("race.unsub", "msg") catch {};
}
if (client.isConnected()) {
reportResult("race_unsub_delivery", true, "");
} else {
reportResult("race_unsub_delivery", false, "disconnected");
}
}
pub fn testSidAllocationRecycling(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("sid_allocation_recycle", false, "connect failed");
return;
};
defer client.deinit();
var seen_sids: [100]u64 = undefined;
var seen_count: usize = 0;
for (0..50) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"recycle.{d}",
.{i},
) catch continue;
const sub = client.subscribeSync(subject) catch {
reportResult("sid_allocation_recycle", false, "subscribe failed");
return;
};
if (seen_count < seen_sids.len) {
for (seen_sids[0..seen_count]) |prev_sid| {
if (prev_sid == sub.sid) {
reportResult("sid_allocation_recycle", false, "SID reused");
sub.deinit();
return;
}
}
seen_sids[seen_count] = sub.sid;
seen_count += 1;
}
sub.deinit();
}
for (1..seen_count) |i| {
if (seen_sids[i] <= seen_sids[i - 1]) {
reportResult("sid_allocation_recycle", false, "non-monotonic SIDs");
return;
}
}
if (client.isConnected()) {
reportResult("sid_allocation_recycle", true, "");
} else {
reportResult("sid_allocation_recycle", false, "disconnected");
}
}
pub fn testMultipleClientsSeparateIo(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const client1 = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_client_separate_io", false, "client1 failed");
return;
};
defer client1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const client2 = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_client_separate_io", false, "client2 failed");
return;
};
defer client2.deinit();
const io3 = utils.newIo(allocator);
defer io3.deinit();
const client3 = nats.Client.connect(
allocator,
io3.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_client_separate_io", false, "client3 failed");
return;
};
defer client3.deinit();
const sub = client1.subscribeSync("separate.io.test") catch {
reportResult("multi_client_separate_io", false, "subscribe failed");
return;
};
defer sub.deinit();
io1.io().sleep(.fromMilliseconds(50), .awake) catch {};
client2.publish("separate.io.test", "from-client2") catch {
reportResult("multi_client_separate_io", false, "pub2 failed");
return;
};
client3.publish("separate.io.test", "from-client3") catch {
reportResult("multi_client_separate_io", false, "pub3 failed");
return;
};
var received: u32 = 0;
for (0..2) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received == 2) {
reportResult("multi_client_separate_io", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/2", .{received}) catch "e";
reportResult("multi_client_separate_io", false, detail);
}
}
pub fn testParallelReceive(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("parallel_recv", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.subscribeSync("parallel.1") catch {
reportResult("parallel_recv", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.subscribeSync("parallel.2") catch {
reportResult("parallel_recv", false, "sub2 failed");
return;
};
defer sub2.deinit();
const sub3 = client.subscribeSync("parallel.3") catch {
reportResult("parallel_recv", false, "sub3 failed");
return;
};
defer sub3.deinit();
client.publish("parallel.1", "msg1") catch {};
client.publish("parallel.2", "msg2") catch {};
client.publish("parallel.3", "msg3") catch {};
client.flush(500_000_000) catch {};
var received: u32 = 0;
if (sub1.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
received += 1;
}
if (sub2.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
received += 1;
}
if (sub3.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
received += 1;
}
if (received == 3) {
reportResult("parallel_recv", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/3",
.{received},
) catch "e";
reportResult("parallel_recv", false, detail);
}
}
pub fn testRapidFlushOperations(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("rapid_flush", false, "connect failed");
return;
};
defer client.deinit();
var success: u32 = 0;
for (0..50) |_| {
client.flushBuffer() catch {
continue;
};
success += 1;
}
if (success != 50) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"flush {d}/50",
.{success},
) catch "e";
reportResult("rapid_flush", false, detail);
return;
}
for (0..50) |_| {
client.publish("flush.test", "x") catch {};
client.flushBuffer() catch {};
}
if (client.isConnected()) {
reportResult("rapid_flush", true, "");
} else {
reportResult("rapid_flush", false, "disconnected");
}
}
pub fn testStatsConcurrency(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stats_concurrency", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("stats.test") catch {
reportResult("stats_concurrency", false, "subscribe failed");
return;
};
defer sub.deinit();
const before = client.stats();
const NUM_MSGS: u64 = 100;
for (0..NUM_MSGS) |_| {
client.publish("stats.test", "stat-msg") catch {};
}
client.flush(500_000_000) catch {};
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(100) catch null) |m| {
m.deinit();
} else break;
}
const after = client.stats();
const msgs_out_diff = after.msgs_out - before.msgs_out;
const msgs_in_diff = after.msgs_in - before.msgs_in;
if (msgs_out_diff != NUM_MSGS) {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"msgs_out: {d} (expect {d})",
.{ msgs_out_diff, NUM_MSGS },
) catch "e";
reportResult("stats_concurrency", false, detail);
return;
}
if (msgs_in_diff < NUM_MSGS) {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"msgs_in: {d} (expect >= {d})",
.{ msgs_in_diff, NUM_MSGS },
) catch "e";
reportResult("stats_concurrency", false, detail);
return;
}
reportResult("stats_concurrency", true, "");
}
pub fn testMixedWriteOrderingPublishBeforeSubscribe(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("mixed_write_ordering", false, "connect failed");
return;
};
defer client.deinit();
const iterations = 100;
for (0..iterations) |i| {
{
var subject_buf: [48]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"ordering.publish.before.sub.{d}",
.{i},
) catch {
reportResult("mixed_write_ordering", false, "subject format failed");
return;
};
client.publish(subject, "queued-before-sub") catch {
reportResult("mixed_write_ordering", false, "publish failed");
return;
};
const sub = client.subscribeSync(subject) catch {
reportResult("mixed_write_ordering", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flush(1_000_000_000) catch {
reportResult("mixed_write_ordering", false, "flush failed");
return;
};
if (sub.nextMsgTimeout(50) catch null) |msg| {
msg.deinit();
var detail_buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&detail_buf,
"pre-sub publish delivered at iter {d}",
.{i},
) catch "unexpected pre-sub delivery";
reportResult("mixed_write_ordering", false, detail);
return;
}
}
}
reportResult("mixed_write_ordering", true, "");
}
fn delayedUnsubscribe(
io: std.Io,
sub: *nats.Subscription,
delay_ms: i64,
) void {
sleepMs(io, delay_ms);
sub.unsubscribe() catch {};
}
pub fn testBlockingNextMsgUnsubscribeWakeup(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_wrap = utils.newIo(allocator);
defer io_wrap.deinit();
const io = io_wrap.io();
const client = nats.Client.connect(
allocator,
io,
url,
.{ .reconnect = false },
) catch {
reportResult("blocking_nextmsg_unsub", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("blocking.nextmsg.unsub") catch {
reportResult("blocking_nextmsg_unsub", false, "subscribe failed");
return;
};
defer sub.deinit();
var wake_thread = io.async(delayedUnsubscribe, .{ io, sub, 20 });
defer _ = wake_thread.cancel(io);
const Sel = std.Io.Select(union(enum) {
recv: anyerror!nats.Client.Message,
timeout: void,
});
var buf: [2]Sel.Union = undefined;
var sel = Sel.init(io, &buf);
sel.async(.recv, nats.Client.Sub.nextMsg, .{sub});
sel.async(.timeout, sleepMs, .{ io, 200 });
const result = sel.await() catch {
sel.cancelDiscard();
reportResult("blocking_nextmsg_unsub", false, "select canceled");
return;
};
sel.cancelDiscard();
switch (result) {
.recv => |recv_result| {
if (recv_result) |msg| {
msg.deinit();
reportResult("blocking_nextmsg_unsub", false, "unexpected message");
} else |err| switch (err) {
error.Closed, error.Canceled => {
reportResult("blocking_nextmsg_unsub", true, "");
},
else => {
reportResult("blocking_nextmsg_unsub", false, @errorName(err));
},
}
},
.timeout => {
reportResult("blocking_nextmsg_unsub", false, "nextMsg did not wake");
},
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testConcurrentSubscribe(allocator);
testRapidPublish(allocator);
testConcurrentSubUnsub(allocator);
testRaceSubscribeVsDelivery(allocator);
testRaceUnsubscribeVsDelivery(allocator);
testSidAllocationRecycling(allocator);
testMultipleClientsSeparateIo(allocator);
testParallelReceive(allocator);
testRapidFlushOperations(allocator);
testStatsConcurrency(allocator);
testMixedWriteOrderingPublishBeforeSubscribe(allocator);
testBlockingNextMsgUnsubscribeWakeup(allocator);
}
================================================
FILE: src/testing/client/connection.zig
================================================
//! Connection Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testConnectionRefused(allocator: std.mem.Allocator) void {
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(
allocator,
io.io(),
"nats://127.0.0.1:19999",
.{ .reconnect = false },
);
if (result) |client| {
client.deinit();
reportResult("connection_refused", false, "expected error");
} else |_| {
reportResult("connection_refused", true, "");
}
}
pub fn testConsecutiveConnections(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
for (0..3) |i| {
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"connect {d} failed",
.{i},
) catch "e";
reportResult("consecutive_connections", false, msg);
return;
};
client.deinit();
}
reportResult("consecutive_connections", true, "");
}
pub fn testIsConnectedState(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("is_connected_state", false, "connect failed");
return;
};
if (!client.isConnected()) {
client.deinit();
reportResult(
"is_connected_state",
false,
"not connected initially",
);
return;
}
client.deinit();
reportResult("is_connected_state", true, "");
}
pub fn testReconnection(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("reconnection", false, "initial connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("reconnection", false, "not connected initially");
return;
}
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnection", false, "server restart failed");
return;
};
reportResult("reconnection", true, "");
}
/// Test: New connection after server restart
pub fn testServerRestartNewConnection(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const client1 = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_restart", false, "initial connect failed");
return;
};
if (!client1.isConnected()) {
client1.deinit();
reportResult("server_restart", false, "not connected");
return;
}
client1.deinit();
manager.stopServer(0, io1.io());
io1.io().sleep(.fromMilliseconds(100), .awake) catch {};
_ = manager.startServer(
allocator,
io1.io(),
.{ .port = test_port },
) catch {
reportResult("server_restart", false, "restart failed");
return;
};
const io2 = utils.newIo(allocator);
defer io2.deinit();
const client2 = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_restart", false, "reconnect failed");
return;
};
defer client2.deinit();
if (client2.isConnected()) {
reportResult("server_restart", true, "");
} else {
reportResult(
"server_restart",
false,
"not connected after restart",
);
}
}
pub fn testConnectionStateAfterOps(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("state_after_ops", false, "connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("state_after_ops", false, "not connected after connect");
return;
}
client.publish("state.test", "data") catch {};
if (!client.isConnected()) {
reportResult("state_after_ops", false, "not connected after publish");
return;
}
const sub = client.subscribeSync("state.sub") catch {
reportResult("state_after_ops", false, "subscribe failed");
return;
};
defer sub.deinit();
if (!client.isConnected()) {
reportResult("state_after_ops", false, "not connected after subscribe");
return;
}
reportResult("state_after_ops", true, "");
}
/// Verifies no resource leaks after many connection cycles.
pub fn testRapidConnectDisconnect(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
// Do 50 connect/disconnect cycles
const CYCLES = 50;
var success: u32 = 0;
for (0..CYCLES) |_| {
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
continue;
};
if (client.isConnected()) {
success += 1;
}
client.deinit();
}
if (success == CYCLES) {
reportResult("rapid_connect_disconnect", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"{d}/50 cycles",
.{success},
) catch "e";
reportResult("rapid_connect_disconnect", false, detail);
}
}
/// Verifies connect options are properly applied.
pub fn testConnectionOptions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.name = "test-client",
.verbose = false,
.pedantic = false,
.echo = true,
.headers = true,
.no_responders = true,
.sub_queue_size = 128,
.reconnect = false,
}) catch {
reportResult("connection_options", false, "connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("connection_options", false, "not connected");
return;
}
const info = client.serverInfo();
if (info == null) {
reportResult("connection_options", false, "no server info");
return;
}
reportResult("connection_options", true, "");
}
/// Verifies drain properly cleans up subscriptions.
pub fn testConnectionDrain(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("connection_drain", false, "connect failed");
return;
};
defer client.deinit();
// Create some subscriptions
const sub1 = client.subscribeSync("drain.1") catch {
reportResult("connection_drain", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.subscribeSync("drain.2") catch {
reportResult("connection_drain", false, "sub2 failed");
return;
};
defer sub2.deinit();
_ = client.drain() catch {
reportResult("connection_drain", false, "drain failed");
return;
};
if (client.isConnected()) {
reportResult(
"connection_drain",
false,
"still connected",
);
return;
}
reportResult("connection_drain", true, "");
}
/// Verifies invalid URLs are rejected properly.
pub fn testInvalidUrlHandling(allocator: std.mem.Allocator) void {
const io = utils.newIo(allocator);
defer io.deinit();
const empty_result = nats.Client.connect(
allocator,
io.io(),
"",
.{ .reconnect = false },
);
if (empty_result) |client| {
client.deinit();
reportResult("invalid_url_handling", false, "empty should fail");
return;
} else |_| {}
const invalid_result = nats.Client.connect(
allocator,
io.io(),
"nats://",
.{ .reconnect = false },
);
if (invalid_result) |client| {
client.deinit();
reportResult("invalid_url_handling", false, "invalid should fail");
return;
} else |_| {}
reportResult("invalid_url_handling", true, "");
}
/// Verifies state machine transitions are correct.
pub fn testConnectionStateTransitions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("connection_state", false, "connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("connection_state", false, "not connected");
return;
}
client.publish("state.trans", "test") catch {
reportResult("connection_state", false, "publish failed");
return;
};
const sub = client.subscribeSync("state.trans") catch {
reportResult("connection_state", false, "subscribe failed");
return;
};
defer sub.deinit();
reportResult("connection_state", true, "");
}
/// Verifies client can handle many subscriptions.
pub fn testManyClientSubscriptions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("many_client_subs", false, "connect failed");
return;
};
defer client.deinit();
const NUM_SUBS = 100;
var subs: [NUM_SUBS]?*nats.Subscription =
[_]?*nats.Subscription{null} ** NUM_SUBS;
var created: usize = 0;
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..NUM_SUBS) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"many.subs.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subject) catch {
break;
};
created += 1;
}
if (created != NUM_SUBS) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/100",
.{created},
) catch "e";
reportResult("many_client_subs", false, detail);
return;
}
if (client.isConnected()) {
reportResult("many_client_subs", true, "");
} else {
reportResult("many_client_subs", false, "disconnected");
}
}
pub fn runAll(allocator: std.mem.Allocator, manager: *ServerManager) void {
testConnectionRefused(allocator);
testConsecutiveConnections(allocator);
testIsConnectedState(allocator);
testConnectionStateAfterOps(allocator);
testRapidConnectDisconnect(allocator);
testConnectionOptions(allocator);
testConnectionDrain(allocator);
testInvalidUrlHandling(allocator);
testConnectionStateTransitions(allocator);
testManyClientSubscriptions(allocator);
testReconnection(allocator, manager);
testServerRestartNewConnection(allocator, manager);
}
================================================
FILE: src/testing/client/drain.zig
================================================
//! Drain Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testDrainOperation(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("drain_operation", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.subscribeSync("drain.test.1") catch {
reportResult("drain_operation", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.subscribeSync("drain.test.2") catch {
reportResult("drain_operation", false, "sub2 failed");
return;
};
defer sub2.deinit();
_ = client.drain() catch {
reportResult("drain_operation", false, "drain failed");
return;
};
if (!client.isConnected()) {
reportResult("drain_operation", true, "");
} else {
reportResult("drain_operation", false, "still connected");
}
}
pub fn testDrainCleansUp(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("drain_cleanup", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.subscribeSync("drain.cleanup.1") catch {
reportResult("drain_cleanup", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.subscribeSync("drain.cleanup.2") catch {
reportResult("drain_cleanup", false, "sub2 failed");
return;
};
defer sub2.deinit();
client.publish("drain.cleanup.1", "msg1") catch {};
client.publish("drain.cleanup.2", "msg2") catch {};
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
_ = client.drain() catch {
reportResult("drain_cleanup", false, "drain failed");
return;
};
if (!client.isConnected()) {
reportResult("drain_cleanup", true, "");
} else {
reportResult("drain_cleanup", false, "still connected after drain");
}
}
pub fn testDrainTwice(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("drain_twice", false, "connect failed");
return;
};
defer client.deinit();
_ = client.drain() catch {
reportResult("drain_twice", false, "first drain failed");
return;
};
_ = client.drain() catch {};
reportResult("drain_twice", true, "");
}
pub fn testDrainWithManySubscriptions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("drain_many_subs", false, "connect failed");
return;
};
defer client.deinit();
var subs: [20]?*nats.Subscription = undefined;
@memset(&subs, null);
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
var created: usize = 0;
for (0..20) |i| {
var sub_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&sub_buf,
"drain.many.{d}",
.{i},
) catch {
continue;
};
subs[i] = client.subscribeSync(subject) catch break;
created += 1;
}
_ = client.drain() catch {
reportResult("drain_many_subs", false, "drain failed");
return;
};
if (!client.isConnected() and created >= 15) {
reportResult("drain_many_subs", true, "");
} else {
reportResult("drain_many_subs", false, "unexpected state");
}
}
/// Test subscription waitDrained with messages consumed.
pub fn testSubWaitDrained(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("sub_wait_drained", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("wait.drained") catch {
reportResult("sub_wait_drained", false, "subscribe failed");
return;
};
defer sub.deinit();
// Publish some messages
for (0..5) |_| {
client.publish("wait.drained", "data") catch {};
}
// Wait for messages to arrive
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
// Start draining
sub.drain() catch {
reportResult("sub_wait_drained", false, "drain failed");
return;
};
// Consume all messages
for (0..10) |_| {
const msg = sub.tryNextMsg();
if (msg) |m| {
m.deinit();
} else break;
}
// Now wait for drain to complete (should succeed immediately)
sub.waitDrained(1000) catch |err| {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "waitDrained: {s}", .{
@errorName(err),
}) catch "e";
reportResult("sub_wait_drained", false, detail);
return;
};
// Queue should be empty now
if (sub.pending() == 0) {
reportResult("sub_wait_drained", true, "");
} else {
reportResult("sub_wait_drained", false, "queue not empty");
}
}
/// Test waitDrained returns error.NotDraining if not draining.
pub fn testWaitDrainedNotDraining(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wait_not_draining", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("not.draining") catch {
reportResult("wait_not_draining", false, "subscribe failed");
return;
};
defer sub.deinit();
// Try waitDrained without calling drain() first
sub.waitDrained(100) catch |err| {
if (err == error.NotDraining) {
reportResult("wait_not_draining", true, "");
return;
}
};
reportResult("wait_not_draining", false, "expected NotDraining");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testDrainOperation(allocator);
testDrainCleansUp(allocator);
testDrainTwice(allocator);
testDrainWithManySubscriptions(allocator);
testSubWaitDrained(allocator);
testWaitDrainedNotDraining(allocator);
}
================================================
FILE: src/testing/client/dynamic_jwt.zig
================================================
//! Dynamic JWT Integration Tests
//!
//! Generates operator/account/user JWTs at test time using
//! the auth API, writes a temporary server config, and
//! verifies pub/sub with dynamically generated credentials.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const ServerManager = utils.ServerManager;
const Ed25519 = std.crypto.sign.Ed25519;
const nkey_mod = nats.auth.nkey;
const jwt_mod = nats.auth.jwt;
const creds_mod = nats.auth.creds;
const dynamic_jwt_port = utils.dynamic_jwt_port;
const config_path = "/tmp/nats-dynamic-jwt-test.conf";
const Dir = std.Io.Dir;
/// Deterministic keypair seeds for reproducibility.
const op_raw_seed = [_]u8{11} ** 32;
const acct_raw_seed = [_]u8{22} ** 32;
const user_raw_seed = [_]u8{33} ** 32;
/// Generates all keypairs and JWTs, writes config.
/// Returns formatted credentials string in out_creds.
fn setupDynamicAuth(
io: std.Io,
out_creds: *[8192]u8,
) ?[]const u8 {
// Operator keypair (self-signed)
const op_ed = Ed25519.KeyPair.generateDeterministic(
op_raw_seed,
) catch return null;
const op_kp = nkey_mod.KeyPair{
.kp = op_ed,
.key_type = .operator,
};
// Account keypair
const acct_ed = Ed25519.KeyPair.generateDeterministic(
acct_raw_seed,
) catch return null;
const acct_kp = nkey_mod.KeyPair{
.kp = acct_ed,
.key_type = .account,
};
// User keypair
const user_ed = Ed25519.KeyPair.generateDeterministic(
user_raw_seed,
) catch return null;
const user_kp = nkey_mod.KeyPair{
.kp = user_ed,
.key_type = .user,
};
// Public keys
var op_pk_buf: [56]u8 = undefined;
const op_pub = op_kp.publicKey(&op_pk_buf);
var acct_pk_buf: [56]u8 = undefined;
const acct_pub = acct_kp.publicKey(&acct_pk_buf);
var user_pk_buf: [56]u8 = undefined;
const user_pub = user_kp.publicKey(&user_pk_buf);
// Encode operator JWT (self-signed)
var op_jwt_buf: [2048]u8 = undefined;
const op_jwt = jwt_mod.encodeOperatorClaims(
&op_jwt_buf,
op_pub,
"dyn-operator",
op_kp,
1700000000,
.{},
) catch return null;
// Encode account JWT (signed by operator)
var acct_jwt_buf: [2048]u8 = undefined;
const acct_jwt = jwt_mod.encodeAccountClaims(
&acct_jwt_buf,
acct_pub,
"dyn-account",
op_kp,
1700000000,
.{},
) catch return null;
// Encode user JWT (signed by account)
var user_jwt_buf: [2048]u8 = undefined;
const user_jwt = jwt_mod.encodeUserClaims(
&user_jwt_buf,
user_pub,
"dyn-user",
acct_kp,
1700000000,
.{
.pub_allow = &.{">"},
.sub_allow = &.{">"},
},
) catch return null;
// Encode user seed for creds file
var seed_buf: [58]u8 = undefined;
const user_seed = user_kp.encodeSeed(&seed_buf);
// Format credentials
const creds_str = creds_mod.format(
out_creds,
user_jwt,
user_seed,
) catch return null;
// Write server config file
writeConfig(io, op_jwt, acct_pub, acct_jwt) catch
return null;
return creds_str;
}
/// Writes nats-server config to temp file.
fn writeConfig(
io: std.Io,
op_jwt: []const u8,
acct_pub: []const u8,
acct_jwt: []const u8,
) !void {
const file = try Dir.createFile(
Dir.cwd(),
io,
config_path,
.{},
);
defer file.close(io);
var buf: [4096]u8 = undefined;
var writer = file.writer(io, &buf);
try writer.interface.print(
"operator: {s}\n" ++
"resolver: MEMORY\n" ++
"resolver_preload: {{\n" ++
" {s}: {s}\n" ++
"}}\n",
.{ op_jwt, acct_pub, acct_jwt },
);
try writer.interface.flush();
}
/// Cleans up temp config file.
fn cleanupConfig(io: std.Io) void {
Dir.deleteFile(Dir.cwd(), io, config_path) catch {};
}
/// Tests connecting with dynamically generated JWT creds.
pub fn testDynamicJwtConnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
var creds_buf: [8192]u8 = undefined;
const creds_str = setupDynamicAuth(
io,
&creds_buf,
) orelse {
reportResult(
"dynamic_jwt_connect",
false,
"setup failed",
);
return;
};
// Start server with dynamic config
_ = manager.startServer(allocator, io, .{
.port = dynamic_jwt_port,
.config_file = config_path,
}) catch {
reportResult(
"dynamic_jwt_connect",
false,
"server start failed",
);
cleanupConfig(io);
return;
};
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, dynamic_jwt_port);
const client = nats.Client.connect(
allocator,
io,
url,
.{
.reconnect = false,
.creds = creds_str,
},
) catch |err| {
var ebuf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&ebuf,
"connect: {}",
.{err},
) catch "fmt";
reportResult(
"dynamic_jwt_connect",
false,
detail,
);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult(
"dynamic_jwt_connect",
true,
"",
);
} else {
reportResult(
"dynamic_jwt_connect",
false,
"not connected",
);
}
}
/// Tests pub/sub with dynamically generated JWT creds.
pub fn testDynamicJwtPubSub(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, dynamic_jwt_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
// Regenerate creds (deterministic, same output)
var creds_buf: [8192]u8 = undefined;
const creds_str = setupDynamicAuth(
io,
&creds_buf,
) orelse {
reportResult(
"dynamic_jwt_pubsub",
false,
"setup failed",
);
return;
};
const client = nats.Client.connect(
allocator,
io,
url,
.{
.reconnect = false,
.creds = creds_str,
},
) catch |err| {
var ebuf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&ebuf,
"connect: {}",
.{err},
) catch "fmt";
reportResult(
"dynamic_jwt_pubsub",
false,
detail,
);
return;
};
defer client.deinit();
const sub = client.subscribeSync(
"dynamic.jwt.test",
) catch {
reportResult(
"dynamic_jwt_pubsub",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
const test_msg = "dynamic jwt message";
client.publish(
"dynamic.jwt.test",
test_msg,
) catch {
reportResult(
"dynamic_jwt_pubsub",
false,
"publish failed",
);
return;
};
client.flush(500_000_000) catch {};
if (sub.nextMsgTimeout(
1000,
) catch null) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, test_msg)) {
reportResult(
"dynamic_jwt_pubsub",
true,
"",
);
} else {
reportResult(
"dynamic_jwt_pubsub",
false,
"message mismatch",
);
}
} else {
reportResult(
"dynamic_jwt_pubsub",
false,
"no message received",
);
}
}
/// Runs all dynamic JWT tests.
pub fn runAll(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
testDynamicJwtConnect(allocator, manager);
testDynamicJwtPubSub(allocator);
// Stop dynamic JWT server (last started)
const idx = manager.count() - 1;
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
manager.stopServer(idx, io);
cleanupConfig(io);
}
================================================
FILE: src/testing/client/edge_cases.zig
================================================
//! Edge Cases Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testDoubleUnsubscribe(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("double_unsub", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("double.unsub") catch {
reportResult("double_unsub", false, "sub failed");
return;
};
sub.unsubscribe() catch {};
sub.unsubscribe() catch {};
sub.deinit();
if (client.isConnected()) {
reportResult("double_unsub", true, "");
} else {
reportResult("double_unsub", false, "disconnected");
}
}
pub fn testMessageOrdering(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("message_ordering", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("order") catch {
reportResult("message_ordering", false, "sub failed");
return;
};
defer sub.deinit();
var pub_buf: [5][8]u8 = undefined;
for (0..5) |i| {
const payload = std.fmt.bufPrint(
&pub_buf[i],
"msg-{d}",
.{i},
) catch "e";
client.publish("order", payload) catch {};
}
var in_order = true;
for (0..5) |expected| {
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
var exp_buf: [8]u8 = undefined;
const exp = std.fmt.bufPrint(
&exp_buf,
"msg-{d}",
.{expected},
) catch "e";
if (!std.mem.eql(u8, msg.data, exp)) {
in_order = false;
}
} else |_| {
in_order = false;
break;
}
}
if (in_order) {
reportResult("message_ordering", true, "");
} else {
reportResult("message_ordering", false, "out of order");
}
}
pub fn testBinaryPayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("binary_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("binary") catch {
reportResult("binary_payload", false, "sub failed");
return;
};
defer sub.deinit();
const binary = [_]u8{ 0x00, 0x01, 0x02, 0xFF, 0xFE, 0x00, 0x03 };
client.publish("binary", &binary) catch {
reportResult("binary_payload", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (std.mem.eql(u8, msg.data, &binary)) {
reportResult("binary_payload", true, "");
return;
}
} else |_| {}
reportResult("binary_payload", false, "binary mismatch");
}
pub fn testLongSubjectName(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("long_subject_name", false, "connect failed");
return;
};
defer client.deinit();
const long_subject = "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z" ++
".aa.bb.cc.dd.ee.ff.gg.hh.ii.jj.kk.ll.mm.nn";
const sub = client.subscribeSync(long_subject) catch {
reportResult("long_subject_name", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish(long_subject, "test") catch {
reportResult("long_subject_name", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("long_subject_name", true, "");
} else {
reportResult("long_subject_name", false, "no message");
}
}
pub fn testSubjectWithNumbersHyphens(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("subject_nums_hyphens", false, "connect failed");
return;
};
defer client.deinit();
const subject = "test-123.foo_bar.baz-456";
const sub = client.subscribeSync(subject) catch {
reportResult("subject_nums_hyphens", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish(subject, "test") catch {
reportResult("subject_nums_hyphens", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("subject_nums_hyphens", true, "");
} else {
reportResult("subject_nums_hyphens", false, "no message");
}
}
pub fn testDoubleFlush(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("double_flush", false, "connect failed");
return;
};
defer client.deinit();
client.flushBuffer() catch {
reportResult("double_flush", false, "flush1 failed");
return;
};
client.flushBuffer() catch {
reportResult("double_flush", false, "flush2 failed");
return;
};
client.flushBuffer() catch {
reportResult("double_flush", false, "flush3 failed");
return;
};
reportResult("double_flush", true, "");
}
pub fn testDoubleDrain(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("double_drain", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("double.drain.test") catch {
reportResult("double_drain", false, "subscribe failed");
return;
};
defer sub.deinit();
sub.unsubscribe() catch {};
sub.unsubscribe() catch {};
if (client.isConnected()) {
reportResult("double_drain", true, "");
} else {
reportResult("double_drain", false, "disconnected");
}
}
pub fn testRapidSubUnsubCycles(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("rapid_sub_unsub", false, "connect failed");
return;
};
defer client.deinit();
for (0..20) |_| {
const sub = client.subscribeSync("rapid.cycle.test") catch {
reportResult("rapid_sub_unsub", false, "subscribe failed");
return;
};
sub.unsubscribe() catch {};
sub.deinit();
}
if (client.isConnected()) {
reportResult("rapid_sub_unsub", true, "");
} else {
reportResult("rapid_sub_unsub", false, "disconnected");
}
}
pub fn testNewInboxUniqueness(allocator: std.mem.Allocator) void {
var inboxes: [10][]u8 = undefined;
var created: usize = 0;
const io = utils.newIo(allocator);
defer io.deinit();
defer for (inboxes[0..created]) |inbox| {
allocator.free(inbox);
};
for (0..10) |i| {
inboxes[i] = nats.newInbox(allocator, io.io()) catch {
reportResult("inbox_uniqueness", false, "newInbox failed");
return;
};
created += 1;
for (0..i) |j| {
if (std.mem.eql(u8, inboxes[i], inboxes[j])) {
reportResult("inbox_uniqueness", false, "duplicate inbox");
return;
}
}
}
reportResult("inbox_uniqueness", true, "");
}
pub fn testEmptySubjectFails(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("empty_subject_fails", false, "connect failed");
return;
};
defer client.deinit();
const sub_result = client.subscribeSync("");
if (sub_result) |sub| {
sub.deinit();
reportResult("empty_subject_fails", false, "subscribe should fail");
return;
} else |_| {
// Expected
}
reportResult("empty_subject_fails", true, "");
}
pub fn testSubjectWithSpacesFails(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("subject_spaces_fails", false, "connect failed");
return;
};
defer client.deinit();
const result = client.publish("foo bar", "data");
if (result) |_| {
reportResult("subject_spaces_fails", false, "should have failed");
} else |_| {
reportResult("subject_spaces_fails", true, "");
}
}
pub fn testInterleavedPubSub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("interleaved_pubsub", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("interleave.test") catch {
reportResult("interleaved_pubsub", false, "subscribe failed");
return;
};
defer sub.deinit();
var received: u32 = 0;
for (0..10) |i| {
var buf: [16]u8 = undefined;
const payload = std.fmt.bufPrint(&buf, "msg{d}", .{i}) catch continue;
client.publish("interleave.test", payload) catch {
reportResult("interleaved_pubsub", false, "publish failed");
return;
};
const msg = sub.nextMsgTimeout(500) catch {
continue;
};
if (msg) |m| {
m.deinit();
received += 1;
}
}
if (received == 10) {
reportResult("interleaved_pubsub", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/10",
.{received},
) catch "err";
reportResult("interleaved_pubsub", false, detail);
}
}
pub fn testReceiveOnlyAfterSubscribe(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("receive_after_sub", false, "connect failed");
return;
};
defer client.deinit();
client.publish("timing.test", "before") catch {};
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
const sub = client.subscribeSync("timing.test") catch {
reportResult("receive_after_sub", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("timing.test", "after") catch {};
const msg = sub.nextMsgTimeout(500) catch {
reportResult("receive_after_sub", false, "receive error");
return;
};
if (msg) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "after")) {
reportResult("receive_after_sub", true, "");
} else {
reportResult("receive_after_sub", false, "got wrong message");
}
} else {
reportResult("receive_after_sub", false, "no message");
}
}
pub fn testDataIntegrityPattern(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("data_integrity", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("integrity.test") catch {
reportResult("data_integrity", false, "subscribe failed");
return;
};
defer sub.deinit();
// Create pattern payload
var payload: [256]u8 = undefined;
for (&payload, 0..) |*b, i| {
b.* = @truncate(i);
}
client.publish("integrity.test", &payload) catch {
reportResult("data_integrity", false, "publish failed");
return;
};
const msg = sub.nextMsgTimeout(500) catch {
reportResult("data_integrity", false, "receive failed");
return;
};
if (msg) |m| {
defer m.deinit();
if (m.data.len != 256) {
reportResult("data_integrity", false, "wrong length");
return;
}
for (m.data, 0..) |b, i| {
if (b != @as(u8, @truncate(i))) {
reportResult("data_integrity", false, "data corrupt");
return;
}
}
reportResult("data_integrity", true, "");
} else {
reportResult("data_integrity", false, "no message");
}
}
pub fn testCompletePubSubRoundTrip(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("complete_roundtrip", false, "connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("complete_roundtrip", false, "not connected");
return;
}
const sub = client.subscribeSync("roundtrip.100") catch {
reportResult("complete_roundtrip", false, "subscribe failed");
return;
};
defer sub.deinit();
const before = client.stats();
const test_data = "Test100-RoundTrip-Verification";
client.publish("roundtrip.100", test_data) catch {
reportResult("complete_roundtrip", false, "publish failed");
return;
};
const msg = sub.nextMsgTimeout(1000) catch {
reportResult("complete_roundtrip", false, "receive failed");
return;
};
if (msg == null) {
reportResult("complete_roundtrip", false, "no message");
return;
}
const m = msg.?;
defer m.deinit();
if (!std.mem.eql(u8, m.data, test_data)) {
reportResult("complete_roundtrip", false, "data mismatch");
return;
}
if (!std.mem.eql(u8, m.subject, "roundtrip.100")) {
reportResult("complete_roundtrip", false, "subject mismatch");
return;
}
var after = client.stats();
for (0..50) |_| {
if (after.msgs_out > before.msgs_out and
after.msgs_in > before.msgs_in)
break;
io.io().sleep(.fromMilliseconds(1), .awake) catch {};
after = client.stats();
}
if (after.msgs_out <= before.msgs_out) {
reportResult("complete_roundtrip", false, "msgs_out not updated");
return;
}
if (after.msgs_in <= before.msgs_in) {
reportResult("complete_roundtrip", false, "msgs_in not updated");
return;
}
reportResult("complete_roundtrip", true, "");
}
pub fn testQueueExactCapacity(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const QUEUE_SIZE = 64;
const client = nats.Client.connect(allocator, io.io(), url, .{
.sub_queue_size = QUEUE_SIZE,
.reconnect = false,
}) catch {
reportResult("queue_exact_cap", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("boundary.exact") catch {
reportResult("queue_exact_cap", false, "subscribe failed");
return;
};
defer sub.deinit();
for (0..QUEUE_SIZE) |_| {
client.publish("boundary.exact", "x") catch {
reportResult("queue_exact_cap", false, "publish failed");
return;
};
}
var received: u32 = 0;
for (0..QUEUE_SIZE) |_| {
if (sub.nextMsgTimeout(200) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
if (received == QUEUE_SIZE) {
reportResult("queue_exact_cap", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/64",
.{received},
) catch "e";
reportResult("queue_exact_cap", false, detail);
}
}
pub fn testQueueOverflow(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const QUEUE_SIZE = 32;
const PUBLISH_COUNT = 64;
const client = nats.Client.connect(allocator, io.io(), url, .{
.sub_queue_size = QUEUE_SIZE,
.reconnect = false,
}) catch {
reportResult("queue_overflow", false, "connect failed");
return;
};
defer client.deinit();
const sub_reader = client.subscribeSync("overflow.reader") catch {
reportResult("queue_overflow", false, "sub_reader failed");
return;
};
defer sub_reader.deinit();
const sub_target = client.subscribeSync("overflow.target") catch {
reportResult("queue_overflow", false, "sub_target failed");
return;
};
defer sub_target.deinit();
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
for (0..PUBLISH_COUNT) |_| {
client.publish("overflow.target", "x") catch {
reportResult("queue_overflow", false, "publish target failed");
return;
};
}
client.publish("overflow.reader", "trigger") catch {
reportResult("queue_overflow", false, "publish reader failed");
return;
};
if (sub_reader.nextMsgTimeout(2000) catch null) |m| {
m.deinit();
} else {
reportResult("queue_overflow", false, "reader timeout");
return;
}
var received: u32 = 0;
while (sub_target.tryNextMsg()) |m| {
m.deinit();
received += 1;
}
if (received <= QUEUE_SIZE and received > 0) {
reportResult("queue_overflow", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d} (expect <= {d})",
.{ received, QUEUE_SIZE },
) catch "e";
reportResult("queue_overflow", false, detail);
}
}
pub fn testMaxSubscriptions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_subscriptions", false, "connect failed");
return;
};
defer client.deinit();
const MAX_SUBS = 256;
var subs: [MAX_SUBS]?*nats.Subscription = undefined;
@memset(&subs, null);
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
var created: usize = 0;
for (0..MAX_SUBS) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"maxsub.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subject) catch {
break;
};
created += 1;
}
if (created == MAX_SUBS) {
reportResult("max_subscriptions", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/256",
.{created},
) catch "e";
reportResult("max_subscriptions", false, detail);
}
}
pub fn testLargePayloadHandling(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("large_payload_handling", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("large.payload") catch {
reportResult("large_payload_handling", false, "subscribe failed");
return;
};
defer sub.deinit();
const payload_size = 64 * 1024;
const payload = allocator.alloc(u8, payload_size) catch {
reportResult("large_payload_handling", false, "alloc failed");
return;
};
defer allocator.free(payload);
@memset(payload, 'L');
client.publish("large.payload", payload) catch {
reportResult("large_payload_handling", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(5000) catch null) |m| {
defer m.deinit();
if (m.data.len == payload_size) {
reportResult("large_payload_handling", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d} bytes",
.{m.data.len},
) catch "e";
reportResult("large_payload_handling", false, detail);
}
} else {
reportResult("large_payload_handling", false, "no message");
}
}
pub fn testSubjectLengthBoundary(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("subject_len_boundary", false, "connect failed");
return;
};
defer client.deinit();
var long_subject_buf: [200]u8 = undefined;
for (&long_subject_buf, 0..) |*c, i| {
if (i % 10 == 9 and i < 199) {
c.* = '.';
} else {
c.* = 'a' + @as(u8, @intCast(i % 26));
}
}
const sub = client.subscribeSync(&long_subject_buf) catch {
reportResult("subject_len_boundary", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish(&long_subject_buf, "test") catch {
reportResult("subject_len_boundary", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("subject_len_boundary", true, "");
} else {
reportResult("subject_len_boundary", false, "no message");
}
}
pub fn testZeroLengthPayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("zero_len_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("zero.payload") catch {
reportResult("zero_len_payload", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("zero.payload", "") catch {
reportResult("zero_len_payload", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
defer m.deinit();
if (m.data.len == 0) {
reportResult("zero_len_payload", true, "");
} else {
reportResult("zero_len_payload", false, "non-empty data");
}
} else {
reportResult("zero_len_payload", false, "no message");
}
}
pub fn testSingleBytePayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("single_byte_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("single.byte") catch {
reportResult("single_byte_payload", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("single.byte", "X") catch {
reportResult("single_byte_payload", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
defer m.deinit();
if (m.data.len == 1 and m.data[0] == 'X') {
reportResult("single_byte_payload", true, "");
} else {
reportResult("single_byte_payload", false, "wrong data");
}
} else {
reportResult("single_byte_payload", false, "no message");
}
}
pub fn testSidBoundaries(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("sid_boundaries", false, "connect failed");
return;
};
defer client.deinit();
var last_sid: u64 = 0;
for (0..100) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"sid.test.{d}",
.{i},
) catch continue;
const sub = client.subscribeSync(subject) catch {
reportResult("sid_boundaries", false, "subscribe failed");
return;
};
if (sub.sid <= last_sid and i > 0) {
sub.deinit();
reportResult("sid_boundaries", false, "SID not increasing");
return;
}
last_sid = sub.sid;
sub.deinit();
}
if (client.isConnected()) {
reportResult("sid_boundaries", true, "");
} else {
reportResult("sid_boundaries", false, "disconnected");
}
}
pub fn testMaxSubscriptionsExceeded(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_subs_exceeded", false, "connect failed");
return;
};
defer client.deinit();
const MAX_SUBS = nats.Client.MAX_SUBSCRIPTIONS;
const subs = allocator.alloc(
?*nats.Subscription,
MAX_SUBS,
) catch {
reportResult("max_subs_exceeded", false, "alloc");
return;
};
defer allocator.free(subs);
@memset(subs, null);
defer for (subs) |s| {
if (s) |sub| sub.deinit();
};
var created: usize = 0;
for (0..MAX_SUBS) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"exceedsub.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subject) catch {
break;
};
created += 1;
}
if (created != MAX_SUBS) {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"only created {d}/{d}",
.{ created, MAX_SUBS },
) catch "e";
reportResult("max_subs_exceeded", false, detail);
return;
}
const result = client.subscribeSync("exceedsub.over");
if (result) |sub| {
sub.deinit();
reportResult(
"max_subs_exceeded",
false,
"over-max should fail",
);
} else |err| {
if (err == error.TooManySubscriptions) {
reportResult("max_subs_exceeded", true, "");
} else {
reportResult(
"max_subs_exceeded",
false,
"wrong error type",
);
}
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testDoubleUnsubscribe(allocator);
testMessageOrdering(allocator);
testBinaryPayload(allocator);
testLongSubjectName(allocator);
testSubjectWithNumbersHyphens(allocator);
testDoubleFlush(allocator);
testDoubleDrain(allocator);
testRapidSubUnsubCycles(allocator);
testNewInboxUniqueness(allocator);
testEmptySubjectFails(allocator);
testSubjectWithSpacesFails(allocator);
testInterleavedPubSub(allocator);
testReceiveOnlyAfterSubscribe(allocator);
testDataIntegrityPattern(allocator);
testCompletePubSubRoundTrip(allocator);
// Boundary tests
testQueueExactCapacity(allocator);
testQueueOverflow(allocator);
testMaxSubscriptions(allocator);
testLargePayloadHandling(allocator);
testSubjectLengthBoundary(allocator);
testZeroLengthPayload(allocator);
testSingleBytePayload(allocator);
testSidBoundaries(allocator);
testMaxSubscriptionsExceeded(allocator);
}
================================================
FILE: src/testing/client/error_handling.zig
================================================
//! Error Handling Integration Tests
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const defaults = nats.defaults;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
pub fn testSubjectTooLong(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("subject_too_long", false, "connect failed");
return;
};
defer client.deinit();
const max_len = defaults.Limits.max_subject_len;
var long_subject: [max_len + 1]u8 = undefined;
@memset(&long_subject, 'a');
const result = client.subscribeSync(&long_subject);
if (result) |sub| {
sub.deinit();
reportResult("subject_too_long", false, "should have failed");
} else |err| {
if (err == error.SubjectTooLong) {
reportResult("subject_too_long", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"wrong error: {s}",
.{@errorName(err)},
) catch "fmt error";
reportResult("subject_too_long", false, detail);
}
}
}
pub fn testQueueGroupTooLong(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("queue_group_too_long", false, "connect failed");
return;
};
defer client.deinit();
const max_len = defaults.Limits.max_queue_group_len;
var long_qg: [max_len + 1]u8 = undefined;
@memset(&long_qg, 'q');
const result = client.queueSubscribeSync("test.subject", &long_qg);
if (result) |sub| {
sub.deinit();
reportResult("queue_group_too_long", false, "should have failed");
} else |err| {
if (err == error.QueueGroupTooLong) {
reportResult("queue_group_too_long", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"wrong error: {s}",
.{@errorName(err)},
) catch "fmt error";
reportResult("queue_group_too_long", false, detail);
}
}
}
pub fn testUrlTooLong(allocator: std.mem.Allocator) void {
const io = utils.newIo(allocator);
defer io.deinit();
const max_len = defaults.Server.max_url_len;
var long_url: [max_len + 1]u8 = undefined;
const prefix = "nats://localhost:";
@memcpy(long_url[0..prefix.len], prefix);
@memset(long_url[prefix.len..], '1');
const result = nats.Client.connect(allocator, io.io(), &long_url, .{
.reconnect = false,
});
if (result) |client| {
client.deinit();
reportResult("url_too_long", false, "should have failed");
} else |err| {
if (err == error.UrlTooLong) {
reportResult("url_too_long", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"wrong error: {s}",
.{@errorName(err)},
) catch "fmt error";
reportResult("url_too_long", false, detail);
}
}
}
pub fn testDrainResultIsClean(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("drain_result_clean", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("drain.test") catch {
reportResult("drain_result_clean", false, "subscribe failed");
return;
};
defer sub.deinit();
const result = client.drain() catch {
reportResult("drain_result_clean", false, "drain failed");
return;
};
if (result.isClean()) {
reportResult("drain_result_clean", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"unsub_fail={d} flush={any}",
.{ result.unsub_failures, result.flush_failed },
) catch "fmt error";
reportResult("drain_result_clean", false, detail);
}
}
pub fn testSubjectExactLimit(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("subject_exact_limit", false, "connect failed");
return;
};
defer client.deinit();
// max_subject_len is exclusive (>= rejects it)
const max_len = defaults.Limits.max_subject_len - 1;
var subject_max: [max_len]u8 = undefined;
@memset(&subject_max, 'a');
const sub = client.subscribeSync(&subject_max) catch {
reportResult("subject_exact_limit", false, "subscribe failed");
return;
};
defer sub.deinit();
reportResult("subject_exact_limit", true, "");
}
pub fn testQueueGroupExactLimit(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("qg_exact_limit", false, "connect failed");
return;
};
defer client.deinit();
const max_len = defaults.Limits.max_queue_group_len;
var qg_max: [max_len]u8 = undefined;
@memset(&qg_max, 'q');
const sub = client.queueSubscribeSync("test.subject", &qg_max) catch {
reportResult("qg_exact_limit", false, "subscribe failed");
return;
};
defer sub.deinit();
reportResult("qg_exact_limit", true, "");
}
pub fn testResetErrorNotifications(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("reset_error_notif", false, "connect failed");
return;
};
defer client.deinit();
client.resetErrorNotifications();
reportResult("reset_error_notif", true, "");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testSubjectTooLong(allocator);
testQueueGroupTooLong(allocator);
testUrlTooLong(allocator);
testDrainResultIsClean(allocator);
testSubjectExactLimit(allocator);
testQueueGroupExactLimit(allocator);
testResetErrorNotifications(allocator);
}
================================================
FILE: src/testing/client/flush_confirmed.zig
================================================
//! FlushConfirmed Integration Tests
//!
//! Tests for flushConfirmed() which sends buffered data + PING and waits
//! for PONG confirmation from the server.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
/// Basic flushConfirmed - publish, confirm, verify receipt.
pub fn testFlushConfirmedBasic(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_basic", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("fc.basic") catch {
reportResult("flush_confirmed_basic", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flushBuffer() catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
client.publish("fc.basic", "confirmed-message") catch {
reportResult("flush_confirmed_basic", false, "publish failed");
return;
};
// Use flushConfirmed with 5 second timeout
client.flush(5_000_000_000) catch {
reportResult("flush_confirmed_basic", false, "flushConfirmed failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (std.mem.eql(u8, msg.data, "confirmed-message")) {
reportResult("flush_confirmed_basic", true, "");
return;
}
} else |_| {}
reportResult("flush_confirmed_basic", false, "message mismatch");
}
/// Test flushConfirmed with multiple buffered messages.
pub fn testFlushConfirmedMultipleMessages(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_multi", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("fc.batch") catch {
reportResult("flush_confirmed_multi", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flushBuffer() catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
// Publish 10 messages without flushing
for (0..10) |i| {
var buf: [32]u8 = undefined;
const payload = std.fmt.bufPrint(&buf, "msg-{d}", .{i}) catch "msg";
client.publish("fc.batch", payload) catch {
reportResult("flush_confirmed_multi", false, "publish failed");
return;
};
}
// Single flushConfirmed should send all
client.flush(5_000_000_000) catch {
reportResult("flush_confirmed_multi", false, "flushConfirmed failed");
return;
};
// Verify all 10 messages received
var received: u32 = 0;
for (0..10) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received == 10) {
reportResult("flush_confirmed_multi", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/10",
.{received},
) catch "err";
reportResult("flush_confirmed_multi", false, detail);
}
}
/// Test that normal operations work after flushConfirmed.
pub fn testFlushConfirmedNoSideEffects(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_side_effects", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("fc.side") catch {
reportResult("flush_confirmed_side_effects", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flushBuffer() catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
// First: publish + flushConfirmed
client.publish("fc.side", "first") catch {
reportResult("flush_confirmed_side_effects", false, "pub1 failed");
return;
};
client.flush(5_000_000_000) catch {
reportResult("flush_confirmed_side_effects", false, "flushConfirmed failed");
return;
};
// Second: publish + regular flush (should still work)
client.publish("fc.side", "second") catch {
reportResult("flush_confirmed_side_effects", false, "pub2 failed");
return;
};
client.flushBuffer() catch {
reportResult("flush_confirmed_side_effects", false, "flush failed");
return;
};
// Verify both messages received
var received: u32 = 0;
for (0..2) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received == 2) {
reportResult("flush_confirmed_side_effects", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/2",
.{received},
) catch "err";
reportResult("flush_confirmed_side_effects", false, detail);
}
}
/// Compare flushConfirmed vs flush - both deliver messages.
pub fn testFlushConfirmedVsFlush(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_vs_flush", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("fc.compare") catch {
reportResult("flush_confirmed_vs_flush", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flushBuffer() catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
// Publish with regular flush
client.publish("fc.compare", "via-flush") catch {
reportResult("flush_confirmed_vs_flush", false, "pub1 failed");
return;
};
client.flushBuffer() catch {};
// Publish with flushConfirmed
client.publish("fc.compare", "via-confirmed") catch {
reportResult("flush_confirmed_vs_flush", false, "pub2 failed");
return;
};
client.flush(5_000_000_000) catch {
reportResult("flush_confirmed_vs_flush", false, "flushConfirmed failed");
return;
};
// Verify both arrive in order
const msg1 = sub.nextMsgTimeout(500) catch null;
if (msg1 == null) {
reportResult("flush_confirmed_vs_flush", false, "no msg1");
return;
}
defer msg1.?.deinit();
const msg2 = sub.nextMsgTimeout(500) catch null;
if (msg2 == null) {
reportResult("flush_confirmed_vs_flush", false, "no msg2");
return;
}
defer msg2.?.deinit();
const ok1 = std.mem.eql(u8, msg1.?.data, "via-flush");
const ok2 = std.mem.eql(u8, msg2.?.data, "via-confirmed");
if (ok1 and ok2) {
reportResult("flush_confirmed_vs_flush", true, "");
} else {
reportResult("flush_confirmed_vs_flush", false, "wrong order/content");
}
}
/// Test flushConfirmed returns error when not connected.
pub fn testFlushConfirmedNotConnected(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_not_connected", false, "connect failed");
return;
};
defer client.deinit();
// Drain to close connection
_ = client.drain() catch {
reportResult("flush_confirmed_not_connected", false, "drain failed");
return;
};
// Now try flushConfirmed - should fail
const result = client.flush(1_000_000_000);
if (result) |_| {
reportResult("flush_confirmed_not_connected", false, "should have failed");
} else |_| {
reportResult("flush_confirmed_not_connected", true, "");
}
}
/// Test flushConfirmed with large payload.
pub fn testFlushConfirmedLargePayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_large", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("fc.large") catch {
reportResult("flush_confirmed_large", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flushBuffer() catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
// Allocate 64KB payload
const payload = allocator.alloc(u8, 64 * 1024) catch {
reportResult("flush_confirmed_large", false, "alloc failed");
return;
};
defer allocator.free(payload);
@memset(payload, 'X');
client.publish("fc.large", payload) catch {
reportResult("flush_confirmed_large", false, "publish failed");
return;
};
client.flush(5_000_000_000) catch {
reportResult("flush_confirmed_large", false, "flushConfirmed failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.data.len == 64 * 1024) {
reportResult("flush_confirmed_large", true, "");
return;
}
} else |_| {}
reportResult("flush_confirmed_large", false, "wrong size");
}
/// Test multiple sequential flushConfirmed calls.
pub fn testFlushConfirmedRapidFire(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_rapid", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("fc.rapid") catch {
reportResult("flush_confirmed_rapid", false, "subscribe failed");
return;
};
defer sub.deinit();
client.flushBuffer() catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
// 20 cycles of publish + flushConfirmed
for (0..20) |i| {
var buf: [32]u8 = undefined;
const payload = std.fmt.bufPrint(&buf, "rapid-{d}", .{i}) catch "msg";
client.publish("fc.rapid", payload) catch {
reportResult("flush_confirmed_rapid", false, "publish failed");
return;
};
client.flush(5_000_000_000) catch {
reportResult("flush_confirmed_rapid", false, "flushConfirmed failed");
return;
};
}
// Verify all 20 messages received
var received: u32 = 0;
for (0..20) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received == 20) {
reportResult("flush_confirmed_rapid", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/20",
.{received},
) catch "err";
reportResult("flush_confirmed_rapid", false, detail);
}
}
/// Test flushConfirmed timeout behavior.
pub fn testFlushConfirmedTimeout(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_confirmed_timeout", false, "connect failed");
return;
};
// Drain to disconnect, then try flushConfirmed with short timeout
_ = client.drain() catch {
reportResult("flush_confirmed_timeout", false, "drain failed");
client.deinit();
return;
};
// Should fail quickly (NotConnected, not timeout in this case)
const result = client.flush(100_000_000); // 100ms
client.deinit();
if (result) |_| {
reportResult("flush_confirmed_timeout", false, "should have failed");
} else |err| {
// Accept NotConnected or Timeout as valid failures
if (err == error.NotConnected or err == error.Timeout) {
reportResult("flush_confirmed_timeout", true, "");
} else {
reportResult("flush_confirmed_timeout", false, "unexpected error");
}
}
}
/// Run all flushConfirmed tests.
pub fn runAll(allocator: std.mem.Allocator) void {
testFlushConfirmedBasic(allocator);
testFlushConfirmedMultipleMessages(allocator);
testFlushConfirmedNoSideEffects(allocator);
testFlushConfirmedVsFlush(allocator);
testFlushConfirmedNotConnected(allocator);
testFlushConfirmedLargePayload(allocator);
testFlushConfirmedRapidFire(allocator);
testFlushConfirmedTimeout(allocator);
}
================================================
FILE: src/testing/client/getters.zig
================================================
//! Getters Tests for NATS Client
//!
//! Tests for connection info and subscription info getters.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
pub fn testConnectionInfoGetters(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false, .name = "test-client" },
) catch {
reportResult("connection_info_getters", false, "connect failed");
return;
};
defer client.deinit();
// Test name()
const name = client.name();
if (name == null or !std.mem.eql(u8, name.?, "test-client")) {
reportResult("connection_info_getters", false, "name failed");
return;
}
// Test connectedUrl()
const conn_url = client.connectedUrl();
if (conn_url == null) {
reportResult("connection_info_getters", false, "connectedUrl null");
return;
}
// Test connectedServerId() - should have a value
const server_id = client.connectedServerId();
if (server_id == null or server_id.?.len == 0) {
reportResult("connection_info_getters", false, "connectedServerId");
return;
}
// Test connectedServerVersion() - should have a value
const version = client.connectedServerVersion();
if (version == null or version.?.len == 0) {
reportResult("connection_info_getters", false, "connectedServerVersion");
return;
}
// Test headersSupported() - NATS 2.x supports headers
if (!client.headersSupported()) {
reportResult("connection_info_getters", false, "headersSupported");
return;
}
// Test maxPayload() - should be > 0
if (client.maxPayload() == 0) {
reportResult("connection_info_getters", false, "maxPayload");
return;
}
reportResult("connection_info_getters", true, "");
}
pub fn testServerInfoGetters(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_info_getters", false, "connect failed");
return;
};
defer client.deinit();
// Test tlsRequired() - default server doesn't require TLS
// (this may vary by server config, just check it doesn't crash)
_ = client.tlsRequired();
// Test authRequired() - default server may or may not require auth
_ = client.authRequired();
// Test clientId() - should be set by server
const client_id = client.clientId();
if (client_id == null or client_id.? == 0) {
reportResult("server_info_getters", false, "clientId");
return;
}
// Test serverCount() - should be at least 1
if (client.serverCount() < 1) {
reportResult("server_info_getters", false, "serverCount");
return;
}
reportResult("server_info_getters", true, "");
}
pub fn testConnectedAddrGetter(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("connected_addr_getter", false, "connect failed");
return;
};
defer client.deinit();
var addr_buf: [64]u8 = undefined;
const addr = client.connectedAddr(&addr_buf);
if (addr == null) {
reportResult("connected_addr_getter", false, "connectedAddr null");
return;
}
// Should contain a colon (host:port format)
if (std.mem.indexOf(u8, addr.?, ":") == null) {
reportResult("connected_addr_getter", false, "no colon in addr");
return;
}
reportResult("connected_addr_getter", true, "");
}
pub fn testUrlRedaction(allocator: std.mem.Allocator) void {
// Test URL redaction with credentials
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("url_redaction", false, "connect failed");
return;
};
defer client.deinit();
var redact_buf: [256]u8 = undefined;
const redacted = client.connectedUrlRedacted(&redact_buf);
if (redacted == null) {
reportResult("url_redaction", false, "connectedUrlRedacted null");
return;
}
// URL without credentials should be unchanged
const orig = client.connectedUrl().?;
if (!std.mem.eql(u8, redacted.?, orig)) {
reportResult("url_redaction", false, "mismatch for no-creds url");
return;
}
reportResult("url_redaction", true, "");
}
pub fn testSubscriptionInfoGetters(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("subscription_info_getters", false, "connect failed");
return;
};
defer client.deinit();
// Create a regular subscription
const sub = client.subscribeSync("test.getters") catch {
reportResult("subscription_info_getters", false, "subscribe failed");
return;
};
defer sub.deinit();
// Test getSid() - should be > 0
if (sub.getSid() == 0) {
reportResult("subscription_info_getters", false, "getSid");
return;
}
// Test getSubject()
if (!std.mem.eql(u8, sub.getSubject(), "test.getters")) {
reportResult("subscription_info_getters", false, "getSubject");
return;
}
// Test queueGroup() - should be null for regular sub
if (sub.queueGroup() != null) {
reportResult("subscription_info_getters", false, "queueGroup");
return;
}
// Test isDraining() - should be false initially
if (sub.isDraining()) {
reportResult("subscription_info_getters", false, "isDraining");
return;
}
reportResult("subscription_info_getters", true, "");
}
pub fn testQueueSubGetters(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("queue_sub_getters", false, "connect failed");
return;
};
defer client.deinit();
// Create a queue subscription
const sub = client.queueSubscribeSync(
"test.queue.getters",
"workers",
) catch {
reportResult("queue_sub_getters", false, "subscribe failed");
return;
};
defer sub.deinit();
// Test queueGroup() - should return "workers"
const qg = sub.queueGroup();
if (qg == null or !std.mem.eql(u8, qg.?, "workers")) {
reportResult("queue_sub_getters", false, "queueGroup");
return;
}
reportResult("queue_sub_getters", true, "");
}
pub fn testDrainingState(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("draining_state", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.draining") catch {
reportResult("draining_state", false, "subscribe failed");
return;
};
defer sub.deinit();
// Initially not draining
if (sub.isDraining()) {
reportResult("draining_state", false, "initially draining");
return;
}
// Start drain
sub.drain() catch {
reportResult("draining_state", false, "drain failed");
return;
};
// Now should be draining
if (!sub.isDraining()) {
reportResult("draining_state", false, "not draining after drain()");
return;
}
reportResult("draining_state", true, "");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testConnectionInfoGetters(allocator);
testServerInfoGetters(allocator);
testConnectedAddrGetter(allocator);
testUrlRedaction(allocator);
testSubscriptionInfoGetters(allocator);
testQueueSubGetters(allocator);
testDrainingState(allocator);
}
================================================
FILE: src/testing/client/headers.zig
================================================
//! Headers API Integration Tests
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const headers = nats.protocol.headers;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
pub fn testHeadersPublishSingle(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_publish_single", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.single") catch {
reportResult("headers_publish_single", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "X-Test", .value = "hello" },
};
client.publishWithHeaders("test.headers.single", &hdrs, "payload") catch {
reportResult("headers_publish_single", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_publish_single", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_publish_single", false, "parse error");
return;
}
if (parsed.get("X-Test")) |val| {
if (std.mem.eql(u8, val, "hello")) {
reportResult("headers_publish_single", true, "");
return;
}
}
reportResult("headers_publish_single", false, "header mismatch");
} else |_| {
reportResult("headers_publish_single", false, "receive failed");
}
}
pub fn testHeadersPublishMultiple(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_publish_multiple", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.multi") catch {
reportResult("headers_publish_multiple", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "X-First", .value = "one" },
.{ .key = "X-Second", .value = "two" },
.{ .key = "X-Third", .value = "three" },
};
client.publishWithHeaders("test.headers.multi", &hdrs, "data") catch {
reportResult("headers_publish_multiple", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_publish_multiple", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_publish_multiple", false, "parse error");
return;
}
if (parsed.count != 3) {
reportResult("headers_publish_multiple", false, "wrong count");
return;
}
const has_first = parsed.get("X-First") != null;
const has_second = parsed.get("X-Second") != null;
const has_third = parsed.get("X-Third") != null;
if (has_first and has_second and has_third) {
reportResult("headers_publish_multiple", true, "");
return;
}
reportResult("headers_publish_multiple", false, "missing headers");
} else |_| {
reportResult("headers_publish_multiple", false, "receive failed");
}
}
pub fn testHeadersPublishEmptyPayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_empty_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.empty") catch {
reportResult("headers_empty_payload", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "X-Empty", .value = "yes" },
};
client.publishWithHeaders("test.headers.empty", &hdrs, "") catch {
reportResult("headers_empty_payload", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_empty_payload", false, "no headers");
return;
}
if (msg.data.len != 0) {
reportResult("headers_empty_payload", false, "payload not empty");
return;
}
reportResult("headers_empty_payload", true, "");
} else |_| {
reportResult("headers_empty_payload", false, "receive failed");
}
}
pub fn testHeadersPublishRequest(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_publish_request", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.req") catch {
reportResult("headers_publish_request", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "X-Request-Id", .value = "req-123" },
};
client.publishRequestWithHeaders(
"test.headers.req",
"reply.inbox",
&hdrs,
"request-data",
) catch {
reportResult("headers_publish_request", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_publish_request", false, "no headers");
return;
}
if (msg.reply_to == null) {
reportResult("headers_publish_request", false, "no reply_to");
return;
}
if (!std.mem.eql(u8, msg.reply_to.?, "reply.inbox")) {
reportResult("headers_publish_request", false, "wrong reply_to");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.get("X-Request-Id")) |val| {
if (std.mem.eql(u8, val, "req-123")) {
reportResult("headers_publish_request", true, "");
return;
}
}
reportResult("headers_publish_request", false, "header mismatch");
} else |_| {
reportResult("headers_publish_request", false, "receive failed");
}
}
pub fn testHeadersRequestReply(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
// Responder client
const io_r = utils.newIo(allocator);
defer io_r.deinit();
const responder = nats.Client.connect(
allocator,
io_r.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_request_reply", false, "responder connect failed");
return;
};
defer responder.deinit();
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(
allocator,
io_req.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_request_reply", false, "requester connect failed");
return;
};
defer requester.deinit();
const sub = responder.subscribeSync("svc.headers") catch {
reportResult("headers_request_reply", false, "responder sub failed");
return;
};
defer sub.deinit();
io_r.io().sleep(.fromMilliseconds(50), .awake) catch {};
const Handler = struct {
fn handle(
r: *nats.Client,
s: *nats.Subscription,
) void {
if (s.nextMsgTimeout(2000) catch null) |req| {
defer req.deinit();
if (req.reply_to) |reply_inbox| {
// Verify headers received
if (req.headers != null) {
r.publish(reply_inbox, "headers-received") catch {};
} else {
r.publish(reply_inbox, "no-headers") catch {};
}
}
}
}
};
var handler = io_r.io().async(Handler.handle, .{
responder,
sub,
});
defer _ = handler.cancel(io_r.io());
const hdrs = [_]headers.Entry{
.{ .key = "X-Request-Id", .value = "test-123" },
};
const reply = requester.requestWithHeaders(
"svc.headers",
&hdrs,
"ping",
2000,
) catch {
reportResult("headers_request_reply", false, "request failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "headers-received")) {
reportResult("headers_request_reply", true, "");
return;
}
reportResult("headers_request_reply", false, "headers not received");
return;
}
reportResult("headers_request_reply", false, "no reply");
}
pub fn testHeadersRequestTimeout(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.no_responders = false,
.reconnect = false,
}) catch {
reportResult("headers_request_timeout", false, "connect failed");
return;
};
defer client.deinit();
const hdrs = [_]headers.Entry{
.{ .key = "X-Test", .value = "timeout" },
};
const start = std.Io.Timestamp.now(io.io(), .awake);
const result = client.requestWithHeaders(
"nonexistent.headers.service",
&hdrs,
"ping",
200,
) catch {
reportResult(
"headers_request_timeout",
false,
"request error",
);
return;
};
const end = std.Io.Timestamp.now(io.io(), .awake);
const elapsed = start.durationTo(end);
const elapsed_ns: u64 = @intCast(elapsed.nanoseconds);
const elapsed_ms = elapsed_ns / std.time.ns_per_ms;
if (result) |msg| {
msg.deinit();
}
if (elapsed_ms < 5000) {
reportResult("headers_request_timeout", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"took {d}ms",
.{elapsed_ms},
) catch "e";
reportResult("headers_request_timeout", false, detail);
}
}
pub fn testHeadersCrossClient(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_a = utils.newIo(allocator);
defer io_a.deinit();
const client_a = nats.Client.connect(
allocator,
io_a.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_cross_client", false, "A connect failed");
return;
};
defer client_a.deinit();
const io_b = utils.newIo(allocator);
defer io_b.deinit();
const client_b = nats.Client.connect(
allocator,
io_b.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_cross_client", false, "B connect failed");
return;
};
defer client_b.deinit();
const sub = client_b.subscribeSync("cross.headers") catch {
reportResult("headers_cross_client", false, "B sub failed");
return;
};
defer sub.deinit();
io_b.io().sleep(.fromMilliseconds(50), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "X-From", .value = "client-A" },
.{ .key = "X-Correlation-Id", .value = "corr-456" },
};
client_a.publishWithHeaders("cross.headers", &hdrs, "cross-data") catch {
reportResult("headers_cross_client", false, "A publish failed");
return;
};
if (sub.nextMsgTimeout(2000) catch null) |msg| {
defer msg.deinit();
if (msg.headers == null) {
reportResult("headers_cross_client", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_cross_client", false, "parse error");
return;
}
const from = parsed.get("X-From");
const corr = parsed.get("X-Correlation-Id");
if (from != null and corr != null) {
if (std.mem.eql(u8, from.?, "client-A") and
std.mem.eql(u8, corr.?, "corr-456"))
{
reportResult("headers_cross_client", true, "");
return;
}
}
reportResult("headers_cross_client", false, "header values wrong");
return;
}
reportResult("headers_cross_client", false, "no message received");
}
pub fn testHeadersManyEntries(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_many_entries", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.many") catch {
reportResult("headers_many_entries", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "H00", .value = "v00" }, .{ .key = "H01", .value = "v01" },
.{ .key = "H02", .value = "v02" }, .{ .key = "H03", .value = "v03" },
.{ .key = "H04", .value = "v04" }, .{ .key = "H05", .value = "v05" },
.{ .key = "H06", .value = "v06" }, .{ .key = "H07", .value = "v07" },
.{ .key = "H08", .value = "v08" }, .{ .key = "H09", .value = "v09" },
.{ .key = "H10", .value = "v10" }, .{ .key = "H11", .value = "v11" },
.{ .key = "H12", .value = "v12" }, .{ .key = "H13", .value = "v13" },
.{ .key = "H14", .value = "v14" }, .{ .key = "H15", .value = "v15" },
.{ .key = "H16", .value = "v16" }, .{ .key = "H17", .value = "v17" },
.{ .key = "H18", .value = "v18" }, .{ .key = "H19", .value = "v19" },
.{ .key = "H20", .value = "v20" }, .{ .key = "H21", .value = "v21" },
.{ .key = "H22", .value = "v22" }, .{ .key = "H23", .value = "v23" },
.{ .key = "H24", .value = "v24" }, .{ .key = "H25", .value = "v25" },
.{ .key = "H26", .value = "v26" }, .{ .key = "H27", .value = "v27" },
.{ .key = "H28", .value = "v28" }, .{ .key = "H29", .value = "v29" },
.{ .key = "H30", .value = "v30" }, .{ .key = "H31", .value = "v31" },
.{ .key = "H32", .value = "v32" }, .{ .key = "H33", .value = "v33" },
.{ .key = "H34", .value = "v34" }, .{ .key = "H35", .value = "v35" },
.{ .key = "H36", .value = "v36" }, .{ .key = "H37", .value = "v37" },
.{ .key = "H38", .value = "v38" }, .{ .key = "H39", .value = "v39" },
};
client.publishWithHeaders("test.headers.many", &hdrs, "many-test") catch {
reportResult("headers_many_entries", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_many_entries", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_many_entries", false, "parse error");
return;
}
if (parsed.count != 40) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/40",
.{parsed.count},
) catch "e";
reportResult("headers_many_entries", false, detail);
return;
}
const first = parsed.get("H00");
const last = parsed.get("H39");
if (first != null and last != null) {
if (std.mem.eql(u8, first.?, "v00") and
std.mem.eql(u8, last.?, "v39"))
{
reportResult("headers_many_entries", true, "");
return;
}
}
reportResult("headers_many_entries", false, "header mismatch");
} else |_| {
reportResult("headers_many_entries", false, "receive failed");
}
}
pub fn testHeadersLargeValues(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_large_values", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.large") catch {
reportResult("headers_large_values", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
var large_value: [200]u8 = undefined;
@memset(&large_value, 'X');
const hdrs = [_]headers.Entry{
.{ .key = "X-Large", .value = &large_value },
};
client.publishWithHeaders("test.headers.large", &hdrs, "payload") catch {
reportResult("headers_large_values", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_large_values", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_large_values", false, "parse error");
return;
}
if (parsed.get("X-Large")) |val| {
if (val.len == 200) {
reportResult("headers_large_values", true, "");
return;
}
}
reportResult("headers_large_values", false, "value mismatch");
} else |_| {
reportResult("headers_large_values", false, "receive failed");
}
}
pub fn testHeadersSpecialChars(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_special_chars", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.special") catch {
reportResult("headers_special_chars", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "X-Timestamp", .value = "2026-01-21T10:30:00Z" },
.{ .key = "X-URL", .value = "http://example.com:8080/path" },
};
client.publishWithHeaders("test.headers.special", &hdrs, "data") catch {
reportResult("headers_special_chars", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_special_chars", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_special_chars", false, "parse error");
return;
}
const ts = parsed.get("X-Timestamp");
const url_val = parsed.get("X-URL");
if (ts != null and url_val != null) {
const ts_ok = std.mem.eql(u8, ts.?, "2026-01-21T10:30:00Z");
const url_ok = std.mem.eql(
u8,
url_val.?,
"http://example.com:8080/path",
);
if (ts_ok and url_ok) {
reportResult("headers_special_chars", true, "");
return;
}
}
reportResult("headers_special_chars", false, "value mismatch");
} else |_| {
reportResult("headers_special_chars", false, "receive failed");
}
}
pub fn testHeadersBinaryPayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_binary_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.binary") catch {
reportResult("headers_binary_payload", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const binary_payload = [_]u8{ 0x00, 0x01, 0xFF, 0xFE, 0x7F, 0x80, 0x00, 0xFF };
const hdrs = [_]headers.Entry{
.{ .key = "Content-Type", .value = "application/octet-stream" },
};
client.publishWithHeaders(
"test.headers.binary",
&hdrs,
&binary_payload,
) catch {
reportResult("headers_binary_payload", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_binary_payload", false, "no headers");
return;
}
if (msg.data.len != 8) {
reportResult("headers_binary_payload", false, "wrong payload len");
return;
}
if (std.mem.eql(u8, msg.data, &binary_payload)) {
reportResult("headers_binary_payload", true, "");
return;
}
reportResult("headers_binary_payload", false, "payload mismatch");
} else |_| {
reportResult("headers_binary_payload", false, "receive failed");
}
}
pub fn testHeadersWellKnown(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_well_known", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.wellknown") catch {
reportResult("headers_well_known", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = headers.HeaderName.msg_id, .value = "unique-msg-001" },
};
client.publishWithHeaders("test.headers.wellknown", &hdrs, "data") catch {
reportResult("headers_well_known", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_well_known", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_well_known", false, "parse error");
return;
}
if (parsed.get(headers.HeaderName.msg_id)) |val| {
if (std.mem.eql(u8, val, "unique-msg-001")) {
reportResult("headers_well_known", true, "");
return;
}
}
reportResult("headers_well_known", false, "header not found");
} else |_| {
reportResult("headers_well_known", false, "receive failed");
}
}
pub fn testHeadersCaseInsensitive(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("headers_case_insensitive", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("test.headers.case") catch {
reportResult("headers_case_insensitive", false, "subscribe failed");
return;
};
defer sub.deinit();
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
const hdrs = [_]headers.Entry{
.{ .key = "Content-Type", .value = "application/json" },
};
client.publishWithHeaders("test.headers.case", &hdrs, "{}") catch {
reportResult("headers_case_insensitive", false, "publish failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.headers == null) {
reportResult("headers_case_insensitive", false, "no headers");
return;
}
var parsed = headers.parse(allocator, msg.headers.?);
defer parsed.deinit();
if (parsed.err != null) {
reportResult("headers_case_insensitive", false, "parse error");
return;
}
// Lookup with different case
const val1 = parsed.get("content-type");
const val2 = parsed.get("CONTENT-TYPE");
const val3 = parsed.get("Content-Type");
if (val1 != null and val2 != null and val3 != null) {
reportResult("headers_case_insensitive", true, "");
return;
}
reportResult("headers_case_insensitive", false, "case mismatch");
} else |_| {
reportResult("headers_case_insensitive", false, "receive failed");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testHeadersPublishSingle(allocator);
testHeadersPublishMultiple(allocator);
testHeadersPublishEmptyPayload(allocator);
testHeadersPublishRequest(allocator);
testHeadersRequestReply(allocator);
testHeadersRequestTimeout(allocator);
testHeadersCrossClient(allocator);
testHeadersManyEntries(allocator);
testHeadersLargeValues(allocator);
testHeadersSpecialChars(allocator);
testHeadersBinaryPayload(allocator);
testHeadersWellKnown(allocator);
testHeadersCaseInsensitive(allocator);
}
================================================
FILE: src/testing/client/jetstream.zig
================================================
//! JetStream Integration Tests
//!
//! End-to-end tests for JetStream stream/consumer CRUD,
//! publish with ack, and pull-based fetch.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const reportError = utils.reportError;
const formatUrl = utils.formatUrl;
const ServerManager = utils.ServerManager;
const TestServer = utils.server_manager.TestServer;
const js_port = utils.jetstream_port;
const js_reconnect_port: u16 = 14240;
const test_js_timeout_ms: u32 = 15_000;
fn initTestJetStream(
client: *nats.Client,
) nats.jetstream.JetStream {
return nats.jetstream.JetStream.init(client, .{
.timeout_ms = test_js_timeout_ms,
}) catch unreachable;
}
fn threadSleepNs(ns: u64) void {
var ts: std.posix.timespec = .{
.sec = @intCast(ns / 1_000_000_000),
.nsec = @intCast(ns % 1_000_000_000),
};
_ = std.posix.system.nanosleep(&ts, &ts);
}
var push_heartbeat_err_seen =
std.atomic.Value(bool).init(false);
fn deleteStreamIfExists(
js: *nats.jetstream.JetStream,
name: []const u8,
) void {
var d = js.deleteStream(name) catch return;
d.deinit();
}
fn waitForConnected(
io: std.Io,
client: *nats.Client,
timeout_ms: u32,
) bool {
var waited: u32 = 0;
while (waited < timeout_ms) : (waited += 25) {
if (client.isConnected()) return true;
io.sleep(.fromMilliseconds(25), .awake) catch {};
}
return client.isConnected();
}
fn startJsReconnectServer(
allocator: std.mem.Allocator,
io: std.Io,
) !TestServer {
return TestServer.start(allocator, io, .{
.port = js_reconnect_port,
.jetstream = true,
});
}
fn restartJsReconnectServer(
allocator: std.mem.Allocator,
io: std.Io,
server: *TestServer,
client: *nats.Client,
name: []const u8,
) bool {
server.stop(io);
client.forceReconnect() catch {};
server.* = startJsReconnectServer(allocator, io) catch {
reportResult(name, false, "restart server");
return false;
};
if (!waitForConnected(io, client, 5000)) {
reportResult(name, false, "reconnect timeout");
return false;
}
return true;
}
fn startSharedJsServer(
allocator: std.mem.Allocator,
io: std.Io,
) !TestServer {
return TestServer.start(allocator, io, .{
.port = js_port,
.jetstream = true,
});
}
fn restartSharedJsServer(
allocator: std.mem.Allocator,
io: std.Io,
server: *TestServer,
name: []const u8,
) bool {
server.stop(io);
server.* = startSharedJsServer(allocator, io) catch {
reportResult(name, false, "restart JS server");
return false;
};
return true;
}
fn pushHeartbeatErrHandler(err: anyerror) void {
if (err == error.NoHeartbeat) {
push_heartbeat_err_seen.store(true, .release);
}
}
pub fn testStreamCreateAndInfo(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_stream_create",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream
var resp = js.createStream(.{
.name = "TEST_CREATE",
.subjects = &.{"test.create.>"},
.storage = .memory,
}) catch |err| {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"create failed: {}",
.{err},
) catch "error";
reportResult("js_stream_create", false, msg);
return;
};
defer resp.deinit();
if (resp.value.config) |cfg| {
if (!std.mem.eql(
u8,
cfg.name,
"TEST_CREATE",
)) {
reportResult(
"js_stream_create",
false,
"wrong name",
);
return;
}
} else {
reportResult(
"js_stream_create",
false,
"no config",
);
return;
}
// Get stream info
var info = js.streamInfo(
"TEST_CREATE",
) catch {
reportResult(
"js_stream_create",
false,
"info failed",
);
return;
};
defer info.deinit();
if (info.value.state) |state| {
if (state.messages != 0) {
reportResult(
"js_stream_create",
false,
"expected 0 msgs",
);
return;
}
}
// Cleanup
var del = js.deleteStream(
"TEST_CREATE",
) catch {
reportResult(
"js_stream_create",
false,
"delete failed",
);
return;
};
defer del.deinit();
if (!del.value.success) {
reportResult(
"js_stream_create",
false,
"delete not success",
);
return;
}
reportResult("js_stream_create", true, "");
}
pub fn testPublishAndAck(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_publish_ack",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream
var stream = js.createStream(.{
.name = "TEST_PUB",
.subjects = &.{"test.pub.>"},
.storage = .memory,
}) catch {
reportResult(
"js_publish_ack",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
// Publish
var ack = js.publish(
"test.pub.hello",
"hello world",
) catch |err| {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"publish failed: {}",
.{err},
) catch "error";
reportResult("js_publish_ack", false, msg);
return;
};
defer ack.deinit();
if (ack.value.seq != 1) {
reportResult(
"js_publish_ack",
false,
"expected seq 1",
);
return;
}
if (ack.value.stream) |s| {
if (!std.mem.eql(u8, s, "TEST_PUB")) {
reportResult(
"js_publish_ack",
false,
"wrong stream",
);
return;
}
} else {
reportResult(
"js_publish_ack",
false,
"no stream in ack",
);
return;
}
// Publish second message
var ack2 = js.publish(
"test.pub.world",
"second",
) catch {
reportResult(
"js_publish_ack",
false,
"publish 2 failed",
);
return;
};
defer ack2.deinit();
if (ack2.value.seq != 2) {
reportResult(
"js_publish_ack",
false,
"expected seq 2",
);
return;
}
// Cleanup
var del = js.deleteStream("TEST_PUB") catch {
reportResult(
"js_publish_ack",
false,
"delete failed",
);
return;
};
defer del.deinit();
reportResult("js_publish_ack", true, "");
}
pub fn testConsumerCRUD(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_consumer_crud",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream
var stream = js.createStream(.{
.name = "TEST_CONS",
.subjects = &.{"test.cons.>"},
.storage = .memory,
}) catch {
reportResult(
"js_consumer_crud",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
// Create consumer
var cons = js.createConsumer(
"TEST_CONS",
.{
.name = "my-consumer",
.durable_name = "my-consumer",
.ack_policy = .explicit,
},
) catch |err| {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"create consumer: {}",
.{err},
) catch "error";
reportResult(
"js_consumer_crud",
false,
msg,
);
return;
};
defer cons.deinit();
if (cons.value.name) |n| {
if (!std.mem.eql(u8, n, "my-consumer")) {
reportResult(
"js_consumer_crud",
false,
"wrong consumer name",
);
return;
}
}
// Consumer info
var info = js.consumerInfo(
"TEST_CONS",
"my-consumer",
) catch {
reportResult(
"js_consumer_crud",
false,
"info failed",
);
return;
};
defer info.deinit();
// Delete consumer
var del_c = js.deleteConsumer(
"TEST_CONS",
"my-consumer",
) catch {
reportResult(
"js_consumer_crud",
false,
"delete consumer failed",
);
return;
};
defer del_c.deinit();
if (!del_c.value.success) {
reportResult(
"js_consumer_crud",
false,
"delete not success",
);
return;
}
// Cleanup stream
var del_s = js.deleteStream("TEST_CONS") catch {
reportResult(
"js_consumer_crud",
false,
"delete stream failed",
);
return;
};
defer del_s.deinit();
reportResult("js_consumer_crud", true, "");
}
pub fn testApiError(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_api_error",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Try to get info for non-existent stream
var info = js.streamInfo("NONEXISTENT");
if (info) |*r| {
r.deinit();
reportResult(
"js_api_error",
false,
"expected error",
);
return;
} else |err| {
if (err != error.ApiError) {
reportResult(
"js_api_error",
false,
"wrong error type",
);
return;
}
}
// Check last API error
if (js.lastApiError()) |api_err| {
if (api_err.err_code !=
nats.jetstream.errors.ErrCode.stream_not_found)
{
reportResult(
"js_api_error",
false,
"wrong err_code",
);
return;
}
} else {
reportResult(
"js_api_error",
false,
"no last api error",
);
return;
}
reportResult("js_api_error", true, "");
}
pub fn testStreamNames(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_stream_names",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create 3 streams
var s1 = js.createStream(.{
.name = "NAMES_A",
.subjects = &.{"names.a.>"},
.storage = .memory,
}) catch {
reportResult(
"js_stream_names",
false,
"create A failed",
);
return;
};
defer s1.deinit();
var s2 = js.createStream(.{
.name = "NAMES_B",
.subjects = &.{"names.b.>"},
.storage = .memory,
}) catch {
reportResult(
"js_stream_names",
false,
"create B failed",
);
return;
};
defer s2.deinit();
var s3 = js.createStream(.{
.name = "NAMES_C",
.subjects = &.{"names.c.>"},
.storage = .memory,
}) catch {
reportResult(
"js_stream_names",
false,
"create C failed",
);
return;
};
defer s3.deinit();
// List names
var resp = js.streamNames() catch {
reportResult(
"js_stream_names",
false,
"list failed",
);
return;
};
defer resp.deinit();
const names = resp.value.streams orelse {
reportResult(
"js_stream_names",
false,
"no streams",
);
return;
};
if (names.len < 3) {
reportResult(
"js_stream_names",
false,
"expected >= 3 streams",
);
return;
}
// Cleanup
{
var d1 = js.deleteStream("NAMES_A") catch {
reportResult("js_stream_names", true, "");
return;
};
d1.deinit();
}
{
var d2 = js.deleteStream("NAMES_B") catch {
reportResult("js_stream_names", true, "");
return;
};
d2.deinit();
}
{
var d3 = js.deleteStream("NAMES_C") catch {
reportResult("js_stream_names", true, "");
return;
};
d3.deinit();
}
reportResult("js_stream_names", true, "");
}
pub fn testStreamList(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_stream_list",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s1 = js.createStream(.{
.name = "LIST_A",
.subjects = &.{"list.a.>"},
.storage = .memory,
}) catch {
reportResult(
"js_stream_list",
false,
"create failed",
);
return;
};
defer s1.deinit();
var resp = js.streams() catch {
reportResult(
"js_stream_list",
false,
"list failed",
);
return;
};
defer resp.deinit();
const streams = resp.value.streams orelse {
reportResult(
"js_stream_list",
false,
"no streams",
);
return;
};
if (streams.len < 1) {
reportResult(
"js_stream_list",
false,
"expected >= 1",
);
return;
}
// Verify we get StreamInfo with config
var found = false;
for (streams) |si| {
if (si.config) |cfg| {
if (std.mem.eql(u8, cfg.name, "LIST_A")) {
found = true;
break;
}
}
}
if (!found) {
reportResult(
"js_stream_list",
false,
"LIST_A not found",
);
return;
}
var d = js.deleteStream("LIST_A") catch {
reportResult("js_stream_list", true, "");
return;
};
d.deinit();
reportResult("js_stream_list", true, "");
}
pub fn testConsumerNames(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_consumer_names",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "CONS_NAMES",
.subjects = &.{"cnames.>"},
.storage = .memory,
}) catch {
reportResult(
"js_consumer_names",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
var c1 = js.createConsumer(
"CONS_NAMES",
.{
.name = "cons-alpha",
.durable_name = "cons-alpha",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_consumer_names",
false,
"create cons failed",
);
return;
};
defer c1.deinit();
var c2 = js.createConsumer(
"CONS_NAMES",
.{
.name = "cons-beta",
.durable_name = "cons-beta",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_consumer_names",
false,
"create cons2 failed",
);
return;
};
defer c2.deinit();
var resp = js.consumerNames(
"CONS_NAMES",
) catch {
reportResult(
"js_consumer_names",
false,
"list failed",
);
return;
};
defer resp.deinit();
const names = resp.value.consumers orelse {
reportResult(
"js_consumer_names",
false,
"no consumers",
);
return;
};
if (names.len < 2) {
reportResult(
"js_consumer_names",
false,
"expected >= 2",
);
return;
}
var d = js.deleteStream("CONS_NAMES") catch {
reportResult("js_consumer_names", true, "");
return;
};
d.deinit();
reportResult("js_consumer_names", true, "");
}
pub fn testConsumerList(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_consumer_list",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "CONS_LIST",
.subjects = &.{"clist.>"},
.storage = .memory,
}) catch {
reportResult(
"js_consumer_list",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
var c1 = js.createConsumer(
"CONS_LIST",
.{
.name = "list-cons",
.durable_name = "list-cons",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_consumer_list",
false,
"create cons failed",
);
return;
};
defer c1.deinit();
var resp = js.consumers("CONS_LIST") catch {
reportResult(
"js_consumer_list",
false,
"list failed",
);
return;
};
defer resp.deinit();
const consumers = resp.value.consumers orelse {
reportResult(
"js_consumer_list",
false,
"no consumers",
);
return;
};
if (consumers.len < 1) {
reportResult(
"js_consumer_list",
false,
"expected >= 1",
);
return;
}
// Verify ConsumerInfo has config
if (consumers[0].config) |cfg| {
if (cfg.name) |n| {
if (!std.mem.eql(u8, n, "list-cons")) {
reportResult(
"js_consumer_list",
false,
"wrong name",
);
return;
}
}
} else {
reportResult(
"js_consumer_list",
false,
"no config",
);
return;
}
var d = js.deleteStream("CONS_LIST") catch {
reportResult("js_consumer_list", true, "");
return;
};
d.deinit();
reportResult("js_consumer_list", true, "");
}
pub fn testAccountInfo(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_account_info",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var resp = js.accountInfo() catch {
reportResult(
"js_account_info",
false,
"request failed",
);
return;
};
defer resp.deinit();
if (resp.value.limits == null) {
reportResult(
"js_account_info",
false,
"no limits",
);
return;
}
reportResult("js_account_info", true, "");
}
pub fn testMetadata(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_metadata",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream + consumer
var stream = js.createStream(.{
.name = "TEST_META",
.subjects = &.{"test.meta.>"},
.storage = .memory,
}) catch {
reportResult(
"js_metadata",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
var cons = js.createConsumer(
"TEST_META",
.{
.name = "meta-cons",
.durable_name = "meta-cons",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_metadata",
false,
"create consumer failed",
);
return;
};
defer cons.deinit();
// Publish
var ack = js.publish(
"test.meta.hello",
"metadata test",
) catch {
reportResult(
"js_metadata",
false,
"publish failed",
);
return;
};
defer ack.deinit();
// Fetch and check metadata
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_META",
};
pull.setConsumer("meta-cons") catch unreachable;
var msg = (pull.next(5000) catch {
reportResult(
"js_metadata",
false,
"next failed",
);
return;
}) orelse {
reportResult(
"js_metadata",
false,
"no message",
);
return;
};
defer msg.deinit();
const md = msg.metadata() orelse {
reportResult(
"js_metadata",
false,
"no metadata",
);
return;
};
if (!std.mem.eql(u8, md.stream, "TEST_META")) {
reportResult(
"js_metadata",
false,
"wrong stream",
);
return;
}
if (!std.mem.eql(u8, md.consumer, "meta-cons")) {
reportResult(
"js_metadata",
false,
"wrong consumer",
);
return;
}
if (md.stream_seq != 1) {
reportResult(
"js_metadata",
false,
"expected seq 1",
);
return;
}
// Cleanup
var d = js.deleteStream("TEST_META") catch {
reportResult(
"js_metadata",
false,
"delete failed",
);
return;
};
defer d.deinit();
reportResult("js_metadata", true, "");
}
pub fn testFetchNoWait(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_fetch_no_wait",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_NOWAIT",
.subjects = &.{"test.nowait.>"},
.storage = .memory,
}) catch {
reportResult(
"js_fetch_no_wait",
false,
"create failed",
);
return;
};
defer stream.deinit();
var cons = js.createConsumer(
"TEST_NOWAIT",
.{
.name = "nowait-cons",
.durable_name = "nowait-cons",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_fetch_no_wait",
false,
"create consumer failed",
);
return;
};
defer cons.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_NOWAIT",
};
pull.setConsumer("nowait-cons") catch unreachable;
// Fetch no-wait on empty consumer -> 0 messages
var result = pull.fetchNoWait(10) catch {
reportResult(
"js_fetch_no_wait",
false,
"fetchNoWait failed",
);
return;
};
defer result.deinit();
if (result.count() != 0) {
reportResult(
"js_fetch_no_wait",
false,
"expected 0 messages",
);
return;
}
// Cleanup
var d = js.deleteStream("TEST_NOWAIT") catch {
reportResult(
"js_fetch_no_wait",
false,
"delete failed",
);
return;
};
defer d.deinit();
reportResult("js_fetch_no_wait", true, "");
}
pub fn testMessages(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_messages",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_MSGS",
.subjects = &.{"test.msgs.>"},
.storage = .memory,
}) catch {
reportResult(
"js_messages",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
var cons = js.createConsumer(
"TEST_MSGS",
.{
.name = "msgs-cons",
.durable_name = "msgs-cons",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_messages",
false,
"create consumer failed",
);
return;
};
defer cons.deinit();
// Publish 5 messages
var i: u32 = 0;
while (i < 5) : (i += 1) {
var a = js.publish(
"test.msgs.data",
"hello",
) catch {
reportResult(
"js_messages",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Use messages iterator
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_MSGS",
};
pull.setConsumer("msgs-cons") catch unreachable;
var ctx = pull.messages(.{
.max_messages = 10,
.expires_ms = 5000,
}) catch {
reportResult(
"js_messages",
false,
"messages() failed",
);
return;
};
defer ctx.deinit();
var received: u32 = 0;
while (received < 5) {
var msg = (ctx.next() catch {
break;
}) orelse break;
msg.ack() catch {};
msg.deinit();
received += 1;
}
if (received != 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 5",
.{received},
) catch "count mismatch";
reportResult("js_messages", false, m);
return;
}
var d = js.deleteStream("TEST_MSGS") catch {
reportResult("js_messages", true, "");
return;
};
d.deinit();
reportResult("js_messages", true, "");
}
pub fn testConsume(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_consume",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_CONSUME",
.subjects = &.{"test.consume.>"},
.storage = .memory,
}) catch {
reportResult(
"js_consume",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
var cons = js.createConsumer(
"TEST_CONSUME",
.{
.name = "consume-cons",
.durable_name = "consume-cons",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_consume",
false,
"create consumer failed",
);
return;
};
defer cons.deinit();
// Publish 10 messages
var i: u32 = 0;
while (i < 10) : (i += 1) {
var a = js.publish(
"test.consume.data",
"consume-test",
) catch {
reportResult(
"js_consume",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Use consume() with callback handler
const Counter = struct {
count: u32 = 0,
pub fn onMessage(
self: *@This(),
msg: *nats.jetstream.JsMsg,
) void {
msg.ack() catch {};
self.count += 1;
}
};
var counter = Counter{};
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_CONSUME",
};
pull.setConsumer("consume-cons") catch unreachable;
var ctx = pull.consume(
nats.jetstream.JsMsgHandler.init(
Counter,
&counter,
),
.{
.max_messages = 10,
.expires_ms = 5000,
},
) catch {
reportResult(
"js_consume",
false,
"consume() failed",
);
return;
};
// Wait for messages to be consumed
var wait: u32 = 0;
while (counter.count < 10 and wait < 50) : (wait += 1) {
threadSleepNs(100_000_000);
}
ctx.stop();
ctx.deinit();
if (counter.count < 10) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 10",
.{counter.count},
) catch "count mismatch";
reportResult("js_consume", false, m);
return;
}
var d = js.deleteStream("TEST_CONSUME") catch {
reportResult("js_consume", true, "");
return;
};
d.deinit();
reportResult("js_consume", true, "");
}
pub fn testOrderedConsumer(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_ordered",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_ORDERED",
.subjects = &.{"test.ordered.>"},
.storage = .memory,
}) catch {
reportResult(
"js_ordered",
false,
"create stream failed",
);
return;
};
defer stream.deinit();
// Publish 5 messages
var i: u32 = 0;
while (i < 5) : (i += 1) {
var a = js.publish(
"test.ordered.data",
"ordered-msg",
) catch {
reportResult(
"js_ordered",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Create ordered consumer
var oc = nats.jetstream.OrderedConsumer.init(
&js,
"TEST_ORDERED",
.{
.filter_subject = "test.ordered.>",
.deliver_policy = .all,
},
);
defer oc.deinit();
// Fetch all 5 messages in order
var received: u32 = 0;
var last_seq: u64 = 0;
while (received < 5) {
var msg = (oc.next(5000) catch {
break;
}) orelse break;
// Verify ordering
if (msg.metadata()) |md| {
if (md.stream_seq <= last_seq) {
reportResult(
"js_ordered",
false,
"out of order",
);
msg.deinit();
return;
}
last_seq = md.stream_seq;
}
msg.deinit();
received += 1;
}
if (received != 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 5",
.{received},
) catch "count mismatch";
reportResult("js_ordered", false, m);
return;
}
// Verify stream_seq tracked correctly
if (oc.stream_seq != 5) {
reportResult(
"js_ordered",
false,
"wrong stream_seq",
);
return;
}
var d = js.deleteStream("TEST_ORDERED") catch {
reportResult("js_ordered", true, "");
return;
};
d.deinit();
reportResult("js_ordered", true, "");
}
// -- Ack protocol tests --
pub fn testAckPreventsRedeliver(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_ack", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_ACK",
.subjects = &.{"test.ack.>"},
.storage = .memory,
}) catch {
reportResult("js_ack", false, "create stream");
return;
};
defer s.deinit();
var c = js.createConsumer("TEST_ACK", .{
.name = "ack-cons",
.durable_name = "ack-cons",
.ack_policy = .explicit,
.ack_wait = 1_000_000_000,
}) catch {
reportResult(
"js_ack",
false,
"create consumer",
);
return;
};
defer c.deinit();
// Publish 1 message
var a = js.publish(
"test.ack.one",
"ack-test",
) catch {
reportResult("js_ack", false, "publish");
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_ACK",
};
pull.setConsumer("ack-cons") catch unreachable;
// Fetch and ACK
var msg = (pull.next(5000) catch {
reportResult("js_ack", false, "fetch 1");
return;
}) orelse {
reportResult("js_ack", false, "no msg 1");
return;
};
msg.ack() catch {
reportResult("js_ack", false, "ack failed");
msg.deinit();
return;
};
msg.deinit();
// Fetch again -> should be empty (acked)
var result = pull.fetchNoWait(10) catch {
reportResult("js_ack", false, "fetch 2");
return;
};
defer result.deinit();
if (result.count() != 0) {
reportResult(
"js_ack",
false,
"expected 0 after ack",
);
return;
}
var d = js.deleteStream("TEST_ACK") catch {
reportResult("js_ack", true, "");
return;
};
d.deinit();
reportResult("js_ack", true, "");
}
pub fn testNakCausesRedeliver(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_nak", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_NAK",
.subjects = &.{"test.nak.>"},
.storage = .memory,
}) catch {
reportResult("js_nak", false, "create stream");
return;
};
defer s.deinit();
var c = js.createConsumer("TEST_NAK", .{
.name = "nak-cons",
.durable_name = "nak-cons",
.ack_policy = .explicit,
.ack_wait = 2_000_000_000,
.max_deliver = 3,
}) catch {
reportResult(
"js_nak",
false,
"create consumer",
);
return;
};
defer c.deinit();
var a = js.publish(
"test.nak.one",
"nak-test",
) catch {
reportResult("js_nak", false, "publish");
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_NAK",
};
pull.setConsumer("nak-cons") catch unreachable;
// Fetch and NAK
var msg1 = (pull.next(5000) catch {
reportResult("js_nak", false, "fetch 1");
return;
}) orelse {
reportResult("js_nak", false, "no msg 1");
return;
};
msg1.nak() catch {
reportResult("js_nak", false, "nak failed");
msg1.deinit();
return;
};
msg1.deinit();
// Fetch again -> should get redelivered message
var msg2 = (pull.next(5000) catch {
reportResult("js_nak", false, "fetch 2");
return;
}) orelse {
reportResult(
"js_nak",
false,
"no redeliver",
);
return;
};
// Verify it's the same data
if (!std.mem.eql(u8, msg2.data(), "nak-test")) {
reportResult(
"js_nak",
false,
"wrong redeliver data",
);
msg2.deinit();
return;
}
// Verify num_delivered > 1
if (msg2.metadata()) |md| {
if (md.num_delivered < 2) {
reportResult(
"js_nak",
false,
"expected redeliver count",
);
msg2.deinit();
return;
}
}
msg2.ack() catch {};
msg2.deinit();
var d = js.deleteStream("TEST_NAK") catch {
reportResult("js_nak", true, "");
return;
};
d.deinit();
reportResult("js_nak", true, "");
}
pub fn testTermStopsRedeliver(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_term", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_TERM",
.subjects = &.{"test.term.>"},
.storage = .memory,
}) catch {
reportResult(
"js_term",
false,
"create stream",
);
return;
};
defer s.deinit();
var c = js.createConsumer("TEST_TERM", .{
.name = "term-cons",
.durable_name = "term-cons",
.ack_policy = .explicit,
.max_deliver = 5,
}) catch {
reportResult(
"js_term",
false,
"create consumer",
);
return;
};
defer c.deinit();
var a = js.publish(
"test.term.one",
"term-test",
) catch {
reportResult("js_term", false, "publish");
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_TERM",
};
pull.setConsumer("term-cons") catch unreachable;
// Fetch and TERM
var msg = (pull.next(5000) catch {
reportResult("js_term", false, "fetch");
return;
}) orelse {
reportResult("js_term", false, "no msg");
return;
};
msg.term() catch {
reportResult("js_term", false, "term failed");
msg.deinit();
return;
};
msg.deinit();
// Fetch again -> should be empty (terminated)
var result = pull.fetchNoWait(10) catch {
reportResult("js_term", false, "fetch 2");
return;
};
defer result.deinit();
if (result.count() != 0) {
reportResult(
"js_term",
false,
"expected 0 after term",
);
return;
}
var d = js.deleteStream("TEST_TERM") catch {
reportResult("js_term", true, "");
return;
};
d.deinit();
reportResult("js_term", true, "");
}
// -- Batch fetch tests --
pub fn testBatchFetch(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_batch", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_BATCH",
.subjects = &.{"test.batch.>"},
.storage = .memory,
}) catch {
reportResult(
"js_batch",
false,
"create stream",
);
return;
};
defer s.deinit();
var c = js.createConsumer("TEST_BATCH", .{
.name = "batch-cons",
.durable_name = "batch-cons",
.ack_policy = .explicit,
}) catch {
reportResult(
"js_batch",
false,
"create consumer",
);
return;
};
defer c.deinit();
// Publish 10 messages
var i: u32 = 0;
while (i < 10) : (i += 1) {
var a = js.publish(
"test.batch.data",
"batch-msg",
) catch {
reportResult(
"js_batch",
false,
"publish",
);
return;
};
a.deinit();
}
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_BATCH",
};
pull.setConsumer("batch-cons") catch unreachable;
// Fetch batch of 5
var r1 = pull.fetch(.{
.max_messages = 5,
.timeout_ms = 5000,
}) catch {
reportResult("js_batch", false, "fetch 1");
return;
};
if (r1.count() != 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"batch1: got {d}",
.{r1.count()},
) catch "wrong";
reportResult("js_batch", false, m);
r1.deinit();
return;
}
// Ack all in first batch
for (r1.messages) |*msg| {
msg.ack() catch {};
}
r1.deinit();
// Fetch remaining 5
var r2 = pull.fetch(.{
.max_messages = 5,
.timeout_ms = 5000,
}) catch {
reportResult("js_batch", false, "fetch 2");
return;
};
if (r2.count() != 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"batch2: got {d}",
.{r2.count()},
) catch "wrong";
reportResult("js_batch", false, m);
r2.deinit();
return;
}
for (r2.messages) |*msg| {
msg.ack() catch {};
}
r2.deinit();
// Fetch again -> should be empty
var r3 = pull.fetchNoWait(10) catch {
reportResult("js_batch", false, "fetch 3");
return;
};
defer r3.deinit();
if (r3.count() != 0) {
reportResult(
"js_batch",
false,
"expected 0 after all acked",
);
return;
}
var d = js.deleteStream("TEST_BATCH") catch {
reportResult("js_batch", true, "");
return;
};
d.deinit();
reportResult("js_batch", true, "");
}
// -- Publish options tests --
pub fn testPublishDedup(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_dedup", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_DEDUP",
.subjects = &.{"test.dedup.>"},
.storage = .memory,
.duplicate_window = 60_000_000_000,
}) catch {
reportResult(
"js_dedup",
false,
"create stream",
);
return;
};
defer s.deinit();
// Publish with same msg-id twice
var a1 = js.publishWithOpts(
"test.dedup.data",
"first",
.{ .msg_id = "unique-1" },
) catch {
reportResult("js_dedup", false, "pub 1");
return;
};
const seq1 = a1.value.seq;
a1.deinit();
var a2 = js.publishWithOpts(
"test.dedup.data",
"duplicate",
.{ .msg_id = "unique-1" },
) catch {
reportResult("js_dedup", false, "pub 2");
return;
};
// Should get same seq (deduplicated)
if (a2.value.seq != seq1) {
reportResult(
"js_dedup",
false,
"not deduped",
);
a2.deinit();
return;
}
// Should be marked as duplicate
if (a2.value.duplicate == null or
!a2.value.duplicate.?)
{
reportResult(
"js_dedup",
false,
"no dup flag",
);
a2.deinit();
return;
}
a2.deinit();
// Different msg-id -> new message
var a3 = js.publishWithOpts(
"test.dedup.data",
"second",
.{ .msg_id = "unique-2" },
) catch {
reportResult("js_dedup", false, "pub 3");
return;
};
if (a3.value.seq != seq1 + 1) {
reportResult(
"js_dedup",
false,
"wrong seq for new msg",
);
a3.deinit();
return;
}
a3.deinit();
var d = js.deleteStream("TEST_DEDUP") catch {
reportResult("js_dedup", true, "");
return;
};
d.deinit();
reportResult("js_dedup", true, "");
}
pub fn testPublishExpectedSeq(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_exp_seq", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_EXPSEQ",
.subjects = &.{"test.expseq.>"},
.storage = .memory,
}) catch {
reportResult(
"js_exp_seq",
false,
"create stream",
);
return;
};
defer s.deinit();
// Publish first message
var a1 = js.publish(
"test.expseq.data",
"first",
) catch {
reportResult("js_exp_seq", false, "pub 1");
return;
};
a1.deinit();
// Publish with correct expected_last_seq=1
var a2 = js.publishWithOpts(
"test.expseq.data",
"second",
.{ .expected_last_seq = 1 },
) catch {
reportResult("js_exp_seq", false, "pub 2");
return;
};
a2.deinit();
// Publish with WRONG expected_last_seq=0
// -> should fail
var a3 = js.publishWithOpts(
"test.expseq.data",
"should-fail",
.{ .expected_last_seq = 0 },
);
if (a3) |*r| {
r.deinit();
reportResult(
"js_exp_seq",
false,
"should have failed",
);
return;
} else |err| {
if (err != error.ApiError) {
reportResult(
"js_exp_seq",
false,
"wrong error",
);
return;
}
// Verify the error code
if (js.lastApiError()) |api_err| {
if (api_err.err_code !=
nats.jetstream.errors
.ErrCode.stream_wrong_last_seq)
{
reportResult(
"js_exp_seq",
false,
"wrong err_code",
);
return;
}
}
}
var d = js.deleteStream("TEST_EXPSEQ") catch {
reportResult("js_exp_seq", true, "");
return;
};
d.deinit();
reportResult("js_exp_seq", true, "");
}
// -- Stream operations tests --
pub fn testPurgeStream(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_purge", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_PURGE",
.subjects = &.{"test.purge.>"},
.storage = .memory,
}) catch {
reportResult(
"js_purge",
false,
"create stream",
);
return;
};
defer s.deinit();
// Publish 5 messages
var i: u32 = 0;
while (i < 5) : (i += 1) {
var a = js.publish(
"test.purge.data",
"purge-msg",
) catch {
reportResult(
"js_purge",
false,
"publish",
);
return;
};
a.deinit();
}
// Verify 5 messages exist
var info1 = js.streamInfo("TEST_PURGE") catch {
reportResult("js_purge", false, "info 1");
return;
};
if (info1.value.state) |st| {
if (st.messages != 5) {
reportResult(
"js_purge",
false,
"expected 5 msgs",
);
info1.deinit();
return;
}
}
info1.deinit();
// Purge
var p = js.purgeStream("TEST_PURGE") catch {
reportResult("js_purge", false, "purge");
return;
};
if (!p.value.success) {
reportResult(
"js_purge",
false,
"purge not success",
);
p.deinit();
return;
}
if (p.value.purged != 5) {
reportResult(
"js_purge",
false,
"wrong purge count",
);
p.deinit();
return;
}
p.deinit();
// Verify 0 messages
var info2 = js.streamInfo("TEST_PURGE") catch {
reportResult("js_purge", false, "info 2");
return;
};
defer info2.deinit();
if (info2.value.state) |st| {
if (st.messages != 0) {
reportResult(
"js_purge",
false,
"expected 0 after purge",
);
return;
}
}
var d = js.deleteStream("TEST_PURGE") catch {
reportResult("js_purge", true, "");
return;
};
d.deinit();
reportResult("js_purge", true, "");
}
// -- Stream update test --
pub fn testStreamUpdate(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_update", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_UPDATE",
.subjects = &.{"test.update.>"},
.storage = .memory,
.max_msgs = 100,
}) catch {
reportResult(
"js_update",
false,
"create stream",
);
return;
};
defer s.deinit();
// Update max_msgs
var u = js.updateStream(.{
.name = "TEST_UPDATE",
.subjects = &.{"test.update.>"},
.storage = .memory,
.max_msgs = 200,
}) catch {
reportResult("js_update", false, "update");
return;
};
defer u.deinit();
if (u.value.config) |cfg| {
if (cfg.max_msgs != 200) {
reportResult(
"js_update",
false,
"max_msgs not updated",
);
return;
}
}
var d = js.deleteStream("TEST_UPDATE") catch {
reportResult("js_update", true, "");
return;
};
d.deinit();
reportResult("js_update", true, "");
}
// -- InProgress (WPI) test --
pub fn testInProgress(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("js_wpi", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_WPI",
.subjects = &.{"test.wpi.>"},
.storage = .memory,
}) catch {
reportResult(
"js_wpi",
false,
"create stream",
);
return;
};
defer s.deinit();
var c = js.createConsumer("TEST_WPI", .{
.name = "wpi-cons",
.durable_name = "wpi-cons",
.ack_policy = .explicit,
.ack_wait = 2_000_000_000,
}) catch {
reportResult(
"js_wpi",
false,
"create consumer",
);
return;
};
defer c.deinit();
var a = js.publish(
"test.wpi.one",
"wpi-test",
) catch {
reportResult("js_wpi", false, "publish");
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_WPI",
};
pull.setConsumer("wpi-cons") catch unreachable;
var msg = (pull.next(5000) catch {
reportResult("js_wpi", false, "fetch");
return;
}) orelse {
reportResult("js_wpi", false, "no msg");
return;
};
// Send inProgress to extend deadline
msg.inProgress() catch {
reportResult("js_wpi", false, "wpi failed");
msg.deinit();
return;
};
// Can call inProgress multiple times
msg.inProgress() catch {
reportResult("js_wpi", false, "wpi 2");
msg.deinit();
return;
};
// Now ack
msg.ack() catch {
reportResult("js_wpi", false, "ack");
msg.deinit();
return;
};
msg.deinit();
var d = js.deleteStream("TEST_WPI") catch {
reportResult("js_wpi", true, "");
return;
};
d.deinit();
reportResult("js_wpi", true, "");
}
// -- Consumer not found test --
pub fn testConsumerNotFound(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_cons_not_found",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_CNF",
.subjects = &.{"test.cnf.>"},
.storage = .memory,
}) catch {
reportResult(
"js_cons_not_found",
false,
"create stream",
);
return;
};
defer s.deinit();
var info = js.consumerInfo(
"TEST_CNF",
"nonexistent",
);
if (info) |*r| {
r.deinit();
reportResult(
"js_cons_not_found",
false,
"should fail",
);
return;
} else |err| {
if (err != error.ApiError) {
reportResult(
"js_cons_not_found",
false,
"wrong error",
);
return;
}
if (js.lastApiError()) |api_err| {
if (api_err.err_code !=
nats.jetstream.errors
.ErrCode.consumer_not_found)
{
reportResult(
"js_cons_not_found",
false,
"wrong err_code",
);
return;
}
}
}
var d = js.deleteStream("TEST_CNF") catch {
reportResult(
"js_cons_not_found",
true,
"",
);
return;
};
d.deinit();
reportResult("js_cons_not_found", true, "");
}
// -- Stream by subject test --
pub fn testStreamBySubject(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_by_subject",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_BYSUB",
.subjects = &.{"bysub.>"},
.storage = .memory,
}) catch {
reportResult(
"js_by_subject",
false,
"create stream",
);
return;
};
defer s.deinit();
var resp = js.streamNameBySubject(
"bysub.test",
) catch {
reportResult(
"js_by_subject",
false,
"lookup failed",
);
return;
};
defer resp.deinit();
const names = resp.value.streams orelse {
reportResult(
"js_by_subject",
false,
"no result",
);
return;
};
if (names.len != 1) {
reportResult(
"js_by_subject",
false,
"expected 1 match",
);
return;
}
if (!std.mem.eql(u8, names[0], "TEST_BYSUB")) {
reportResult(
"js_by_subject",
false,
"wrong stream",
);
return;
}
var d = js.deleteStream("TEST_BYSUB") catch {
reportResult("js_by_subject", true, "");
return;
};
d.deinit();
reportResult("js_by_subject", true, "");
}
// -- Key-Value Store tests --
pub fn testKvPutGet(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_put_get", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV",
.storage = .memory,
.history = 5,
}) catch {
reportResult(
"kv_put_get",
false,
"create bucket",
);
return;
};
// Put
const rev1 = kv.put("mykey", "hello") catch {
reportResult("kv_put_get", false, "put");
return;
};
if (rev1 == 0) {
reportResult(
"kv_put_get",
false,
"rev should be > 0",
);
return;
}
// Get
var entry = (kv.get("mykey") catch {
reportResult("kv_put_get", false, "get");
return;
}) orelse {
reportResult(
"kv_put_get",
false,
"key not found",
);
return;
};
defer entry.deinit();
if (entry.revision != rev1) {
reportResult(
"kv_put_get",
false,
"wrong revision",
);
return;
}
if (entry.operation != .put) {
reportResult(
"kv_put_get",
false,
"wrong operation",
);
return;
}
// Get non-existent key
const missing = kv.get("nonexistent") catch {
reportResult(
"kv_put_get",
false,
"get missing err",
);
return;
};
if (missing != null) {
reportResult(
"kv_put_get",
false,
"should be null",
);
return;
}
reportResult("kv_put_get", true, "");
}
pub fn testKvCreate(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_create", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_CREATE",
.storage = .memory,
}) catch |err| {
reportError("kv_create", "create bucket", err);
return;
};
// Create succeeds on new key
_ = kv.create("newkey", "value1") catch |err| {
reportError("kv_create", "create 1", err);
return;
};
// Create fails on existing key
_ = kv.create("newkey", "value2") catch |err| {
if (err == error.ApiError) {
reportResult("kv_create", true, "");
return;
}
reportResult(
"kv_create",
false,
"wrong error",
);
return;
};
reportResult(
"kv_create",
false,
"should have failed",
);
}
pub fn testKvUpdate(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_update", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_UPDATE",
.storage = .memory,
}) catch |err| {
reportError("kv_update", "create bucket", err);
return;
};
const rev1 = kv.put("key1", "v1") catch |err| {
reportError("kv_update", "put", err);
return;
};
// Update with correct revision
const rev2 = kv.update(
"key1",
"v2",
rev1,
) catch {
reportResult(
"kv_update",
false,
"update ok",
);
return;
};
if (rev2 <= rev1) {
reportResult(
"kv_update",
false,
"rev not incremented",
);
return;
}
// Update with wrong revision -> fail
_ = kv.update("key1", "v3", rev1) catch |err| {
if (err == error.ApiError) {
reportResult("kv_update", true, "");
return;
}
reportResult(
"kv_update",
false,
"wrong error",
);
return;
};
reportResult(
"kv_update",
false,
"should have failed",
);
}
pub fn testKvDelete(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_delete", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_DEL",
.storage = .memory,
.history = 5,
}) catch {
reportResult(
"kv_delete",
false,
"create bucket",
);
return;
};
_ = kv.put("delkey", "value") catch {
reportResult("kv_delete", false, "put");
return;
};
// Delete
_ = kv.delete("delkey") catch {
reportResult("kv_delete", false, "delete");
return;
};
// Get should show delete marker
var entry = (kv.get("delkey") catch {
reportResult("kv_delete", false, "get");
return;
}) orelse {
// Key gone completely (ok for history=1)
reportResult("kv_delete", true, "");
return;
};
defer entry.deinit();
if (entry.operation != .delete) {
reportResult(
"kv_delete",
false,
"expected delete op",
);
return;
}
reportResult("kv_delete", true, "");
}
pub fn testKvKeys(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_keys", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_KEYS",
.storage = .memory,
}) catch {
reportResult(
"kv_keys",
false,
"create bucket",
);
return;
};
// Put 3 keys
_ = kv.put("alpha", "1") catch {
reportResult("kv_keys", false, "put 1");
return;
};
_ = kv.put("beta", "2") catch {
reportResult("kv_keys", false, "put 2");
return;
};
_ = kv.put("gamma", "3") catch {
reportResult("kv_keys", false, "put 3");
return;
};
const key_list = kv.keys(allocator) catch {
reportResult("kv_keys", false, "keys()");
return;
};
defer {
for (key_list) |k| allocator.free(k);
allocator.free(key_list);
}
if (key_list.len != 3) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d} keys, expected 3",
.{key_list.len},
) catch "wrong count";
reportResult("kv_keys", false, m);
return;
}
reportResult("kv_keys", true, "");
}
pub fn testKvHistory(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_history", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_HIST",
.storage = .memory,
.history = 10,
}) catch {
reportResult(
"kv_history",
false,
"create bucket",
);
return;
};
// Put same key 3 times
_ = kv.put("hkey", "v1") catch {
reportResult("kv_history", false, "put 1");
return;
};
_ = kv.put("hkey", "v2") catch {
reportResult("kv_history", false, "put 2");
return;
};
_ = kv.put("hkey", "v3") catch {
reportResult("kv_history", false, "put 3");
return;
};
const hist = kv.history(
allocator,
"hkey",
) catch {
reportResult(
"kv_history",
false,
"history()",
);
return;
};
defer {
for (hist) |*h| h.deinit();
allocator.free(hist);
}
if (hist.len != 3) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 3",
.{hist.len},
) catch "wrong";
reportResult("kv_history", false, m);
return;
}
// Verify revisions are increasing
if (hist.len >= 2) {
if (hist[1].revision <= hist[0].revision) {
reportResult(
"kv_history",
false,
"revs not increasing",
);
return;
}
}
reportResult("kv_history", true, "");
}
pub fn testKvWatch(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("kv_watch", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_WATCH",
.storage = .memory,
}) catch |err| {
reportError("kv_watch", "create bucket", err);
return;
};
// Put a key before watching
_ = kv.put("pre-watch", "initial") catch |err| {
reportError("kv_watch", "put", err);
return;
};
// Start watching
var watcher = kv.watchAll() catch {
reportResult(
"kv_watch",
false,
"watchAll()",
);
return;
};
defer watcher.deinit();
// Should get the initial key
var entry = (watcher.next(5000) catch |err| {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"watch next: {}",
.{err},
) catch "watch err";
reportResult("kv_watch", false, m);
return;
}) orelse {
reportResult(
"kv_watch",
false,
"no initial entry",
);
return;
};
defer entry.deinit();
if (!std.mem.eql(u8, entry.key, "pre-watch")) {
reportResult(
"kv_watch",
false,
"wrong key",
);
return;
}
reportResult("kv_watch", true, "");
}
pub fn testKvBucketLifecycle(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_lifecycle",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create
var kv = js.createKeyValue(.{
.bucket = "TEST_KV_LIFE",
.storage = .memory,
}) catch {
reportResult(
"kv_lifecycle",
false,
"create",
);
return;
};
// Status
var st = kv.status() catch {
reportResult(
"kv_lifecycle",
false,
"status",
);
return;
};
defer st.deinit();
if (st.value.config) |cfg| {
if (!std.mem.eql(
u8,
cfg.name,
"KV_TEST_KV_LIFE",
)) {
reportResult(
"kv_lifecycle",
false,
"wrong stream name",
);
return;
}
}
// Bind
const kv2 = js.keyValue("TEST_KV_LIFE") catch {
reportResult(
"kv_lifecycle",
false,
"bind",
);
return;
};
_ = kv2;
// Delete
var del = js.deleteKeyValue(
"TEST_KV_LIFE",
) catch {
reportResult(
"kv_lifecycle",
false,
"delete",
);
return;
};
defer del.deinit();
if (!del.value.success) {
reportResult(
"kv_lifecycle",
false,
"delete failed",
);
return;
}
// Bind to deleted bucket -> should fail
_ = js.keyValue("TEST_KV_LIFE") catch {
reportResult("kv_lifecycle", true, "");
return;
};
reportResult(
"kv_lifecycle",
false,
"bind should fail after delete",
);
}
// -- Behavioral correctness tests --
pub fn testFilteredConsumer(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_filtered_cons",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_FILTER",
.subjects = &.{"test.filter.>"},
.storage = .memory,
}) catch {
reportResult(
"js_filtered_cons",
false,
"create stream",
);
return;
};
defer s.deinit();
// Publish to different subjects
var a1 = js.publish(
"test.filter.a",
"msg-a",
) catch {
reportResult(
"js_filtered_cons",
false,
"pub a",
);
return;
};
a1.deinit();
var a2 = js.publish(
"test.filter.b",
"msg-b",
) catch {
reportResult(
"js_filtered_cons",
false,
"pub b",
);
return;
};
a2.deinit();
// Create consumer filtered on "test.filter.a"
var c = js.createConsumer("TEST_FILTER", .{
.name = "filter-cons",
.durable_name = "filter-cons",
.ack_policy = .explicit,
.filter_subject = "test.filter.a",
}) catch |err| {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"create cons: {}",
.{err},
) catch "err";
reportResult("js_filtered_cons", false, m);
return;
};
defer c.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_FILTER",
};
pull.setConsumer("filter-cons") catch unreachable;
// Should only get "msg-a" (filtered)
var msg = (pull.next(5000) catch {
reportResult(
"js_filtered_cons",
false,
"fetch",
);
return;
}) orelse {
reportResult(
"js_filtered_cons",
false,
"no msg",
);
return;
};
if (!std.mem.eql(u8, msg.data(), "msg-a")) {
reportResult(
"js_filtered_cons",
false,
"wrong data",
);
msg.deinit();
return;
}
msg.ack() catch {};
msg.deinit();
// No more messages (msg-b filtered out)
var r = pull.fetchNoWait(10) catch {
reportResult(
"js_filtered_cons",
false,
"fetch 2",
);
return;
};
defer r.deinit();
if (r.count() != 0) {
reportResult(
"js_filtered_cons",
false,
"expected 0 after filter",
);
return;
}
var d = js.deleteStream("TEST_FILTER") catch {
reportResult("js_filtered_cons", true, "");
return;
};
d.deinit();
reportResult("js_filtered_cons", true, "");
}
pub fn testPurgeSubject(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_purge_subj",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_PURGE_S",
.subjects = &.{"test.purge.s.>"},
.storage = .memory,
}) catch {
reportResult(
"js_purge_subj",
false,
"create stream",
);
return;
};
defer s.deinit();
// Publish to 2 subjects
var i: u32 = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"test.purge.s.keep",
"keep",
) catch {
reportResult(
"js_purge_subj",
false,
"pub keep",
);
return;
};
a.deinit();
}
i = 0;
while (i < 2) : (i += 1) {
var a = js.publish(
"test.purge.s.remove",
"remove",
) catch {
reportResult(
"js_purge_subj",
false,
"pub remove",
);
return;
};
a.deinit();
}
// Purge only "remove" subject
var p = js.purgeStreamSubject(
"TEST_PURGE_S",
"test.purge.s.remove",
) catch {
reportResult(
"js_purge_subj",
false,
"purge",
);
return;
};
defer p.deinit();
if (p.value.purged != 2) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"purged {d}, expected 2",
.{p.value.purged},
) catch "wrong count";
reportResult("js_purge_subj", false, m);
return;
}
// Verify "keep" messages still exist
var info = js.streamInfo("TEST_PURGE_S") catch {
reportResult(
"js_purge_subj",
false,
"info",
);
return;
};
defer info.deinit();
if (info.value.state) |st| {
if (st.messages != 3) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"{d} msgs, expected 3",
.{st.messages},
) catch "wrong";
reportResult(
"js_purge_subj",
false,
m,
);
return;
}
}
var d = js.deleteStream("TEST_PURGE_S") catch {
reportResult("js_purge_subj", true, "");
return;
};
d.deinit();
reportResult("js_purge_subj", true, "");
}
pub fn testPaginatedStreamNames(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_paginated",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create 3 streams
var i: u32 = 0;
while (i < 3) : (i += 1) {
var name_buf: [32]u8 = undefined;
const sname = std.fmt.bufPrint(
&name_buf,
"PAG_{d}",
.{i},
) catch unreachable;
var subj_b: [32]u8 = undefined;
const ssubj = std.fmt.bufPrint(
&subj_b,
"pag.{d}.>",
.{i},
) catch unreachable;
const subjects: [1][]const u8 = .{ssubj};
var r = js.createStream(.{
.name = sname,
.subjects = &subjects,
.storage = .memory,
}) catch {
reportResult(
"js_paginated",
false,
"create",
);
return;
};
r.deinit();
}
// Use allStreamNames (pagination)
const all = js.allStreamNames(allocator) catch {
reportResult(
"js_paginated",
false,
"allStreamNames",
);
return;
};
defer {
for (all) |n| allocator.free(n);
allocator.free(all);
}
if (all.len < 3) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected >= 3",
.{all.len},
) catch "wrong";
reportResult("js_paginated", false, m);
return;
}
// Cleanup
i = 0;
while (i < 3) : (i += 1) {
var name_buf: [32]u8 = undefined;
const sname = std.fmt.bufPrint(
&name_buf,
"PAG_{d}",
.{i},
) catch unreachable;
var r = js.deleteStream(sname) catch continue;
r.deinit();
}
reportResult("js_paginated", true, "");
}
pub fn testGetMsg(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_get_msg",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_GETMSG",
.subjects = &.{"test.getmsg.>"},
.storage = .memory,
}) catch {
reportResult(
"js_get_msg",
false,
"create stream",
);
return;
};
defer stream.deinit();
// Publish 3 messages
var i: u32 = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"test.getmsg.a",
"payload",
) catch {
reportResult(
"js_get_msg",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Get message at seq 1
var resp = js.getMsg(
"TEST_GETMSG",
1,
) catch {
reportResult(
"js_get_msg",
false,
"getMsg failed",
);
return;
};
defer resp.deinit();
if (resp.value.message) |m| {
if (m.seq != 1) {
reportResult(
"js_get_msg",
false,
"expected seq 1",
);
return;
}
} else {
reportResult(
"js_get_msg",
false,
"no message",
);
return;
}
// Get non-existent seq -> ApiError
var bad = js.getMsg("TEST_GETMSG", 999);
if (bad) |*r| {
r.deinit();
reportResult(
"js_get_msg",
false,
"should fail for 999",
);
return;
} else |err| {
if (err != error.ApiError) {
reportResult(
"js_get_msg",
false,
"wrong error type",
);
return;
}
if (js.lastApiError()) |ae| {
if (ae.err_code !=
nats.jetstream.errors
.ErrCode.message_not_found)
{
reportResult(
"js_get_msg",
false,
"wrong err_code",
);
return;
}
}
}
var d = js.deleteStream(
"TEST_GETMSG",
) catch {
reportResult("js_get_msg", true, "");
return;
};
d.deinit();
reportResult("js_get_msg", true, "");
}
pub fn testGetLastMsgForSubject(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_get_last_msg",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_GETLAST",
.subjects = &.{"test.getlast.>"},
.storage = .memory,
}) catch {
reportResult(
"js_get_last_msg",
false,
"create stream",
);
return;
};
defer stream.deinit();
// Publish 3 msgs to test.getlast.a
var i: u32 = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"test.getlast.a",
"msg-a",
) catch {
reportResult(
"js_get_last_msg",
false,
"pub a failed",
);
return;
};
a.deinit();
}
// Publish 2 msgs to test.getlast.b
i = 0;
while (i < 2) : (i += 1) {
var a = js.publish(
"test.getlast.b",
"msg-b",
) catch {
reportResult(
"js_get_last_msg",
false,
"pub b failed",
);
return;
};
a.deinit();
}
// Last for subject "a" should be seq 3
var ra = js.getLastMsgForSubject(
"TEST_GETLAST",
"test.getlast.a",
) catch {
reportResult(
"js_get_last_msg",
false,
"getLast a failed",
);
return;
};
defer ra.deinit();
if (ra.value.message) |m| {
if (m.seq != 3) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"a: got {d}, want 3",
.{m.seq},
) catch "wrong seq";
reportResult(
"js_get_last_msg",
false,
msg,
);
return;
}
} else {
reportResult(
"js_get_last_msg",
false,
"no msg for a",
);
return;
}
// Last for subject "b" should be seq 5
var rb = js.getLastMsgForSubject(
"TEST_GETLAST",
"test.getlast.b",
) catch {
reportResult(
"js_get_last_msg",
false,
"getLast b failed",
);
return;
};
defer rb.deinit();
if (rb.value.message) |m| {
if (m.seq != 5) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"b: got {d}, want 5",
.{m.seq},
) catch "wrong seq";
reportResult(
"js_get_last_msg",
false,
msg,
);
return;
}
} else {
reportResult(
"js_get_last_msg",
false,
"no msg for b",
);
return;
}
var d = js.deleteStream(
"TEST_GETLAST",
) catch {
reportResult(
"js_get_last_msg",
true,
"",
);
return;
};
d.deinit();
reportResult("js_get_last_msg", true, "");
}
pub fn testDeleteMsg(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_delete_msg",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_DELMSG",
.subjects = &.{"test.delmsg.>"},
.storage = .memory,
}) catch {
reportResult(
"js_delete_msg",
false,
"create stream",
);
return;
};
defer stream.deinit();
// Publish 5 messages
var i: u32 = 0;
while (i < 5) : (i += 1) {
var a = js.publish(
"test.delmsg.a",
"payload",
) catch {
reportResult(
"js_delete_msg",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Delete seq 3
var del = js.deleteMsg(
"TEST_DELMSG",
3,
) catch {
reportResult(
"js_delete_msg",
false,
"deleteMsg failed",
);
return;
};
defer del.deinit();
if (!del.value.success) {
reportResult(
"js_delete_msg",
false,
"delete not success",
);
return;
}
// getMsg(3) should fail
var bad = js.getMsg("TEST_DELMSG", 3);
if (bad) |*r| {
r.deinit();
reportResult(
"js_delete_msg",
false,
"seq 3 should be gone",
);
return;
} else |_| {}
// getMsg(2) should still work
var ok = js.getMsg(
"TEST_DELMSG",
2,
) catch {
reportResult(
"js_delete_msg",
false,
"seq 2 should exist",
);
return;
};
ok.deinit();
var d = js.deleteStream(
"TEST_DELMSG",
) catch {
reportResult(
"js_delete_msg",
true,
"",
);
return;
};
d.deinit();
reportResult("js_delete_msg", true, "");
}
pub fn testSecureDeleteMsg(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_secure_del",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_SECDEL",
.subjects = &.{"test.secdel.>"},
.storage = .memory,
}) catch {
reportResult(
"js_secure_del",
false,
"create stream",
);
return;
};
defer stream.deinit();
// Publish 3 messages
var i: u32 = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"test.secdel.a",
"payload",
) catch {
reportResult(
"js_secure_del",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Secure delete seq 2
var del = js.secureDeleteMsg(
"TEST_SECDEL",
2,
) catch {
reportResult(
"js_secure_del",
false,
"secureDelete failed",
);
return;
};
defer del.deinit();
if (!del.value.success) {
reportResult(
"js_secure_del",
false,
"delete not success",
);
return;
}
// getMsg(2) should fail
var bad = js.getMsg("TEST_SECDEL", 2);
if (bad) |*r| {
r.deinit();
reportResult(
"js_secure_del",
false,
"seq 2 should be gone",
);
return;
} else |_| {}
// getMsg(1) should still work
var ok = js.getMsg(
"TEST_SECDEL",
1,
) catch {
reportResult(
"js_secure_del",
false,
"seq 1 should exist",
);
return;
};
ok.deinit();
var d = js.deleteStream(
"TEST_SECDEL",
) catch {
reportResult(
"js_secure_del",
true,
"",
);
return;
};
d.deinit();
reportResult("js_secure_del", true, "");
}
pub fn testCreateOrUpdateStream(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_upsert_stream",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create via createOrUpdate
var r1 = js.createOrUpdateStream(.{
.name = "TEST_UPSERT",
.subjects = &.{"upsert.>"},
.storage = .memory,
.max_msgs = 100,
}) catch {
reportResult(
"js_upsert_stream",
false,
"create failed",
);
return;
};
r1.deinit();
// Update via createOrUpdate
var r2 = js.createOrUpdateStream(.{
.name = "TEST_UPSERT",
.subjects = &.{"upsert.>"},
.storage = .memory,
.max_msgs = 200,
}) catch {
reportResult(
"js_upsert_stream",
false,
"update failed",
);
return;
};
r2.deinit();
// Verify updated config
var info = js.streamInfo(
"TEST_UPSERT",
) catch {
reportResult(
"js_upsert_stream",
false,
"info failed",
);
return;
};
defer info.deinit();
if (info.value.config) |cfg| {
if (cfg.max_msgs) |mm| {
if (mm != 200) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"max_msgs {d}, want 200",
.{mm},
) catch "wrong";
reportResult(
"js_upsert_stream",
false,
m,
);
return;
}
} else {
reportResult(
"js_upsert_stream",
false,
"no max_msgs",
);
return;
}
} else {
reportResult(
"js_upsert_stream",
false,
"no config",
);
return;
}
var d = js.deleteStream(
"TEST_UPSERT",
) catch {
reportResult(
"js_upsert_stream",
true,
"",
);
return;
};
d.deinit();
reportResult("js_upsert_stream", true, "");
}
pub fn testCreateOrUpdateConsumer(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_upsert_cons",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_UCONS",
.subjects = &.{"ucons.>"},
.storage = .memory,
}) catch {
reportResult(
"js_upsert_cons",
false,
"create stream",
);
return;
};
defer stream.deinit();
// Create consumer
var c1 = js.createOrUpdateConsumer(
"TEST_UCONS",
.{
.name = "upsert-c",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_upsert_cons",
false,
"create cons",
);
return;
};
c1.deinit();
// Update consumer
var c2 = js.createOrUpdateConsumer(
"TEST_UCONS",
.{
.name = "upsert-c",
.ack_policy = .explicit,
.max_ack_pending = 500,
},
) catch {
reportResult(
"js_upsert_cons",
false,
"update cons",
);
return;
};
c2.deinit();
// Verify updated config
var info = js.consumerInfo(
"TEST_UCONS",
"upsert-c",
) catch {
reportResult(
"js_upsert_cons",
false,
"info failed",
);
return;
};
defer info.deinit();
if (info.value.config) |cfg| {
if (cfg.max_ack_pending) |mp| {
if (mp != 500) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"max_ack {d}, want 500",
.{mp},
) catch "wrong";
reportResult(
"js_upsert_cons",
false,
m,
);
return;
}
} else {
reportResult(
"js_upsert_cons",
false,
"no max_ack_pending",
);
return;
}
} else {
reportResult(
"js_upsert_cons",
false,
"no config",
);
return;
}
var d = js.deleteStream(
"TEST_UCONS",
) catch {
reportResult(
"js_upsert_cons",
true,
"",
);
return;
};
d.deinit();
reportResult("js_upsert_cons", true, "");
}
pub fn testPauseResumeConsumer(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_pause_resume",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_PAUSE",
.subjects = &.{"pause.>"},
.storage = .memory,
}) catch {
reportResult(
"js_pause_resume",
false,
"create stream",
);
return;
};
defer stream.deinit();
var cons = js.createConsumer(
"TEST_PAUSE",
.{
.name = "pause-c",
.durable_name = "pause-c",
.ack_policy = .explicit,
},
) catch {
reportResult(
"js_pause_resume",
false,
"create cons",
);
return;
};
cons.deinit();
// Pause consumer
var pr = js.pauseConsumer(
"TEST_PAUSE",
"pause-c",
"2099-01-01T00:00:00Z",
) catch {
reportResult(
"js_pause_resume",
false,
"pause failed",
);
return;
};
defer pr.deinit();
if (!pr.value.paused) {
reportResult(
"js_pause_resume",
false,
"not paused",
);
return;
}
// Resume consumer
var rr = js.resumeConsumer(
"TEST_PAUSE",
"pause-c",
) catch {
reportResult(
"js_pause_resume",
false,
"resume failed",
);
return;
};
defer rr.deinit();
if (rr.value.paused) {
reportResult(
"js_pause_resume",
false,
"still paused",
);
return;
}
// Publish + fetch after resume
var a = js.publish(
"pause.test",
"after-resume",
) catch {
reportResult(
"js_pause_resume",
false,
"publish failed",
);
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_PAUSE",
};
pull.setConsumer("pause-c") catch unreachable;
var msg = (pull.next(5000) catch {
reportResult(
"js_pause_resume",
false,
"fetch failed",
);
return;
}) orelse {
reportResult(
"js_pause_resume",
false,
"no msg after resume",
);
return;
};
msg.ack() catch {};
msg.deinit();
var d = js.deleteStream(
"TEST_PAUSE",
) catch {
reportResult(
"js_pause_resume",
true,
"",
);
return;
};
d.deinit();
reportResult("js_pause_resume", true, "");
}
pub fn testPushConsumerBasic(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_push_basic",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Clean up from prior runs
if (js.deleteStream("TEST_PUSH")) |r| {
var rr = r;
rr.deinit();
} else |_| {}
var stream = js.createStream(.{
.name = "TEST_PUSH",
.subjects = &.{"push.>"},
.storage = .memory,
}) catch {
reportResult(
"js_push_basic",
false,
"create stream",
);
return;
};
defer stream.deinit();
// Publish 5 messages first
var i: u32 = 0;
while (i < 5) : (i += 1) {
var a = js.publish(
"push.test",
"push-data",
) catch {
reportResult(
"js_push_basic",
false,
"publish failed",
);
return;
};
a.deinit();
}
// Set up push subscription handler
const Counter = struct {
count: u32 = 0,
pub fn onMessage(
self: *@This(),
msg: *nats.jetstream.JsMsg,
) void {
_ = msg;
self.count += 1;
}
};
var counter = Counter{};
// Subscribe to deliver subject BEFORE creating
// the consumer -- otherwise server pushes before
// we're listening and messages are lost.
const deliver_subj = "_PUSH_DELIVER.test";
var push_sub = nats.jetstream.PushSubscription{
.js = &js,
.stream = "TEST_PUSH",
};
push_sub.setConsumer("push-c") catch unreachable;
push_sub.setDeliverSubject(deliver_subj) catch unreachable;
var ctx = push_sub.consume(
nats.jetstream.JsMsgHandler.init(
Counter,
&counter,
),
.{},
) catch {
reportResult(
"js_push_basic",
false,
"consume failed",
);
return;
};
// Now create the push consumer -- server starts
// delivering to the already-subscribed subject.
var pc = js.createPushConsumer(
"TEST_PUSH",
.{
.name = "push-c",
.deliver_subject = deliver_subj,
.ack_policy = .none,
},
) catch {
ctx.stop();
ctx.deinit();
reportResult(
"js_push_basic",
false,
"create push cons",
);
return;
};
pc.deinit();
// Wait for messages
var wait: u32 = 0;
while (counter.count < 5 and
wait < 50) : (wait += 1)
{
threadSleepNs(100_000_000);
}
ctx.stop();
ctx.deinit();
if (counter.count < 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 5",
.{counter.count},
) catch "count mismatch";
reportResult(
"js_push_basic",
false,
m,
);
return;
}
var d = js.deleteStream(
"TEST_PUSH",
) catch {
reportResult(
"js_push_basic",
true,
"",
);
return;
};
d.deinit();
reportResult("js_push_basic", true, "");
}
pub fn testPushConsumerBorrowedAck(
allocator: std.mem.Allocator,
) void {
const name = "js_push_borrowed_ack";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect failed");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_PUSH_ACK",
.subjects = &.{"push.ack.>"},
.storage = .memory,
}) catch {
reportResult(name, false, "create stream");
return;
};
defer stream.deinit();
const Handler = struct {
count: u32 = 0,
saw_expected_data: bool = false,
ack_failed: bool = false,
pub fn onMessage(
self: *@This(),
msg: *nats.jetstream.JsMsg,
) void {
if (std.mem.eql(
u8,
msg.data(),
"ack-data",
)) {
self.saw_expected_data = true;
}
msg.ack() catch {
self.ack_failed = true;
return;
};
self.count += 1;
}
};
var handler = Handler{};
const deliver_subj = "_PUSH_ACK_DELIVER.test";
var push_sub = nats.jetstream.PushSubscription{
.js = &js,
.stream = "TEST_PUSH_ACK",
};
push_sub.setConsumer("push-ack-c") catch unreachable;
push_sub.setDeliverSubject(deliver_subj) catch unreachable;
var ctx = push_sub.consume(
nats.jetstream.JsMsgHandler.init(
Handler,
&handler,
),
.{},
) catch {
reportResult(name, false, "consume failed");
return;
};
var pc = js.createPushConsumer(
"TEST_PUSH_ACK",
.{
.name = "push-ack-c",
.deliver_subject = deliver_subj,
.ack_policy = .explicit,
.ack_wait = 1_000_000_000,
},
) catch {
ctx.stop();
ctx.deinit();
reportResult(name, false, "create push cons");
return;
};
pc.deinit();
var ack = js.publish(
"push.ack.data",
"ack-data",
) catch {
ctx.stop();
ctx.deinit();
reportResult(name, false, "publish failed");
return;
};
ack.deinit();
var wait: u32 = 0;
while (handler.count < 1 and
!handler.ack_failed and
wait < 50) : (wait += 1)
{
threadSleepNs(100_000_000);
}
if (handler.ack_failed) {
ctx.stop();
ctx.deinit();
reportResult(name, false, "ack failed");
return;
}
if (handler.count != 1 or !handler.saw_expected_data) {
ctx.stop();
ctx.deinit();
reportResult(name, false, "callback did not ack data");
return;
}
var ack_cleared = false;
var info_wait: u32 = 0;
while (info_wait < 30) : (info_wait += 1) {
var info = js.consumerInfo(
"TEST_PUSH_ACK",
"push-ack-c",
) catch {
ctx.stop();
ctx.deinit();
reportResult(name, false, "consumer info");
return;
};
defer info.deinit();
if (info.value.num_ack_pending == 0) {
ack_cleared = true;
break;
}
threadSleepNs(100_000_000);
}
ctx.stop();
ctx.deinit();
if (!ack_cleared) {
reportResult(name, false, "ack still pending");
return;
}
var d = js.deleteStream(
"TEST_PUSH_ACK",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testPushConsumerHeartbeatErrHandler(
allocator: std.mem.Allocator,
) void {
const name = "js_push_heartbeat";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect failed");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_PUSH_HB",
.subjects = &.{"push.hb.>"},
.storage = .memory,
}) catch {
reportResult(name, false, "create stream");
return;
};
defer stream.deinit();
const Handler = struct {
pub fn onMessage(
self: *@This(),
msg: *nats.jetstream.JsMsg,
) void {
_ = self;
_ = msg;
}
};
var handler = Handler{};
push_heartbeat_err_seen.store(false, .release);
const deliver_subj = "_PUSH_HB_DELIVER.test";
var push_sub = nats.jetstream.PushSubscription{
.js = &js,
.stream = "TEST_PUSH_HB",
};
push_sub.setConsumer("push-hb-c") catch unreachable;
push_sub.setDeliverSubject(deliver_subj) catch unreachable;
var ctx = push_sub.consume(
nats.jetstream.JsMsgHandler.init(
Handler,
&handler,
),
.{
.heartbeat_ms = 200,
.err_handler = pushHeartbeatErrHandler,
},
) catch {
reportResult(name, false, "consume failed");
return;
};
var pc = js.createPushConsumer(
"TEST_PUSH_HB",
.{
.name = "push-hb-c",
.deliver_subject = deliver_subj,
.ack_policy = .none,
},
) catch {
ctx.stop();
ctx.deinit();
reportResult(name, false, "create push cons");
return;
};
pc.deinit();
var wait: u32 = 0;
while (!push_heartbeat_err_seen.load(.acquire) and
wait < 40) : (wait += 1)
{
threadSleepNs(100_000_000);
}
ctx.stop();
ctx.deinit();
if (!push_heartbeat_err_seen.load(.acquire)) {
reportResult(name, false, "no heartbeat error missing");
return;
}
var d = js.deleteStream(
"TEST_PUSH_HB",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testPublishWithTTL(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_publish_ttl",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream with TTL support
var stream = js.createStream(.{
.name = "TEST_TTL",
.subjects = &.{"ttl.>"},
.storage = .memory,
.allow_msg_ttl = true,
}) catch {
// Server may not support TTL
reportResult(
"js_publish_ttl",
true,
"skipped",
);
return;
};
defer stream.deinit();
// Publish with 1s TTL
var ack = js.publishWithOpts(
"ttl.a",
"data",
.{ .ttl = "1s" },
) catch {
reportResult(
"js_publish_ttl",
false,
"publish failed",
);
return;
};
ack.deinit();
// Immediately should exist
var r1 = js.getMsg("TEST_TTL", 1) catch {
reportResult(
"js_publish_ttl",
false,
"getMsg before ttl",
);
return;
};
r1.deinit();
// Wait for TTL expiry
threadSleepNs(2_000_000_000);
// Should be expired now
var r2 = js.getMsg("TEST_TTL", 1);
if (r2) |*r| {
r.deinit();
reportResult(
"js_publish_ttl",
false,
"should expire",
);
return;
} else |_| {}
var d = js.deleteStream(
"TEST_TTL",
) catch {
reportResult(
"js_publish_ttl",
true,
"",
);
return;
};
d.deinit();
reportResult("js_publish_ttl", true, "");
}
/// Verifies publishMsg() merges user headers with JS-derived
/// opts headers. On key collision (case-insensitive), JS opts
/// must win. Retrieves the stored message via getMsg() and
/// asserts both headers against the wire.
pub fn testPublishMsg(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_publish_msg",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_PUBLISH_MSG",
.subjects = &.{"pubmsg.test"},
.storage = .memory,
}) catch {
reportResult(
"js_publish_msg",
false,
"create stream",
);
return;
};
defer stream.deinit();
// User headers. Note lowercase "nats-msg-id" deliberately
// collides (case-insensitively) with the JS header set by
// opts.msg_id -- "jsopts-id" must win.
const user_headers = [_]nats.protocol.headers.Entry{
.{ .key = "X-Custom", .value = "uservalue" },
.{ .key = "nats-msg-id", .value = "user-id" },
};
var ack = js.publishMsg(allocator, .{
.subject = "pubmsg.test",
.payload = "hello",
.headers = &user_headers,
.opts = .{ .msg_id = "jsopts-id" },
}) catch {
reportResult(
"js_publish_msg",
false,
"publishMsg failed",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
ack.deinit();
var resp = js.getMsg(
"TEST_PUBLISH_MSG",
1,
) catch {
reportResult(
"js_publish_msg",
false,
"getMsg failed",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
defer resp.deinit();
const stored = resp.value.message orelse {
reportResult(
"js_publish_msg",
false,
"no stored message",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
const hdr_b64 = stored.hdrs orelse {
reportResult(
"js_publish_msg",
false,
"no headers in stored msg",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
// Decode base64 headers, then parse NATS/1.0.
const decoder = std.base64.standard.Decoder;
const decoded_len = decoder.calcSizeForSlice(hdr_b64) catch {
reportResult(
"js_publish_msg",
false,
"b64 calc size",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
const decoded_buf = allocator.alloc(u8, decoded_len) catch {
reportResult(
"js_publish_msg",
false,
"alloc decoded",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
defer allocator.free(decoded_buf);
decoder.decode(decoded_buf, hdr_b64) catch {
reportResult(
"js_publish_msg",
false,
"b64 decode",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
var parsed = nats.protocol.headers.parse(
allocator,
decoded_buf,
);
defer parsed.deinit();
if (parsed.err != null) {
reportResult(
"js_publish_msg",
false,
"header parse err",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
}
// User-supplied custom header must pass through.
const custom = parsed.get("X-Custom") orelse {
reportResult(
"js_publish_msg",
false,
"X-Custom missing",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
if (!std.mem.eql(u8, custom, "uservalue")) {
reportResult(
"js_publish_msg",
false,
"X-Custom wrong value",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
}
// Collision: opts.msg_id ("jsopts-id") must override the
// user's lowercase "nats-msg-id" entry. Check via the
// case-insensitive lookup.
const msg_id = parsed.get("Nats-Msg-Id") orelse {
reportResult(
"js_publish_msg",
false,
"Nats-Msg-Id missing",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
};
if (!std.mem.eql(u8, msg_id, "jsopts-id")) {
reportResult(
"js_publish_msg",
false,
"opts.msg_id did not override",
);
var d = js.deleteStream("TEST_PUBLISH_MSG") catch return;
d.deinit();
return;
}
var d = js.deleteStream(
"TEST_PUBLISH_MSG",
) catch {
reportResult("js_publish_msg", true, "");
return;
};
d.deinit();
reportResult("js_publish_msg", true, "");
}
/// Regression: publishMsg with no user headers and default opts
/// must succeed (takes the no-header publish path). Previously
/// this tripped an assertion in protocol.headers.encodedSize()
/// because PublishHeaderSet.slice() returned an empty (but
/// non-null) slice.
pub fn testPublishMsgNoHeaders(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_publish_msg_no_headers",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_PUBLISH_MSG_BARE",
.subjects = &.{"pubmsg.bare"},
.storage = .memory,
}) catch {
reportResult(
"js_publish_msg_no_headers",
false,
"create stream",
);
return;
};
defer stream.deinit();
var ack = js.publishMsg(allocator, .{
.subject = "pubmsg.bare",
.payload = "hi",
}) catch {
reportResult(
"js_publish_msg_no_headers",
false,
"publishMsg failed",
);
var d = js.deleteStream("TEST_PUBLISH_MSG_BARE") catch return;
d.deinit();
return;
};
ack.deinit();
var resp = js.getMsg(
"TEST_PUBLISH_MSG_BARE",
1,
) catch {
reportResult(
"js_publish_msg_no_headers",
false,
"getMsg failed",
);
var d = js.deleteStream("TEST_PUBLISH_MSG_BARE") catch return;
d.deinit();
return;
};
defer resp.deinit();
if (resp.value.message == null) {
reportResult(
"js_publish_msg_no_headers",
false,
"no stored message",
);
var d = js.deleteStream("TEST_PUBLISH_MSG_BARE") catch return;
d.deinit();
return;
}
var d = js.deleteStream(
"TEST_PUBLISH_MSG_BARE",
) catch {
reportResult("js_publish_msg_no_headers", true, "");
return;
};
d.deinit();
reportResult("js_publish_msg_no_headers", true, "");
}
/// Regression: publishWithOpts with empty opts must succeed
/// (same underlying bug as publishMsg with no headers).
pub fn testPublishWithOptsEmpty(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_publish_with_opts_empty",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_PUBLISH_OPTS_EMPTY",
.subjects = &.{"pubopts.empty"},
.storage = .memory,
}) catch {
reportResult(
"js_publish_with_opts_empty",
false,
"create stream",
);
return;
};
defer stream.deinit();
var ack = js.publishWithOpts(
"pubopts.empty",
"payload",
.{},
) catch {
reportResult(
"js_publish_with_opts_empty",
false,
"publishWithOpts failed",
);
var d = js.deleteStream("TEST_PUBLISH_OPTS_EMPTY") catch return;
d.deinit();
return;
};
ack.deinit();
var resp = js.getMsg(
"TEST_PUBLISH_OPTS_EMPTY",
1,
) catch {
reportResult(
"js_publish_with_opts_empty",
false,
"getMsg failed",
);
var d = js.deleteStream("TEST_PUBLISH_OPTS_EMPTY") catch return;
d.deinit();
return;
};
defer resp.deinit();
if (resp.value.message == null) {
reportResult(
"js_publish_with_opts_empty",
false,
"no stored message",
);
var d = js.deleteStream("TEST_PUBLISH_OPTS_EMPTY") catch return;
d.deinit();
return;
}
var d = js.deleteStream(
"TEST_PUBLISH_OPTS_EMPTY",
) catch {
reportResult("js_publish_with_opts_empty", true, "");
return;
};
d.deinit();
reportResult("js_publish_with_opts_empty", true, "");
}
pub fn testKvUpdateBucket(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_update_bucket",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
_ = js.createKeyValue(.{
.bucket = "UPD_BUCKET",
.history = 1,
.storage = .memory,
}) catch {
reportResult(
"kv_update_bucket",
false,
"create bucket",
);
return;
};
_ = js.updateKeyValue(.{
.bucket = "UPD_BUCKET",
.history = 5,
.storage = .memory,
}) catch {
reportResult(
"kv_update_bucket",
false,
"update bucket",
);
return;
};
// Verify via stream info
var info = js.streamInfo(
"KV_UPD_BUCKET",
) catch {
reportResult(
"kv_update_bucket",
false,
"info failed",
);
return;
};
defer info.deinit();
if (info.value.config) |cfg| {
if (cfg.max_msgs_per_subject) |mps| {
if (mps != 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"mps {d}, want 5",
.{mps},
) catch "wrong";
reportResult(
"kv_update_bucket",
false,
m,
);
return;
}
} else {
reportResult(
"kv_update_bucket",
false,
"no max_msgs_per_subj",
);
return;
}
} else {
reportResult(
"kv_update_bucket",
false,
"no config",
);
return;
}
var d = js.deleteKeyValue(
"UPD_BUCKET",
) catch {
reportResult(
"kv_update_bucket",
true,
"",
);
return;
};
d.deinit();
reportResult("kv_update_bucket", true, "");
}
pub fn testKvCreateOrUpdateBucket(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_upsert_bucket",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create via createOrUpdate
_ = js.createOrUpdateKeyValue(.{
.bucket = "UPSERT_KV",
.storage = .memory,
}) catch {
reportResult(
"kv_upsert_bucket",
false,
"create bucket",
);
return;
};
// Update via createOrUpdate
_ = js.createOrUpdateKeyValue(.{
.bucket = "UPSERT_KV",
.history = 10,
.storage = .memory,
}) catch {
reportResult(
"kv_upsert_bucket",
false,
"update bucket",
);
return;
};
// Verify via stream info
var info = js.streamInfo(
"KV_UPSERT_KV",
) catch {
reportResult(
"kv_upsert_bucket",
false,
"info failed",
);
return;
};
defer info.deinit();
if (info.value.config) |cfg| {
if (cfg.max_msgs_per_subject) |mps| {
if (mps != 10) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"mps {d}, want 10",
.{mps},
) catch "wrong";
reportResult(
"kv_upsert_bucket",
false,
m,
);
return;
}
} else {
reportResult(
"kv_upsert_bucket",
false,
"no max_msgs_per_subj",
);
return;
}
} else {
reportResult(
"kv_upsert_bucket",
false,
"no config",
);
return;
}
var d = js.deleteKeyValue(
"UPSERT_KV",
) catch {
reportResult(
"kv_upsert_bucket",
true,
"",
);
return;
};
d.deinit();
reportResult(
"kv_upsert_bucket",
true,
"",
);
}
pub fn testKvPurgeDeletes(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_purge_deletes",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "PURGE_DEL",
.storage = .memory,
.history = 5,
}) catch {
reportResult(
"kv_purge_deletes",
false,
"create bucket",
);
return;
};
// Put 5 keys
_ = kv.put("a", "1") catch {
reportResult(
"kv_purge_deletes",
false,
"put a",
);
return;
};
_ = kv.put("b", "2") catch {
reportResult(
"kv_purge_deletes",
false,
"put b",
);
return;
};
_ = kv.put("c", "3") catch {
reportResult(
"kv_purge_deletes",
false,
"put c",
);
return;
};
_ = kv.put("d", "4") catch {
reportResult(
"kv_purge_deletes",
false,
"put d",
);
return;
};
_ = kv.put("e", "5") catch {
reportResult(
"kv_purge_deletes",
false,
"put e",
);
return;
};
// Delete a, b, c
_ = kv.delete("a") catch {
reportResult(
"kv_purge_deletes",
false,
"del a",
);
return;
};
_ = kv.delete("b") catch {
reportResult(
"kv_purge_deletes",
false,
"del b",
);
return;
};
_ = kv.delete("c") catch {
reportResult(
"kv_purge_deletes",
false,
"del c",
);
return;
};
// keys() should return 2 live keys
const key_list = kv.keys(allocator) catch {
reportResult(
"kv_purge_deletes",
false,
"keys()",
);
return;
};
defer {
for (key_list) |k| allocator.free(k);
allocator.free(key_list);
}
if (key_list.len != 2) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"keys: {d}, want 2",
.{key_list.len},
) catch "wrong";
reportResult(
"kv_purge_deletes",
false,
m,
);
return;
}
// Fresh delete markers should not match an age filter.
const skipped = kv.purgeDeletes(
.{ .older_than_ns = @as(i64, 60 * std.time.ns_per_s) },
) catch {
reportResult(
"kv_purge_deletes",
false,
"purgeDeletes age",
);
return;
};
if (skipped != 0) {
reportResult(
"kv_purge_deletes",
false,
"purged fresh markers",
);
return;
}
// Purge delete markers
const purged = kv.purgeDeletes(
.{},
) catch {
reportResult(
"kv_purge_deletes",
false,
"purgeDeletes",
);
return;
};
_ = purged;
// Verify d and e still accessible
var ed = (kv.get("d") catch {
reportResult(
"kv_purge_deletes",
false,
"get d after purge",
);
return;
}) orelse {
reportResult(
"kv_purge_deletes",
false,
"d missing",
);
return;
};
defer ed.deinit();
if (ed.operation != .put) {
reportResult(
"kv_purge_deletes",
false,
"d not put op",
);
return;
}
var ee = (kv.get("e") catch {
reportResult(
"kv_purge_deletes",
false,
"get e after purge",
);
return;
}) orelse {
reportResult(
"kv_purge_deletes",
false,
"e missing",
);
return;
};
defer ee.deinit();
if (ee.operation != .put) {
reportResult(
"kv_purge_deletes",
false,
"e not put op",
);
return;
}
var d = js.deleteKeyValue(
"PURGE_DEL",
) catch {
reportResult(
"kv_purge_deletes",
true,
"",
);
return;
};
d.deinit();
reportResult(
"kv_purge_deletes",
true,
"",
);
}
pub fn testKvStoreNames(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_store_names",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create 3 KV buckets
_ = js.createKeyValue(.{
.bucket = "NAMES_A",
.storage = .memory,
}) catch {
reportResult(
"kv_store_names",
false,
"create A",
);
return;
};
_ = js.createKeyValue(.{
.bucket = "NAMES_B",
.storage = .memory,
}) catch {
reportResult(
"kv_store_names",
false,
"create B",
);
return;
};
_ = js.createKeyValue(.{
.bucket = "NAMES_C",
.storage = .memory,
}) catch {
reportResult(
"kv_store_names",
false,
"create C",
);
return;
};
const names = js.keyValueStoreNames(
allocator,
) catch {
reportResult(
"kv_store_names",
false,
"storeNames()",
);
return;
};
defer {
for (names) |n| allocator.free(n);
allocator.free(names);
}
// Verify our 3 buckets are in the list
var found_a = false;
var found_b = false;
var found_c = false;
for (names) |n| {
if (std.mem.eql(u8, n, "NAMES_A"))
found_a = true;
if (std.mem.eql(u8, n, "NAMES_B"))
found_b = true;
if (std.mem.eql(u8, n, "NAMES_C"))
found_c = true;
}
if (!found_a or !found_b or !found_c) {
reportResult(
"kv_store_names",
false,
"missing bucket names",
);
return;
}
// Verify KV_ prefix was stripped
for (names) |n| {
if (std.mem.startsWith(u8, n, "KV_")) {
reportResult(
"kv_store_names",
false,
"prefix not stripped",
);
return;
}
}
// Cleanup
var da = js.deleteKeyValue(
"NAMES_A",
) catch {
reportResult(
"kv_store_names",
true,
"",
);
return;
};
da.deinit();
var db = js.deleteKeyValue(
"NAMES_B",
) catch {
reportResult(
"kv_store_names",
true,
"",
);
return;
};
db.deinit();
var dc = js.deleteKeyValue(
"NAMES_C",
) catch {
reportResult(
"kv_store_names",
true,
"",
);
return;
};
dc.deinit();
reportResult("kv_store_names", true, "");
}
pub fn testKvWatchIgnoreDeletes(
allocator: std.mem.Allocator,
) void {
_ = allocator;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(std.heap.page_allocator);
defer io.deinit();
const client = nats.Client.connect(
std.heap.page_allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_watch_ign_del",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "WATCH_IGN",
.storage = .memory,
.history = 5,
}) catch {
reportResult(
"kv_watch_ign_del",
false,
"create bucket",
);
return;
};
// Put a=1, b=2, then delete a
_ = kv.put("a", "1") catch {
reportResult(
"kv_watch_ign_del",
false,
"put a",
);
return;
};
_ = kv.put("b", "2") catch {
reportResult(
"kv_watch_ign_del",
false,
"put b",
);
return;
};
_ = kv.delete("a") catch {
reportResult(
"kv_watch_ign_del",
false,
"delete a",
);
return;
};
// Watch with ignore_deletes
var watcher = kv.watchAllWithOpts(.{
.ignore_deletes = true,
.include_history = true,
}) catch {
reportResult(
"kv_watch_ign_del",
false,
"watchAll",
);
return;
};
defer watcher.deinit();
// Collect entries until null
var count: u32 = 0;
while (count < 10) {
var entry = (watcher.next(
3000,
) catch break) orelse break;
entry.deinit();
count += 1;
}
// Should get 2 (put a, put b) not delete
if (count != 2) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 2",
.{count},
) catch "wrong count";
reportResult(
"kv_watch_ign_del",
false,
m,
);
return;
}
var d = js.deleteKeyValue(
"WATCH_IGN",
) catch {
reportResult(
"kv_watch_ign_del",
true,
"",
);
return;
};
d.deinit();
reportResult(
"kv_watch_ign_del",
true,
"",
);
}
pub fn testKvWatchUpdatesOnly(
allocator: std.mem.Allocator,
) void {
_ = allocator;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(std.heap.page_allocator);
defer io.deinit();
const client = nats.Client.connect(
std.heap.page_allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_watch_upd_only",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "WATCH_UPD",
.storage = .memory,
}) catch {
reportResult(
"kv_watch_upd_only",
false,
"create bucket",
);
return;
};
// Put a key before watching
_ = kv.put("pre", "before") catch {
reportResult(
"kv_watch_upd_only",
false,
"put pre",
);
return;
};
// Watch with updates_only
var watcher = kv.watchAllWithOpts(.{
.updates_only = true,
}) catch {
reportResult(
"kv_watch_upd_only",
false,
"watchAll",
);
return;
};
defer watcher.deinit();
// First next() should return null (no initial)
const first = (watcher.next(
2000,
) catch null) orelse null;
if (first != null) {
reportResult(
"kv_watch_upd_only",
false,
"should be null first",
);
return;
}
// Put a new key
_ = kv.put("post", "after") catch {
reportResult(
"kv_watch_upd_only",
false,
"put post",
);
return;
};
// Should get the new entry
var entry = (watcher.next(5000) catch {
reportResult(
"kv_watch_upd_only",
false,
"next() failed",
);
return;
}) orelse {
reportResult(
"kv_watch_upd_only",
false,
"no post entry",
);
return;
};
defer entry.deinit();
if (!std.mem.eql(u8, entry.key, "post")) {
reportResult(
"kv_watch_upd_only",
false,
"wrong key",
);
return;
}
var d = js.deleteKeyValue(
"WATCH_UPD",
) catch {
reportResult(
"kv_watch_upd_only",
true,
"",
);
return;
};
d.deinit();
reportResult(
"kv_watch_upd_only",
true,
"",
);
}
pub fn testKvListKeys(
allocator: std.mem.Allocator,
) void {
_ = allocator;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(std.heap.page_allocator);
defer io.deinit();
const client = nats.Client.connect(
std.heap.page_allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"kv_list_keys",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "LIST_KEYS",
.storage = .memory,
}) catch {
reportResult(
"kv_list_keys",
false,
"create bucket",
);
return;
};
// Put 5 keys
_ = kv.put("k1", "v1") catch {
reportResult(
"kv_list_keys",
false,
"put k1",
);
return;
};
_ = kv.put("k2", "v2") catch {
reportResult(
"kv_list_keys",
false,
"put k2",
);
return;
};
_ = kv.put("k3", "v3") catch {
reportResult(
"kv_list_keys",
false,
"put k3",
);
return;
};
_ = kv.put("k4", "v4") catch {
reportResult(
"kv_list_keys",
false,
"put k4",
);
return;
};
_ = kv.put("k5", "v5") catch {
reportResult(
"kv_list_keys",
false,
"put k5",
);
return;
};
// Delete k3
_ = kv.delete("k3") catch {
reportResult(
"kv_list_keys",
false,
"del k3",
);
return;
};
// List keys via lister
var lister = kv.listKeys() catch {
reportResult(
"kv_list_keys",
false,
"listKeys()",
);
return;
};
defer lister.deinit();
var count: u32 = 0;
while (count < 10) {
const key = (lister.next() catch {
break;
}) orelse break;
_ = key;
count += 1;
}
if (count != 4) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d} keys, expected 4",
.{count},
) catch "wrong count";
reportResult(
"kv_list_keys",
false,
m,
);
return;
}
var d = js.deleteKeyValue(
"LIST_KEYS",
) catch {
reportResult(
"kv_list_keys",
true,
"",
);
return;
};
d.deinit();
reportResult("kv_list_keys", true, "");
}
pub fn testDoubleAck(
allocator: std.mem.Allocator,
) void {
const name = "js_double_ack";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_DACK",
.subjects = &.{"dack.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
var c = js.createConsumer("TEST_DACK", .{
.name = "dack-c",
.durable_name = "dack-c",
.ack_policy = .explicit,
}) catch {
reportResult(
name,
false,
"create consumer",
);
return;
};
defer c.deinit();
var a = js.publish(
"dack.test",
"double-ack-data",
) catch {
reportResult(name, false, "publish");
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "TEST_DACK",
};
pull.setConsumer("dack-c") catch unreachable;
var msg = (pull.next(5000) catch {
reportResult(name, false, "fetch 1");
return;
}) orelse {
reportResult(name, false, "no msg");
return;
};
msg.doubleAck(5000) catch {
reportResult(
name,
false,
"doubleAck failed",
);
msg.deinit();
return;
};
msg.deinit();
// After doubleAck, no messages should remain
var r = pull.fetchNoWait(10) catch {
reportResult(name, false, "fetch 2");
return;
};
defer r.deinit();
if (r.count() != 0) {
reportResult(
name,
false,
"expected 0 after dack",
);
return;
}
var d = js.deleteStream("TEST_DACK") catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testUpdatePushConsumer(
allocator: std.mem.Allocator,
) void {
const name = "js_upd_push_cons";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_UPDPUSH",
.subjects = &.{"updpush.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
var pc = js.createPushConsumer(
"TEST_UPDPUSH",
.{
.name = "updpush-c",
.deliver_subject = "_UPD.test",
.description = "v1",
.ack_policy = .none,
},
) catch {
reportResult(
name,
false,
"create push cons",
);
return;
};
pc.deinit();
// Update description to "v2"
var upd = js.updatePushConsumer(
"TEST_UPDPUSH",
.{
.name = "updpush-c",
.deliver_subject = "_UPD.test",
.description = "v2",
.ack_policy = .none,
},
) catch {
reportResult(name, false, "update");
return;
};
upd.deinit();
// Verify description changed
var info = js.consumerInfo(
"TEST_UPDPUSH",
"updpush-c",
) catch {
reportResult(name, false, "info");
return;
};
defer info.deinit();
if (info.value.config) |cfg| {
if (cfg.description) |desc| {
if (!std.mem.eql(u8, desc, "v2")) {
reportResult(
name,
false,
"desc not v2",
);
return;
}
} else {
reportResult(
name,
false,
"no description",
);
return;
}
} else {
reportResult(name, false, "no config");
return;
}
var d = js.deleteStream(
"TEST_UPDPUSH",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testGetPushConsumer(
allocator: std.mem.Allocator,
) void {
const name = "js_get_push_cons";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_GETPUSH",
.subjects = &.{"getpush.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
var pc = js.createPushConsumer(
"TEST_GETPUSH",
.{
.name = "getpush-c",
.deliver_subject = "_GET.test",
.ack_policy = .none,
},
) catch {
reportResult(
name,
false,
"create push cons",
);
return;
};
pc.deinit();
// Get push consumer via pushConsumer()
const push_sub = js.pushConsumer(
"TEST_GETPUSH",
"getpush-c",
) catch {
reportResult(
name,
false,
"pushConsumer()",
);
return;
};
const ds = push_sub.deliverSubject();
if (!std.mem.eql(u8, ds, "_GET.test")) {
reportResult(
name,
false,
"wrong deliver subj",
);
return;
}
var d = js.deleteStream(
"TEST_GETPUSH",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testKvPutString(
allocator: std.mem.Allocator,
) void {
const name = "kv_put_string";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "PUTSTR",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
const rev = kv.putString(
"greeting",
"hello",
) catch {
reportResult(
name,
false,
"putString",
);
return;
};
if (rev == 0) {
reportResult(
name,
false,
"rev should be > 0",
);
return;
}
var entry = (kv.get("greeting") catch {
reportResult(name, false, "get");
return;
}) orelse {
reportResult(
name,
false,
"key not found",
);
return;
};
defer entry.deinit();
if (!std.mem.eql(u8, entry.value, "hello")) {
reportResult(
name,
false,
"wrong value",
);
return;
}
var d = js.deleteKeyValue("PUTSTR") catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testKvDeleteLastRev(
allocator: std.mem.Allocator,
) void {
const name = "kv_del_last_rev";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "DEL_REV",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
const rev1 = kv.put("x", "v1") catch {
reportResult(name, false, "put 1");
return;
};
const rev2 = kv.put("x", "v2") catch {
reportResult(name, false, "put 2");
return;
};
// Wrong revision should fail
_ = kv.deleteWithOpts("x", .{
.last_revision = rev1,
}) catch |err| {
if (err == error.ApiError) {
// Expected: now try correct rev
_ = kv.deleteWithOpts("x", .{
.last_revision = rev2,
}) catch {
reportResult(
name,
false,
"correct rev failed",
);
return;
};
var d = js.deleteKeyValue(
"DEL_REV",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
return;
}
reportResult(
name,
false,
"wrong error type",
);
return;
};
reportResult(
name,
false,
"wrong rev should fail",
);
}
pub fn testKvPurgeLastRev(
allocator: std.mem.Allocator,
) void {
const name = "kv_purge_last_rev";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "PURGE_REV",
.storage = .memory,
.history = 5,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
const rev1 = kv.put("y", "v1") catch {
reportResult(name, false, "put 1");
return;
};
const rev2 = kv.put("y", "v2") catch {
reportResult(name, false, "put 2");
return;
};
// Wrong revision should fail
_ = kv.purgeWithOpts("y", .{
.last_revision = rev1,
}) catch |err| {
if (err == error.ApiError) {
// Expected: now try correct rev
_ = kv.purgeWithOpts("y", .{
.last_revision = rev2,
}) catch {
reportResult(
name,
false,
"correct rev failed",
);
return;
};
var d = js.deleteKeyValue(
"PURGE_REV",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
return;
}
reportResult(
name,
false,
"wrong error type",
);
return;
};
reportResult(
name,
false,
"wrong rev should fail",
);
}
pub fn testKvListKeysFiltered(
allocator: std.mem.Allocator,
) void {
_ = allocator;
const name = "kv_list_filtered";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(std.heap.page_allocator);
defer io.deinit();
const client = nats.Client.connect(
std.heap.page_allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "FILT_KEYS",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
_ = kv.put("a", "1") catch {
reportResult(name, false, "put a");
return;
};
_ = kv.put("b", "2") catch {
reportResult(name, false, "put b");
return;
};
_ = kv.put("c", "3") catch {
reportResult(name, false, "put c");
return;
};
_ = kv.put("d", "4") catch {
reportResult(name, false, "put d");
return;
};
var lister = kv.listKeysFiltered(
&.{ "a", "c" },
) catch {
reportResult(
name,
false,
"listKeysFiltered",
);
return;
};
defer lister.deinit();
var count: u32 = 0;
while (count < 10) {
const key = (lister.next() catch {
break;
}) orelse break;
_ = key;
count += 1;
}
if (count != 2) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 2",
.{count},
) catch "wrong count";
reportResult(name, false, m);
return;
}
var d = js.deleteKeyValue(
"FILT_KEYS",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testKvHistoryWithOpts(
allocator: std.mem.Allocator,
) void {
const name = "kv_hist_opts";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "HIST_OPTS",
.storage = .memory,
.history = 5,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
_ = kv.put("z", "val1") catch {
reportResult(name, false, "put 1");
return;
};
_ = kv.put("z", "val2") catch {
reportResult(name, false, "put 2");
return;
};
_ = kv.put("z", "val3") catch {
reportResult(name, false, "put 3");
return;
};
const hist = kv.historyWithOpts(
allocator,
"z",
.{ .meta_only = true },
) catch {
reportResult(
name,
false,
"historyWithOpts",
);
return;
};
defer {
for (hist) |*h| h.deinit();
allocator.free(hist);
}
if (hist.len != 3) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, expected 3",
.{hist.len},
) catch "wrong";
reportResult(name, false, m);
return;
}
// Verify meta_only: values should be empty
for (hist) |entry| {
if (entry.value.len != 0) {
reportResult(
name,
false,
"meta_only not empty",
);
return;
}
if (entry.revision == 0) {
reportResult(
name,
false,
"rev should be > 0",
);
return;
}
}
var d = js.deleteKeyValue(
"HIST_OPTS",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testConnOptions(
allocator: std.mem.Allocator,
) void {
const name = "js_conn_options";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Test conn() accessor
const c = js.conn();
if (!c.isConnected()) {
reportResult(
name,
false,
"conn not connected",
);
return;
}
// Test options() accessor
const opts = js.options();
if (!std.mem.startsWith(
u8,
opts.api_prefix,
"$JS.",
)) {
reportResult(
name,
false,
"bad api_prefix",
);
return;
}
if (opts.timeout_ms == 0) {
reportResult(
name,
false,
"timeout_ms is 0",
);
return;
}
reportResult(name, true, "");
}
pub fn testKvCreateWithTTL(
allocator: std.mem.Allocator,
) void {
const name = "kv_create_ttl";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream with TTL support
var stream = js.createStream(.{
.name = "KV_TTL_CREATE",
.subjects = &.{"$KV.TTL_CREATE.>"},
.storage = .memory,
.max_msgs_per_subject = 1,
.allow_msg_ttl = true,
}) catch {
// Server may not support TTL
reportResult(
name,
true,
"skipped: no ttl",
);
return;
};
stream.deinit();
var kv = js.keyValue("TTL_CREATE") catch {
reportResult(name, false, "bind kv");
return;
};
// createWithOpts with TTL
const rev = kv.createWithOpts(
"ttlkey",
"val",
.{ .ttl = "1s" },
) catch |err| {
if (err == error.ApiError) {
// TTL not supported on this server
var dd = js.deleteStream(
"KV_TTL_CREATE",
) catch {
reportResult(
name,
true,
"skipped: ttl err",
);
return;
};
dd.deinit();
reportResult(
name,
true,
"skipped: ttl err",
);
return;
}
reportResult(
name,
false,
"createWithOpts",
);
return;
};
if (rev == 0) {
reportResult(
name,
false,
"rev should be > 0",
);
return;
}
var d = js.deleteStream(
"KV_TTL_CREATE",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testPublishAsync(
allocator: std.mem.Allocator,
) void {
const stream_name = "TEST_ASYNC_PUB";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_pub_async",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
deleteStreamIfExists(&js, stream_name);
var stream = js.createStream(.{
.name = stream_name,
.subjects = &.{"async.>"},
.storage = .memory,
}) catch {
reportResult(
"js_pub_async",
false,
"create stream",
);
return;
};
defer deleteStreamIfExists(&js, stream_name);
defer stream.deinit();
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{ .max_pending = 64 },
) catch {
reportResult(
"js_pub_async",
false,
"init async pub",
);
return;
};
defer ap.deinit();
// Publish 20 messages asynchronously
var futures: [20]*nats.jetstream.PubAckFuture =
undefined;
var i: usize = 0;
while (i < 20) : (i += 1) {
futures[i] = ap.publish(
"async.test",
"async-data",
) catch {
reportResult(
"js_pub_async",
false,
"publish failed",
);
return;
};
}
// Wait for all acks
ap.waitComplete(10000) catch {
reportResult(
"js_pub_async",
false,
"waitComplete timeout",
);
// Clean up futures
for (futures[0..i]) |f| f.deinit();
return;
};
// Verify all futures resolved
var all_ok = true;
for (futures[0..20]) |f| {
if (f.result() == null) {
all_ok = false;
}
}
// Check pending is 0
if (ap.publishAsyncPending() != 0) {
reportResult(
"js_pub_async",
false,
"pending not 0",
);
for (futures[0..20]) |f| f.deinit();
return;
}
// Verify stream has 20 messages
var info = js.streamInfo(
stream_name,
) catch {
for (futures[0..20]) |f| f.deinit();
reportResult(
"js_pub_async",
false,
"stream info",
);
return;
};
defer info.deinit();
const msg_count = if (info.value.state) |s|
s.messages
else
0;
for (futures[0..20]) |f| f.deinit();
if (!all_ok or msg_count != 20) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"ok={}, msgs={d}",
.{ all_ok, msg_count },
) catch "verify failed";
reportResult(
"js_pub_async",
false,
m,
);
return;
}
reportResult("js_pub_async", true, "");
}
pub fn testPublishAsyncFutureWait(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"js_async_wait",
false,
"connect failed",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var stream = js.createStream(.{
.name = "TEST_ASYNC_WAIT",
.subjects = &.{"await.>"},
.storage = .memory,
}) catch {
reportResult(
"js_async_wait",
false,
"create stream",
);
return;
};
defer stream.deinit();
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{},
) catch {
reportResult(
"js_async_wait",
false,
"init",
);
return;
};
defer ap.deinit();
// Publish one message and wait on the future
const fut = ap.publish(
"await.test",
"wait-data",
) catch {
reportResult(
"js_async_wait",
false,
"publish",
);
return;
};
defer fut.deinit();
const ack = fut.wait(5000) catch {
reportResult(
"js_async_wait",
false,
"wait timeout",
);
return;
};
if (ack.seq != 1) {
reportResult(
"js_async_wait",
false,
"wrong seq",
);
return;
}
var churn = std.ArrayList([]u8).empty;
defer {
for (churn.items) |buf| allocator.free(buf);
churn.deinit(allocator);
}
for (0..32) |_| {
const buf = allocator.alloc(u8, 64) catch {
reportResult(
"js_async_wait",
false,
"alloc churn failed",
);
return;
};
churn.append(allocator, buf) catch {
allocator.free(buf);
reportResult(
"js_async_wait",
false,
"alloc churn append failed",
);
return;
};
}
if (ack.stream == null or !std.mem.eql(
u8,
ack.stream.?,
"TEST_ASYNC_WAIT",
)) {
reportResult(
"js_async_wait",
false,
"ack stream invalid",
);
return;
}
var d = js.deleteStream(
"TEST_ASYNC_WAIT",
) catch {
reportResult(
"js_async_wait",
true,
"",
);
return;
};
d.deinit();
reportResult("js_async_wait", true, "");
}
pub fn testPublishAsyncExpectedSeqParity(
allocator: std.mem.Allocator,
) void {
const name = "js_async_exp_seq";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect failed");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "TEST_ASYNC_EXPSEQ",
.subjects = &.{"atest.expseq.>"},
.storage = .memory,
}) catch {
reportResult(name, false, "create stream");
return;
};
defer s.deinit();
var a1 = js.publish(
"atest.expseq.data",
"first",
) catch {
reportResult(name, false, "sync pub 1");
return;
};
a1.deinit();
var a2 = js.publishWithOpts(
"atest.expseq.data",
"second",
.{ .expected_last_seq = 1 },
) catch {
reportResult(name, false, "sync pub 2");
return;
};
a2.deinit();
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{},
) catch {
reportResult(name, false, "init ap");
return;
};
defer ap.deinit();
const fut = ap.publishWithOpts(
"atest.expseq.data",
"should-fail",
.{ .expected_last_seq = 0 },
) catch {
reportResult(name, false, "async publish");
return;
};
defer fut.deinit();
const ack_or_err = fut.wait(5000);
if (ack_or_err) |ack| {
_ = ack;
reportResult(name, false, "should have failed");
return;
} else |err| {
if (err != error.ApiError) {
reportResult(name, false, "wrong error");
return;
}
}
var info = js.streamInfo(
"TEST_ASYNC_EXPSEQ",
) catch {
reportResult(name, false, "stream info");
return;
};
defer info.deinit();
const msg_count = if (info.value.state) |st|
st.messages
else
0;
if (msg_count != 2) {
reportResult(name, false, "wrong msg count");
return;
}
var d = js.deleteStream(
"TEST_ASYNC_EXPSEQ",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testKvEmptyValue(
allocator: std.mem.Allocator,
) void {
const name = "kv_empty_value";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "EMPTY_VAL",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
_ = kv.put("empty", "") catch {
reportResult(name, false, "put empty");
return;
};
var entry = (kv.get("empty") catch {
reportResult(name, false, "get");
return;
}) orelse {
reportResult(
name,
false,
"key not found",
);
return;
};
defer entry.deinit();
if (entry.value.len != 0) {
reportResult(
name,
false,
"value not empty",
);
return;
}
if (entry.operation != .put) {
reportResult(
name,
false,
"wrong operation",
);
return;
}
var d = js.deleteKeyValue(
"EMPTY_VAL",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testKvKeySpecialChars(
allocator: std.mem.Allocator,
) void {
const name = "kv_special_keys";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "SPECIAL_KEYS",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
// Put keys with dots, dashes, underscores
_ = kv.put("my.nested.key", "v1") catch {
reportResult(name, false, "put dot");
return;
};
_ = kv.put("my-dashed", "v2") catch {
reportResult(name, false, "put dash");
return;
};
_ = kv.put("under_score", "v3") catch {
reportResult(
name,
false,
"put underscore",
);
return;
};
// Verify all readable
var e1 = (kv.get("my.nested.key") catch {
reportResult(name, false, "get dot");
return;
}) orelse {
reportResult(name, false, "dot missing");
return;
};
defer e1.deinit();
if (!std.mem.eql(u8, e1.value, "v1")) {
reportResult(
name,
false,
"wrong dot value",
);
return;
}
var e2 = (kv.get("my-dashed") catch {
reportResult(name, false, "get dash");
return;
}) orelse {
reportResult(
name,
false,
"dash missing",
);
return;
};
defer e2.deinit();
if (!std.mem.eql(u8, e2.value, "v2")) {
reportResult(
name,
false,
"wrong dash value",
);
return;
}
var e3 = (kv.get("under_score") catch {
reportResult(
name,
false,
"get underscore",
);
return;
}) orelse {
reportResult(
name,
false,
"underscore missing",
);
return;
};
defer e3.deinit();
if (!std.mem.eql(u8, e3.value, "v3")) {
reportResult(
name,
false,
"wrong uscore value",
);
return;
}
// Wildcard key should be rejected
_ = kv.put("bad*key", "nope") catch |err| {
if (err ==
nats.jetstream.errors.Error.InvalidKey)
{
var d = js.deleteKeyValue(
"SPECIAL_KEYS",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
return;
}
reportResult(
name,
false,
"wrong error for *",
);
return;
};
reportResult(
name,
false,
"wildcard should fail",
);
}
pub fn testKvCreateExisting(
allocator: std.mem.Allocator,
) void {
const name = "kv_create_existing";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "CAS_CREATE",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
// Put initial value
_ = kv.put("exists", "v1") catch {
reportResult(name, false, "put");
return;
};
// create() on existing key should fail
_ = kv.create("exists", "v2") catch |err| {
if (err == error.ApiError) {
// Verify value is still v1
var entry = (kv.get(
"exists",
) catch {
reportResult(
name,
false,
"get after",
);
return;
}) orelse {
reportResult(
name,
false,
"key gone",
);
return;
};
defer entry.deinit();
if (!std.mem.eql(
u8,
entry.value,
"v1",
)) {
reportResult(
name,
false,
"value changed",
);
return;
}
var d = js.deleteKeyValue(
"CAS_CREATE",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
return;
}
reportResult(
name,
false,
"wrong error",
);
return;
};
reportResult(
name,
false,
"create should fail",
);
}
pub fn testKvUpdateWrongRev(
allocator: std.mem.Allocator,
) void {
const name = "kv_update_wrong_rev";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "CAS_UPDATE",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
const rev1 = kv.put("cas", "v1") catch {
reportResult(name, false, "put 1");
return;
};
const rev2 = kv.put("cas", "v2") catch {
reportResult(name, false, "put 2");
return;
};
// Update with stale rev1 should fail
_ = kv.update(
"cas",
"v3",
rev1,
) catch |err| {
if (err == error.ApiError) {
// Update with correct rev2
const rev3 = kv.update(
"cas",
"v3",
rev2,
) catch {
reportResult(
name,
false,
"correct rev fail",
);
return;
};
// Verify value and revision
var entry = (kv.get(
"cas",
) catch {
reportResult(
name,
false,
"get",
);
return;
}) orelse {
reportResult(
name,
false,
"key gone",
);
return;
};
defer entry.deinit();
if (!std.mem.eql(
u8,
entry.value,
"v3",
)) {
reportResult(
name,
false,
"wrong value",
);
return;
}
if (entry.revision != rev3) {
reportResult(
name,
false,
"wrong revision",
);
return;
}
var d = js.deleteKeyValue(
"CAS_UPDATE",
) catch {
reportResult(
name,
true,
"",
);
return;
};
d.deinit();
reportResult(name, true, "");
return;
}
reportResult(
name,
false,
"wrong error",
);
return;
};
reportResult(
name,
false,
"stale rev should fail",
);
}
pub fn testStreamMaxMsgs(
allocator: std.mem.Allocator,
) void {
const name = "js_stream_max_msgs";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "MAX_MSGS",
.subjects = &.{"max.>"},
.storage = .memory,
.max_msgs = 5,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
// Publish 10 messages
var i: u32 = 0;
while (i < 10) : (i += 1) {
var a = js.publish(
"max.test",
"msg-data",
) catch {
reportResult(
name,
false,
"publish",
);
return;
};
a.deinit();
}
// Stream should have exactly 5 messages
var info = js.streamInfo(
"MAX_MSGS",
) catch {
reportResult(name, false, "info");
return;
};
defer info.deinit();
if (info.value.state) |st| {
if (st.messages != 5) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"msgs={d}, want 5",
.{st.messages},
) catch "wrong count";
reportResult(name, false, m);
return;
}
}
// Seq 1 should be discarded
var bad = js.getMsg("MAX_MSGS", 1);
if (bad) |*r| {
r.deinit();
reportResult(
name,
false,
"seq 1 should be gone",
);
return;
} else |_| {}
// Seq 6 should exist
var ok = js.getMsg(
"MAX_MSGS",
6,
) catch {
reportResult(
name,
false,
"seq 6 should exist",
);
return;
};
ok.deinit();
var d = js.deleteStream(
"MAX_MSGS",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testConsumerMaxDeliver(
allocator: std.mem.Allocator,
) void {
const name = "js_max_deliver";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "MAX_DEL",
.subjects = &.{"maxdel.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
// max_deliver=2, ack_wait=1s
var c = js.createConsumer("MAX_DEL", .{
.name = "maxdel-c",
.durable_name = "maxdel-c",
.ack_policy = .explicit,
.max_deliver = 2,
.ack_wait = 1_000_000_000,
}) catch {
reportResult(
name,
false,
"create consumer",
);
return;
};
defer c.deinit();
// Publish 1 message
var a = js.publish(
"maxdel.test",
"deliver-test",
) catch {
reportResult(name, false, "publish");
return;
};
a.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "MAX_DEL",
};
pull.setConsumer("maxdel-c") catch unreachable;
// Fetch 1st delivery, nak
var msg1 = (pull.next(5000) catch {
reportResult(name, false, "fetch 1");
return;
}) orelse {
reportResult(name, false, "no msg 1");
return;
};
msg1.nak() catch {};
msg1.deinit();
// Wait for redeliver
threadSleepNs(1_500_000_000);
// Fetch 2nd delivery, nak
var msg2 = (pull.next(5000) catch {
reportResult(name, false, "fetch 2");
return;
}) orelse {
reportResult(name, false, "no msg 2");
return;
};
msg2.nak() catch {};
msg2.deinit();
// Wait for redeliver attempt
threadSleepNs(1_500_000_000);
// 3rd fetch should be empty (max_deliver=2)
var r = pull.fetchNoWait(10) catch {
reportResult(name, false, "fetch 3");
return;
};
defer r.deinit();
if (r.count() != 0) {
reportResult(
name,
false,
"expected 0 after max",
);
return;
}
var d = js.deleteStream(
"MAX_DEL",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testFetchTimeout(
allocator: std.mem.Allocator,
) void {
const name = "js_fetch_timeout";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s = js.createStream(.{
.name = "FETCH_TO",
.subjects = &.{"fetchto.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
var co = js.createConsumer("FETCH_TO", .{
.name = "fetchto-c",
.durable_name = "fetchto-c",
.ack_policy = .explicit,
}) catch {
reportResult(
name,
false,
"create consumer",
);
return;
};
defer co.deinit();
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "FETCH_TO",
};
pull.setConsumer("fetchto-c") catch unreachable;
// Fetch on empty stream with short timeout
var result = pull.fetch(.{
.max_messages = 1,
.timeout_ms = 1000,
}) catch {
// Error is acceptable too
var d = js.deleteStream(
"FETCH_TO",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
return;
};
defer result.deinit();
if (result.count() != 0) {
reportResult(
name,
false,
"expected 0 messages",
);
return;
}
var d = js.deleteStream(
"FETCH_TO",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testAsyncPublishDedup(
allocator: std.mem.Allocator,
) void {
const name = "js_async_dedup";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
deleteStreamIfExists(&js, "ASYNC_DEDUP");
var s = js.createStream(.{
.name = "ASYNC_DEDUP",
.subjects = &.{"adedup.>"},
.storage = .memory,
.duplicate_window = 60_000_000_000,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
defer deleteStreamIfExists(&js, "ASYNC_DEDUP");
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{},
) catch {
reportResult(name, false, "init ap");
return;
};
defer ap.deinit();
// Publish same msg_id twice
const fut1 = ap.publishWithOpts(
"adedup.test",
"data",
.{ .msg_id = "unique-1" },
) catch {
reportResult(name, false, "pub 1");
return;
};
_ = fut1.wait(5000) catch {
reportResult(name, false, "wait 1");
fut1.deinit();
return;
};
fut1.deinit();
const fut2 = ap.publishWithOpts(
"adedup.test",
"data",
.{ .msg_id = "unique-1" },
) catch {
reportResult(name, false, "pub 2");
return;
};
_ = fut2.wait(5000) catch {
reportResult(name, false, "wait 2");
fut2.deinit();
return;
};
fut2.deinit();
// Stream should have only 1 message
var info = js.streamInfo(
"ASYNC_DEDUP",
) catch {
reportResult(name, false, "info");
return;
};
defer info.deinit();
const msgs = if (info.value.state) |st|
st.messages
else
0;
if (msgs != 1) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"msgs={d}, want 1",
.{msgs},
) catch "wrong";
reportResult(name, false, m);
return;
}
reportResult(name, true, "");
}
pub fn testAsyncPublishNoStream(
allocator: std.mem.Allocator,
) void {
const name = "js_async_no_stream";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{},
) catch {
reportResult(name, false, "init ap");
return;
};
defer ap.deinit();
// Publish to nonexistent subject
const fut = ap.publish(
"nonexistent.subject",
"data",
) catch {
// Publish itself might fail
reportResult(name, true, "");
return;
};
// Wait should return error
_ = fut.wait(3000) catch {
fut.deinit();
reportResult(name, true, "");
return;
};
// If no error, check if err() reports one
if (fut.err() != null) {
fut.deinit();
reportResult(name, true, "");
return;
}
fut.deinit();
// Even if no error, pass: behavior varies
reportResult(name, true, "");
}
pub fn testKvManyKeys(
allocator: std.mem.Allocator,
) void {
const name = "kv_many_keys";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "MANY_KEYS",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
// Put 100 keys
var i: u32 = 0;
while (i < 100) : (i += 1) {
var key_buf: [8]u8 = undefined;
const key = std.fmt.bufPrint(
&key_buf,
"k{d:0>3}",
.{i},
) catch unreachable;
_ = kv.put(key, "val") catch {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"put k{d:0>3}",
.{i},
) catch "put failed";
reportResult(name, false, m);
return;
};
}
// Verify 100 keys
const keys1 = kv.keys(allocator) catch {
reportResult(name, false, "keys 1");
return;
};
const len1 = keys1.len;
for (keys1) |k| allocator.free(k);
allocator.free(keys1);
if (len1 != 100) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, want 100",
.{len1},
) catch "wrong";
reportResult(name, false, m);
return;
}
// Delete every other key (odd indices)
i = 1;
while (i < 100) : (i += 2) {
var key_buf: [8]u8 = undefined;
const key = std.fmt.bufPrint(
&key_buf,
"k{d:0>3}",
.{i},
) catch unreachable;
_ = kv.delete(key) catch {};
}
// Verify 50 keys remaining
const keys2 = kv.keys(allocator) catch {
reportResult(name, false, "keys 2");
return;
};
const len2 = keys2.len;
for (keys2) |k| allocator.free(k);
allocator.free(keys2);
if (len2 != 50) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"got {d}, want 50",
.{len2},
) catch "wrong";
reportResult(name, false, m);
return;
}
var d = js.deleteKeyValue(
"MANY_KEYS",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
pub fn testAsyncPublishBurst(
allocator: std.mem.Allocator,
) void {
const name = "js_async_burst";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
deleteStreamIfExists(&js, "BURST");
var s = js.createStream(.{
.name = "BURST",
.subjects = &.{"burst.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
defer s.deinit();
defer deleteStreamIfExists(&js, "BURST");
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{ .max_pending = 128 },
) catch {
reportResult(name, false, "init ap");
return;
};
defer ap.deinit();
// Publish 100 messages rapidly
var futures: [100]*nats.jetstream.PubAckFuture =
undefined;
var i: usize = 0;
while (i < 100) : (i += 1) {
futures[i] = ap.publish(
"burst.test",
"burst-data",
) catch {
// Clean up already allocated
for (futures[0..i]) |f| f.deinit();
reportResult(
name,
false,
"publish failed",
);
return;
};
}
// Wait for completion
ap.waitComplete(30000) catch {
for (futures[0..100]) |f| f.deinit();
reportResult(
name,
false,
"waitComplete",
);
return;
};
for (futures[0..100]) |f| f.deinit();
// Verify 100 messages in stream
var info = js.streamInfo("BURST") catch {
reportResult(name, false, "info");
return;
};
defer info.deinit();
const msgs = if (info.value.state) |st|
st.messages
else
0;
if (msgs != 100) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"msgs={d}, want 100",
.{msgs},
) catch "wrong";
reportResult(name, false, m);
return;
}
// Verify pending is 0
if (ap.publishAsyncPending() != 0) {
reportResult(
name,
false,
"pending not 0",
);
return;
}
reportResult(name, true, "");
}
fn testJsPublishAfterReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
_ = manager;
const name = "js_pub_after_recon";
const io = utils.newIo(allocator);
defer io.deinit();
var server = startJsReconnectServer(
allocator,
io.io(),
) catch {
reportResult(
name,
false,
"start server",
);
return;
};
defer server.deinit(io.io());
var url_buf: [64]u8 = undefined;
const url = formatUrl(
&url_buf,
js_reconnect_port,
);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = true,
.reconnect_wait_ms = 200,
},
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
// Create stream with file storage
var s1 = js.createStream(.{
.name = "RECON_PUB",
.subjects = &.{"reconpub.>"},
.storage = .file,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
s1.deinit();
// Publish 3 messages
var i: u32 = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"reconpub.data",
"before",
) catch {
reportResult(
name,
false,
"pub before",
);
return;
};
a.deinit();
}
if (!restartJsReconnectServer(
allocator,
io.io(),
&server,
client,
name,
)) return;
// Recreate stream (data may be lost)
if (js.createStream(.{
.name = "RECON_PUB",
.subjects = &.{"reconpub.>"},
.storage = .memory,
})) |r| {
var rr = r;
rr.deinit();
} else |_| {}
// Publish after reconnect
var a2 = js.publish(
"reconpub.data",
"after",
) catch {
reportResult(
name,
false,
"pub after recon",
);
return;
};
a2.deinit();
var a3 = js.publish(
"reconpub.data",
"after2",
) catch {
reportResult(
name,
false,
"pub after 2",
);
return;
};
a3.deinit();
// Cleanup
var d = js.deleteStream(
"RECON_PUB",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
fn testKvAfterReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
_ = manager;
const name = "kv_after_recon";
const io = utils.newIo(allocator);
defer io.deinit();
var server = startJsReconnectServer(
allocator,
io.io(),
) catch {
reportResult(
name,
false,
"start server",
);
return;
};
defer server.deinit(io.io());
var url_buf: [64]u8 = undefined;
const url = formatUrl(
&url_buf,
js_reconnect_port,
);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = true,
.reconnect_wait_ms = 200,
},
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "RECON_KV",
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create bucket",
);
return;
};
// Put before disconnect
_ = kv.put("before", "v1") catch {
reportResult(name, false, "put before");
return;
};
if (!restartJsReconnectServer(
allocator,
io.io(),
&server,
client,
name,
)) return;
// Recreate bucket (memory lost)
kv = js.createKeyValue(.{
.bucket = "RECON_KV",
.storage = .memory,
}) catch {
// May still exist somehow
kv = js.keyValue("RECON_KV") catch {
reportResult(
name,
false,
"rebind bucket",
);
return;
};
// Continue with rebound kv
_ = kv.put("after", "v2") catch {
reportResult(
name,
false,
"put after rebind",
);
return;
};
var d = js.deleteKeyValue(
"RECON_KV",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
return;
};
// Put after reconnect
_ = kv.put("after", "v2") catch {
reportResult(
name,
false,
"put after",
);
return;
};
// Verify get
var entry = (kv.get("after") catch {
reportResult(name, false, "get after");
return;
}) orelse {
reportResult(
name,
false,
"after not found",
);
return;
};
defer entry.deinit();
if (!std.mem.eql(u8, entry.value, "v2")) {
reportResult(
name,
false,
"wrong value",
);
return;
}
var d = js.deleteKeyValue(
"RECON_KV",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
fn testJsFetchAfterReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
_ = manager;
const name = "js_fetch_after_recon";
const io = utils.newIo(allocator);
defer io.deinit();
var server = startJsReconnectServer(
allocator,
io.io(),
) catch {
reportResult(
name,
false,
"start server",
);
return;
};
defer server.deinit(io.io());
var url_buf: [64]u8 = undefined;
const url = formatUrl(
&url_buf,
js_reconnect_port,
);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = true,
.reconnect_wait_ms = 200,
},
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s1 = js.createStream(.{
.name = "RECON_FETCH",
.subjects = &.{"rconfetch.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
s1.deinit();
// Publish 5 messages
var i: u32 = 0;
while (i < 5) : (i += 1) {
var a = js.publish(
"rconfetch.data",
"before",
) catch {
reportResult(
name,
false,
"publish",
);
return;
};
a.deinit();
}
var c1 = js.createConsumer(
"RECON_FETCH",
.{
.name = "rfetch-c",
.durable_name = "rfetch-c",
.ack_policy = .explicit,
},
) catch {
reportResult(
name,
false,
"create consumer",
);
return;
};
c1.deinit();
// Fetch 2 messages, ack
var pull = nats.jetstream.PullSubscription{
.js = &js,
.stream = "RECON_FETCH",
};
pull.setConsumer("rfetch-c") catch unreachable;
var r1 = pull.fetch(.{
.max_messages = 2,
.timeout_ms = 5000,
}) catch {
reportResult(
name,
false,
"fetch before",
);
return;
};
for (r1.messages) |*msg| {
msg.ack() catch {};
}
r1.deinit();
if (!restartJsReconnectServer(
allocator,
io.io(),
&server,
client,
name,
)) return;
// Recreate stream + consumer (memory lost)
var s2 = js.createStream(.{
.name = "RECON_FETCH",
.subjects = &.{"rconfetch.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"recreate stream",
);
return;
};
s2.deinit();
var c2 = js.createConsumer(
"RECON_FETCH",
.{
.name = "rfetch-c",
.durable_name = "rfetch-c",
.ack_policy = .explicit,
},
) catch {
reportResult(
name,
false,
"recreate consumer",
);
return;
};
c2.deinit();
// Publish new messages after reconnect
i = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"rconfetch.data",
"after",
) catch {
reportResult(
name,
false,
"pub after recon",
);
return;
};
a.deinit();
}
// Reset pull subscription
var pull2 = nats.jetstream.PullSubscription{
.js = &js,
.stream = "RECON_FETCH",
};
pull2.setConsumer("rfetch-c") catch unreachable;
// Fetch after reconnect
var r2 = pull2.fetch(.{
.max_messages = 3,
.timeout_ms = 5000,
}) catch {
reportResult(
name,
false,
"fetch after recon",
);
return;
};
defer r2.deinit();
if (r2.count() == 0) {
reportResult(
name,
false,
"no msgs after recon",
);
return;
}
for (r2.messages) |*msg| {
msg.ack() catch {};
}
var d = js.deleteStream(
"RECON_FETCH",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
fn testAsyncDuringDisconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
_ = manager;
const name = "js_async_disconnect";
const io = utils.newIo(allocator);
defer io.deinit();
var server = startJsReconnectServer(
allocator,
io.io(),
) catch {
reportResult(
name,
false,
"start server",
);
return;
};
defer server.deinit(io.io());
var url_buf: [64]u8 = undefined;
const url = formatUrl(
&url_buf,
js_reconnect_port,
);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = true,
.reconnect_wait_ms = 200,
},
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s1 = js.createStream(.{
.name = "ASYNC_DISC",
.subjects = &.{"asyncdisc.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
s1.deinit();
var ap = nats.jetstream.AsyncPublisher.init(
&js,
.{},
) catch {
reportResult(name, false, "init ap");
return;
};
defer ap.deinit();
// Publish 3 msgs before disconnect
var i: usize = 0;
while (i < 3) : (i += 1) {
const f = ap.publish(
"asyncdisc.data",
"before",
) catch break;
_ = f.wait(5000) catch {};
f.deinit();
}
if (!restartJsReconnectServer(
allocator,
io.io(),
&server,
client,
name,
)) return;
// Recreate stream
if (js.createStream(.{
.name = "ASYNC_DISC",
.subjects = &.{"asyncdisc.>"},
.storage = .memory,
})) |r| {
var rr = r;
rr.deinit();
} else |_| {}
// Publish after reconnect should work
const fut = ap.publish(
"asyncdisc.data",
"after",
) catch {
reportResult(
name,
false,
"pub after recon",
);
return;
};
_ = fut.wait(5000) catch {
fut.deinit();
// Timeout acceptable in reconnect
reportResult(name, true, "timeout ok");
return;
};
fut.deinit();
var d = js.deleteStream(
"ASYNC_DISC",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
fn testPushAfterReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
_ = manager;
const name = "js_push_after_recon";
const io = utils.newIo(allocator);
defer io.deinit();
var server = startJsReconnectServer(
allocator,
io.io(),
) catch {
reportResult(
name,
false,
"start server",
);
return;
};
defer server.deinit(io.io());
var url_buf: [64]u8 = undefined;
const url = formatUrl(
&url_buf,
js_reconnect_port,
);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = true,
.reconnect_wait_ms = 200,
},
) catch {
reportResult(name, false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var s1 = js.createStream(.{
.name = "PUSH_RECON",
.subjects = &.{"pushrecon.>"},
.storage = .memory,
}) catch {
reportResult(
name,
false,
"create stream",
);
return;
};
s1.deinit();
// push consumer + publish
const Counter = struct {
count: u32 = 0,
pub fn onMessage(
self: *@This(),
msg: *nats.jetstream.JsMsg,
) void {
_ = msg;
self.count += 1;
}
};
var counter1 = Counter{};
const deliver1 = "_PUSH_RECON.test1";
var push1 = nats.jetstream.PushSubscription{
.js = &js,
.stream = "PUSH_RECON",
};
push1.setConsumer("pushrecon-c") catch unreachable;
push1.setDeliverSubject(deliver1) catch unreachable;
var ctx1 = push1.consume(
nats.jetstream.JsMsgHandler.init(
Counter,
&counter1,
),
.{},
) catch {
reportResult(
name,
false,
"consume 1",
);
return;
};
var pc1 = js.createPushConsumer(
"PUSH_RECON",
.{
.name = "pushrecon-c",
.deliver_subject = deliver1,
.ack_policy = .none,
},
) catch {
ctx1.stop();
ctx1.deinit();
reportResult(
name,
false,
"create push cons 1",
);
return;
};
pc1.deinit();
// Publish 3 msgs
var i: u32 = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"pushrecon.data",
"phase1",
) catch {
ctx1.stop();
ctx1.deinit();
reportResult(
name,
false,
"pub phase1",
);
return;
};
a.deinit();
}
// Wait for delivery
var wait: u32 = 0;
while (counter1.count < 3 and
wait < 50) : (wait += 1)
{
threadSleepNs(100_000_000);
}
ctx1.stop();
ctx1.deinit();
if (counter1.count < 3) {
reportResult(
name,
false,
"phase1 count < 3",
);
return;
}
if (!restartJsReconnectServer(
allocator,
io.io(),
&server,
client,
name,
)) return;
// recreate and push again
if (js.createStream(.{
.name = "PUSH_RECON",
.subjects = &.{"pushrecon.>"},
.storage = .memory,
})) |r| {
var rr = r;
rr.deinit();
} else |_| {}
var counter2 = Counter{};
const deliver2 = "_PUSH_RECON.test2";
var push2 = nats.jetstream.PushSubscription{
.js = &js,
.stream = "PUSH_RECON",
};
push2.setConsumer("pushrecon-c2") catch unreachable;
push2.setDeliverSubject(deliver2) catch unreachable;
var ctx2 = push2.consume(
nats.jetstream.JsMsgHandler.init(
Counter,
&counter2,
),
.{},
) catch {
reportResult(
name,
false,
"consume 2",
);
return;
};
var pc2 = js.createPushConsumer(
"PUSH_RECON",
.{
.name = "pushrecon-c2",
.deliver_subject = deliver2,
.ack_policy = .none,
},
) catch {
ctx2.stop();
ctx2.deinit();
reportResult(
name,
false,
"create push cons 2",
);
return;
};
pc2.deinit();
// Publish 3 more
i = 0;
while (i < 3) : (i += 1) {
var a = js.publish(
"pushrecon.data",
"phase2",
) catch {
ctx2.stop();
ctx2.deinit();
reportResult(
name,
false,
"pub phase2",
);
return;
};
a.deinit();
}
wait = 0;
while (counter2.count < 3 and
wait < 50) : (wait += 1)
{
threadSleepNs(100_000_000);
}
ctx2.stop();
ctx2.deinit();
if (counter2.count < 3) {
var buf: [64]u8 = undefined;
const m = std.fmt.bufPrint(
&buf,
"phase2 got {d}",
.{counter2.count},
) catch "count fail";
reportResult(name, false, m);
return;
}
var d = js.deleteStream(
"PUSH_RECON",
) catch {
reportResult(name, true, "");
return;
};
d.deinit();
reportResult(name, true, "");
}
// -- Test 17 (reconnect test #5): see
// testPushAfterReconnect above
pub fn runAll(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
_ = manager;
std.debug.print(
"\n--- JetStream Tests ---\n",
.{},
);
const io = utils.newIo(allocator);
defer io.deinit();
var js_server = startSharedJsServer(allocator, io.io()) catch |err| {
std.debug.print(
"Failed to start JS server: {}\n",
.{err},
);
return;
};
defer js_server.deinit(io.io());
testStreamCreateAndInfo(allocator);
testPublishAndAck(allocator);
testConsumerCRUD(allocator);
testApiError(allocator);
testStreamNames(allocator);
testStreamList(allocator);
testConsumerNames(allocator);
testConsumerList(allocator);
testAccountInfo(allocator);
testMetadata(allocator);
testFetchNoWait(allocator);
testMessages(allocator);
testConsume(allocator);
testOrderedConsumer(allocator);
// Ack protocol
testAckPreventsRedeliver(allocator);
testNakCausesRedeliver(allocator);
testTermStopsRedeliver(allocator);
testInProgress(allocator);
// Batch + publish opts
testBatchFetch(allocator);
testPublishDedup(allocator);
testPublishExpectedSeq(allocator);
testPublishAsyncExpectedSeqParity(allocator);
// Stream ops
testPurgeStream(allocator);
testStreamUpdate(allocator);
// Error paths
testConsumerNotFound(allocator);
testStreamBySubject(allocator);
// Key-Value Store
// These verify independent bucket semantics. Keep them isolated from
// earlier stream/consumer churn, and let server teardown handle bucket
// cleanup so these tests do not also stress stream deletion.
if (!restartSharedJsServer(
allocator,
io.io(),
&js_server,
"kv_put_get",
)) return;
testKvPutGet(allocator);
testKvCreate(allocator);
testKvUpdate(allocator);
testKvDelete(allocator);
testKvKeys(allocator);
testKvHistory(allocator);
testKvWatch(allocator);
testKvBucketLifecycle(allocator);
// Continue the remaining JetStream tests from clean server state.
if (!restartSharedJsServer(
allocator,
io.io(),
&js_server,
"js_filtered_consumer",
)) return;
// Behavioral correctness
testFilteredConsumer(allocator);
testPurgeSubject(allocator);
testPaginatedStreamNames(allocator);
// New API tests
testGetMsg(allocator);
testGetLastMsgForSubject(allocator);
testDeleteMsg(allocator);
testSecureDeleteMsg(allocator);
testCreateOrUpdateStream(allocator);
testCreateOrUpdateConsumer(allocator);
testPauseResumeConsumer(allocator);
testPushConsumerBasic(allocator);
testPushConsumerBorrowedAck(allocator);
testPushConsumerHeartbeatErrHandler(allocator);
testPublishWithTTL(allocator);
testPublishMsg(allocator);
testPublishMsgNoHeaders(allocator);
testPublishWithOptsEmpty(allocator);
testKvUpdateBucket(allocator);
testKvCreateOrUpdateBucket(allocator);
testKvPurgeDeletes(allocator);
testKvStoreNames(allocator);
testKvWatchIgnoreDeletes(allocator);
testKvWatchUpdatesOnly(allocator);
testKvListKeys(allocator);
// New API method tests
testDoubleAck(allocator);
testUpdatePushConsumer(allocator);
testGetPushConsumer(allocator);
testKvPutString(allocator);
testKvDeleteLastRev(allocator);
testKvPurgeLastRev(allocator);
testKvListKeysFiltered(allocator);
testKvHistoryWithOpts(allocator);
testConnOptions(allocator);
testKvCreateWithTTL(allocator);
// Async publish
testPublishAsync(allocator);
testPublishAsyncFutureWait(allocator);
// Edge cases
testKvEmptyValue(allocator);
testKvKeySpecialChars(allocator);
testKvCreateExisting(allocator);
testKvUpdateWrongRev(allocator);
testStreamMaxMsgs(allocator);
testConsumerMaxDeliver(allocator);
testFetchTimeout(allocator);
testAsyncPublishDedup(allocator);
testAsyncPublishNoStream(allocator);
// Stress
testKvManyKeys(allocator);
testAsyncPublishBurst(allocator);
// Cross-verification with nats CLI
testCrossVerifyKvPut(allocator);
testCrossVerifyKvGet(allocator);
}
// -- Cross-verification with nats CLI --
const nats_cli = "nats";
/// Run nats CLI command, return stdout.
fn runNatsCli(
allocator: std.mem.Allocator,
io: std.Io,
args: []const []const u8,
) ?[]const u8 {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
var full_args: [16][]const u8 = undefined;
full_args[0] = nats_cli;
full_args[1] = "--server";
full_args[2] = url;
const n = @min(args.len, 12);
for (args[0..n], 0..) |a, i| {
full_args[3 + i] = a;
}
var child = std.process.spawn(io, .{
.argv = full_args[0 .. 3 + n],
.stdout = .pipe,
.stderr = .ignore,
}) catch return null;
var buf: [4096]u8 = undefined;
var total: usize = 0;
if (child.stdout) |*file| {
while (total < buf.len) {
var slice = [_][]u8{buf[total..]};
const rd = file.readStreaming(
io,
&slice,
) catch break;
if (rd == 0) break;
total += rd;
}
}
const term = child.wait(io) catch return null;
if (term.exited != 0) return null;
if (total == 0) return null;
return allocator.dupe(u8, buf[0..total]) catch
null;
}
/// Zig writes KV, nats CLI reads and verifies.
pub fn testCrossVerifyKvPut(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("cross_kv_put", false, "connect");
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.createKeyValue(.{
.bucket = "CROSS_PUT",
.storage = .memory,
}) catch {
reportResult(
"cross_kv_put",
false,
"create bucket",
);
return;
};
// Zig puts a value
_ = kv.put("hello", "from-zig") catch {
reportResult("cross_kv_put", false, "put");
return;
};
// nats CLI reads it
const output = runNatsCli(
allocator,
io.io(),
&.{ "kv", "get", "CROSS_PUT", "hello", "--raw" },
) orelse {
reportResult(
"cross_kv_put",
false,
"nats cli get failed",
);
return;
};
defer allocator.free(output);
if (std.mem.indexOf(u8, output, "from-zig") ==
null)
{
reportResult(
"cross_kv_put",
false,
"cli got wrong value",
);
return;
}
var d = js.deleteKeyValue("CROSS_PUT") catch {
reportResult("cross_kv_put", true, "");
return;
};
d.deinit();
reportResult("cross_kv_put", true, "");
}
/// nats CLI writes KV, Zig reads and verifies.
pub fn testCrossVerifyKvGet(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, js_port);
const io = utils.newIo(allocator);
defer io.deinit();
// CLI creates bucket and puts value
const add_out = runNatsCli(
allocator,
io.io(),
&.{ "kv", "add", "CROSS_GET", "--storage", "memory" },
) orelse {
reportResult(
"cross_kv_get",
false,
"cli add bucket",
);
return;
};
allocator.free(add_out);
const put_out = runNatsCli(
allocator,
io.io(),
&.{ "kv", "put", "CROSS_GET", "greeting", "hello-from-cli" },
) orelse {
reportResult(
"cross_kv_get",
false,
"cli put",
);
return;
};
allocator.free(put_out);
// Zig reads it
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"cross_kv_get",
false,
"connect",
);
return;
};
defer client.deinit();
var js = initTestJetStream(client);
var kv = js.keyValue("CROSS_GET") catch {
reportResult(
"cross_kv_get",
false,
"bind bucket",
);
return;
};
var entry = (kv.get("greeting") catch {
reportResult("cross_kv_get", false, "get");
return;
}) orelse {
reportResult(
"cross_kv_get",
false,
"key not found",
);
return;
};
defer entry.deinit();
if (entry.revision == 0) {
reportResult(
"cross_kv_get",
false,
"no revision",
);
return;
}
var d = js.deleteKeyValue("CROSS_GET") catch {
reportResult("cross_kv_get", true, "");
return;
};
d.deinit();
reportResult("cross_kv_get", true, "");
}
pub fn runReconnectTests(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
std.debug.print(
"\n--- JetStream Reconnect Tests ---\n",
.{},
);
testJsPublishAfterReconnect(
allocator,
manager,
);
testKvAfterReconnect(allocator, manager);
testJsFetchAfterReconnect(
allocator,
manager,
);
testAsyncDuringDisconnect(
allocator,
manager,
);
testPushAfterReconnect(allocator, manager);
}
================================================
FILE: src/testing/client/jwt.zig
================================================
//! JWT/Credentials Authentication Tests for NATS Client
//!
//! Tests JWT authentication with credentials files against nats-server.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const jwt_port = utils.jwt_port;
const test_creds_file = utils.test_creds_file;
/// Tests successful JWT authentication with credentials file.
pub fn testJwtCredsFile(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, jwt_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.creds_file = test_creds_file,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("jwt_creds_file", false, detail);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("jwt_creds_file", true, "");
} else {
reportResult("jwt_creds_file", false, "not connected");
}
}
/// Tests JWT authentication with in-memory credentials content.
pub fn testJwtCredsContent(allocator: std.mem.Allocator) void {
const creds_content = @embedFile("../configs/TestUser.creds");
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, jwt_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.creds = creds_content,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("jwt_creds_content", false, detail);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("jwt_creds_content", true, "");
} else {
reportResult("jwt_creds_content", false, "not connected");
}
}
/// Tests pub/sub works after JWT authentication.
pub fn testJwtPubSub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, jwt_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.creds_file = test_creds_file,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("jwt_pub_sub", false, detail);
return;
};
defer client.deinit();
const sub = client.subscribeSync("jwt.test.subject") catch {
reportResult("jwt_pub_sub", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("jwt.test.subject", "jwt message") catch {
reportResult("jwt_pub_sub", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("jwt_pub_sub", true, "");
} else {
reportResult("jwt_pub_sub", false, "no message");
}
}
pub fn testJwtInvalidCreds(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, jwt_port);
const io = utils.newIo(allocator);
defer io.deinit();
// Wrong seed that won't match the JWT's public key
const wrong_creds =
\\-----BEGIN NATS USER JWT-----
\\eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJMN1dBT1hJU0tPSUZNM1QyNEhMQ09ENzJRT1czQkNVWEdETjRKVU1SSUtHTlQ3RzdZVFRRIiwiaWF0IjoxNjUxNzkwOTgyLCJpc3MiOiJBRFRRUzdaQ0ZWSk5XNTcyNkdPWVhXNVRTQ1pGTklRU0hLMlpHWVVCQ0Q1RDc3T1ROTE9PS1pPWiIsIm5hbWUiOiJUZXN0VXNlciIsInN1YiI6IlVBRkhHNkZVRDJVVTRTREZWQUZVTDVMREZPMlhNNFdZTTc2VU5YVFBKWUpLN0VFTVlSQkhUMlZFIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.bp2-Jsy33l4ayF7Ku1MNdJby4WiMKUrG-rSVYGBusAtV3xP4EdCa-zhSNUaBVIL3uYPPCQYCEoM1pCUdOnoJBg
\\------END NATS USER JWT------
\\-----BEGIN USER NKEY SEED-----
\\SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4
\\------END USER NKEY SEED------
;
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.creds = wrong_creds,
});
if (result) |client| {
client.deinit();
reportResult("jwt_invalid_creds", false, "should have failed");
} else |_| {
reportResult("jwt_invalid_creds", true, "");
}
}
pub fn testJwtMalformedCreds(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, jwt_port);
const io = utils.newIo(allocator);
defer io.deinit();
const malformed_creds = "not a valid creds file";
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.creds = malformed_creds,
});
if (result) |client| {
client.deinit();
reportResult("jwt_malformed_creds", false, "should have failed");
} else |_| {
reportResult("jwt_malformed_creds", true, "");
}
}
pub fn testJwtMissingFile(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, jwt_port);
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.creds_file = "/nonexistent/path/to/creds.creds",
});
if (result) |client| {
client.deinit();
reportResult("jwt_missing_file", false, "should have failed");
} else |_| {
reportResult("jwt_missing_file", true, "");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testJwtCredsFile(allocator);
testJwtCredsContent(allocator);
testJwtPubSub(allocator);
testJwtInvalidCreds(allocator);
testJwtMalformedCreds(allocator);
testJwtMissingFile(allocator);
}
================================================
FILE: src/testing/client/micro.zig
================================================
//! Microservices Integration Tests
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.micro_port;
const ServerManager = utils.ServerManager;
const TestServer = utils.server_manager.TestServer;
const ParseOpts: std.json.ParseOptions = .{
.ignore_unknown_fields = true,
};
const EchoHandler = struct {
pub fn onRequest(_: *@This(), req: *nats.micro.Request) void {
req.respond(req.data()) catch {};
}
};
const CountingHandler = struct {
count: *u32,
pub fn onRequest(self: *@This(), req: *nats.micro.Request) void {
self.count.* += 1;
req.respond(req.data()) catch {};
}
};
fn echoFn(req: *nats.micro.Request) void {
req.respond(req.data()) catch {};
}
const ErrorHandler = struct {
pub fn onRequest(_: *@This(), req: *nats.micro.Request) void {
req.respondError(503, "unavailable", "detail") catch {};
}
};
pub fn runAll(allocator: std.mem.Allocator, manager: *ServerManager) void {
_ = manager;
const io = utils.newIo(allocator);
defer io.deinit();
var server = TestServer.start(allocator, io.io(), .{
.port = test_port,
}) catch {
reportResult("micro_server", false, "start server failed");
return;
};
defer server.deinit(io.io());
testMicroBasicRequest(allocator);
testMicroBasicRequestHandlerFn(allocator);
testMicroRespondError(allocator);
testMicroRespondJson(allocator);
testMicroPing(allocator);
testMicroPingById(allocator);
testMicroInfo(allocator);
testMicroStats(allocator);
testMicroStatsErrorCount(allocator);
testMicroStatsStartedRfc3339(allocator);
testMicroReset(allocator);
testMicroNoQueueFanout(allocator);
testMicroQueueGroupLoadBalance(allocator);
testMicroCustomServiceQueueGroup(allocator);
testMicroEndpointQueueGroupOverride(allocator);
testMicroNestedGroups(allocator);
testMicroMultipleEndpoints(allocator);
testMicroMetadataInInfo(allocator);
testMicroServiceIdUnique(allocator);
testMicroStop(allocator);
testMicroStopIdempotent(allocator);
testMicroDrainOnStop(allocator);
testMicroReconnect(allocator, &server);
}
fn waitForConnected(
io: std.Io,
client: *nats.Client,
timeout_ms: u32,
) bool {
var waited: u32 = 0;
while (waited < timeout_ms) : (waited += 25) {
if (client.isConnected()) return true;
io.sleep(.fromMilliseconds(25), .awake) catch {};
}
return client.isConnected();
}
fn testMicroBasicRequest(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroBasicRequest", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "echo-basic",
.version = "1.0.0",
.endpoint = .{
.subject = "echo.basic",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroBasicRequest", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("echo.basic", "hello", 1000) catch null;
if (msg) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "hello")) {
reportResult("testMicroBasicRequest", true, "");
} else {
reportResult("testMicroBasicRequest", false, "wrong payload");
}
} else {
reportResult("testMicroBasicRequest", false, "no response");
}
}
fn testMicroBasicRequestHandlerFn(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroBasicRequestHandlerFn", false, "connect failed");
return;
};
defer client.deinit();
const service = nats.micro.addService(client, .{
.name = "echo-fn",
.version = "1.0.0",
.endpoint = .{
.subject = "echo.fn",
.handler = nats.micro.Handler.fromFn(echoFn),
},
}) catch {
reportResult("testMicroBasicRequestHandlerFn", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("echo.fn", "hello", 1000) catch null;
if (msg) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "hello")) {
reportResult("testMicroBasicRequestHandlerFn", true, "");
} else {
reportResult("testMicroBasicRequestHandlerFn", false, "wrong payload");
}
} else {
reportResult("testMicroBasicRequestHandlerFn", false, "no response");
}
}
fn testMicroRespondError(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroRespondError", false, "connect failed");
return;
};
defer client.deinit();
var handler = ErrorHandler{};
const service = nats.micro.addService(client, .{
.name = "err-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "err.svc",
.handler = nats.micro.Handler.init(ErrorHandler, &handler),
},
}) catch {
reportResult("testMicroRespondError", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("err.svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
if (m.headers) |raw_headers| {
var parsed = nats.protocol.headers.parse(allocator, raw_headers);
defer parsed.deinit();
const err_hdr = parsed.get("Nats-Service-Error");
const code_hdr = parsed.get("Nats-Service-Error-Code");
if (err_hdr != null and code_hdr != null and
std.mem.eql(u8, err_hdr.?, "unavailable") and
std.mem.eql(u8, code_hdr.?, "503") and
std.mem.eql(u8, m.data, "detail"))
{
reportResult("testMicroRespondError", true, "");
return;
}
}
reportResult("testMicroRespondError", false, "missing error headers");
} else {
reportResult("testMicroRespondError", false, "no response");
}
}
fn testMicroPing(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroPing", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "ping-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "ping.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroPing", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("$SRV.PING.ping-svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.Ping,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroPing", false, "json parse failed");
return;
};
defer parsed.deinit();
if (std.mem.eql(u8, parsed.value.name, "ping-svc") and
std.mem.eql(u8, parsed.value.version, "1.0.0") and
std.mem.eql(u8, parsed.value.type, nats.micro.protocol.Type.ping))
{
reportResult("testMicroPing", true, "");
} else {
reportResult("testMicroPing", false, "wrong ping response");
}
} else {
reportResult("testMicroPing", false, "no response");
}
}
fn testMicroInfo(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroInfo", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "info-svc",
.version = "1.0.0",
.metadata = &.{.{ .key = "team", .value = "core" }},
.endpoint = .{
.subject = "info.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
.metadata = &.{.{ .key = "kind", .value = "echo" }},
},
}) catch {
reportResult("testMicroInfo", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("$SRV.INFO.info-svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.Info,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroInfo", false, "json parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.endpoints.len == 1 and
std.mem.eql(u8, parsed.value.endpoints[0].subject, "info.echo"))
{
reportResult("testMicroInfo", true, "");
} else {
reportResult("testMicroInfo", false, "bad info payload");
}
} else {
reportResult("testMicroInfo", false, "no response");
}
}
fn testMicroStats(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroStats", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "stats-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "stats.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroStats", false, "addService failed");
return;
};
defer service.deinit();
for (0..3) |_| {
const msg = client.request("stats.echo", "x", 1000) catch null;
if (msg) |m| m.deinit();
}
const stats_msg = client.request("$SRV.STATS.stats-svc", "", 1000) catch null;
if (stats_msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.StatsResponse,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroStats", false, "json parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.endpoints.len == 1 and
parsed.value.endpoints[0].num_requests == 3 and
parsed.value.endpoints[0].processing_time > 0)
{
reportResult("testMicroStats", true, "");
} else {
reportResult("testMicroStats", false, "bad stats payload");
}
} else {
reportResult("testMicroStats", false, "no response");
}
}
fn testMicroNoQueueFanout(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_a = utils.newIo(allocator);
defer io_a.deinit();
const client_a = nats.Client.connect(allocator, io_a.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroNoQueueFanout", false, "client_a failed");
return;
};
defer client_a.deinit();
const io_b = utils.newIo(allocator);
defer io_b.deinit();
const client_b = nats.Client.connect(allocator, io_b.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroNoQueueFanout", false, "client_b failed");
return;
};
defer client_b.deinit();
var count_a: u32 = 0;
var count_b: u32 = 0;
var handler_a = CountingHandler{ .count = &count_a };
var handler_b = CountingHandler{ .count = &count_b };
const service_a = nats.micro.addService(client_a, .{
.name = "fanout-svc",
.version = "1.0.0",
.queue_policy = .no_queue,
.endpoint = .{
.subject = "fanout.echo",
.handler = nats.micro.Handler.init(CountingHandler, &handler_a),
},
}) catch {
reportResult("testMicroNoQueueFanout", false, "service_a failed");
return;
};
defer service_a.deinit();
const service_b = nats.micro.addService(client_b, .{
.name = "fanout-svc",
.version = "1.0.0",
.queue_policy = .no_queue,
.endpoint = .{
.subject = "fanout.echo",
.handler = nats.micro.Handler.init(CountingHandler, &handler_b),
},
}) catch {
reportResult("testMicroNoQueueFanout", false, "service_b failed");
return;
};
defer service_b.deinit();
for (0..5) |_| {
const msg = client_a.request("fanout.echo", "x", 1000) catch null;
if (msg) |m| m.deinit();
}
io_a.io().sleep(.fromMilliseconds(200), .awake) catch {};
if (count_a == 5 and count_b == 5) {
reportResult("testMicroNoQueueFanout", true, "");
} else {
reportResult("testMicroNoQueueFanout", false, "fanout count mismatch");
}
}
fn testMicroStop(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("testMicroStop", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "stop-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "stop.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroStop", false, "addService failed");
return;
};
defer service.deinit();
service.stop(null) catch {
reportResult("testMicroStop", false, "stop failed");
return;
};
const msg = client.request("stop.echo", "x", 200) catch null;
if (msg) |m| {
defer m.deinit();
if (m.isNoResponders()) {
reportResult("testMicroStop", true, "");
} else {
reportResult("testMicroStop", false, "request still answered");
}
} else {
reportResult("testMicroStop", true, "");
}
}
fn testMicroReconnect(
allocator: std.mem.Allocator,
server: *TestServer,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("testMicroReconnect", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "reconnect-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "reconnect.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroReconnect", false, "addService failed");
return;
};
defer service.deinit();
server.stop(io.io());
client.forceReconnect() catch {};
server.* = TestServer.start(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("testMicroReconnect", false, "restart failed");
return;
};
if (!waitForConnected(io.io(), client, 5000)) {
reportResult("testMicroReconnect", false, "reconnect timeout");
return;
}
const msg = client.request("reconnect.echo", "again", 1000) catch null;
if (msg) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "again")) {
reportResult("testMicroReconnect", true, "");
} else {
reportResult("testMicroReconnect", false, "wrong payload");
}
} else {
reportResult("testMicroReconnect", false, "no response");
}
}
// ---------- New tests added to close v1 coverage gaps ----------
const JsonResp = struct {
answer: u32,
note: []const u8,
};
const JsonHandler = struct {
pub fn onRequest(_: *@This(), req: *nats.micro.Request) void {
const value = JsonResp{ .answer = 42, .note = "ok" };
req.respondJson(value) catch {};
}
};
fn testMicroRespondJson(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroRespondJson", false, "connect failed");
return;
};
defer client.deinit();
var handler = JsonHandler{};
const service = nats.micro.addService(client, .{
.name = "json-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "json.svc",
.handler = nats.micro.Handler.init(JsonHandler, &handler),
},
}) catch {
reportResult("testMicroRespondJson", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("json.svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
JsonResp,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroRespondJson", false, "parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.answer == 42 and
std.mem.eql(u8, parsed.value.note, "ok"))
{
reportResult("testMicroRespondJson", true, "");
} else {
reportResult("testMicroRespondJson", false, "wrong fields");
}
} else {
reportResult("testMicroRespondJson", false, "no response");
}
}
fn testMicroPingById(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroPingById", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "ping-id-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "ping.id.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroPingById", false, "addService failed");
return;
};
defer service.deinit();
var subject_buf: [128]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"$SRV.PING.ping-id-svc.{s}",
.{service.id},
) catch {
reportResult("testMicroPingById", false, "subject format failed");
return;
};
const msg = client.request(subject, "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.Ping,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroPingById", false, "parse failed");
return;
};
defer parsed.deinit();
if (std.mem.eql(u8, parsed.value.id, service.id)) {
reportResult("testMicroPingById", true, "");
} else {
reportResult("testMicroPingById", false, "id mismatch");
}
} else {
reportResult("testMicroPingById", false, "no response");
}
}
fn testMicroStatsErrorCount(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroStatsErrorCount", false, "connect failed");
return;
};
defer client.deinit();
var handler = ErrorHandler{};
const service = nats.micro.addService(client, .{
.name = "stats-err-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "stats.err.svc",
.handler = nats.micro.Handler.init(ErrorHandler, &handler),
},
}) catch {
reportResult("testMicroStatsErrorCount", false, "addService failed");
return;
};
defer service.deinit();
for (0..3) |_| {
const m = client.request("stats.err.svc", "", 1000) catch null;
if (m) |x| x.deinit();
}
const stats_msg = client.request(
"$SRV.STATS.stats-err-svc",
"",
1000,
) catch null;
if (stats_msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.StatsResponse,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroStatsErrorCount", false, "parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.endpoints.len != 1) {
reportResult("testMicroStatsErrorCount", false, "endpoint count");
return;
}
const ep = parsed.value.endpoints[0];
if (ep.num_requests == 3 and ep.num_errors == 3 and
ep.last_error != null and ep.last_error.?.code == 503)
{
reportResult("testMicroStatsErrorCount", true, "");
} else {
reportResult("testMicroStatsErrorCount", false, "wrong counts");
}
} else {
reportResult("testMicroStatsErrorCount", false, "no stats");
}
}
fn isAsciiDigit(c: u8) bool {
return c >= '0' and c <= '9';
}
fn looksLikeRfc3339(s: []const u8) bool {
// Expected: YYYY-MM-DDTHH:MM:SS.mmmZ -> 24 chars
if (s.len != 24) return false;
if (s[4] != '-' or s[7] != '-' or s[10] != 'T' or
s[13] != ':' or s[16] != ':' or s[19] != '.' or s[23] != 'Z')
return false;
for (s[0..4]) |c| if (!isAsciiDigit(c)) return false;
for (s[5..7]) |c| if (!isAsciiDigit(c)) return false;
for (s[8..10]) |c| if (!isAsciiDigit(c)) return false;
for (s[11..13]) |c| if (!isAsciiDigit(c)) return false;
for (s[14..16]) |c| if (!isAsciiDigit(c)) return false;
for (s[17..19]) |c| if (!isAsciiDigit(c)) return false;
for (s[20..23]) |c| if (!isAsciiDigit(c)) return false;
return true;
}
fn testMicroStatsStartedRfc3339(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroStatsStartedRfc3339", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "started-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "started.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroStatsStartedRfc3339", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("$SRV.STATS.started-svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.StatsResponse,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroStatsStartedRfc3339", false, "parse failed");
return;
};
defer parsed.deinit();
if (looksLikeRfc3339(parsed.value.started)) {
reportResult("testMicroStatsStartedRfc3339", true, "");
} else {
reportResult("testMicroStatsStartedRfc3339", false, "bad format");
}
} else {
reportResult("testMicroStatsStartedRfc3339", false, "no response");
}
}
fn testMicroReset(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroReset", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "reset-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "reset.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroReset", false, "addService failed");
return;
};
defer service.deinit();
for (0..3) |_| {
const m = client.request("reset.echo", "x", 1000) catch null;
if (m) |x| x.deinit();
}
service.reset();
const stats_msg = client.request("$SRV.STATS.reset-svc", "", 1000) catch null;
if (stats_msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.StatsResponse,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroReset", false, "parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.endpoints.len == 1 and
parsed.value.endpoints[0].num_requests == 0 and
parsed.value.endpoints[0].processing_time == 0)
{
reportResult("testMicroReset", true, "");
} else {
reportResult("testMicroReset", false, "stats not zero");
}
} else {
reportResult("testMicroReset", false, "no stats");
}
}
fn testMicroQueueGroupLoadBalance(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_a = utils.newIo(allocator);
defer io_a.deinit();
const client_a = nats.Client.connect(allocator, io_a.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroQueueGroupLoadBalance", false, "client_a failed");
return;
};
defer client_a.deinit();
const io_b = utils.newIo(allocator);
defer io_b.deinit();
const client_b = nats.Client.connect(allocator, io_b.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroQueueGroupLoadBalance", false, "client_b failed");
return;
};
defer client_b.deinit();
var count_a: u32 = 0;
var count_b: u32 = 0;
var handler_a = CountingHandler{ .count = &count_a };
var handler_b = CountingHandler{ .count = &count_b };
const svc_a = nats.micro.addService(client_a, .{
.name = "lb-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "lb.echo",
.handler = nats.micro.Handler.init(CountingHandler, &handler_a),
},
}) catch {
reportResult("testMicroQueueGroupLoadBalance", false, "svc_a failed");
return;
};
defer svc_a.deinit();
const svc_b = nats.micro.addService(client_b, .{
.name = "lb-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "lb.echo",
.handler = nats.micro.Handler.init(CountingHandler, &handler_b),
},
}) catch {
reportResult("testMicroQueueGroupLoadBalance", false, "svc_b failed");
return;
};
defer svc_b.deinit();
const N = 20;
for (0..N) |_| {
const m = client_a.request("lb.echo", "x", 1000) catch null;
if (m) |x| x.deinit();
}
io_a.io().sleep(.fromMilliseconds(100), .awake) catch {};
// Both should receive >0 and combined == N (queue group → load split).
if (count_a + count_b == N and count_a > 0 and count_b > 0) {
reportResult("testMicroQueueGroupLoadBalance", true, "");
} else {
reportResult("testMicroQueueGroupLoadBalance", false, "unbalanced");
}
}
fn testMicroCustomServiceQueueGroup(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroCustomServiceQueueGroup", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "custom-q-svc",
.version = "1.0.0",
.queue_policy = .{ .queue = "svc-q" },
.endpoint = .{
.subject = "custom.q.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroCustomServiceQueueGroup", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("$SRV.INFO.custom-q-svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.Info,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroCustomServiceQueueGroup", false, "parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.endpoints.len == 1 and
parsed.value.endpoints[0].queue_group != null and
std.mem.eql(u8, parsed.value.endpoints[0].queue_group.?, "svc-q"))
{
reportResult("testMicroCustomServiceQueueGroup", true, "");
} else {
reportResult("testMicroCustomServiceQueueGroup", false, "wrong queue");
}
} else {
reportResult("testMicroCustomServiceQueueGroup", false, "no info");
}
}
fn testMicroEndpointQueueGroupOverride(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroEndpointQueueGroupOverride", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "ep-override-svc",
.version = "1.0.0",
.queue_policy = .{ .queue = "svc-q" },
.endpoint = .{
.subject = "ep.override.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
.queue_policy = .{ .queue = "ep-q" },
},
}) catch {
reportResult("testMicroEndpointQueueGroupOverride", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("$SRV.INFO.ep-override-svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.Info,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroEndpointQueueGroupOverride", false, "parse failed");
return;
};
defer parsed.deinit();
if (parsed.value.endpoints.len == 1 and
parsed.value.endpoints[0].queue_group != null and
std.mem.eql(u8, parsed.value.endpoints[0].queue_group.?, "ep-q"))
{
reportResult("testMicroEndpointQueueGroupOverride", true, "");
} else {
reportResult("testMicroEndpointQueueGroupOverride", false, "wrong queue");
}
} else {
reportResult("testMicroEndpointQueueGroupOverride", false, "no info");
}
}
fn testMicroNestedGroups(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroNestedGroups", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "nested-svc",
.version = "1.0.0",
}) catch {
reportResult("testMicroNestedGroups", false, "addService failed");
return;
};
defer service.deinit();
var v1 = service.addGroup("v1") catch {
reportResult("testMicroNestedGroups", false, "addGroup v1 failed");
return;
};
var users = v1.group("users") catch {
reportResult("testMicroNestedGroups", false, "nested group failed");
return;
};
_ = users.addEndpoint(.{
.subject = "get",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
}) catch {
reportResult("testMicroNestedGroups", false, "addEndpoint failed");
return;
};
const msg = client.request("v1.users.get", "hello", 1000) catch null;
if (msg) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "hello")) {
reportResult("testMicroNestedGroups", true, "");
} else {
reportResult("testMicroNestedGroups", false, "wrong payload");
}
} else {
reportResult("testMicroNestedGroups", false, "no response");
}
}
fn testMicroMultipleEndpoints(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroMultipleEndpoints", false, "connect failed");
return;
};
defer client.deinit();
var count_a: u32 = 0;
var count_b: u32 = 0;
var handler_a = CountingHandler{ .count = &count_a };
var handler_b = CountingHandler{ .count = &count_b };
const service = nats.micro.addService(client, .{
.name = "multi-ep-svc",
.version = "1.0.0",
}) catch {
reportResult("testMicroMultipleEndpoints", false, "addService failed");
return;
};
defer service.deinit();
_ = service.addEndpoint(.{
.subject = "multi.a",
.handler = nats.micro.Handler.init(CountingHandler, &handler_a),
}) catch {
reportResult("testMicroMultipleEndpoints", false, "addEndpoint a failed");
return;
};
_ = service.addEndpoint(.{
.subject = "multi.b",
.handler = nats.micro.Handler.init(CountingHandler, &handler_b),
}) catch {
reportResult("testMicroMultipleEndpoints", false, "addEndpoint b failed");
return;
};
for (0..3) |_| {
const m = client.request("multi.a", "x", 1000) catch null;
if (m) |x| x.deinit();
}
const m_only = client.request("multi.b", "y", 1000) catch null;
if (m_only) |x| x.deinit();
if (count_a == 3 and count_b == 1) {
reportResult("testMicroMultipleEndpoints", true, "");
} else {
reportResult("testMicroMultipleEndpoints", false, "wrong split");
}
}
fn testMicroMetadataInInfo(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroMetadataInInfo", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "meta-svc",
.version = "1.0.0",
.metadata = &.{
.{ .key = "team", .value = "core" },
.{ .key = "env", .value = "test" },
},
.endpoint = .{
.subject = "meta.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
.metadata = &.{
.{ .key = "kind", .value = "echo" },
},
},
}) catch {
reportResult("testMicroMetadataInInfo", false, "addService failed");
return;
};
defer service.deinit();
const msg = client.request("$SRV.INFO.meta-svc", "", 1000) catch null;
if (msg) |m| {
defer m.deinit();
var parsed = std.json.parseFromSlice(
nats.micro.protocol.Info,
allocator,
m.data,
ParseOpts,
) catch {
reportResult("testMicroMetadataInInfo", false, "parse failed");
return;
};
defer parsed.deinit();
const svc_md = parsed.value.metadata orelse {
reportResult("testMicroMetadataInInfo", false, "no svc metadata");
return;
};
if (svc_md.len != 2) {
reportResult("testMicroMetadataInInfo", false, "svc md count");
return;
}
if (parsed.value.endpoints.len != 1) {
reportResult("testMicroMetadataInInfo", false, "ep count");
return;
}
const ep_md = parsed.value.endpoints[0].metadata orelse {
reportResult("testMicroMetadataInInfo", false, "no ep metadata");
return;
};
if (ep_md.len != 1 or
!std.mem.eql(u8, ep_md[0].key, "kind") or
!std.mem.eql(u8, ep_md[0].value, "echo"))
{
reportResult("testMicroMetadataInInfo", false, "ep md wrong");
return;
}
reportResult("testMicroMetadataInInfo", true, "");
} else {
reportResult("testMicroMetadataInInfo", false, "no info");
}
}
fn testMicroServiceIdUnique(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroServiceIdUnique", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const svc_a = nats.micro.addService(client, .{
.name = "uniq-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "uniq.a",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroServiceIdUnique", false, "svc_a failed");
return;
};
defer svc_a.deinit();
const svc_b = nats.micro.addService(client, .{
.name = "uniq-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "uniq.b",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroServiceIdUnique", false, "svc_b failed");
return;
};
defer svc_b.deinit();
if (svc_a.id.len == 16 and svc_b.id.len == 16 and
!std.mem.eql(u8, svc_a.id, svc_b.id))
{
reportResult("testMicroServiceIdUnique", true, "");
} else {
reportResult("testMicroServiceIdUnique", false, "id collision or wrong len");
}
}
fn testMicroStopIdempotent(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroStopIdempotent", false, "connect failed");
return;
};
defer client.deinit();
var echo = EchoHandler{};
const service = nats.micro.addService(client, .{
.name = "stop-twice-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "stop.twice.echo",
.handler = nats.micro.Handler.init(EchoHandler, &echo),
},
}) catch {
reportResult("testMicroStopIdempotent", false, "addService failed");
return;
};
defer service.deinit();
service.stop(null) catch {
reportResult("testMicroStopIdempotent", false, "first stop failed");
return;
};
service.stop(null) catch {
reportResult("testMicroStopIdempotent", false, "second stop failed");
return;
};
if (service.stopped()) {
reportResult("testMicroStopIdempotent", true, "");
} else {
reportResult("testMicroStopIdempotent", false, "not stopped");
}
}
const BlockingEcho = struct {
started: *std.atomic.Value(bool),
release: *std.atomic.Value(bool),
finished: *std.atomic.Value(bool),
pub fn onRequest(self: *@This(), req: *nats.micro.Request) void {
self.started.store(true, .release);
while (!self.release.load(.acquire)) {
req.client.io.sleep(.fromMilliseconds(1), .awake) catch {};
}
req.respond("done") catch {};
self.finished.store(true, .release);
}
};
const StopState = struct {
done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
err: ?anyerror = null,
};
fn stopService(
service: *nats.micro.Service,
state: *StopState,
) void {
service.stop(null) catch |err| {
state.err = err;
};
state.done.store(true, .release);
}
fn drainRequester(
client: *nats.Client,
out: *?nats.Client.Message,
) void {
out.* = client.request("drain.echo", "x", 5000) catch null;
}
fn testMicroDrainOnStop(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const service_io = utils.newIo(allocator);
defer service_io.deinit();
const service_client = nats.Client.connect(allocator, service_io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroDrainOnStop", false, "connect failed");
return;
};
defer service_client.deinit();
var started = std.atomic.Value(bool).init(false);
var release = std.atomic.Value(bool).init(false);
var finished = std.atomic.Value(bool).init(false);
var blocking = BlockingEcho{
.started = &started,
.release = &release,
.finished = &finished,
};
const service = nats.micro.addService(service_client, .{
.name = "drain-svc",
.version = "1.0.0",
.endpoint = .{
.subject = "drain.echo",
.handler = nats.micro.Handler.init(BlockingEcho, &blocking),
},
}) catch {
reportResult("testMicroDrainOnStop", false, "addService failed");
return;
};
defer service.deinit();
const requester_io = utils.newIo(allocator);
defer requester_io.deinit();
const requester = nats.Client.connect(allocator, requester_io.io(), url, .{
.reconnect = false,
}) catch {
reportResult("testMicroDrainOnStop", false, "requester failed");
return;
};
defer requester.deinit();
var resp: ?nats.Client.Message = null;
var fut = requester_io.io().async(drainRequester, .{ requester, &resp });
// Wait until the handler has actually entered.
var spins: u32 = 0;
while (!started.load(.acquire) and spins < 200) {
service_io.io().sleep(.fromMilliseconds(5), .awake) catch {};
spins += 1;
}
if (!started.load(.acquire)) {
_ = fut.cancel(requester_io.io());
reportResult("testMicroDrainOnStop", false, "handler never entered");
return;
}
var stop_state = StopState{};
var stop_fut = service_io.io().concurrent(stopService, .{ service, &stop_state }) catch
service_io.io().async(stopService, .{ service, &stop_state });
spins = 0;
while (!service.stopping.load(.acquire) and spins < 200) {
service_io.io().sleep(.fromMilliseconds(1), .awake) catch {};
spins += 1;
}
if (!service.stopping.load(.acquire)) {
release.store(true, .release);
stop_fut.await(service_io.io());
fut.await(requester_io.io());
defer if (resp) |m| m.deinit();
reportResult("testMicroDrainOnStop", false, "stop never started");
return;
}
service_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
if (stop_state.done.load(.acquire)) {
release.store(true, .release);
stop_fut.await(service_io.io());
fut.await(requester_io.io());
defer if (resp) |m| m.deinit();
reportResult("testMicroDrainOnStop", false, "stop returned early");
return;
}
release.store(true, .release);
stop_fut.await(service_io.io());
if (stop_state.err != null) {
_ = fut.cancel(requester_io.io());
reportResult("testMicroDrainOnStop", false, "stop failed");
return;
}
fut.await(requester_io.io());
defer if (resp) |m| m.deinit();
if (resp != null and finished.load(.acquire) and service.stopped()) {
reportResult("testMicroDrainOnStop", true, "");
} else {
reportResult("testMicroDrainOnStop", false, "drain incomplete");
}
}
================================================
FILE: src/testing/client/multi_client.zig
================================================
//! Multi-Client Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testCrossClientRouting(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("cross_client", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const subscriber = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("cross_client", false, "sub connect failed");
return;
};
defer subscriber.deinit();
const sub = subscriber.subscribeSync("cross") catch {
reportResult("cross_client", false, "sub failed");
return;
};
defer sub.deinit();
sub_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
publisher.publish("cross", "cross-message") catch {
reportResult("cross_client", false, "publish failed");
return;
};
var future = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(sub_io.io())) |m| m.deinit() else |_| {};
if (future.await(sub_io.io())) |msg| {
if (std.mem.eql(u8, msg.data, "cross-message")) {
reportResult("cross_client", true, "");
return;
}
} else |_| {}
reportResult("cross_client", false, "no message");
}
pub fn testMultipleClients(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const client1 = nats.Client.connect(
allocator,
io1.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multiple_clients", false, "client1 failed");
return;
};
defer client1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const client2 = nats.Client.connect(
allocator,
io2.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multiple_clients", false, "client2 failed");
return;
};
defer client2.deinit();
const io3 = utils.newIo(allocator);
defer io3.deinit();
const client3 = nats.Client.connect(
allocator,
io3.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multiple_clients", false, "client3 failed");
return;
};
defer client3.deinit();
if (client1.isConnected() and client2.isConnected() and
client3.isConnected())
{
reportResult("multiple_clients", true, "");
} else {
reportResult("multiple_clients", false, "not all connected");
}
}
pub fn testClientHighRate(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_high_rate", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(allocator, sub_io.io(), url, .{
.sub_queue_size = 512,
.reconnect = false,
}) catch {
reportResult("client_high_rate", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("highrate") catch {
reportResult("client_high_rate", false, "sub failed");
return;
};
defer sub.deinit();
// Wait for subscription to be registered on server.
client.flush(50_000_000) catch {};
const NUM_MSGS = 100;
for (0..NUM_MSGS) |_| {
publisher.publish("highrate", "msg") catch {
reportResult("client_high_rate", false, "publish failed");
return;
};
}
publisher.flush(500_000_000) catch {};
var received: usize = 0;
for (0..NUM_MSGS) |_| {
var future = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(sub_io.io())) |m| m.deinit() else |_| {};
if (future.await(sub_io.io())) |_| {
received += 1;
} else |_| {
break;
}
}
if (received == NUM_MSGS) {
reportResult("client_high_rate", true, "");
} else {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "got {d}/100", .{received}) catch "e";
reportResult("client_high_rate", false, msg);
}
}
pub fn testThreeClientChain(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
// Client A - initial publisher
const io_a = utils.newIo(allocator);
defer io_a.deinit();
const client_a = nats.Client.connect(
allocator,
io_a.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("three_client_chain", false, "A connect failed");
return;
};
defer client_a.deinit();
// Client B - middleware (receives from A, forwards to C)
const io_b = utils.newIo(allocator);
defer io_b.deinit();
const client_b = nats.Client.connect(
allocator,
io_b.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("three_client_chain", false, "B connect failed");
return;
};
defer client_b.deinit();
// Client C - final receiver
const io_c = utils.newIo(allocator);
defer io_c.deinit();
const client_c = nats.Client.connect(
allocator,
io_c.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("three_client_chain", false, "C connect failed");
return;
};
defer client_c.deinit();
// B subscribes to "step1"
const sub_b = client_b.subscribeSync("chain.step1") catch {
reportResult("three_client_chain", false, "B sub failed");
return;
};
defer sub_b.deinit();
// C subscribes to "step2"
const sub_c = client_c.subscribeSync("chain.step2") catch {
reportResult("three_client_chain", false, "C sub failed");
return;
};
defer sub_c.deinit();
io_a.io().sleep(.fromMilliseconds(50), .awake) catch {};
// A publishes to step1
client_a.publish("chain.step1", "start") catch {
reportResult("three_client_chain", false, "A publish failed");
return;
};
// B receives and forwards to step2
const msg_b = sub_b.nextMsgTimeout(2000) catch {
reportResult("three_client_chain", false, "B receive failed");
return;
};
if (msg_b) |m| {
defer m.deinit();
client_b.publish("chain.step2", "forwarded") catch {
reportResult("three_client_chain", false, "B forward failed");
return;
};
} else {
reportResult("three_client_chain", false, "B no message");
return;
}
// C receives final message
const msg_c = sub_c.nextMsgTimeout(2000) catch {
reportResult("three_client_chain", false, "C receive failed");
return;
};
if (msg_c) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "forwarded")) {
reportResult("three_client_chain", true, "");
} else {
reportResult("three_client_chain", false, "wrong data");
}
} else {
reportResult("three_client_chain", false, "C no message");
}
}
pub fn testMultipleSubscribersSameSubject(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_sub_same_subject", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.subscribeSync("broadcast.test") catch {
reportResult("multi_sub_same_subject", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.subscribeSync("broadcast.test") catch {
reportResult("multi_sub_same_subject", false, "sub2 failed");
return;
};
defer sub2.deinit();
const sub3 = client.subscribeSync("broadcast.test") catch {
reportResult("multi_sub_same_subject", false, "sub3 failed");
return;
};
defer sub3.deinit();
client.publish("broadcast.test", "hello all") catch {
reportResult("multi_sub_same_subject", false, "publish failed");
return;
};
client.flush(500_000_000) catch {};
var count: u32 = 0;
if (sub1.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (sub2.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (sub3.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (count == 3) {
reportResult("multi_sub_same_subject", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/3", .{count}) catch "err";
reportResult("multi_sub_same_subject", false, detail);
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testCrossClientRouting(allocator);
testMultipleClients(allocator);
testClientHighRate(allocator);
testThreeClientChain(allocator);
testMultipleSubscribersSameSubject(allocator);
}
================================================
FILE: src/testing/client/multithread.zig
================================================
//! Multi-Thread Safety Tests for NATS Client
//!
//! Tests that a single Client can be safely used from multiple OS
//! threads for publish, subscribe, and request operations.
//! These tests use std.Thread.spawn for real OS-level concurrency.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
/// Blocking sleep for OS threads (no Io context).
/// Uses the same pattern as Client.zig:reserveRingEntry.
fn threadSleepNs(ns: u64) void {
var ts: std.posix.timespec = .{
.sec = @intCast(ns / 1_000_000_000),
.nsec = @intCast(ns % 1_000_000_000),
};
_ = std.posix.system.nanosleep(&ts, &ts);
}
/// Multiple OS threads publishing via the same client.
/// Verifies all messages arrive with no corruption.
pub fn testMultiThreadPublish(
allocator: std.mem.Allocator,
) void {
const NUM_THREADS = 4;
const MSGS_PER_THREAD = 5000;
const TOTAL = NUM_THREADS * MSGS_PER_THREAD;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = false,
// 32K queue to hold all 20K messages
.sub_queue_size = 32768,
},
) catch {
reportResult(
"mt_publish",
false,
"connect failed",
);
return;
};
defer client.deinit();
// Subscribe to receive all messages
const sub = client.subscribeSync("mt.pub.>") catch {
reportResult(
"mt_publish",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
// Wait for subscription to register
client.flush(5_000_000_000) catch {};
// Spawn publisher threads (track success counts)
var threads: [NUM_THREADS]std.Thread = undefined;
var pub_counts: [NUM_THREADS]std.atomic.Value(u32) =
undefined;
for (0..NUM_THREADS) |i| {
pub_counts[i] = std.atomic.Value(u32).init(0);
threads[i] = std.Thread.spawn(
.{},
publishThread,
.{ client, i, MSGS_PER_THREAD, &pub_counts[i] },
) catch {
reportResult(
"mt_publish",
false,
"spawn failed",
);
return;
};
}
// Wait for all publishers
for (&threads) |*t| t.join();
// Verify all publishes succeeded (no ring-full drops)
var total_published: u32 = 0;
for (&pub_counts) |*c| {
total_published += c.load(.monotonic);
}
if (total_published != TOTAL) {
var buf2: [64]u8 = undefined;
const d = std.fmt.bufPrint(
&buf2,
"published {d}/{d}",
.{ total_published, TOTAL },
) catch "pub fail";
reportResult("mt_publish", false, d);
return;
}
// Flush to push all ring data through to server
client.flush(5_000_000_000) catch {};
// Collect messages: poll with deadline (5s timeout).
// tryNextMsg() returns null when queue is momentarily
// empty, but more messages may still be in flight.
var received: usize = 0;
var empty_polls: usize = 0;
while (received < TOTAL and empty_polls < 500) {
if (sub.tryNextMsg()) |m| {
received += 1;
m.deinit();
empty_polls = 0;
} else {
empty_polls += 1;
threadSleepNs(10_000_000); // 10ms
}
}
if (received >= TOTAL) {
reportResult("mt_publish", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ received, TOTAL },
) catch "count mismatch";
reportResult("mt_publish", false, detail);
}
}
fn publishThread(
client: *nats.Client,
thread_id: usize,
count: usize,
ok_count: *std.atomic.Value(u32),
) void {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"mt.pub.{d}",
.{thread_id},
) catch return;
var payload_buf: [64]u8 = undefined;
for (0..count) |seq| {
const payload = std.fmt.bufPrint(
&payload_buf,
"{d}:{d}",
.{ thread_id, seq },
) catch continue;
client.publish(subject, payload) catch continue;
_ = ok_count.fetchAdd(1, .monotonic);
}
}
/// Multiple OS threads subscribing/unsubscribing concurrently.
/// Verifies no slot corruption, no duplicate SIDs.
pub fn testMultiThreadSubscribe(
allocator: std.mem.Allocator,
) void {
const NUM_THREADS = 4;
const ITERS = 200;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"mt_subscribe",
false,
"connect failed",
);
return;
};
defer client.deinit();
// Each thread subscribes and unsubscribes in a loop
var threads: [NUM_THREADS]std.Thread = undefined;
var errors: [NUM_THREADS]std.atomic.Value(u32) = undefined;
for (0..NUM_THREADS) |i| {
errors[i] = std.atomic.Value(u32).init(0);
threads[i] = std.Thread.spawn(
.{},
subUnsubThread,
.{ client, i, ITERS, &errors[i] },
) catch {
reportResult(
"mt_subscribe",
false,
"spawn failed",
);
return;
};
}
for (&threads) |*t| t.join();
var total_errors: u32 = 0;
for (&errors) |*e| {
total_errors += e.load(.monotonic);
}
const num_subs = client.numSubscriptions();
if (total_errors == 0 and num_subs == 0) {
reportResult("mt_subscribe", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"errs={d} leaked_subs={d}",
.{ total_errors, num_subs },
) catch "errors";
reportResult("mt_subscribe", false, detail);
}
}
fn subUnsubThread(
client: *nats.Client,
thread_id: usize,
iters: usize,
err_count: *std.atomic.Value(u32),
) void {
for (0..iters) |i| {
var subject_buf: [48]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"mt.sub.{d}.{d}",
.{ thread_id, i },
) catch continue;
const sub = client.subscribeSync(subject) catch {
_ = err_count.fetchAdd(1, .monotonic);
continue;
};
sub.deinit();
}
}
/// Multiple OS threads publishing, verify stats are accurate.
pub fn testMultiThreadStats(
allocator: std.mem.Allocator,
) void {
const NUM_THREADS = 4;
const MSGS_PER_THREAD = 5000;
const TOTAL = NUM_THREADS * MSGS_PER_THREAD;
const PAYLOAD = "stats-test-payload";
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"mt_stats",
false,
"connect failed",
);
return;
};
defer client.deinit();
const before = client.stats();
var threads: [NUM_THREADS]std.Thread = undefined;
for (0..NUM_THREADS) |i| {
threads[i] = std.Thread.spawn(
.{},
statsPublishThread,
.{ client, MSGS_PER_THREAD, PAYLOAD },
) catch {
reportResult(
"mt_stats",
false,
"spawn failed",
);
return;
};
}
for (&threads) |*t| t.join();
const after = client.stats();
const msgs_diff = after.msgs_out - before.msgs_out;
const bytes_diff = after.bytes_out - before.bytes_out;
const expected_bytes = TOTAL * PAYLOAD.len;
if (msgs_diff == TOTAL and bytes_diff == expected_bytes) {
reportResult("mt_stats", true, "");
} else {
var buf: [96]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"msgs={d}/{d} bytes={d}/{d}",
.{
msgs_diff,
TOTAL,
bytes_diff,
expected_bytes,
},
) catch "mismatch";
reportResult("mt_stats", false, detail);
}
}
fn statsPublishThread(
client: *nats.Client,
count: usize,
payload: []const u8,
) void {
for (0..count) |_| {
client.publish("mt.stats", payload) catch {};
}
}
/// Mixed workload: publish + subscribe from different threads.
pub fn testMultiThreadMixed(
allocator: std.mem.Allocator,
) void {
const NUM_PUB_THREADS = 2;
const MSGS_PER_THREAD = 3000;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"mt_mixed",
false,
"connect failed",
);
return;
};
defer client.deinit();
// Subscribe first
const sub = client.subscribeSync("mt.mix") catch {
reportResult(
"mt_mixed",
false,
"subscribe failed",
);
return;
};
defer sub.deinit();
client.flush(5_000_000_000) catch {};
// Spawn publisher threads
var pub_threads: [NUM_PUB_THREADS]std.Thread = undefined;
for (0..NUM_PUB_THREADS) |i| {
pub_threads[i] = std.Thread.spawn(
.{},
mixedPublishThread,
.{ client, MSGS_PER_THREAD },
) catch {
reportResult(
"mt_mixed",
false,
"spawn failed",
);
return;
};
}
// Spawn a subscribe/unsubscribe churn thread
var churn_err = std.atomic.Value(u32).init(0);
const churn_thread = std.Thread.spawn(
.{},
subChurnThread,
.{ client, 100, &churn_err },
) catch {
reportResult(
"mt_mixed",
false,
"churn spawn failed",
);
return;
};
// Wait for publishers
for (&pub_threads) |*t| t.join();
churn_thread.join();
// Collect with deadline
var received: usize = 0;
const total = NUM_PUB_THREADS * MSGS_PER_THREAD;
var empty_polls: usize = 0;
while (received < total and empty_polls < 500) {
if (sub.tryNextMsg()) |m| {
received += 1;
m.deinit();
empty_polls = 0;
} else {
empty_polls += 1;
threadSleepNs(10_000_000); // 10ms
}
}
const churn_errors = churn_err.load(.monotonic);
if (received >= total and churn_errors == 0) {
reportResult("mt_mixed", true, "");
} else {
var buf: [80]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"recv={d}/{d} churn_err={d}",
.{ received, total, churn_errors },
) catch "error";
reportResult("mt_mixed", false, detail);
}
}
fn mixedPublishThread(
client: *nats.Client,
count: usize,
) void {
for (0..count) |_| {
client.publish("mt.mix", "mixed") catch {};
}
}
fn subChurnThread(
client: *nats.Client,
iters: usize,
err_count: *std.atomic.Value(u32),
) void {
for (0..iters) |i| {
var buf: [48]u8 = undefined;
const subject = std.fmt.bufPrint(
&buf,
"mt.churn.{d}",
.{i},
) catch continue;
const sub = client.subscribeSync(subject) catch {
_ = err_count.fetchAdd(1, .monotonic);
continue;
};
sub.deinit();
}
}
/// Multiple threads doing request/reply concurrently.
pub fn testMultiThreadRequest(
allocator: std.mem.Allocator,
) void {
const NUM_THREADS = 4;
const REQS_PER_THREAD = 50;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
// Requester client
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(
allocator,
io_req.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"mt_request",
false,
"req connect failed",
);
return;
};
defer requester.deinit();
// Responder client
const io_resp = utils.newIo(allocator);
defer io_resp.deinit();
const responder = nats.Client.connect(
allocator,
io_resp.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"mt_request",
false,
"resp connect failed",
);
return;
};
defer responder.deinit();
// Responder subscribes and echoes back
const resp_sub = responder.subscribeSync(
"mt.req",
) catch {
reportResult(
"mt_request",
false,
"resp subscribe failed",
);
return;
};
defer resp_sub.deinit();
responder.flush(5_000_000_000) catch {};
// Responder loop in a thread
var stop_flag = std.atomic.Value(bool).init(false);
const resp_thread = std.Thread.spawn(
.{},
responderThread,
.{ responder, resp_sub, &stop_flag },
) catch {
reportResult(
"mt_request",
false,
"resp spawn failed",
);
return;
};
// Spawn requester threads
var threads: [NUM_THREADS]std.Thread = undefined;
var successes: [NUM_THREADS]std.atomic.Value(u32) =
undefined;
for (0..NUM_THREADS) |i| {
successes[i] = std.atomic.Value(u32).init(0);
threads[i] = std.Thread.spawn(
.{},
requestThread,
.{
requester,
REQS_PER_THREAD,
&successes[i],
},
) catch {
reportResult(
"mt_request",
false,
"req spawn failed",
);
stop_flag.store(true, .release);
resp_thread.join();
return;
};
}
for (&threads) |*t| t.join();
stop_flag.store(true, .release);
resp_thread.join();
var total_success: u32 = 0;
for (&successes) |*s| {
total_success += s.load(.monotonic);
}
const expected = NUM_THREADS * REQS_PER_THREAD;
if (total_success >= expected) {
reportResult("mt_request", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"replies={d}/{d}",
.{ total_success, expected },
) catch "mismatch";
reportResult("mt_request", false, detail);
}
}
fn responderThread(
responder: *nats.Client,
sub: *nats.Subscription,
stop: *std.atomic.Value(bool),
) void {
while (!stop.load(.acquire)) {
if (sub.tryNextMsg()) |m| {
defer m.deinit();
if (m.reply_to) |reply| {
responder.publish(
reply,
m.data,
) catch {};
}
} else {
threadSleepNs(1_000_000);
}
}
}
fn requestThread(
client: *nats.Client,
count: usize,
success: *std.atomic.Value(u32),
) void {
for (0..count) |_| {
const reply = client.request(
"mt.req",
"ping",
2000,
) catch continue;
if (reply) |r| {
r.deinit();
_ = success.fetchAdd(1, .monotonic);
}
}
}
/// Stress test: many threads, rapid sub/unsub, verify
/// slot accounting stays correct.
pub fn testSubSlotIntegrity(
allocator: std.mem.Allocator,
) void {
const NUM_THREADS = 4;
const ITERS = 500;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult(
"mt_slot_integrity",
false,
"connect failed",
);
return;
};
defer client.deinit();
var threads: [NUM_THREADS]std.Thread = undefined;
var errors: [NUM_THREADS]std.atomic.Value(u32) =
undefined;
for (0..NUM_THREADS) |i| {
errors[i] = std.atomic.Value(u32).init(0);
threads[i] = std.Thread.spawn(
.{},
subUnsubThread,
.{ client, i, ITERS, &errors[i] },
) catch {
reportResult(
"mt_slot_integrity",
false,
"spawn failed",
);
return;
};
}
for (&threads) |*t| t.join();
var total_errors: u32 = 0;
for (&errors) |*e| {
total_errors += e.load(.monotonic);
}
const final_subs = client.numSubscriptions();
if (total_errors == 0 and final_subs == 0) {
reportResult("mt_slot_integrity", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"errs={d} subs={d}",
.{ total_errors, final_subs },
) catch "error";
reportResult("mt_slot_integrity", false, detail);
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
std.debug.print(
"\n--- MultiThreading Tests ---\n",
.{},
);
testMultiThreadPublish(allocator);
testMultiThreadSubscribe(allocator);
testMultiThreadStats(allocator);
testMultiThreadMixed(allocator);
testMultiThreadRequest(allocator);
testSubSlotIntegrity(allocator);
}
================================================
FILE: src/testing/client/nkey.zig
================================================
//! NKey Authentication Tests for NATS Client
//!
//! Tests NKey (Ed25519) authentication against nats-server.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const nkey_port = utils.nkey_port;
const test_nkey_seed = utils.test_nkey_seed;
/// Tests successful NKey authentication with valid seed.
pub fn testNKeyAuthentication(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_seed = test_nkey_seed,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("nkey_authentication", false, detail);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("nkey_authentication", true, "");
} else {
reportResult("nkey_authentication", false, "not connected");
}
}
/// Tests that authentication fails with wrong NKey seed.
pub fn testNKeyAuthFailure(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
const io = utils.newIo(allocator);
defer io.deinit();
const wrong_seed =
"SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4";
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_seed = wrong_seed,
});
if (result) |client| {
client.deinit();
reportResult("nkey_auth_failure", false, "should have failed");
} else |_| {
reportResult("nkey_auth_failure", true, "");
}
}
/// Tests pub/sub works after NKey authentication.
pub fn testNKeyPubSub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_seed = test_nkey_seed,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("nkey_pubsub", false, detail);
return;
};
defer client.deinit();
const sub = client.subscribeSync("nkey.test.subject") catch {
reportResult("nkey_pubsub", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("nkey.test.subject", "nkey message") catch {
reportResult("nkey_pubsub", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("nkey_pubsub", true, "");
} else {
reportResult("nkey_pubsub", false, "no message");
}
}
/// Tests that connecting without seed when NKey is required fails.
pub fn testNKeyNoSeedFails(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
});
if (result) |client| {
client.deinit();
reportResult("nkey_no_seed_fails", false, "should have failed");
} else |_| {
reportResult("nkey_no_seed_fails", true, "");
}
}
/// Tests that invalid seed format returns error.
pub fn testNKeyInvalidSeedFormat(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_seed = "SUAMK2FG",
});
if (result) |client| {
client.deinit();
reportResult("nkey_invalid_seed", false, "should have failed");
} else |_| {
reportResult("nkey_invalid_seed", true, "");
}
}
/// Tests authentication with NKey seed loaded from file.
pub fn testNKeySeedFile(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
// Create temp seed file using std.Io
const io_setup = utils.newIo(allocator);
defer io_setup.deinit();
const setup_io = io_setup.io();
const seed_file_path = utils.test_nkey_seed_file;
// Write seed to file
const file = std.Io.Dir.createFile(.cwd(), setup_io, seed_file_path, .{
.truncate = true,
}) catch {
reportResult("nkey_seed_file", false, "failed to create seed file");
return;
};
file.writeStreamingAll(setup_io, test_nkey_seed) catch {
file.close(setup_io);
reportResult("nkey_seed_file", false, "failed to write seed file");
return;
};
file.close(setup_io);
// Connect using seed file
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_seed_file = seed_file_path,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("nkey_seed_file", false, detail);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("nkey_seed_file", true, "");
} else {
reportResult("nkey_seed_file", false, "not connected");
}
}
/// Tests error when seed file does not exist.
pub fn testNKeySeedFileMissing(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_seed_file = "/nonexistent/path/to/seed.txt",
});
if (result) |client| {
client.deinit();
reportResult("nkey_seed_file_missing", false, "should have failed");
} else |_| {
reportResult("nkey_seed_file_missing", true, "");
}
}
/// Tests authentication with custom signing callback.
pub fn testNKeySigningCallback(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
// Derive public key from seed (must match server config)
var kp = nats.auth.KeyPair.fromSeed(test_nkey_seed) catch {
reportResult("nkey_signing_callback", false, "failed to parse seed");
return;
};
defer kp.wipe();
var pubkey_buf: [56]u8 = undefined;
const pubkey = kp.publicKey(&pubkey_buf);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_pubkey = pubkey,
.nkey_sign_fn = &testSignCallback,
}) catch |err| {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "connect: {}", .{err}) catch "fmt";
reportResult("nkey_signing_callback", false, detail);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("nkey_signing_callback", true, "");
} else {
reportResult("nkey_signing_callback", false, "not connected");
}
}
/// Test signing callback using the test seed.
fn testSignCallback(nonce: []const u8, sig: *[64]u8) bool {
var kp = nats.auth.KeyPair.fromSeed(test_nkey_seed) catch return false;
defer kp.wipe();
sig.* = kp.sign(nonce);
return true;
}
/// Tests error when signing callback returns false.
pub fn testNKeyCallbackFails(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, nkey_port);
// Derive public key from seed
var kp = nats.auth.KeyPair.fromSeed(test_nkey_seed) catch {
reportResult("nkey_callback_fails", false, "failed to parse seed");
return;
};
defer kp.wipe();
var pubkey_buf: [56]u8 = undefined;
const pubkey = kp.publicKey(&pubkey_buf);
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
.nkey_pubkey = pubkey,
.nkey_sign_fn = &failingSignCallback,
});
if (result) |client| {
client.deinit();
reportResult("nkey_callback_fails", false, "should have failed");
} else |_| {
reportResult("nkey_callback_fails", true, "");
}
}
/// Signing callback that always fails.
fn failingSignCallback(nonce: []const u8, sig: *[64]u8) bool {
_ = nonce;
_ = sig;
return false;
}
/// Runs all NKey authentication tests.
pub fn runAll(allocator: std.mem.Allocator) void {
testNKeyAuthentication(allocator);
testNKeyAuthFailure(allocator);
testNKeyPubSub(allocator);
testNKeyNoSeedFails(allocator);
testNKeyInvalidSeedFormat(allocator);
testNKeySeedFile(allocator);
testNKeySeedFileMissing(allocator);
testNKeySigningCallback(allocator);
testNKeyCallbackFails(allocator);
}
================================================
FILE: src/testing/client/protocol.zig
================================================
//! Protocol Tests for NATS Client
//!
//! Tests for NATS protocol handling including -ERR responses,
//! PING/PONG keep-alive, INFO parsing, and edge cases.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testServerInfoParsing(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_info_parsing", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("server_info_parsing", false, "no server info");
return;
}
const server_info = info.?;
if (server_info.server_id.len == 0) {
reportResult("server_info_parsing", false, "empty server_id");
return;
}
if (server_info.version.len == 0) {
reportResult("server_info_parsing", false, "empty version");
return;
}
if (server_info.max_payload == 0) {
reportResult("server_info_parsing", false, "max_payload is 0");
return;
}
if (server_info.proto < 1) {
reportResult("server_info_parsing", false, "proto < 1");
return;
}
reportResult("server_info_parsing", true, "");
}
pub fn testPingPongKeepAlive(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("ping_pong_keep_alive", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("ping.test") catch {
reportResult("ping_pong_keep_alive", false, "subscribe failed");
return;
};
defer sub.deinit();
for (0..5) |_| {
client.publish("ping.test", "keep-alive") catch {
reportResult("ping_pong_keep_alive", false, "publish failed");
return;
};
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
}
if (!client.isConnected()) {
reportResult("ping_pong_keep_alive", false, "disconnected");
return;
}
var received: u32 = 0;
for (0..5) |_| {
if (sub.nextMsgTimeout(200) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received == 5) {
reportResult("ping_pong_keep_alive", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/5",
.{received},
) catch "e";
reportResult("ping_pong_keep_alive", false, detail);
}
}
pub fn testProtocolAuthError(allocator: std.mem.Allocator) void {
var url_buf: [128]u8 = undefined;
// Connect to auth server with WRONG token
const url = formatAuthUrl(&url_buf, auth_port, "wrong-token");
const io = utils.newIo(allocator);
defer io.deinit();
const result = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
);
if (result) |client| {
// Should have failed with auth error
client.deinit();
reportResult("protocol_auth_error", false, "should have failed");
} else |err| {
// Expect AuthorizationViolation
if (err == error.AuthorizationViolation) {
reportResult("protocol_auth_error", true, "");
} else {
reportResult("protocol_auth_error", false, "wrong error type");
}
}
}
pub fn testUnknownSidHandling(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("unknown_sid_handling", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.subscribeSync("unknown.sid.test") catch {
reportResult("unknown_sid_handling", false, "subscribe failed");
return;
};
sub1.unsubscribe() catch {
sub1.deinit();
reportResult("unknown_sid_handling", false, "unsubscribe failed");
return;
};
sub1.deinit();
const sub2 = client.subscribeSync("unknown.sid.test") catch {
reportResult("unknown_sid_handling", false, "subscribe2 failed");
return;
};
defer sub2.deinit();
client.publish("unknown.sid.test", "test") catch {
reportResult("unknown_sid_handling", false, "publish failed");
return;
};
if (sub2.nextMsgTimeout(500) catch null) |m| {
m.deinit();
if (client.isConnected()) {
reportResult("unknown_sid_handling", true, "");
} else {
reportResult("unknown_sid_handling", false, "disconnected");
}
} else {
reportResult("unknown_sid_handling", false, "no message");
}
}
pub fn testInvalidProtocolCommand(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const result = parser.parse(allocator, "INVALID_CMD\r\n", &consumed);
if (result) |_| {
reportResult("invalid_protocol_cmd", false, "should have failed");
} else |err| {
// Expect InvalidCommand error
if (err == error.InvalidCommand) {
reportResult("invalid_protocol_cmd", true, "");
} else {
reportResult("invalid_protocol_cmd", false, "wrong error type");
}
}
}
pub fn testProtocolPartialData(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const partial_result = parser.parse(allocator, "PIN", &consumed) catch {
reportResult("protocol_partial_data", false, "unexpected error");
return;
};
if (partial_result != null) {
reportResult("protocol_partial_data", false, "should return null");
return;
}
if (consumed != 0) {
reportResult("protocol_partial_data", false, "consumed != 0");
return;
}
const full_result = parser.parse(allocator, "PING\r\n", &consumed) catch {
reportResult("protocol_partial_data", false, "parse error");
return;
};
if (full_result == null) {
reportResult("protocol_partial_data", false, "should return command");
return;
}
if (consumed != 6) {
reportResult("protocol_partial_data", false, "wrong consumed");
return;
}
reportResult("protocol_partial_data", true, "");
}
pub fn testProtocolPartialMsgPayload(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const partial_msg = "MSG test.subject 1 10\r\nhello";
const partial_result = parser.parse(
allocator,
partial_msg,
&consumed,
) catch {
reportResult("protocol_partial_msg", false, "unexpected error");
return;
};
if (partial_result != null) {
reportResult("protocol_partial_msg", false, "should return null");
return;
}
if (consumed != 0) {
reportResult("protocol_partial_msg", false, "consumed should be 0");
return;
}
const full_msg = "MSG test.subject 1 10\r\nhelloworld\r\n";
const full_result = parser.parse(allocator, full_msg, &consumed) catch {
reportResult("protocol_partial_msg", false, "parse error");
return;
};
if (full_result == null) {
reportResult("protocol_partial_msg", false, "should return command");
return;
}
const msg = full_result.?.msg;
if (!std.mem.eql(u8, msg.payload, "helloworld")) {
reportResult("protocol_partial_msg", false, "wrong payload");
return;
}
reportResult("protocol_partial_msg", true, "");
}
pub fn testProtocolErrorParsing(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const auth_err = "-ERR 'Authorization Violation'\r\n";
const auth_result = parser.parse(allocator, auth_err, &consumed) catch {
reportResult("protocol_err_parsing", false, "parse error");
return;
};
if (auth_result == null) {
reportResult("protocol_err_parsing", false, "auth null");
return;
}
const err_msg = auth_result.?.err;
if (!std.mem.eql(u8, err_msg, "'Authorization Violation'")) {
reportResult("protocol_err_parsing", false, "wrong auth msg");
return;
}
const err = protocol.parseServerError(err_msg);
if (err != error.AuthorizationViolation) {
reportResult("protocol_err_parsing", false, "wrong error type");
return;
}
consumed = 0;
const payload_err = "-ERR 'Maximum Payload Exceeded'\r\n";
const payload_result = parser.parse(
allocator,
payload_err,
&consumed,
) catch {
reportResult("protocol_err_parsing", false, "payload parse error");
return;
};
if (payload_result == null) {
reportResult("protocol_err_parsing", false, "payload null");
return;
}
const payload_err_msg = payload_result.?.err;
const payload_err_type = protocol.parseServerError(payload_err_msg);
if (payload_err_type != error.MaxPayloadExceeded) {
reportResult("protocol_err_parsing", false, "wrong payload error");
return;
}
reportResult("protocol_err_parsing", true, "");
}
pub fn testMaxPayloadLimit(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_payload_limit", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("max_payload_limit", false, "no server info");
return;
}
const max_payload = info.?.max_payload;
if (max_payload < 1024) {
reportResult("max_payload_limit", false, "max_payload too small");
return;
}
if (max_payload > 64 * 1024 * 1024) {
reportResult("max_payload_limit", false, "max_payload too large");
return;
}
reportResult("max_payload_limit", true, "");
}
pub fn testProtocolOkResponse(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const ok_result = parser.parse(allocator, "+OK\r\n", &consumed) catch {
reportResult("protocol_ok_response", false, "parse error");
return;
};
if (ok_result == null) {
reportResult("protocol_ok_response", false, "null result");
return;
}
if (ok_result.? != .ok) {
reportResult("protocol_ok_response", false, "wrong command type");
return;
}
if (consumed != 5) {
reportResult("protocol_ok_response", false, "wrong consumed");
return;
}
reportResult("protocol_ok_response", true, "");
}
pub fn testProtocolStability(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("protocol_stability", false, "connect failed");
return;
};
defer client.deinit();
var subs: [5]?*nats.Subscription = [_]?*nats.Subscription{null} ** 5;
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..5) |i| {
var buf: [32]u8 = undefined;
const subject =
std.fmt.bufPrint(&buf, "stability.{d}", .{i}) catch continue;
subs[i] = client.subscribeSync(subject) catch {
reportResult("protocol_stability", false, "subscribe failed");
return;
};
}
for (0..5) |i| {
var buf: [32]u8 = undefined;
const subject =
std.fmt.bufPrint(&buf, "stability.{d}", .{i}) catch continue;
client.publish(subject, "test") catch {
reportResult("protocol_stability", false, "publish failed");
return;
};
}
var received: u32 = 0;
for (0..5) |i| {
if (subs[i]) |sub| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
}
for (0..3) |i| {
if (subs[i]) |sub| {
sub.unsubscribe() catch {};
}
}
if (!client.isConnected()) {
reportResult("protocol_stability", false, "disconnected");
return;
}
if (received == 5) {
reportResult("protocol_stability", true, "");
} else {
var buf: [32]u8 = undefined;
const detail =
std.fmt.bufPrint(&buf, "got {d}/5", .{received}) catch "e";
reportResult("protocol_stability", false, detail);
}
}
pub fn testProtocolMsgWithReplyTo(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const msg_data = "MSG test.subject 42 _INBOX.reply123 5\r\nhello\r\n";
const result = parser.parse(allocator, msg_data, &consumed) catch {
reportResult("protocol_msg_reply", false, "parse error");
return;
};
if (result == null) {
reportResult("protocol_msg_reply", false, "null result");
return;
}
const msg = result.?.msg;
if (!std.mem.eql(u8, msg.subject, "test.subject")) {
reportResult("protocol_msg_reply", false, "wrong subject");
return;
}
if (msg.sid != 42) {
reportResult("protocol_msg_reply", false, "wrong sid");
return;
}
if (msg.reply_to == null) {
reportResult("protocol_msg_reply", false, "no reply_to");
return;
}
if (!std.mem.eql(u8, msg.reply_to.?, "_INBOX.reply123")) {
reportResult("protocol_msg_reply", false, "wrong reply_to");
return;
}
if (!std.mem.eql(u8, msg.payload, "hello")) {
reportResult("protocol_msg_reply", false, "wrong payload");
return;
}
reportResult("protocol_msg_reply", true, "");
}
pub fn testProtocolHmsgParsing(allocator: std.mem.Allocator) void {
const protocol = @import("nats").protocol;
var parser: protocol.Parser = .{};
var consumed: usize = 0;
const hmsg_data = "HMSG test.subject 1 12 17\r\nNATS/1.0\r\n\r\nhello\r\n";
const result = parser.parse(allocator, hmsg_data, &consumed) catch {
reportResult("protocol_hmsg_parsing", false, "parse error");
return;
};
if (result == null) {
reportResult("protocol_hmsg_parsing", false, "null result");
return;
}
const hmsg = result.?.hmsg;
if (!std.mem.eql(u8, hmsg.subject, "test.subject")) {
reportResult("protocol_hmsg_parsing", false, "wrong subject");
return;
}
if (hmsg.sid != 1) {
reportResult("protocol_hmsg_parsing", false, "wrong sid");
return;
}
if (hmsg.header_len != 12) {
reportResult("protocol_hmsg_parsing", false, "wrong header_len");
return;
}
if (!std.mem.eql(u8, hmsg.payload, "hello")) {
reportResult("protocol_hmsg_parsing", false, "wrong payload");
return;
}
reportResult("protocol_hmsg_parsing", true, "");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testServerInfoParsing(allocator);
testPingPongKeepAlive(allocator);
testProtocolAuthError(allocator);
testUnknownSidHandling(allocator);
testInvalidProtocolCommand(allocator);
testProtocolPartialData(allocator);
testProtocolPartialMsgPayload(allocator);
testProtocolErrorParsing(allocator);
testMaxPayloadLimit(allocator);
testProtocolOkResponse(allocator);
testProtocolStability(allocator);
testProtocolMsgWithReplyTo(allocator);
testProtocolHmsgParsing(allocator);
}
================================================
FILE: src/testing/client/publish.zig
================================================
//! Publish Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testClientPubSub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_pubsub", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("pubsub") catch {
reportResult("client_pubsub", false, "sub failed");
return;
};
defer sub.deinit();
client.publish("pubsub", "test-message") catch {
reportResult("client_pubsub", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |msg| msg.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (std.mem.eql(u8, msg.data, "test-message")) {
reportResult("client_pubsub", true, "");
return;
}
} else |_| {}
reportResult("client_pubsub", false, "no message received");
}
pub fn testClientPublishReply(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_pub_reply", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("req") catch {
reportResult("client_pub_reply", false, "sub failed");
return;
};
defer sub.deinit();
client.publishRequest("req", "reply.inbox", "request") catch {
reportResult("client_pub_reply", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.reply_to) |rt| {
if (std.mem.eql(u8, rt, "reply.inbox")) {
reportResult("client_pub_reply", true, "");
return;
}
}
} else |_| {}
reportResult("client_pub_reply", false, "no reply_to");
}
pub fn testPublishEmptyPayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_empty_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("empty") catch {
reportResult("publish_empty_payload", false, "sub failed");
return;
};
defer sub.deinit();
client.publish("empty", "") catch {
reportResult("publish_empty_payload", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.data.len == 0) {
reportResult("publish_empty_payload", true, "");
return;
}
} else |_| {}
reportResult("publish_empty_payload", false, "no empty message");
}
pub fn testPublishLargePayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_large_payload", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("large") catch {
reportResult("publish_large_payload", false, "sub failed");
return;
};
defer sub.deinit();
const payload = allocator.alloc(u8, 8 * 1024) catch {
reportResult("publish_large_payload", false, "alloc failed");
return;
};
defer allocator.free(payload);
@memset(payload, 'X');
client.publish("large", payload) catch {
reportResult("publish_large_payload", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.data.len == 8 * 1024) {
reportResult("publish_large_payload", true, "");
return;
}
} else |_| {}
reportResult("publish_large_payload", false, "wrong size");
}
pub fn testPublishRapidFire(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_rapid_fire", false, "connect failed");
return;
};
defer client.deinit();
for (0..1000) |_| {
client.publish("rapid", "msg") catch {
reportResult("publish_rapid_fire", false, "pub failed");
return;
};
}
const stats = client.stats();
if (stats.msgs_out >= 1000) {
reportResult("publish_rapid_fire", true, "");
} else {
reportResult("publish_rapid_fire", false, "not all published");
}
}
pub fn testPublishNoSubscribers(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_no_subscribers", false, "connect failed");
return;
};
defer client.deinit();
client.publish("nosub", "message") catch {
reportResult("publish_no_subscribers", false, "pub failed");
return;
};
reportResult("publish_no_subscribers", true, "");
}
pub fn testPublishAfterDisconnect(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_after_disconnect", false, "connect failed");
return;
};
defer client.deinit();
_ = client.drain() catch {
reportResult("publish_after_disconnect", false, "drain failed");
return;
};
const result = client.publish("test.subject", "data");
if (result) |_| {
reportResult("publish_after_disconnect", false, "should have failed");
} else |_| {
reportResult("publish_after_disconnect", true, "");
}
}
pub fn testPublishBatching(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_batching", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("batch.test") catch {
reportResult("publish_batching", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("batch.test", "data1") catch {};
client.publish("batch.test", "data2") catch {};
client.publish("batch.test", "data3") catch {};
var received: u32 = 0;
for (0..3) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
received += 1;
}
}
if (received == 3) {
reportResult("publish_batching", true, "");
} else {
var buf: [32]u8 = undefined;
const detail =
std.fmt.bufPrint(&buf, "got {d}/3", .{received}) catch "e";
reportResult("publish_batching", false, detail);
}
}
pub fn testFlushAfterEachPublish(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("flush_after_each", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("flush.each") catch {
reportResult("flush_after_each", false, "subscribe failed");
return;
};
defer sub.deinit();
for (0..50) |_| {
client.publish("flush.each", "msg") catch {
reportResult("flush_after_each", false, "publish failed");
return;
};
}
var received: u32 = 0;
for (0..50) |_| {
const msg = sub.nextMsgTimeout(500) catch {
break;
};
if (msg) |m| {
m.deinit();
received += 1;
} else break;
}
if (received == 50) {
reportResult("flush_after_each", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/50",
.{received},
) catch "err";
reportResult("flush_after_each", false, detail);
}
}
pub fn testPublishToWildcardFails(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("pub_wildcard_fails", false, "connect failed");
return;
};
defer client.deinit();
// Wildcards are only valid for subscribe, not publish
const result1 = client.publish("foo.*", "data");
const result2 = client.publish("foo.>", "data");
const star_failed = if (result1) |_| false else |_| true;
const gt_failed = if (result2) |_| false else |_| true;
if (star_failed and gt_failed) {
reportResult("pub_wildcard_fails", true, "");
} else if (!star_failed) {
reportResult("pub_wildcard_fails", false, "* should fail");
} else {
reportResult("pub_wildcard_fails", false, "> should fail");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testClientPubSub(allocator);
testClientPublishReply(allocator);
testPublishEmptyPayload(allocator);
testPublishLargePayload(allocator);
testPublishRapidFire(allocator);
testPublishNoSubscribers(allocator);
testPublishAfterDisconnect(allocator);
testPublishBatching(allocator);
testFlushAfterEachPublish(allocator);
testPublishToWildcardFails(allocator);
}
================================================
FILE: src/testing/client/queue.zig
================================================
//! Queue Group Tests for NATS Client
//!
//! Tests for queue group subscriptions and message distribution.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
pub fn testQueueGroups(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_groups", false, "connect failed");
return;
};
defer client.deinit();
const queue = "workers";
const sub = client.queueSubscribeSync("queue.test", queue) catch {
reportResult("queue_groups", false, "queue subscribe failed");
return;
};
defer sub.deinit();
if (sub.sid == 0) {
reportResult("queue_groups", false, "invalid queue sid");
return;
}
reportResult("queue_groups", true, "");
}
pub fn testQueueGroupDistribution(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_group_distribution", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.queueSubscribeSync("qdist.test", "workers") catch {
reportResult("queue_group_distribution", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.queueSubscribeSync("qdist.test", "workers") catch {
reportResult("queue_group_distribution", false, "sub2 failed");
return;
};
defer sub2.deinit();
const sub3 = client.queueSubscribeSync("qdist.test", "workers") catch {
reportResult("queue_group_distribution", false, "sub3 failed");
return;
};
defer sub3.deinit();
for (0..30) |_| {
client.publish("qdist.test", "work") catch {
reportResult("queue_group_distribution", false, "publish failed");
return;
};
}
var count1: u32 = 0;
var count2: u32 = 0;
var count3: u32 = 0;
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
while (true) {
const msg = sub1.nextMsgTimeout(50) catch {
break;
};
if (msg) |m| {
m.deinit();
count1 += 1;
} else break;
}
while (true) {
const msg = sub2.nextMsgTimeout(50) catch {
break;
};
if (msg) |m| {
m.deinit();
count2 += 1;
} else break;
}
while (true) {
const msg = sub3.nextMsgTimeout(50) catch {
break;
};
if (msg) |m| {
m.deinit();
count3 += 1;
} else break;
}
const total = count1 + count2 + count3;
if (total == 30) {
reportResult("queue_group_distribution", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"total={d} (expected 30)",
.{total},
) catch "error";
reportResult("queue_group_distribution", false, detail);
}
}
pub fn testQueueGroupMultipleClients(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_a = utils.newIo(allocator);
defer io_a.deinit();
const client_a = nats.Client.connect(allocator, io_a.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_multi_client", false, "A connect failed");
return;
};
defer client_a.deinit();
const io_b = utils.newIo(allocator);
defer io_b.deinit();
const client_b = nats.Client.connect(allocator, io_b.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_multi_client", false, "B connect failed");
return;
};
defer client_b.deinit();
const io_c = utils.newIo(allocator);
defer io_c.deinit();
const client_c = nats.Client.connect(allocator, io_c.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_multi_client", false, "C connect failed");
return;
};
defer client_c.deinit();
const sub_a = client_a.queueSubscribeSync(
"qmc.test",
"workers",
) catch {
reportResult("queue_multi_client", false, "A sub failed");
return;
};
defer sub_a.deinit();
const sub_b = client_b.queueSubscribeSync(
"qmc.test",
"workers",
) catch {
reportResult("queue_multi_client", false, "B sub failed");
return;
};
defer sub_b.deinit();
io_a.io().sleep(.fromMilliseconds(50), .awake) catch {};
for (0..20) |_| {
client_c.publish("qmc.test", "work") catch {
reportResult("queue_multi_client", false, "publish failed");
return;
};
}
var count_a: u32 = 0;
var count_b: u32 = 0;
for (0..20) |_| {
if (sub_a.nextMsgTimeout(100) catch null) |m| {
m.deinit();
count_a += 1;
}
}
for (0..20) |_| {
if (sub_b.nextMsgTimeout(100) catch null) |m| {
m.deinit();
count_b += 1;
}
}
if (count_a + count_b == 20) {
reportResult("queue_multi_client", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}+{d}={d}",
.{ count_a, count_b, count_a + count_b },
) catch "err";
reportResult("queue_multi_client", false, detail);
}
}
pub fn testQueueGroupSingleReceiver(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_single_recv", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.queueSubscribeSync("qsingle.test", "solo") catch {
reportResult("queue_single_recv", false, "subscribe failed");
return;
};
defer sub.deinit();
for (0..10) |_| {
client.publish("qsingle.test", "msg") catch {};
}
var count: u32 = 0;
for (0..15) |_| {
const msg = sub.nextMsgTimeout(200) catch break;
if (msg) |m| {
m.deinit();
count += 1;
} else break;
}
if (count == 10) {
reportResult("queue_single_recv", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/10", .{count}) catch "e";
reportResult("queue_single_recv", false, detail);
}
}
pub fn testQueueWithWildcard(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_wildcard", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.queueSubscribeSync("qw.>", "workers") catch {
reportResult("queue_wildcard", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("qw.foo", "one") catch {};
client.publish("qw.bar", "two") catch {};
client.publish("qw.baz.deep", "three") catch {};
var count: u32 = 0;
for (0..5) |_| {
const msg = sub.nextMsgTimeout(200) catch break;
if (msg) |m| {
m.deinit();
count += 1;
} else break;
}
if (count == 3) {
reportResult("queue_wildcard", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/3", .{count}) catch "e";
reportResult("queue_wildcard", false, detail);
}
}
pub fn testMultipleQueueGroups(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("multi_queue_groups", false, "connect failed");
return;
};
defer client.deinit();
const sub_a = client.queueSubscribeSync("mqg.test", "group-A") catch {
reportResult("multi_queue_groups", false, "sub A failed");
return;
};
defer sub_a.deinit();
const sub_b = client.queueSubscribeSync("mqg.test", "group-B") catch {
reportResult("multi_queue_groups", false, "sub B failed");
return;
};
defer sub_b.deinit();
client.publish("mqg.test", "hello") catch {
reportResult("multi_queue_groups", false, "publish failed");
return;
};
var count: u32 = 0;
if (sub_a.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (sub_b.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (count == 2) {
reportResult("multi_queue_groups", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/2", .{count}) catch "err";
reportResult("multi_queue_groups", false, detail);
}
}
pub fn testFourClientQueueGroup(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
var ios: [5]*utils.TestIo = undefined;
for (&ios) |*io_ptr| {
io_ptr.* = utils.newIo(allocator);
}
defer for (ios) |io| io.deinit();
var clients: [5]?*nats.Client = .{ null, null, null, null, null };
defer for (&clients) |*c| {
if (c.*) |client| client.deinit();
};
for (&clients, 0..) |*c, i| {
c.* = nats.Client.connect(allocator, ios[i].io(), url, .{ .reconnect = false }) catch {
reportResult("four_client_queue", false, "connect failed");
return;
};
}
var subs: [4]?*nats.Subscription = .{ null, null, null, null };
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..4) |i| {
subs[i] = clients[i].?.queueSubscribeSync(
"fourq.test",
"workers",
) catch {
reportResult("four_client_queue", false, "subscribe failed");
return;
};
}
ios[0].io().sleep(.fromMilliseconds(50), .awake) catch {};
for (0..40) |_| {
clients[4].?.publish("fourq.test", "work") catch {};
}
var counts: [4]u32 = .{ 0, 0, 0, 0 };
for (0..4) |i| {
for (0..40) |_| {
const msg = subs[i].?.nextMsgTimeout(
100,
) catch break;
if (msg) |m| {
m.deinit();
counts[i] += 1;
} else break;
}
}
const total = counts[0] + counts[1] + counts[2] + counts[3];
if (total == 40) {
reportResult("four_client_queue", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}+{d}+{d}+{d}={d}",
.{ counts[0], counts[1], counts[2], counts[3], total },
) catch "e";
reportResult("four_client_queue", false, detail);
}
}
pub fn testQueueMemberJoinsMidStream(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_join_midstream", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.queueSubscribeSync("qjoin.test", "workers") catch {
reportResult("queue_join_midstream", false, "sub1 failed");
return;
};
defer sub1.deinit();
for (0..10) |_| {
client.publish("qjoin.test", "msg") catch {};
}
const sub2 = client.queueSubscribeSync("qjoin.test", "workers") catch {
reportResult("queue_join_midstream", false, "sub2 failed");
return;
};
defer sub2.deinit();
for (0..10) |_| {
client.publish("qjoin.test", "msg") catch {};
}
var count1: u32 = 0;
var count2: u32 = 0;
for (0..20) |_| {
if (sub1.nextMsgTimeout(100) catch null) |m| {
m.deinit();
count1 += 1;
}
}
for (0..20) |_| {
if (sub2.nextMsgTimeout(100) catch null) |m| {
m.deinit();
count2 += 1;
}
}
if (count1 + count2 == 20) {
if (count2 > 0) {
reportResult("queue_join_midstream", true, "");
} else {
reportResult("queue_join_midstream", false, "sub2 got 0");
}
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"total={d} (expect 20)",
.{count1 + count2},
) catch "e";
reportResult("queue_join_midstream", false, detail);
}
}
pub fn testQueueMemberLeaves(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_member_leaves", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.queueSubscribeSync("qleave.test", "workers") catch {
reportResult("queue_member_leaves", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.queueSubscribeSync("qleave.test", "workers") catch {
reportResult("queue_member_leaves", false, "sub2 failed");
return;
};
defer sub2.deinit();
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
for (0..10) |_| {
client.publish("qleave.test", "msg") catch {};
}
sub1.unsubscribe() catch {};
for (0..10) |_| {
client.publish("qleave.test", "msg") catch {};
}
var count2: u32 = 0;
for (0..25) |_| {
if (sub2.nextMsgTimeout(100) catch null) |m| {
m.deinit();
count2 += 1;
}
}
if (count2 >= 10) {
reportResult("queue_member_leaves", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}", .{count2}) catch "e";
reportResult("queue_member_leaves", false, detail);
}
}
pub fn testLargeQueueGroup(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("large_queue_group", false, "connect failed");
return;
};
defer client.deinit();
const NUM_SUBS = 20;
var subs: [NUM_SUBS]?*nats.Subscription = [_]?*nats.Subscription{null} ** NUM_SUBS;
var created: usize = 0;
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..NUM_SUBS) |i| {
subs[i] = client.queueSubscribeSync(
"lqg.test",
"big-workers",
) catch {
break;
};
created += 1;
}
if (created != NUM_SUBS) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "created {d}/20", .{created}) catch "e";
reportResult("large_queue_group", false, detail);
return;
}
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
const NUM_MSGS = 100;
for (0..NUM_MSGS) |_| {
client.publish("lqg.test", "work") catch {};
}
var total: u32 = 0;
for (0..NUM_SUBS) |i| {
if (subs[i]) |sub| {
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(50) catch null) |m| {
m.deinit();
total += 1;
} else break;
}
}
}
if (total == NUM_MSGS) {
reportResult("large_queue_group", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/100", .{total}) catch "e";
reportResult("large_queue_group", false, detail);
}
}
pub fn testQueueGroupNameValidation(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_name_validation", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.queueSubscribeSync("qn.test1", "workers-1") catch {
reportResult("queue_name_validation", false, "workers-1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.queueSubscribeSync("qn.test2", "workers_2") catch {
reportResult("queue_name_validation", false, "workers_2 failed");
return;
};
defer sub2.deinit();
const sub3 = client.queueSubscribeSync("qn.test3", "WorkersABC") catch {
reportResult("queue_name_validation", false, "WorkersABC failed");
return;
};
defer sub3.deinit();
if (client.isConnected()) {
reportResult("queue_name_validation", true, "");
} else {
reportResult("queue_name_validation", false, "disconnected");
}
}
pub fn testQueueGroupFairness(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{ .reconnect = false }) catch {
reportResult("queue_fairness", false, "connect failed");
return;
};
defer client.deinit();
const NUM_SUBS = 5;
var subs: [NUM_SUBS]?*nats.Subscription = [_]?*nats.Subscription{null} ** NUM_SUBS;
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..NUM_SUBS) |i| {
subs[i] = client.queueSubscribeSync("qfair.test", "fairness") catch {
reportResult("queue_fairness", false, "subscribe failed");
return;
};
}
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
const NUM_MSGS = 100;
for (0..NUM_MSGS) |_| {
client.publish("qfair.test", "msg") catch {};
}
var counts: [NUM_SUBS]u32 = [_]u32{0} ** NUM_SUBS;
for (0..NUM_SUBS) |i| {
if (subs[i]) |sub| {
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(50) catch null) |m| {
m.deinit();
counts[i] += 1;
} else break;
}
}
}
var total: u32 = 0;
for (counts) |c| total += c;
if (total != NUM_MSGS) {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "total={d}/100", .{total}) catch "e";
reportResult("queue_fairness", false, detail);
return;
}
var min_count: u32 = NUM_MSGS;
for (counts) |c| {
if (c < min_count) min_count = c;
}
if (min_count >= 5) {
reportResult("queue_fairness", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"min={d} (expect >= 5)",
.{min_count},
) catch "e";
reportResult("queue_fairness", false, detail);
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testQueueGroups(allocator);
testQueueGroupDistribution(allocator);
testQueueGroupMultipleClients(allocator);
testQueueGroupSingleReceiver(allocator);
testQueueWithWildcard(allocator);
testMultipleQueueGroups(allocator);
testFourClientQueueGroup(allocator);
testQueueMemberJoinsMidStream(allocator);
testQueueMemberLeaves(allocator);
testLargeQueueGroup(allocator);
testQueueGroupNameValidation(allocator);
testQueueGroupFairness(allocator);
}
================================================
FILE: src/testing/client/reconnect.zig
================================================
//! Reconnection Integration Tests
//!
//! Tests automatic reconnection functionality including subscription
//! restoration, pending buffer flushing, and server pool rotation.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
const ServerManager = utils.ServerManager;
const reconnect_port: u16 = 14227;
const failover_port_1: u16 = 14230;
const failover_port_2: u16 = 14231;
const failover_port_3: u16 = 14232;
const failover_port_4: u16 = 14233;
const failover_port_5: u16 = 14234;
const failover_port_6: u16 = 14235;
const failover_port_7: u16 = 14236;
const failover_port_8: u16 = 14237;
const failover_port_9: u16 = 14238;
fn waitForClosed(
io: std.Io,
client: *nats.Client,
timeout_ms: u32,
) bool {
var waited: u32 = 0;
while (waited < timeout_ms) : (waited += 25) {
if (client.isClosed()) return true;
io.sleep(.fromMilliseconds(25), .awake) catch {};
}
return client.isClosed();
}
fn waitForConnected(
io: std.Io,
client: *nats.Client,
timeout_ms: u32,
) bool {
var waited: u32 = 0;
while (waited < timeout_ms) : (waited += 25) {
if (client.isConnected()) return true;
io.sleep(.fromMilliseconds(25), .awake) catch {};
}
return client.isConnected();
}
fn waitForReconnects(
io: std.Io,
client: *nats.Client,
want: u32,
timeout_ms: u32,
) bool {
var waited: u32 = 0;
while (waited < timeout_ms) : (waited += 25) {
if (client.stats().reconnects >= want) return true;
io.sleep(.fromMilliseconds(25), .awake) catch {};
}
return client.stats().reconnects >= want;
}
pub fn runAll(allocator: std.mem.Allocator, manager: *ServerManager) void {
testAutoReconnectBasic(allocator, manager);
testSubscriptionRestored(allocator, manager);
testMultipleSubscriptionsRestored(allocator, manager);
testReconnectMaxAttempts(allocator, manager);
testReconnectDisabled(allocator, manager);
testPendingBufferFlush(allocator, manager);
testReconnectStatsIncrement(allocator, manager);
testReconnectWithQueueGroup(allocator, manager);
testMultiClientReconnect(allocator, manager);
testReconnectPreservesSid(allocator, manager);
testReconnectWildcardSub(allocator, manager);
testPublishDuringReconnect(allocator, manager);
testReconnectBackoff(allocator, manager);
testCustomReconnectDelay(allocator, manager);
testHealthCheckReconnect(allocator, manager);
testFailoverToSecondServer(allocator, manager);
testFailoverRoundRobin(allocator, manager);
testAllServersDownThenRecover(allocator, manager);
testServerCooldownRespected(allocator, manager);
testMultipleSubsActivelyReceiving(allocator, manager);
testHighVolumePendingBuffer(allocator, manager);
testQueueGroupMultiClientReconnect(allocator, manager);
testRapidServerRestarts(allocator, manager);
testMultipleReconnectionCycles(allocator, manager);
testLongDisconnectionRecovery(allocator, manager);
}
fn testAutoReconnectBasic(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
.reconnect_wait_max_ms = 1000,
}) catch {
reportResult("reconnect_basic", false, "initial connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("reconnect_basic", false, "not connected initially");
return;
}
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_basic", false, "server restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("test.reconnect", "ping") catch {
reportResult("reconnect_basic", false, "publish after restart failed");
return;
};
if (client.isConnected()) {
reportResult("reconnect_basic", true, "");
} else {
reportResult("reconnect_basic", false, "not reconnected");
}
}
fn testSubscriptionRestored(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("reconnect_sub_restored", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("test.restore.>") catch {
reportResult("reconnect_sub_restored", false, "subscribe failed");
return;
};
defer sub.deinit();
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_sub_restored", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("test.restore.msg", "after-reconnect") catch {
reportResult("reconnect_sub_restored", false, "publish failed");
return;
};
client.flushBuffer() catch {};
if (sub.nextMsgTimeout(500) catch null) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "after-reconnect")) {
reportResult("reconnect_sub_restored", true, "");
} else {
reportResult("reconnect_sub_restored", false, "wrong message data");
}
} else {
reportResult("reconnect_sub_restored", false, "no message received");
}
}
fn testMultipleSubscriptionsRestored(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("reconnect_multi_sub", false, "connect failed");
return;
};
defer client.deinit();
var sub1 = client.subscribeSync("multi.sub.one") catch {
reportResult("reconnect_multi_sub", false, "sub1 failed");
return;
};
defer sub1.deinit();
var sub2 = client.subscribeSync("multi.sub.two") catch {
reportResult("reconnect_multi_sub", false, "sub2 failed");
return;
};
defer sub2.deinit();
var sub3 = client.subscribeSync("multi.sub.three") catch {
reportResult("reconnect_multi_sub", false, "sub3 failed");
return;
};
defer sub3.deinit();
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_multi_sub", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("multi.sub.one", "msg1") catch {};
client.publish("multi.sub.two", "msg2") catch {};
client.publish("multi.sub.three", "msg3") catch {};
var received: u8 = 0;
if (sub1.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
received += 1;
}
if (sub2.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
received += 1;
}
if (sub3.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
received += 1;
}
if (received == 3) {
reportResult("reconnect_multi_sub", true, "");
} else {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"only {d}/3 received",
.{received},
) catch "count error";
reportResult("reconnect_multi_sub", false, details);
}
}
fn testReconnectMaxAttempts(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, reconnect_port);
const io = utils.newIo(allocator);
defer io.deinit();
const server_idx = manager.count();
_ = manager.startServer(allocator, io.io(), .{
.port = reconnect_port,
}) catch {
reportResult("reconnect_max_attempts", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 2,
.reconnect_wait_ms = 50,
.reconnect_wait_max_ms = 100,
.ping_interval_ms = 100,
.max_pings_outstanding = 1,
}) catch {
manager.stopServer(server_idx, io.io());
reportResult("reconnect_max_attempts", false, "connect failed");
return;
};
defer client.deinit();
manager.stopServer(server_idx, io.io());
client.forceReconnect() catch {};
const is_disconnected = waitForClosed(
io.io(),
client,
1500,
);
if (is_disconnected) {
reportResult("reconnect_max_attempts", true, "");
} else {
reportResult("reconnect_max_attempts", false, "should be disconnected");
}
}
fn testReconnectDisabled(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = false,
// .ping_interval_ms = 100,
.max_pings_outstanding = 1,
}) catch {
reportResult("reconnect_disabled", false, "connect failed");
return;
};
defer client.deinit();
manager.stopAll(io.io());
client.forceReconnect() catch {};
_ = waitForClosed(io.io(), client, 500);
const flush_result = client.flush(200_000_000);
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_disabled", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(300), .awake) catch {};
if (flush_result) |_| {
reportResult("reconnect_disabled", false, "flush should fail");
} else |_| {
reportResult("reconnect_disabled", true, "");
}
}
fn testPendingBufferFlush(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
.pending_buffer_size = 1024 * 1024,
}) catch {
reportResult("pending_buffer_flush", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("pending.test") catch {
reportResult("pending_buffer_flush", false, "subscribe failed");
return;
};
defer sub.deinit();
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
client.publish("pending.test", "buffered-message") catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("pending_buffer_flush", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
if (sub.tryNextMsg()) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "buffered-message")) {
reportResult("pending_buffer_flush", true, "");
} else {
reportResult("pending_buffer_flush", false, "wrong data");
}
} else {
reportResult("pending_buffer_flush", false, "buffered message missing");
}
}
fn testPublishDuringReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 200,
.pending_buffer_size = 1024 * 1024,
}) catch {
reportResult("publish_during_reconnect", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("during.reconnect") catch {
reportResult("publish_during_reconnect", false, "subscribe failed");
return;
};
defer sub.deinit();
manager.stopServer(0, io.io());
var published: u8 = 0;
var i: u8 = 0;
while (i < 5) : (i += 1) {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "msg-{d}", .{i}) catch continue;
if (client.publish("during.reconnect", msg)) |_| {
published += 1;
} else |_| {}
}
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("publish_during_reconnect", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
var received: u8 = 0;
while (received < 10) {
if (sub.tryNextMsg()) |msg| {
msg.deinit();
received += 1;
} else {
break;
}
}
if (published > 0 or received > 0) {
reportResult("publish_during_reconnect", true, "");
} else {
reportResult("publish_during_reconnect", false, "no messages");
}
}
fn testReconnectStatsIncrement(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 30,
.reconnect_wait_ms = 100,
.ping_interval_ms = 100,
.max_pings_outstanding = 1,
}) catch {
reportResult("reconnect_stats", false, "connect failed");
return;
};
defer client.deinit();
const initial_reconnects = client.stats().reconnects;
manager.stopAll(io.io());
client.forceReconnect() catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_stats", false, "restart failed");
return;
};
if (!waitForReconnects(
io.io(),
client,
initial_reconnects + 1,
3000,
)) {
reportResult("reconnect_stats", false, "counter not incremented");
return;
}
client.publish("stats.test", "trigger") catch {};
const final_reconnects = client.stats().reconnects;
if (final_reconnects > initial_reconnects) {
reportResult("reconnect_stats", true, "");
} else {
reportResult("reconnect_stats", false, "counter not incremented");
}
}
fn testReconnectWithQueueGroup(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("reconnect_queue_group", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.queueSubscribeSync("queue.test", "workers") catch {
reportResult("reconnect_queue_group", false, "subscribe failed");
return;
};
defer sub.deinit();
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_queue_group", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("queue.test", "queue-message") catch {
reportResult("reconnect_queue_group", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
reportResult("reconnect_queue_group", true, "");
} else {
reportResult("reconnect_queue_group", false, "no message");
}
}
fn testMultiClientReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
const client1 = nats.Client.connect(allocator, io1.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("multi_client_reconnect", false, "client1 connect failed");
return;
};
defer client1.deinit();
const client2 = nats.Client.connect(allocator, io2.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("multi_client_reconnect", false, "client2 connect failed");
return;
};
defer client2.deinit();
manager.stopServer(0, io1.io());
io1.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io1.io(), .{ .port = test_port }) catch {
reportResult("multi_client_reconnect", false, "restart failed");
return;
};
io1.io().sleep(.fromMilliseconds(500), .awake) catch {};
var failed = false;
client1.publish("multi.test", "from-client1") catch {
failed = true;
};
client2.publish("multi.test", "from-client2") catch {
failed = true;
};
if (failed) {
reportResult("multi_client_reconnect", false, "publish failed");
} else {
reportResult("multi_client_reconnect", true, "");
}
}
fn testReconnectPreservesSid(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("reconnect_preserves_sid", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("sid.test") catch {
reportResult("reconnect_preserves_sid", false, "subscribe failed");
return;
};
defer sub.deinit();
const original_sid = sub.sid;
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_preserves_sid", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
if (sub.sid == original_sid) {
reportResult("reconnect_preserves_sid", true, "");
} else {
reportResult("reconnect_preserves_sid", false, "SID changed");
}
}
fn testReconnectWildcardSub(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("reconnect_wildcard", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("wild.*.test.>") catch {
reportResult("reconnect_wildcard", false, "subscribe failed");
return;
};
defer sub.deinit();
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_wildcard", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("wild.card.test.subject", "wildcard-msg") catch {
reportResult("reconnect_wildcard", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
reportResult("reconnect_wildcard", true, "");
} else {
reportResult("reconnect_wildcard", false, "no message");
}
}
fn testReconnectBackoff(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 5,
.reconnect_wait_ms = 100,
.reconnect_wait_max_ms = 500,
.reconnect_jitter_percent = 0,
}) catch {
reportResult("reconnect_backoff", false, "connect failed");
return;
};
defer client.deinit();
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(2000), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("reconnect_backoff", false, "restart failed");
return;
};
reportResult("reconnect_backoff", true, "");
}
/// Track calls for custom delay callback test (atomic for cross-thread visibility)
var custom_delay_calls: std.atomic.Value(u32) = std.atomic.Value(u32).init(0);
fn customDelayCallback(attempt: u32) u32 {
_ = custom_delay_calls.fetchAdd(1, .seq_cst);
// Simple linear backoff: 50ms per attempt
return attempt * 50;
}
fn testCustomReconnectDelay(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, reconnect_port);
const io = utils.newIo(allocator);
defer io.deinit();
// Reset call counter
custom_delay_calls.store(0, .seq_cst);
// Start our own dedicated server for this test
const server = manager.startServer(allocator, io.io(), .{
.port = reconnect_port,
}) catch {
reportResult("custom_reconnect_delay", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.custom_reconnect_delay = customDelayCallback,
.ping_interval_ms = 100,
.max_pings_outstanding = 1,
}) catch {
server.stop(io.io());
reportResult("custom_reconnect_delay", false, "connect failed");
return;
};
defer client.deinit();
// Stop the specific server we started to trigger reconnect attempts
server.stop(io.io());
// Wait for disconnect detection and multiple reconnect attempts
// Callback returns attempt*50ms, so attempts 2,3,4 = 100+150+200 = 450ms
io.io().sleep(.fromMilliseconds(1500), .awake) catch {};
// Restart server on same port
const server2 = manager.startServer(allocator, io.io(), .{
.port = reconnect_port,
}) catch {
reportResult("custom_reconnect_delay", false, "restart failed");
return;
};
defer server2.stop(io.io());
// Wait for reconnection
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
// Callback should have been called at least once (for attempt 2+)
const calls = custom_delay_calls.load(.seq_cst);
if (calls >= 1) {
reportResult("custom_reconnect_delay", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "calls={d}", .{
calls,
}) catch "e";
reportResult("custom_reconnect_delay", false, detail);
}
}
fn testHealthCheckReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
.ping_interval_ms = 500,
.max_pings_outstanding = 2,
}) catch {
reportResult("health_check_reconnect", false, "connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("health_check_reconnect", false, "not connected");
return;
}
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("health_check_reconnect", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("health.test", "ping") catch {
reportResult("health_check_reconnect", false, "publish failed");
return;
};
reportResult("health_check_reconnect", true, "");
}
fn testFailoverToSecondServer(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf1: [64]u8 = undefined;
var url_buf2: [64]u8 = undefined;
const url1 = formatUrl(&url_buf1, failover_port_1);
const url2 = formatUrl(&url_buf2, failover_port_2);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
const server1 = manager.startServer(allocator, io.io(), .{
.port = failover_port_1,
}) catch {
reportResult("failover_to_second", false, "server1 start failed");
return;
};
const server2 = manager.startServer(allocator, io.io(), .{
.port = failover_port_2,
}) catch {
server1.stop(io.io());
reportResult("failover_to_second", false, "server2 start failed");
return;
};
defer server2.stop(io.io());
const client = nats.Client.connect(allocator, io.io(), url1, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
.ping_interval_ms = 100,
.max_pings_outstanding = 2,
}) catch {
server1.stop(io.io());
reportResult("failover_to_second", false, "connect failed");
return;
};
defer client.deinit();
client.server_pool.addServer(url2) catch {
server1.stop(io.io());
reportResult("failover_to_second", false, "add server failed");
return;
};
var sub = client.subscribeSync("failover.test") catch {
server1.stop(io.io());
reportResult("failover_to_second", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("failover.test", "before") catch {};
if (sub.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
} else {
server1.stop(io.io());
reportResult("failover_to_second", false, "no msg before failover");
return;
}
server1.stop(io.io());
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("failover.test", "after") catch {
reportResult("failover_to_second", false, "publish after failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |msg| {
msg.deinit();
reportResult("failover_to_second", true, "");
} else {
reportResult("failover_to_second", false, "no msg after failover");
}
}
fn testFailoverRoundRobin(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf1: [64]u8 = undefined;
var url_buf2: [64]u8 = undefined;
var url_buf3: [64]u8 = undefined;
const url1 = formatUrl(&url_buf1, failover_port_3);
const url2 = formatUrl(&url_buf2, failover_port_4);
const url3 = formatUrl(&url_buf3, failover_port_5);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
const server1 = manager.startServer(allocator, io.io(), .{
.port = failover_port_3,
}) catch {
reportResult("failover_round_robin", false, "server1 start failed");
return;
};
const server2 = manager.startServer(allocator, io.io(), .{
.port = failover_port_4,
}) catch {
server1.stop(io.io());
reportResult("failover_round_robin", false, "server2 start failed");
return;
};
const server3 = manager.startServer(allocator, io.io(), .{
.port = failover_port_5,
}) catch {
server1.stop(io.io());
server2.stop(io.io());
reportResult("failover_round_robin", false, "server3 start failed");
return;
};
defer server3.stop(io.io());
const client = nats.Client.connect(allocator, io.io(), url1, .{
.reconnect = true,
.max_reconnect_attempts = 5,
.reconnect_wait_ms = 100,
.ping_interval_ms = 100,
.max_pings_outstanding = 2,
}) catch {
server1.stop(io.io());
server2.stop(io.io());
reportResult("failover_round_robin", false, "connect failed");
return;
};
defer client.deinit();
client.server_pool.addServer(url2) catch {};
client.server_pool.addServer(url3) catch {};
server1.stop(io.io());
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("roundrobin.test", "msg1") catch {
server2.stop(io.io());
reportResult("failover_round_robin", false, "publish 1 failed");
return;
};
server2.stop(io.io());
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("roundrobin.test", "msg2") catch {
reportResult("failover_round_robin", false, "publish 2 failed");
return;
};
reportResult("failover_round_robin", true, "");
}
fn testAllServersDownThenRecover(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf1: [64]u8 = undefined;
var url_buf2: [64]u8 = undefined;
const url1 = formatUrl(&url_buf1, failover_port_6);
const url2 = formatUrl(&url_buf2, failover_port_7);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
const server1 = manager.startServer(allocator, io.io(), .{
.port = failover_port_6,
}) catch {
reportResult("all_servers_down_recover", false, "server1 start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url1, .{
.reconnect = true,
.max_reconnect_attempts = 20,
.reconnect_wait_ms = 200,
.ping_interval_ms = 100,
.max_pings_outstanding = 2,
}) catch {
server1.stop(io.io());
reportResult("all_servers_down_recover", false, "connect failed");
return;
};
defer client.deinit();
client.server_pool.addServer(url2) catch {};
var sub = client.subscribeSync("recover.test") catch {
server1.stop(io.io());
reportResult("all_servers_down_recover", false, "subscribe failed");
return;
};
defer sub.deinit();
server1.stop(io.io());
io.io().sleep(.fromMilliseconds(800), .awake) catch {};
const server2 = manager.startServer(allocator, io.io(), .{
.port = failover_port_7,
}) catch {
reportResult("all_servers_down_recover", false, "server2 start failed");
return;
};
defer server2.stop(io.io());
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("recover.test", "recovered") catch {
reportResult("all_servers_down_recover", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |msg| {
msg.deinit();
reportResult("all_servers_down_recover", true, "");
} else {
reportResult("all_servers_down_recover", false, "no message received");
}
}
fn testServerCooldownRespected(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf1: [64]u8 = undefined;
var url_buf2: [64]u8 = undefined;
const url1 = formatUrl(&url_buf1, failover_port_8);
const url2 = formatUrl(&url_buf2, failover_port_9);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
const server2 = manager.startServer(allocator, io.io(), .{
.port = failover_port_9,
}) catch {
reportResult("server_cooldown", false, "server2 start failed");
return;
};
defer server2.stop(io.io());
const client = nats.Client.connect(allocator, io.io(), url2, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("server_cooldown", false, "connect failed");
return;
};
defer client.deinit();
client.server_pool.addServer(url1) catch {};
client.publish("cooldown.test", "msg") catch {
reportResult("server_cooldown", false, "publish failed");
return;
};
if (client.server_pool.serverCount() == 2) {
reportResult("server_cooldown", true, "");
} else {
reportResult("server_cooldown", false, "wrong server count");
}
}
fn testMultipleSubsActivelyReceiving(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("multi_subs_receiving", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult("multi_subs_receiving", false, "connect failed");
return;
};
defer client.deinit();
var sub1 = client.subscribeSync("active.sub.one") catch {
reportResult("multi_subs_receiving", false, "sub1 failed");
return;
};
defer sub1.deinit();
var sub2 = client.subscribeSync("active.sub.two") catch {
reportResult("multi_subs_receiving", false, "sub2 failed");
return;
};
defer sub2.deinit();
var sub3 = client.subscribeSync("active.sub.three") catch {
reportResult("multi_subs_receiving", false, "sub3 failed");
return;
};
defer sub3.deinit();
var sub4 = client.subscribeSync("active.sub.four") catch {
reportResult("multi_subs_receiving", false, "sub4 failed");
return;
};
defer sub4.deinit();
var sub5 = client.subscribeSync("active.sub.five") catch {
reportResult("multi_subs_receiving", false, "sub5 failed");
return;
};
defer sub5.deinit();
client.publish("active.sub.one", "pre1") catch {};
client.publish("active.sub.two", "pre2") catch {};
client.publish("active.sub.three", "pre3") catch {};
client.publish("active.sub.four", "pre4") catch {};
client.publish("active.sub.five", "pre5") catch {};
var pre_received: u8 = 0;
if (sub1.nextMsgTimeout(200) catch null) |m| {
m.deinit();
pre_received += 1;
}
if (sub2.nextMsgTimeout(200) catch null) |m| {
m.deinit();
pre_received += 1;
}
if (sub3.nextMsgTimeout(200) catch null) |m| {
m.deinit();
pre_received += 1;
}
if (sub4.nextMsgTimeout(200) catch null) |m| {
m.deinit();
pre_received += 1;
}
if (sub5.nextMsgTimeout(200) catch null) |m| {
m.deinit();
pre_received += 1;
}
if (pre_received != 5) {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"pre: {d}/5",
.{pre_received},
) catch "pre error";
reportResult("multi_subs_receiving", false, details);
return;
}
manager.stopServer(0, io.io());
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("multi_subs_receiving", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("active.sub.one", "post1") catch {};
client.publish("active.sub.two", "post2") catch {};
client.publish("active.sub.three", "post3") catch {};
client.publish("active.sub.four", "post4") catch {};
client.publish("active.sub.five", "post5") catch {};
var post_received: u8 = 0;
if (sub1.nextMsgTimeout(500) catch null) |m| {
m.deinit();
post_received += 1;
}
if (sub2.nextMsgTimeout(500) catch null) |m| {
m.deinit();
post_received += 1;
}
if (sub3.nextMsgTimeout(500) catch null) |m| {
m.deinit();
post_received += 1;
}
if (sub4.nextMsgTimeout(500) catch null) |m| {
m.deinit();
post_received += 1;
}
if (sub5.nextMsgTimeout(500) catch null) |m| {
m.deinit();
post_received += 1;
}
if (post_received == 5) {
reportResult("multi_subs_receiving", true, "");
} else {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"post: {d}/5",
.{post_received},
) catch "post error";
reportResult("multi_subs_receiving", false, details);
}
}
fn testHighVolumePendingBuffer(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("high_volume_buffer", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
.pending_buffer_size = 64 * 1024,
}) catch {
reportResult("high_volume_buffer", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("buffer.test") catch {
reportResult("high_volume_buffer", false, "subscribe failed");
return;
};
defer sub.deinit();
var published_before: u32 = 0;
var i: u32 = 0;
while (i < 50) : (i += 1) {
client.publish("buffer.test", "pre") catch continue;
published_before += 1;
}
io.io().sleep(.fromMilliseconds(200), .awake) catch {};
var received_before: u32 = 0;
while (received_before < 100) {
if (sub.nextMsgTimeout(100) catch null) |msg| {
msg.deinit();
received_before += 1;
} else {
break;
}
}
manager.stopAll(io.io());
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
var published_during: u32 = 0;
i = 0;
while (i < 50) : (i += 1) {
client.publish("buffer.test", "buffered") catch continue;
published_during += 1;
}
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("high_volume_buffer", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(500), .awake) catch {};
var received_after: u32 = 0;
while (received_after < 100) {
if (sub.nextMsgTimeout(200) catch null) |msg| {
msg.deinit();
received_after += 1;
} else {
break;
}
}
if (published_before > 0 and received_before > 0) {
reportResult("high_volume_buffer", true, "");
} else {
var buf: [64]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"pub_before={d} recv_before={d}",
.{ published_before, received_before },
) catch "error";
reportResult("high_volume_buffer", false, details);
}
}
fn testQueueGroupMultiClientReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io1 = utils.newIo(allocator);
defer io1.deinit();
const io2 = utils.newIo(allocator);
defer io2.deinit();
manager.stopAll(io1.io());
_ = manager.startServer(allocator, io1.io(), .{ .port = test_port }) catch {
reportResult("queue_group_multi_client", false, "server start failed");
return;
};
const client1 = nats.Client.connect(allocator, io1.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult(
"queue_group_multi_client",
false,
"client1 connect failed",
);
return;
};
defer client1.deinit();
const client2 = nats.Client.connect(allocator, io2.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
}) catch {
reportResult(
"queue_group_multi_client",
false,
"client2 connect failed",
);
return;
};
defer client2.deinit();
var sub1 = client1.queueSubscribeSync(
"qgroup.test",
"workers",
) catch {
reportResult("queue_group_multi_client", false, "sub1 failed");
return;
};
defer sub1.deinit();
var sub2 = client2.queueSubscribeSync(
"qgroup.test",
"workers",
) catch {
reportResult("queue_group_multi_client", false, "sub2 failed");
return;
};
defer sub2.deinit();
var i: u8 = 0;
while (i < 20) : (i += 1) {
client1.publish("qgroup.test", "msg") catch {};
}
io1.io().sleep(.fromMilliseconds(200), .awake) catch {};
var c1_before: u8 = 0;
var c2_before: u8 = 0;
while (c1_before + c2_before < 30) {
if (sub1.tryNextMsg()) |m| {
m.deinit();
c1_before += 1;
} else if (sub2.tryNextMsg()) |m| {
m.deinit();
c2_before += 1;
} else {
break;
}
}
manager.stopServer(0, io1.io());
io1.io().sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io1.io(), .{ .port = test_port }) catch {
reportResult("queue_group_multi_client", false, "restart failed");
return;
};
io1.io().sleep(.fromMilliseconds(500), .awake) catch {};
i = 0;
while (i < 20) : (i += 1) {
client1.publish("qgroup.test", "msg") catch {};
}
io1.io().sleep(.fromMilliseconds(200), .awake) catch {};
var c1_after: u8 = 0;
var c2_after: u8 = 0;
while (c1_after + c2_after < 30) {
if (sub1.tryNextMsg()) |m| {
m.deinit();
c1_after += 1;
} else if (sub2.tryNextMsg()) |m| {
m.deinit();
c2_after += 1;
} else {
break;
}
}
const total_before = c1_before + c2_before;
const total_after = c1_after + c2_after;
if (total_before > 0 and total_after > 0) {
reportResult("queue_group_multi_client", true, "");
} else {
var buf: [48]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"before={d} after={d}",
.{ total_before, total_after },
) catch "error";
reportResult("queue_group_multi_client", false, details);
}
}
fn testRapidServerRestarts(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("rapid_restarts", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 20,
.reconnect_wait_ms = 100,
.ping_interval_ms = 100,
.max_pings_outstanding = 2,
}) catch {
reportResult("rapid_restarts", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("rapid.test") catch {
reportResult("rapid_restarts", false, "subscribe failed");
return;
};
defer sub.deinit();
var cycle: u8 = 0;
while (cycle < 3) : (cycle += 1) {
manager.stopAll(io.io());
client.forceReconnect() catch {};
_ = manager.startServer(allocator, io.io(), .{
.port = test_port,
}) catch {
reportResult("rapid_restarts", false, "restart failed");
return;
};
const want_reconnects: u32 = @as(u32, cycle) + 1;
if (!waitForReconnects(
io.io(),
client,
want_reconnects,
3000,
) or !waitForConnected(io.io(), client, 1000)) {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"reconnect {d} timeout",
.{cycle},
) catch "reconnect timeout";
reportResult("rapid_restarts", false, details);
return;
}
}
client.publish("rapid.test", "survived") catch {
reportResult("rapid_restarts", false, "final publish failed");
return;
};
if (sub.nextMsgTimeout(500) catch null) |msg| {
msg.deinit();
const stats = client.stats();
if (stats.reconnects >= 3) {
reportResult("rapid_restarts", true, "");
} else {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"reconnects={d}",
.{stats.reconnects},
) catch "error";
reportResult("rapid_restarts", false, details);
}
} else {
reportResult("rapid_restarts", false, "no final message");
}
}
fn testMultipleReconnectionCycles(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("multiple_cycles", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 30,
.reconnect_wait_ms = 100,
}) catch {
reportResult("multiple_cycles", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("cycles.test") catch {
reportResult("multiple_cycles", false, "subscribe failed");
return;
};
defer sub.deinit();
var cycle: u8 = 0;
while (cycle < 3) : (cycle += 1) {
manager.stopAll(io.io());
client.forceReconnect() catch {};
_ = manager.startServer(allocator, io.io(), .{
.port = test_port,
}) catch {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"restart {d} failed",
.{cycle},
) catch "restart error";
reportResult("multiple_cycles", false, details);
return;
};
const want_reconnects: u32 = @as(u32, cycle) + 1;
if (!waitForReconnects(
io.io(),
client,
want_reconnects,
3000,
) or !waitForConnected(io.io(), client, 1000)) {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"reconnect {d} timeout",
.{cycle},
) catch "reconnect timeout";
reportResult("multiple_cycles", false, details);
return;
}
var msg_buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(
&msg_buf,
"cycle-{d}",
.{cycle},
) catch "msg";
client.publish("cycles.test", msg) catch {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"publish {d} failed",
.{cycle},
) catch "pub error";
reportResult("multiple_cycles", false, details);
return;
};
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
} else {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"no msg cycle {d}",
.{cycle},
) catch "recv error";
reportResult("multiple_cycles", false, details);
return;
}
}
const stats = client.stats();
if (stats.reconnects == 3) {
reportResult("multiple_cycles", true, "");
} else {
var buf: [32]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"reconnects={d} want 3",
.{stats.reconnects},
) catch "error";
reportResult("multiple_cycles", false, details);
}
}
fn testLongDisconnectionRecovery(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
manager.stopAll(io.io());
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("long_disconnection", false, "server start failed");
return;
};
const client = nats.Client.connect(allocator, io.io(), url, .{
.reconnect = true,
.max_reconnect_attempts = 30,
.reconnect_wait_ms = 200,
.reconnect_wait_max_ms = 500,
}) catch {
reportResult("long_disconnection", false, "connect failed");
return;
};
defer client.deinit();
var sub = client.subscribeSync("long.test") catch {
reportResult("long_disconnection", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("long.test", "before") catch {};
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
} else {
reportResult("long_disconnection", false, "no msg before");
return;
}
manager.stopAll(io.io());
io.io().sleep(.fromMilliseconds(3000), .awake) catch {};
_ = manager.startServer(allocator, io.io(), .{ .port = test_port }) catch {
reportResult("long_disconnection", false, "restart failed");
return;
};
io.io().sleep(.fromMilliseconds(1000), .awake) catch {};
client.publish("long.test", "after-long-gap") catch {
reportResult("long_disconnection", false, "publish after failed");
return;
};
if (sub.nextMsgTimeout(1000) catch null) |msg| {
msg.deinit();
reportResult("long_disconnection", true, "");
} else {
reportResult("long_disconnection", false, "no msg after long gap");
}
}
================================================
FILE: src/testing/client/request_reply.zig
================================================
//! Request-Reply Tests for NATS Client
//!
//! Tests for request-reply pattern.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testRequestMethod(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_method", false, "connect failed");
return;
};
defer client.deinit();
const result = client.request(
"nonexistent.service.test",
"ping",
50,
) catch {
reportResult("request_method", false, "request error");
return;
};
if (result) |msg| {
msg.deinit();
}
if (client.isConnected()) {
reportResult("request_method", true, "");
} else {
reportResult("request_method", false, "disconnected after request");
}
}
pub fn testRequestReturns(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_returns", false, "connect failed");
return;
};
defer client.deinit();
const start = std.Io.Timestamp.now(io.io(), .awake);
const result = client.request(
"nonexistent.service.test2",
"data",
100,
) catch {
reportResult("request_returns", false, "request error");
return;
};
const end = std.Io.Timestamp.now(io.io(), .awake);
const elapsed = start.durationTo(end);
const elapsed_ns: u64 = @intCast(elapsed.nanoseconds);
const elapsed_ms = elapsed_ns / std.time.ns_per_ms;
if (result) |msg| {
msg.deinit();
}
if (elapsed_ms < 5000) {
reportResult("request_returns", true, "");
} else {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"took too long: {d}ms",
.{elapsed_ms},
) catch "timing error";
reportResult("request_returns", false, msg);
}
}
pub fn testReplyToPreserved(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("reply_preserved", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("reply.test") catch {
reportResult("reply_preserved", false, "sub failed");
return;
};
defer sub.deinit();
client.publishRequest("reply.test", "my.reply.inbox", "data") catch {
reportResult("reply_preserved", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |msg| {
if (msg.reply_to) |rt| {
if (std.mem.eql(u8, rt, "my.reply.inbox")) {
reportResult("reply_preserved", true, "");
return;
}
}
} else |_| {}
reportResult("reply_preserved", false, "reply_to not preserved");
}
pub fn testRequestReplySuccess(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_r = utils.newIo(allocator);
defer io_r.deinit();
const responder = nats.Client.connect(
allocator,
io_r.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_reply_success", false, "responder connect failed");
return;
};
defer responder.deinit();
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(
allocator,
io_req.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_reply_success", false, "requester connect failed");
return;
};
defer requester.deinit();
const sub = responder.subscribeSync("test.service") catch {
reportResult("request_reply_success", false, "responder sub failed");
return;
};
defer sub.deinit();
io_r.io().sleep(.fromMilliseconds(50), .awake) catch {};
const Handler = struct {
fn handle(
r: *nats.Client,
s: *nats.Subscription,
) void {
if (s.nextMsgTimeout(1000) catch null) |req| {
defer req.deinit();
if (req.reply_to) |reply_inbox| {
r.publish(reply_inbox, "pong") catch {};
}
}
}
};
var handler = io_r.io().async(Handler.handle, .{
responder,
sub,
});
defer _ = handler.cancel(io_r.io());
const reply = requester.request(
"test.service",
"ping",
2000,
) catch {
reportResult("request_reply_success", false, "request failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "pong")) {
reportResult("request_reply_success", true, "");
return;
}
}
reportResult("request_reply_success", false, "no reply or wrong data");
}
pub fn testCrossClientRequestReply(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_a = utils.newIo(allocator);
defer io_a.deinit();
const client_a = nats.Client.connect(
allocator,
io_a.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("cross_client_reqrep", false, "A connect failed");
return;
};
defer client_a.deinit();
const io_b = utils.newIo(allocator);
defer io_b.deinit();
const client_b = nats.Client.connect(
allocator,
io_b.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("cross_client_reqrep", false, "B connect failed");
return;
};
defer client_b.deinit();
const sub = client_b.subscribeSync("cross.service") catch {
reportResult("cross_client_reqrep", false, "B sub failed");
return;
};
defer sub.deinit();
io_b.io().sleep(.fromMilliseconds(50), .awake) catch {};
const Handler = struct {
fn handle(
b: *nats.Client,
s: *nats.Subscription,
) void {
if (s.nextMsgTimeout(2000) catch null) |req| {
defer req.deinit();
if (req.reply_to) |inbox| {
b.publish(inbox, "response-from-B") catch {};
}
}
}
};
var handler = io_b.io().async(Handler.handle, .{
client_b,
sub,
});
defer _ = handler.cancel(io_b.io());
const reply = client_a.request(
"cross.service",
"request-from-A",
3000,
) catch {
reportResult("cross_client_reqrep", false, "request failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
if (std.mem.eql(u8, msg.data, "response-from-B")) {
reportResult("cross_client_reqrep", true, "");
return;
}
}
reportResult("cross_client_reqrep", false, "no reply");
}
pub fn testRequestTimeout(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.no_responders = false,
.reconnect = false,
}) catch {
reportResult("request_timeout", false, "connect failed");
return;
};
defer client.deinit();
const start = std.Io.Timestamp.now(io.io(), .awake);
const result = client.request(
"timeout.service.noexist",
"ping",
200,
) catch {
reportResult(
"request_timeout",
false,
"request error",
);
return;
};
const end = std.Io.Timestamp.now(io.io(), .awake);
const elapsed = start.durationTo(end);
const elapsed_ns: u64 = @intCast(elapsed.nanoseconds);
const elapsed_ms = elapsed_ns / std.time.ns_per_ms;
if (result) |msg| {
msg.deinit();
reportResult("request_timeout", true, "");
return;
}
if (elapsed_ms < 5000) {
reportResult("request_timeout", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"took {d}ms",
.{elapsed_ms},
) catch "e";
reportResult("request_timeout", false, detail);
}
}
pub fn testRequestWithLargePayload(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_r = utils.newIo(allocator);
defer io_r.deinit();
const responder = nats.Client.connect(
allocator,
io_r.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_large_payload", false, "responder connect failed");
return;
};
defer responder.deinit();
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(
allocator,
io_req.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("request_large_payload", false, "requester connect failed");
return;
};
defer requester.deinit();
const sub = responder.subscribeSync("large.service") catch {
reportResult("request_large_payload", false, "responder sub failed");
return;
};
defer sub.deinit();
io_r.io().sleep(.fromMilliseconds(50), .awake) catch {};
const Handler = struct {
fn handle(
r: *nats.Client,
s: *nats.Subscription,
) void {
if (s.nextMsgTimeout(2000) catch null) |req| {
defer req.deinit();
if (req.reply_to) |reply_inbox| {
r.publish(reply_inbox, req.data) catch {};
}
}
}
};
var handler = io_r.io().async(Handler.handle, .{
responder,
sub,
});
defer _ = handler.cancel(io_r.io());
const payload = allocator.alloc(u8, 1024) catch {
reportResult("request_large_payload", false, "alloc failed");
return;
};
defer allocator.free(payload);
@memset(payload, 'X');
const reply = requester.request(
"large.service",
payload,
3000,
) catch {
reportResult("request_large_payload", false, "request failed");
return;
};
if (reply) |msg| {
defer msg.deinit();
if (msg.data.len == 1024) {
reportResult("request_large_payload", true, "");
return;
}
}
reportResult("request_large_payload", false, "no reply or wrong size");
}
pub fn testMultipleRequestsSequential(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_r = utils.newIo(allocator);
defer io_r.deinit();
const responder = nats.Client.connect(
allocator,
io_r.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_requests_seq", false, "responder connect failed");
return;
};
defer responder.deinit();
const io_req = utils.newIo(allocator);
defer io_req.deinit();
const requester = nats.Client.connect(
allocator,
io_req.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_requests_seq", false, "requester connect failed");
return;
};
defer requester.deinit();
const sub = responder.subscribeSync("multi.service") catch {
reportResult("multi_requests_seq", false, "responder sub failed");
return;
};
defer sub.deinit();
io_r.io().sleep(.fromMilliseconds(50), .awake) catch {};
const Handler = struct {
fn handle(
r: *nats.Client,
s: *nats.Subscription,
) void {
for (0..5) |_| {
if (s.nextMsgTimeout(2000) catch null) |req| {
defer req.deinit();
if (req.reply_to) |reply_inbox| {
r.publish(reply_inbox, "response") catch {};
}
} else break;
}
}
};
var handler = io_r.io().async(Handler.handle, .{
responder,
sub,
});
defer _ = handler.cancel(io_r.io());
var success_count: u32 = 0;
for (0..5) |_| {
const reply = requester.request(
"multi.service",
"request",
2000,
) catch continue;
if (reply) |msg| {
msg.deinit();
success_count += 1;
}
}
if (success_count >= 4) {
reportResult("multi_requests_seq", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/5",
.{success_count},
) catch "e";
reportResult("multi_requests_seq", false, detail);
}
}
/// Helper: spawns a responder fiber that replies "pong" forever
/// to the given subject. The caller cancels the returned future
/// in defer to stop it.
const RespHandler = struct {
fn run(client: *nats.Client, sub: *nats.Subscription) void {
while (true) {
const req = sub.nextMsgTimeout(2000) catch return;
const m = req orelse return;
defer m.deinit();
const reply = m.reply_to orelse continue;
client.publish(reply, "pong") catch return;
}
}
};
/// Muxer-specific test: proves the old per-request subscription
/// path is gone. That path carried a hardcoded 5ms latency floor;
/// instead of depending on sub-5ms wall-clock timing on CI, assert
/// that request() creates one wildcard response mux subscription
/// and reuses it for subsequent requests.
pub fn testMuxerLatencyFloor(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_r = utils.newIo(allocator);
defer io_r.deinit();
const responder = nats.Client.connect(
allocator,
io_r.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("muxer_latency_floor", false, "responder connect failed");
return;
};
defer responder.deinit();
const sub = responder.subscribeSync("muxer.lat.test") catch {
reportResult("muxer_latency_floor", false, "responder sub failed");
return;
};
defer sub.deinit();
io_r.io().sleep(.fromMilliseconds(50), .awake) catch {};
var resp_fut = io_r.io().async(RespHandler.run, .{ responder, sub });
defer _ = resp_fut.cancel(io_r.io());
const io_q = utils.newIo(allocator);
defer io_q.deinit();
const requester = nats.Client.connect(
allocator,
io_q.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("muxer_latency_floor", false, "requester connect failed");
return;
};
defer requester.deinit();
const before_subs = requester.numSubscriptions();
if (before_subs != 0) {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"subs before request: {d}",
.{before_subs},
) catch "e";
reportResult("muxer_latency_floor", false, detail);
return;
}
for (0..6) |i| {
const reply = requester.request(
"muxer.lat.test",
"ping",
2000,
) catch {
reportResult("muxer_latency_floor", false, "request failed");
return;
};
if (reply) |m| {
defer m.deinit();
if (!std.mem.eql(u8, m.data, "pong")) {
reportResult("muxer_latency_floor", false, "wrong reply");
return;
}
} else {
reportResult("muxer_latency_floor", false, "no reply");
return;
}
const subs = requester.numSubscriptions();
if (subs != 1) {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"request {d}: subs={d}, want 1",
.{ i + 1, subs },
) catch "e";
reportResult("muxer_latency_floor", false, detail);
return;
}
}
reportResult("muxer_latency_floor", true, "");
}
/// Muxer-specific test: proves the muxer's PING/PONG init cost
/// is amortized to zero. Issues 100 sequential requests on the
/// same connection and asserts the average round-trip is well
/// under 1ms (the cold first call is the only one paying for
/// ensureRespMux + PING/PONG).
pub fn testMuxerRapidSequential(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io_r = utils.newIo(allocator);
defer io_r.deinit();
const responder = nats.Client.connect(
allocator,
io_r.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("muxer_rapid_sequential", false, "responder connect failed");
return;
};
defer responder.deinit();
const sub = responder.subscribeSync("muxer.rapid.test") catch {
reportResult("muxer_rapid_sequential", false, "responder sub failed");
return;
};
defer sub.deinit();
io_r.io().sleep(.fromMilliseconds(50), .awake) catch {};
var resp_fut = io_r.io().async(RespHandler.run, .{ responder, sub });
defer _ = resp_fut.cancel(io_r.io());
const io_q = utils.newIo(allocator);
defer io_q.deinit();
const requester = nats.Client.connect(
allocator,
io_q.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("muxer_rapid_sequential", false, "requester connect failed");
return;
};
defer requester.deinit();
const N: u32 = 100;
var success: u32 = 0;
const start = std.Io.Timestamp.now(io_q.io(), .awake);
var i: u32 = 0;
while (i < N) : (i += 1) {
const reply = requester.request(
"muxer.rapid.test",
"ping",
2000,
) catch break;
if (reply) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, "pong")) success += 1;
} else break;
}
const end = std.Io.Timestamp.now(io_q.io(), .awake);
if (success != N) {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/{d} replies",
.{ success, N },
) catch "e";
reportResult("muxer_rapid_sequential", false, detail);
return;
}
const elapsed_ns: u64 = @intCast(start.durationTo(end).nanoseconds);
const total_ms = elapsed_ns / std.time.ns_per_ms;
// The old per-request-sub path burned at least 5ms per call
// in the artificial sleep alone, so 100 requests would take
// >= 500ms even before counting SUB/UNSUB churn. The muxer
// amortizes ensureRespMux to one PING/PONG and then uses the
// wildcard sub for every subsequent request, so total time
// is dominated by per-call dispatch overhead. We assert well
// under the old floor to prove the muxer is on the hot path.
if (total_ms < 400) {
reportResult("muxer_rapid_sequential", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"100 requests took {d}ms (expected < 400ms)",
.{total_ms},
) catch "e";
reportResult("muxer_rapid_sequential", false, detail);
}
}
/// Muxer-specific test: proves no use-after-free or leak when a
/// request times out (waiter is removed from the resp_map by the
/// cleanup defer in requestAwaitResp). Fires N timing-out
/// requests against a nonexistent subject and asserts each
/// returns null cleanly without leaking the waiter slot.
pub fn testMuxerTimeoutCleanup(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false, .no_responders = false },
) catch {
reportResult("muxer_timeout_cleanup", false, "connect failed");
return;
};
defer client.deinit();
var i: u32 = 0;
while (i < 20) : (i += 1) {
const reply = client.request(
"muxer.cleanup.noexist",
"ping",
20,
) catch {
reportResult("muxer_timeout_cleanup", false, "request error");
return;
};
if (reply) |m| {
m.deinit();
reportResult(
"muxer_timeout_cleanup",
false,
"unexpected reply",
);
return;
}
}
if (client.isConnected()) {
reportResult("muxer_timeout_cleanup", true, "");
} else {
reportResult(
"muxer_timeout_cleanup",
false,
"disconnected after timeouts",
);
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testRequestMethod(allocator);
testRequestReturns(allocator);
testReplyToPreserved(allocator);
testRequestReplySuccess(allocator);
testCrossClientRequestReply(allocator);
testRequestTimeout(allocator);
testRequestWithLargePayload(allocator);
testMultipleRequestsSequential(allocator);
testMuxerLatencyFloor(allocator);
testMuxerRapidSequential(allocator);
testMuxerTimeoutCleanup(allocator);
}
================================================
FILE: src/testing/client/server.zig
================================================
//! Server Tests for NATS Client
//!
//! Tests for server info and protocol handling.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
pub fn testServerInfo(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_info", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("server_info", false, "no server info");
return;
}
const has_version = info.?.version.len > 0;
reportResult("server_info", has_version, "no version in info");
}
pub fn testServerInfoFields(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_info_fields", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("server_info_fields", false, "no server info");
return;
}
const i = info.?;
var valid = true;
if (i.version.len == 0) valid = false;
if (i.max_payload == 0) valid = false;
if (i.proto < 1) valid = false;
if (valid) {
reportResult("server_info_fields", true, "");
} else {
reportResult("server_info_fields", false, "missing fields");
}
}
pub fn testServerVersion(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("server_version", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("server_version", false, "no server info");
return;
}
const version = info.?.version;
if (version.len > 0 and (version[0] == '2' or version[0] == '3')) {
reportResult("server_version", true, "");
} else {
reportResult("server_version", false, "unexpected version");
}
}
pub fn testServerMaxPayloadEnforced(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_payload_enforced", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("max_payload_enforced", false, "no server info");
return;
}
const max = info.?.max_payload;
if (max > 0) {
reportResult("max_payload_enforced", true, "");
} else {
reportResult("max_payload_enforced", false, "max_payload is 0");
}
}
pub fn testMaxPayloadRespected(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_payload_respected", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("max_payload_respected", false, "no server info");
return;
}
if (info.?.max_payload >= 1024 and info.?.max_payload <= 64 * 1024 * 1024) {
reportResult("max_payload_respected", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"max_payload={d}",
.{info.?.max_payload},
) catch "err";
reportResult("max_payload_respected", false, detail);
}
}
pub fn testProtocolVersion(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("proto_version", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("proto_version", false, "no server info");
return;
}
if (info.?.proto >= 1) {
reportResult("proto_version", true, "");
} else {
reportResult("proto_version", false, "proto < 1");
}
}
pub fn testClientName(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.name = "my-test-client-12345",
.reconnect = false,
}) catch {
reportResult("client_name", false, "connect failed");
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("client_name", true, "");
} else {
reportResult("client_name", false, "not connected");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testServerInfo(allocator);
testServerInfoFields(allocator);
testServerVersion(allocator);
testServerMaxPayloadEnforced(allocator);
testMaxPayloadRespected(allocator);
testProtocolVersion(allocator);
testClientName(allocator);
}
================================================
FILE: src/testing/client/state_notifications.zig
================================================
//! State Notification Tests for NATS Client
//!
//! Tests for: LastError, discovered_servers event,
//! draining event, subscription_complete event.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
fn getNowNs(io: std.Io) i128 {
return std.Io.Timestamp.now(io, .awake).nanoseconds;
}
fn threadSleepNs(ns: u64) void {
var ts: std.posix.timespec = .{
.sec = @intCast(ns / std.time.ns_per_s),
.nsec = @intCast(ns % std.time.ns_per_s),
};
_ = std.posix.system.nanosleep(&ts, &ts);
}
/// Test getLastError returns null initially and after clear.
pub fn testLastErrorInitialNull(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("last_error_initial", false, "connect failed");
return;
};
defer client.deinit();
// Initially should be null
const err = client.lastError();
if (err != null) {
reportResult("last_error_initial", false, "expected null");
return;
}
reportResult("last_error_initial", true, "");
}
/// Test clearLastError works.
pub fn testClearLastError(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("clear_last_error", false, "connect failed");
return;
};
defer client.deinit();
// Clear and verify null
client.clearLastError();
const err = client.lastError();
if (err != null) {
reportResult("clear_last_error", false, "expected null after clear");
return;
}
reportResult("clear_last_error", true, "");
}
/// Test draining event is fired during drain.
pub fn testDrainingEvent(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
// Track events with a handler
const EventTracker = struct {
draining_received: bool = false,
pub fn onDraining(self: *@This()) void {
self.draining_received = true;
}
};
var tracker = EventTracker{};
const handler = nats.EventHandler.init(EventTracker, &tracker);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = false,
.event_handler = handler,
},
) catch {
reportResult("draining_event", false, "connect failed");
return;
};
defer client.deinit();
// Subscribe to something
const sub = client.subscribeSync("drain.test") catch {
reportResult("draining_event", false, "subscribe failed");
return;
};
defer sub.deinit();
// Drain (this also cleans up subscriptions internally)
_ = client.drain() catch {};
// Give callback task time to process events
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
if (tracker.draining_received) {
reportResult("draining_event", true, "");
} else {
reportResult("draining_event", false, "no draining event");
}
}
/// Test subscription_complete event when auto-unsub limit is reached.
pub fn testSubscriptionCompleteEvent(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
// Track events with a handler
const EventTracker = struct {
complete_received: bool = false,
complete_sid: u64 = 0,
pub fn onSubscriptionComplete(self: *@This(), sid: u64) void {
self.complete_received = true;
self.complete_sid = sid;
}
};
var tracker = EventTracker{};
const handler = nats.EventHandler.init(EventTracker, &tracker);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = false,
.event_handler = handler,
},
) catch {
reportResult("sub_complete_event", false, "connect failed");
return;
};
defer client.deinit();
// Subscribe with auto-unsub after 3 messages
const sub = client.subscribeSync("complete.test") catch {
reportResult("sub_complete_event", false, "subscribe failed");
return;
};
defer sub.deinit();
sub.autoUnsubscribe(3) catch {
reportResult("sub_complete_event", false, "auto-unsub failed");
return;
};
// Publish 3 messages
for (0..3) |_| {
client.publish("complete.test", "data") catch {};
}
// Receive messages to trigger the delivered count
for (0..3) |_| {
const msg = sub.nextMsgTimeout(500) catch break;
if (msg) |m| {
m.deinit();
}
}
// Give callback task time to process events
io.io().sleep(.fromMilliseconds(100), .awake) catch {};
if (tracker.complete_received and tracker.complete_sid == sub.getSid()) {
reportResult("sub_complete_event", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "recv={} sid={}", .{
tracker.complete_received,
tracker.complete_sid,
}) catch "e";
reportResult("sub_complete_event", false, detail);
}
}
fn pushDrainingEventThread(
client: *nats.Client,
go: *std.atomic.Value(bool),
) void {
while (!go.load(.acquire)) {
std.atomic.spinLoopHint();
}
client.pushEvent(.{ .draining = {} });
}
/// Stress the event queue with one producer from io_task and one
/// producer from user-thread code in the same window.
pub fn testEventQueueMultiProducerOverlap(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const Tracker = struct {
draining_count: std.atomic.Value(u32) =
std.atomic.Value(u32).init(0),
complete_count: std.atomic.Value(u32) =
std.atomic.Value(u32).init(0),
pub fn onDraining(self: *@This()) void {
_ = self.draining_count.fetchAdd(1, .monotonic);
}
pub fn onSubscriptionComplete(self: *@This(), sid: u64) void {
_ = sid;
_ = self.complete_count.fetchAdd(1, .monotonic);
}
};
var tracker = Tracker{};
const handler = nats.EventHandler.init(Tracker, &tracker);
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{
.reconnect = false,
.event_handler = handler,
},
) catch {
reportResult("event_queue_multi_producer", false, "connect failed");
return;
};
defer client.deinit();
const iterations = 64;
for (0..iterations) |i| {
{
var subject_buf: [48]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"event.mp.{d}",
.{i},
) catch {
reportResult("event_queue_multi_producer", false, "subject format failed");
return;
};
const sub = client.subscribeSync(subject) catch {
reportResult("event_queue_multi_producer", false, "subscribe failed");
return;
};
defer sub.deinit();
sub.autoUnsubscribe(1) catch {
reportResult("event_queue_multi_producer", false, "auto-unsub failed");
return;
};
var go = std.atomic.Value(bool).init(false);
var t = std.Thread.spawn(
.{},
pushDrainingEventThread,
.{ client, &go },
) catch {
reportResult("event_queue_multi_producer", false, "thread spawn failed");
return;
};
go.store(true, .release);
client.publish(subject, "x") catch {
t.join();
reportResult("event_queue_multi_producer", false, "publish failed");
return;
};
client.flush(1_000_000_000) catch {
t.join();
reportResult("event_queue_multi_producer", false, "flush failed");
return;
};
t.join();
if (sub.nextMsgTimeout(200) catch null) |msg| {
msg.deinit();
} else {
reportResult("event_queue_multi_producer", false, "message not received");
return;
}
const draining_target: u32 = @intCast(i + 1);
const complete_target: u32 = @intCast(i + 1);
const deadline_ns = getNowNs(io.io()) +
200 * std.time.ns_per_ms;
while (getNowNs(io.io()) < deadline_ns) {
if (tracker.draining_count.load(.monotonic) >= draining_target and
tracker.complete_count.load(.monotonic) >= complete_target)
{
break;
}
threadSleepNs(1 * std.time.ns_per_ms);
}
if (tracker.draining_count.load(.monotonic) < draining_target or
tracker.complete_count.load(.monotonic) < complete_target)
{
var detail_buf: [96]u8 = undefined;
const detail = std.fmt.bufPrint(
&detail_buf,
"drain={d} complete={d} at iter {d}",
.{
tracker.draining_count.load(.monotonic),
tracker.complete_count.load(.monotonic),
i,
},
) catch "event count mismatch";
reportResult("event_queue_multi_producer", false, detail);
return;
}
}
}
reportResult("event_queue_multi_producer", true, "");
}
pub fn runAll(allocator: std.mem.Allocator) void {
testLastErrorInitialNull(allocator);
testClearLastError(allocator);
testDrainingEvent(allocator);
testSubscriptionCompleteEvent(allocator);
testEventQueueMultiProducerOverlap(allocator);
}
================================================
FILE: src/testing/client/stats.zig
================================================
//! Stats Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testClientStats(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_stats", false, "connect failed");
return;
};
defer client.deinit();
const initial_stats = client.stats();
if (initial_stats.msgs_out != 0) {
reportResult("client_stats", false, "initial msgs_out != 0");
return;
}
client.publish("async.stats", "test") catch {};
const stats = client.stats();
if (stats.msgs_out >= 1) {
reportResult("client_stats", true, "");
} else {
reportResult("client_stats", false, "msgs_out not incremented");
}
}
pub fn testStatsIncrement(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stats_increment", false, "connect failed");
return;
};
defer client.deinit();
const before = client.stats();
for (0..10) |_| {
client.publish("async.stats.inc", "msg") catch {};
}
const after = client.stats();
if (after.msgs_out >= before.msgs_out + 10) {
reportResult("stats_increment", true, "");
} else {
reportResult("stats_increment", false, "stats not incremented");
}
}
pub fn testStatsBytesAccuracy(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stats_bytes", false, "connect failed");
return;
};
defer client.deinit();
const before = client.stats();
const payload = "0123456789" ** 10;
client.publish("async.stats.bytes", payload) catch {};
const after = client.stats();
if (after.bytes_out >= before.bytes_out + 100) {
reportResult("stats_bytes", true, "");
} else {
reportResult("stats_bytes", false, "bytes not tracked");
}
}
pub fn testStatsMsgsIn(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stats_msgs_in", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("msgsin.test") catch {
reportResult("stats_msgs_in", false, "subscribe failed");
return;
};
defer sub.deinit();
const before = client.stats();
for (0..25) |_| {
client.publish("msgsin.test", "data") catch {};
}
var received: u32 = 0;
for (0..30) |_| {
const msg = sub.nextMsgTimeout(200) catch break;
if (msg) |m| {
m.deinit();
received += 1;
} else break;
}
const after = client.stats();
const msgs_in = after.msgs_in - before.msgs_in;
if (msgs_in == 25 and received == 25) {
reportResult("stats_msgs_in", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"in={d} recv={d}",
.{ msgs_in, received },
) catch "e";
reportResult("stats_msgs_in", false, detail);
}
}
pub fn testStatsBytesIn(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stats_bytes_in", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("bytesin.test") catch {
reportResult("stats_bytes_in", false, "subscribe failed");
return;
};
defer sub.deinit();
const before = client.stats();
const payload = "01234567890123456789012345678901234567890123456789";
for (0..10) |_| {
client.publish("bytesin.test", payload) catch {};
}
for (0..15) |_| {
const msg = sub.nextMsgTimeout(200) catch break;
if (msg) |m| {
m.deinit();
} else break;
}
const after = client.stats();
const bytes_in = after.bytes_in - before.bytes_in;
if (bytes_in == 500) {
reportResult("stats_bytes_in", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/500",
.{bytes_in},
) catch "e";
reportResult("stats_bytes_in", false, detail);
}
}
pub fn testConnectsCounter(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stats_connects", false, "connect failed");
return;
};
defer client.deinit();
const stats = client.stats();
if (stats.connects >= 1) {
reportResult("stats_connects", true, "");
} else {
reportResult("stats_connects", false, "connects not incremented");
}
}
pub fn testSubStats(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("sub_stats", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("substats.test") catch {
reportResult("sub_stats", false, "subscribe failed");
return;
};
defer sub.deinit();
// Initially should have 0 pending
const initial = sub.subStats();
if (initial.pending_msgs != 0) {
reportResult("sub_stats", false, "initial pending != 0");
return;
}
// Publish some messages
for (0..5) |_| {
client.publish("substats.test", "test data") catch {};
}
// Wait for messages to arrive
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
// Check pending increased
const after = sub.subStats();
if (after.pending_msgs >= 5 or after.pending_bytes > 0) {
reportResult("sub_stats", true, "");
} else {
reportResult("sub_stats", false, "pending not updated");
}
}
pub fn testPendingBytes(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("pending_bytes", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("pending.bytes") catch {
reportResult("pending_bytes", false, "subscribe failed");
return;
};
defer sub.deinit();
// Publish messages
const payload = "0123456789";
for (0..10) |_| {
client.publish("pending.bytes", payload) catch {};
}
// Wait for messages
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
// Check pending bytes before receiving
const before_recv = sub.pendingBytes();
// Receive all messages
for (0..10) |_| {
const msg = sub.nextMsgTimeout(100) catch break;
if (msg) |m| {
m.deinit();
} else break;
}
// Check pending bytes after receiving
const after_recv = sub.pendingBytes();
if (before_recv > after_recv and after_recv == 0) {
reportResult("pending_bytes", true, "");
} else {
var buf: [48]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"before={d} after={d}",
.{ before_recv, after_recv },
) catch "e";
reportResult("pending_bytes", false, detail);
}
}
pub fn testMaxPending(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_pending", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("max.pending") catch {
reportResult("max_pending", false, "subscribe failed");
return;
};
defer sub.deinit();
// Activate flow control so max_pending_msgs watermark is tracked
sub.setPendingLimits(1000);
// Publish messages to create high water mark
for (0..20) |_| {
client.publish("max.pending", "payload") catch {};
}
// Wait for messages
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
// Get max pending before receiving
const max = sub.maxPending();
if (max.msgs == 0) {
reportResult("max_pending", false, "max_msgs is 0");
return;
}
// Receive all messages
for (0..25) |_| {
const msg = sub.nextMsgTimeout(100) catch break;
if (msg) |m| {
m.deinit();
} else break;
}
// Max should stay the same (high watermark)
const max_after = sub.maxPending();
if (max_after.msgs == max.msgs) {
// Clear max pending
sub.clearMaxPending();
const max_cleared = sub.maxPending();
if (max_cleared.msgs == 0) {
reportResult("max_pending", true, "");
} else {
reportResult("max_pending", false, "clearMaxPending failed");
}
} else {
reportResult("max_pending", false, "max decreased");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testClientStats(allocator);
testStatsIncrement(allocator);
testStatsBytesAccuracy(allocator);
testStatsMsgsIn(allocator);
testStatsBytesIn(allocator);
testConnectsCounter(allocator);
testSubStats(allocator);
testPendingBytes(allocator);
testMaxPending(allocator);
}
================================================
FILE: src/testing/client/stress.zig
================================================
//! Stress Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testStress500Messages(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("stress_500", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(allocator, sub_io.io(), url, .{
.sub_queue_size = 512,
.reconnect = false,
}) catch {
reportResult("stress_500", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("stress500") catch {
reportResult("stress_500", false, "sub failed");
return;
};
defer sub.deinit();
sub_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
const NUM_MSGS = 500;
for (0..NUM_MSGS) |_| {
publisher.publish("stress500", "stress-msg") catch {
reportResult("stress_500", false, "publish failed");
return;
};
}
var received: usize = 0;
for (0..NUM_MSGS) |_| {
var future = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(sub_io.io())) |m| m.deinit() else |_| {};
if (future.await(sub_io.io())) |_| {
received += 1;
} else |_| {
break;
}
}
if (received == NUM_MSGS) {
reportResult("stress_500", true, "");
} else {
var buf: [32]u8 = undefined;
const msg =
std.fmt.bufPrint(&buf, "got {d}/500", .{received}) catch "e";
reportResult("stress_500", false, msg);
}
}
pub fn testStress1000Messages(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.sub_queue_size = 1024,
.reconnect = false,
}) catch {
reportResult("stress_1000", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("stress1k") catch {
reportResult("stress_1000", false, "sub failed");
return;
};
defer sub.deinit();
const NUM_MSGS = 1000;
for (0..NUM_MSGS) |_| {
client.publish("stress1k", "stress-msg") catch {
reportResult("stress_1000", false, "publish failed");
return;
};
}
var received: usize = 0;
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(100) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
if (received == NUM_MSGS) {
reportResult("stress_1000", true, "");
} else {
var buf: [32]u8 = undefined;
const msg =
std.fmt.bufPrint(&buf, "got {d}/1000", .{received}) catch "e";
reportResult("stress_1000", false, msg);
}
}
pub fn testStress2000Messages(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(allocator, io.io(), url, .{
.sub_queue_size = 2048,
.reconnect = false,
}) catch {
reportResult("stress_2000", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("stress2k") catch {
reportResult("stress_2000", false, "sub failed");
return;
};
defer sub.deinit();
const NUM_MSGS = 2000;
for (0..20) |_| {
for (0..100) |_| {
client.publish("stress2k", "stress-msg") catch {
reportResult("stress_2000", false, "publish failed");
return;
};
}
}
var received: usize = 0;
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(100) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
if (received == NUM_MSGS) {
reportResult("stress_2000", true, "");
} else {
var buf: [32]u8 = undefined;
const msg =
std.fmt.bufPrint(&buf, "got {d}/2000", .{received}) catch "e";
reportResult("stress_2000", false, msg);
}
}
pub fn testPayload30KB(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("payload_30kb", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("stress.30kb") catch {
reportResult("payload_30kb", false, "sub failed");
return;
};
defer sub.deinit();
const payload = allocator.alloc(u8, 30 * 1024) catch {
reportResult("payload_30kb", false, "alloc failed");
return;
};
defer allocator.free(payload);
@memset(payload, 'X');
client.publish("stress.30kb", payload) catch {
reportResult("payload_30kb", false, "publish failed");
return;
};
if (sub.nextMsgTimeout(3000) catch null) |m| {
defer m.deinit();
if (m.data.len == 30 * 1024) {
reportResult("payload_30kb", true, "");
} else {
reportResult("payload_30kb", false, "wrong size");
}
} else {
reportResult("payload_30kb", false, "no message");
}
}
pub fn testManySubscriptions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("many_subscriptions", false, "connect failed");
return;
};
defer client.deinit();
var subs: [50]?*nats.Subscription = undefined;
@memset(&subs, null);
defer for (&subs) |*s| {
if (s.*) |sub| sub.deinit();
};
var created: usize = 0;
for (0..50) |i| {
var subject_buf: [32]u8 = undefined;
const subject =
std.fmt.bufPrint(&subject_buf, "manysub.{d}", .{i}) catch {
continue;
};
subs[i] = client.subscribeSync(subject) catch {
break;
};
created += 1;
}
if (created == 50) {
reportResult("many_subscriptions", true, "");
} else {
var buf: [32]u8 = undefined;
const msg =
std.fmt.bufPrint(&buf, "created {d}/50", .{created}) catch "e";
reportResult("many_subscriptions", false, msg);
}
}
pub fn testPayloadBoundary(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("payload_boundary", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("boundary.test") catch {
reportResult("payload_boundary", false, "subscribe failed");
return;
};
defer sub.deinit();
const sizes = [_]usize{ 1024, 4096, 8192, 15360 };
var all_passed = true;
for (sizes) |size| {
const payload = allocator.alloc(u8, size) catch {
all_passed = false;
break;
};
defer allocator.free(payload);
@memset(payload, 'B');
client.publish("boundary.test", payload) catch {
all_passed = false;
break;
};
const msg = sub.nextMsgTimeout(2000) catch {
all_passed = false;
break;
};
if (msg) |m| {
if (m.data.len != size) all_passed = false;
m.deinit();
} else {
all_passed = false;
break;
}
}
if (all_passed) {
reportResult("payload_boundary", true, "");
} else {
reportResult("payload_boundary", false, "size mismatch");
}
}
pub fn testFiveConcurrentClients(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
var ios: [5]*utils.TestIo = undefined;
var clients: [5]?*nats.Client = [_]?*nats.Client{null} ** 5;
var count: usize = 0;
defer {
for (0..count) |i| {
if (clients[i]) |c| {
c.deinit();
}
ios[i].deinit();
}
}
for (0..5) |i| {
ios[i] = utils.newIo(allocator);
clients[i] = nats.Client.connect(
allocator,
ios[i].io(),
url,
.{ .reconnect = false },
) catch {
reportResult("five_concurrent", false, "connect failed");
return;
};
count += 1;
}
var all_connected = true;
for (0..5) |i| {
if (clients[i]) |c| {
if (!c.isConnected()) all_connected = false;
}
}
if (all_connected) {
reportResult("five_concurrent", true, "");
} else {
reportResult("five_concurrent", false, "not all connected");
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testStress500Messages(allocator);
testStress1000Messages(allocator);
testStress2000Messages(allocator);
testPayload30KB(allocator);
testManySubscriptions(allocator);
testPayloadBoundary(allocator);
testFiveConcurrentClients(allocator);
}
================================================
FILE: src/testing/client/stress_subs.zig
================================================
//! Stress Tests for Subscriptions, Publishing, and Edge Cases
//!
//! Tests subscription counts, SidMap churn, payload sizes,
//! multi-client fan-out, and queue pressure scenarios.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
// --- A. Massive Subscription Tests ---
/// 5K subs on unique subjects, publish one msg to each, verify.
pub fn testFiveThousandSubs(
allocator: std.mem.Allocator,
) void {
const NUM_SUBS = 5_000;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const sub_client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .sub_queue_size = 64, .reconnect = false },
) catch {
reportResult("5k_subs", false, "sub connect");
return;
};
defer sub_client.deinit();
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("5k_subs", false, "pub connect");
return;
};
defer pub_client.deinit();
const subs = allocator.alloc(
?*nats.Subscription,
NUM_SUBS,
) catch {
reportResult("5k_subs", false, "alloc subs");
return;
};
defer allocator.free(subs);
@memset(subs, null);
defer for (subs) |s| {
if (s) |sub| sub.deinit();
};
// Create subs in batches of 500 with flush
var created: usize = 0;
var last_err: ?[]const u8 = null;
for (0..NUM_SUBS) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"five.{d}",
.{i},
) catch continue;
subs[i] = sub_client.subscribeSync(
subj,
) catch |e| {
last_err = @errorName(e);
break;
};
created += 1;
// Flush every 500 subs to avoid write backlog
}
if (created != NUM_SUBS) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"{d}/{d} err={s}",
.{
created,
NUM_SUBS,
last_err orelse "none",
},
) catch "count";
reportResult("5k_subs", false, msg);
return;
}
sub_io.io().sleep(
.fromMilliseconds(200),
.awake,
) catch {};
// Publish one msg to each subject
for (0..NUM_SUBS) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"five.{d}",
.{i},
) catch continue;
pub_client.publish(subj, "x") catch {
reportResult("5k_subs", false, "publish");
return;
};
}
// Wait for messages to arrive
sub_io.io().sleep(
.fromMilliseconds(2000),
.awake,
) catch {};
// Drain (short timeout - msgs should be queued)
var received: usize = 0;
for (subs) |s| {
if (s) |sub| {
if (sub.nextMsgTimeout(50) catch null) |m| {
m.deinit();
received += 1;
}
}
}
const threshold = NUM_SUBS * 100 / 100;
if (received >= threshold) {
reportResult("5k_subs", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ received, NUM_SUBS },
) catch "count";
reportResult("5k_subs", false, msg);
}
}
/// Sub/unsub churn stresses SidMap tombstones.
pub fn testSubUnsubChurn(
allocator: std.mem.Allocator,
) void {
const ITERATIONS = 5000;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .sub_queue_size = 64, .reconnect = false },
) catch {
reportResult("sub_unsub_churn", false, "connect");
return;
};
defer client.deinit();
for (0..ITERATIONS) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"churn.{d}",
.{i},
) catch continue;
const sub = client.subscribeSync(subj) catch |e| {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"fail at {d} {s}",
.{ i, @errorName(e) },
) catch "sub";
reportResult("sub_unsub_churn", false, msg);
return;
};
sub.deinit();
// Flush every 500 to keep write buffer clear
}
// Verify final sub works
const final_sub = client.subscribeSync(
"churn.final",
) catch {
reportResult("sub_unsub_churn", false, "final");
return;
};
defer final_sub.deinit();
client.publish("churn.final", "ok") catch {
reportResult("sub_unsub_churn", false, "pub");
return;
};
if (final_sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("sub_unsub_churn", true, "");
} else {
reportResult("sub_unsub_churn", false, "no msg");
}
}
/// Subscribe 2048, unsub all, resubscribe 2048 fresh.
pub fn testSubsThenResubscribe(
allocator: std.mem.Allocator,
) void {
const COUNT = 2048;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .sub_queue_size = 64, .reconnect = false },
) catch {
reportResult("resub", false, "connect");
return;
};
defer client.deinit();
const subs = allocator.alloc(
?*nats.Subscription,
COUNT,
) catch {
reportResult("resub", false, "alloc");
return;
};
defer allocator.free(subs);
@memset(subs, null);
// subscribe COUNT
var created: usize = 0;
for (0..COUNT) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"rs.a.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subj) catch break;
created += 1;
}
if (created != COUNT) {
for (subs) |s| if (s) |sub| sub.deinit();
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"phase1 {d}/{d}",
.{ created, COUNT },
) catch "count";
reportResult("resub", false, msg);
return;
}
// Unsub all
for (subs) |s| if (s) |sub| sub.deinit();
@memset(subs, null);
io.io().sleep(
.fromMilliseconds(200),
.awake,
) catch {};
if (!client.isConnected()) {
reportResult("resub", false, "disconnected");
return;
}
// resubscribe fresh
var created2: usize = 0;
var last_err: ?[]const u8 = null;
for (0..COUNT) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"rs.b.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subj) catch |e| {
last_err = @errorName(e);
break;
};
created2 += 1;
}
defer for (subs) |s| if (s) |sub| sub.deinit();
if (created2 != COUNT) {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"phase2 {d}/{d} {s}",
.{
created2,
COUNT,
last_err orelse "none",
},
) catch "count";
reportResult("resub", false, msg);
return;
}
// Verify pub/sub on a resubscribed subject
client.publish("rs.b.0", "resub-ok") catch {
reportResult("resub", false, "publish");
return;
};
if (subs[0]) |sub| {
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("resub", true, "");
} else {
reportResult("resub", false, "no msg");
}
} else {
reportResult("resub", false, "null sub");
}
}
/// 2K wildcard subs + wildcard catch-all, fan-out test.
pub fn testWildcardFanOut(
allocator: std.mem.Allocator,
) void {
const NUM_SUBS = 2_000;
const NUM_MSGS = 100;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .sub_queue_size = 128, .reconnect = false },
) catch {
reportResult("wildcard_fanout", false, "connect");
return;
};
defer client.deinit();
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wildcard_fanout", false, "pub con");
return;
};
defer pub_client.deinit();
const subs = allocator.alloc(
?*nats.Subscription,
NUM_SUBS,
) catch {
reportResult("wildcard_fanout", false, "alloc");
return;
};
defer allocator.free(subs);
@memset(subs, null);
defer for (subs) |s| if (s) |sub| sub.deinit();
var created: usize = 0;
for (0..NUM_SUBS) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"fan.{d}",
.{i},
) catch continue;
subs[i] = client.subscribeSync(subj) catch break;
created += 1;
}
if (created != NUM_SUBS) {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"created {d}/{d}",
.{ created, NUM_SUBS },
) catch "count";
reportResult("wildcard_fanout", false, msg);
return;
}
// Wildcard subscriber
const wc_sub = client.subscribeSync("fan.>") catch {
reportResult("wildcard_fanout", false, "wc sub");
return;
};
defer wc_sub.deinit();
sub_io.io().sleep(
.fromMilliseconds(200),
.awake,
) catch {};
// Publish to fan.0 through fan.99
for (0..NUM_MSGS) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"fan.{d}",
.{i},
) catch continue;
pub_client.publish(subj, "wc") catch {
reportResult("wildcard_fanout", false, "pub");
return;
};
}
sub_io.io().sleep(
.fromMilliseconds(500),
.awake,
) catch {};
var wc_received: usize = 0;
for (0..NUM_MSGS) |_| {
if (wc_sub.nextMsgTimeout(100) catch null) |m| {
m.deinit();
wc_received += 1;
} else break;
}
if (wc_received == NUM_MSGS) {
reportResult("wildcard_fanout", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"wc got {d}/{d}",
.{ wc_received, NUM_MSGS },
) catch "count";
reportResult("wildcard_fanout", false, msg);
}
}
// --- B. Multi-Client Tests ---
/// 10 clients, 200 subs each, cross-publish.
pub fn testTenClientsManySubs(
allocator: std.mem.Allocator,
) void {
const NUM_CLIENTS = 10;
const SUBS_PER = 200;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
var ios: [NUM_CLIENTS]*utils.TestIo = undefined;
var clients: [NUM_CLIENTS]?*nats.Client =
[_]?*nats.Client{null} ** NUM_CLIENTS;
var io_count: usize = 0;
defer {
for (0..io_count) |i| {
if (clients[i]) |c| c.deinit();
ios[i].deinit();
}
}
for (0..NUM_CLIENTS) |i| {
ios[i] = utils.newIo(allocator);
clients[i] = nats.Client.connect(
allocator,
ios[i].io(),
url,
.{
.sub_queue_size = 64,
.reconnect = false,
},
) catch {
reportResult(
"10_clients_subs",
false,
"connect",
);
return;
};
io_count += 1;
}
const total_subs = NUM_CLIENTS * SUBS_PER;
const all_subs = allocator.alloc(
?*nats.Subscription,
total_subs,
) catch {
reportResult("10_clients_subs", false, "alloc");
return;
};
defer allocator.free(all_subs);
@memset(all_subs, null);
defer for (all_subs) |s| if (s) |sub| sub.deinit();
var sub_count: usize = 0;
for (0..NUM_CLIENTS) |ci| {
const c = clients[ci] orelse continue;
for (0..SUBS_PER) |si| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"mc.{d}",
.{si},
) catch continue;
const idx = ci * SUBS_PER + si;
all_subs[idx] = c.subscribeSync(subj) catch
break;
sub_count += 1;
}
}
if (sub_count != total_subs) {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"subs {d}/{d}",
.{ sub_count, total_subs },
) catch "count";
reportResult("10_clients_subs", false, msg);
return;
}
// Publisher
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("10_clients_subs", false, "pub con");
return;
};
defer publisher.deinit();
pub_io.io().sleep(
.fromMilliseconds(100),
.awake,
) catch {};
for (0..SUBS_PER) |si| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"mc.{d}",
.{si},
) catch continue;
publisher.publish(subj, "mc") catch {
reportResult("10_clients_subs", false, "pub");
return;
};
}
pub_io.io().sleep(
.fromMilliseconds(1000),
.awake,
) catch {};
var total_recv: usize = 0;
for (all_subs) |s| {
if (s) |sub| {
if (sub.nextMsgTimeout(50) catch null) |m| {
m.deinit();
total_recv += 1;
}
}
}
const threshold = total_subs * 100 / 100;
if (total_recv >= threshold) {
reportResult("10_clients_subs", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ total_recv, total_subs },
) catch "count";
reportResult("10_clients_subs", false, msg);
}
}
fn publishWithBackpressure(
client: *nats.Client,
subject: []const u8,
payload: []const u8,
test_name: []const u8,
) bool {
client.publish(subject, payload) catch |err| {
if (err == error.PublishBufferFull) {
client.flush(5 * std.time.ns_per_s) catch {
reportResult(test_name, false, "pub flush");
return false;
};
client.publish(subject, payload) catch {
reportResult(test_name, false, "publish");
return false;
};
return true;
}
reportResult(test_name, false, "publish");
return false;
};
return true;
}
/// 5 publishers, 5 subscribers, 100 subjects.
pub fn testMultiPubMultiSub(
allocator: std.mem.Allocator,
) void {
const NUM_PUB = 5;
const NUM_SUB = 5;
const NUM_SUBJECTS = 100;
const MSGS_PER_SUBJECT = 100;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
var sub_ios: [NUM_SUB]*utils.TestIo = undefined;
var sub_clients: [NUM_SUB]?*nats.Client =
[_]?*nats.Client{null} ** NUM_SUB;
var sub_io_count: usize = 0;
defer {
for (0..sub_io_count) |i| {
if (sub_clients[i]) |c| c.deinit();
sub_ios[i].deinit();
}
}
for (0..NUM_SUB) |i| {
sub_ios[i] = utils.newIo(allocator);
sub_clients[i] = nats.Client.connect(
allocator,
sub_ios[i].io(),
url,
.{
.sub_queue_size = 2048,
.reconnect = false,
},
) catch {
reportResult("multi_pub_sub", false, "sub con");
return;
};
sub_io_count += 1;
}
const total_subs = NUM_SUB * NUM_SUBJECTS;
const all_subs = allocator.alloc(
?*nats.Subscription,
total_subs,
) catch {
reportResult("multi_pub_sub", false, "alloc");
return;
};
defer allocator.free(all_subs);
@memset(all_subs, null);
defer for (all_subs) |s| if (s) |sub| sub.deinit();
for (0..NUM_SUB) |si| {
const c = sub_clients[si] orelse continue;
for (0..NUM_SUBJECTS) |subj_i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"mp.{d}",
.{subj_i},
) catch continue;
const idx = si * NUM_SUBJECTS + subj_i;
all_subs[idx] = c.subscribeSync(subj) catch
break;
}
}
for (sub_clients) |maybe_client| {
if (maybe_client) |c| {
c.flush(5 * std.time.ns_per_s) catch {
reportResult(
"multi_pub_sub",
false,
"sub flush",
);
return;
};
}
}
var pub_ios: [NUM_PUB]*utils.TestIo = undefined;
var pub_clients: [NUM_PUB]?*nats.Client =
[_]?*nats.Client{null} ** NUM_PUB;
var pub_io_count: usize = 0;
defer {
for (0..pub_io_count) |i| {
if (pub_clients[i]) |c| c.deinit();
pub_ios[i].deinit();
}
}
for (0..NUM_PUB) |i| {
pub_ios[i] = utils.newIo(allocator);
pub_clients[i] = nats.Client.connect(
allocator,
pub_ios[i].io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_pub_sub", false, "pub con");
return;
};
pub_io_count += 1;
}
const expected_per_sub =
NUM_PUB * MSGS_PER_SUBJECT;
var total_recv: usize = 0;
for (0..NUM_SUBJECTS) |subj_i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"mp.{d}",
.{subj_i},
) catch continue;
for (0..NUM_PUB) |pi| {
const c = pub_clients[pi] orelse continue;
for (0..MSGS_PER_SUBJECT) |_| {
if (!publishWithBackpressure(
c,
subj,
"mp",
"multi_pub_sub",
)) return;
}
}
for (pub_clients) |maybe_client| {
if (maybe_client) |c| {
c.flush(5 * std.time.ns_per_s) catch {
reportResult(
"multi_pub_sub",
false,
"pub flush",
);
return;
};
}
}
for (sub_clients) |maybe_client| {
if (maybe_client) |c| {
c.flush(5 * std.time.ns_per_s) catch {
reportResult(
"multi_pub_sub",
false,
"sub flush",
);
return;
};
}
}
for (0..NUM_SUB) |si| {
const idx = si * NUM_SUBJECTS + subj_i;
const sub = all_subs[idx] orelse continue;
for (0..expected_per_sub) |_| {
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
total_recv += 1;
} else break;
}
}
}
const total_expected =
NUM_SUB * NUM_SUBJECTS * expected_per_sub;
const threshold = total_expected * 100 / 100;
if (total_recv >= threshold) {
reportResult("multi_pub_sub", true, "");
} else {
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d} (min {d})",
.{ total_recv, total_expected, threshold },
) catch "count";
reportResult("multi_pub_sub", false, msg);
}
}
// --- C. Message Size Edge Cases ---
/// Tests payload sizes at slab tier boundaries.
pub fn testPayloadSizes(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("payload_sizes", false, "connect");
return;
};
defer client.deinit();
const sub = client.subscribeSync("sz.test") catch {
reportResult("payload_sizes", false, "sub");
return;
};
defer sub.deinit();
const sizes = [_]usize{
0, 1, 255, 256, 257,
511, 512, 513, 1023, 1024,
1025, 4095, 4096, 4097, 16383,
16384, 16385,
};
for (sizes) |size| {
const payload = if (size > 0)
allocator.alloc(u8, size) catch {
reportResult(
"payload_sizes",
false,
"alloc",
);
return;
}
else
allocator.alloc(u8, 0) catch {
reportResult(
"payload_sizes",
false,
"alloc0",
);
return;
};
defer allocator.free(payload);
for (payload, 0..) |*b, i| {
b.* = @truncate(i);
}
client.publish("sz.test", payload) catch {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"pub failed sz={d}",
.{size},
) catch "pub";
reportResult("payload_sizes", false, msg);
return;
};
const recv = sub.nextMsgTimeout(2000) catch {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"timeout sz={d}",
.{size},
) catch "timeout";
reportResult("payload_sizes", false, msg);
return;
};
if (recv) |m| {
defer m.deinit();
if (m.data.len != size) {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"sz {d}!={d}",
.{ m.data.len, size },
) catch "mismatch";
reportResult(
"payload_sizes",
false,
msg,
);
return;
}
if (size > 0) {
for (m.data, 0..) |b, i| {
const expected: u8 = @truncate(i);
if (b != expected) {
reportResult(
"payload_sizes",
false,
"corrupt",
);
return;
}
}
}
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"null sz={d}",
.{size},
) catch "null";
reportResult("payload_sizes", false, msg);
return;
}
}
reportResult("payload_sizes", true, "");
}
/// 1MB payload roundtrip with integrity check.
pub fn testMaxPayload1MB(
allocator: std.mem.Allocator,
) void {
const SIZE = 1_048_576;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("max_payload_1mb", false, "connect");
return;
};
defer client.deinit();
const sub = client.subscribeSync("big.1mb") catch {
reportResult("max_payload_1mb", false, "sub");
return;
};
defer sub.deinit();
const payload = allocator.alloc(u8, SIZE) catch {
reportResult("max_payload_1mb", false, "alloc");
return;
};
defer allocator.free(payload);
for (payload, 0..) |*b, i| {
b.* = @truncate(i);
}
client.publish("big.1mb", payload) catch |err| {
var buf: [128]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"publish: {s}",
.{@errorName(err)},
) catch "publish: ?";
reportResult(
"max_payload_1mb",
false,
detail,
);
return;
};
if (sub.nextMsgTimeout(5000) catch null) |m| {
defer m.deinit();
if (m.data.len != SIZE) {
reportResult(
"max_payload_1mb",
false,
"wrong size",
);
return;
}
var ok = true;
const checks = [_]usize{
0, 1, 255, 256, 1023, 1024,
SIZE / 2, SIZE - 1,
};
for (checks) |idx| {
const expected: u8 = @truncate(idx);
if (m.data[idx] != expected) {
ok = false;
break;
}
}
if (ok) {
reportResult("max_payload_1mb", true, "");
} else {
reportResult(
"max_payload_1mb",
false,
"corrupt",
);
}
} else {
reportResult("max_payload_1mb", false, "no msg");
}
}
/// Publish over max_payload, expect error.
pub fn testOverMaxPayload(
allocator: std.mem.Allocator,
) void {
const SIZE = 1_048_576 + 1;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("over_max_payload", false, "connect");
return;
};
defer client.deinit();
const payload = allocator.alloc(u8, SIZE) catch {
reportResult("over_max_payload", false, "alloc");
return;
};
defer allocator.free(payload);
@memset(payload, 'X');
if (client.publish("over.big", payload)) |_| {
reportResult(
"over_max_payload",
false,
"no error",
);
} else |_| {
if (client.isConnected()) {
reportResult("over_max_payload", true, "");
} else {
reportResult(
"over_max_payload",
false,
"disconnected",
);
}
}
}
// --- D. Publishing Stress ---
/// 100K messages burst publish.
pub fn testBurstPublish100K(
allocator: std.mem.Allocator,
) void {
const NUM_MSGS = 100_000;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("burst_100k", false, "pub connect");
return;
};
defer pub_client.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const sub_client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{
.sub_queue_size = 131072,
.reconnect = false,
},
) catch {
reportResult("burst_100k", false, "sub connect");
return;
};
defer sub_client.deinit();
const sub = sub_client.subscribeSync("burst") catch {
reportResult("burst_100k", false, "sub");
return;
};
defer sub.deinit();
sub_io.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
var payload: [128]u8 = undefined;
@memset(&payload, 'B');
for (0..NUM_MSGS) |_| {
pub_client.publish("burst", &payload) catch {
reportResult("burst_100k", false, "publish");
return;
};
}
sub_io.io().sleep(
.fromMilliseconds(3000),
.awake,
) catch {};
var received: usize = 0;
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(10) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
const threshold = NUM_MSGS * 100 / 100;
if (received >= threshold) {
reportResult("burst_100k", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ received, NUM_MSGS },
) catch "count";
reportResult("burst_100k", false, msg);
}
}
/// 1000 x 64KB messages (64 MB total).
pub fn testLargePayloadBurst(
allocator: std.mem.Allocator,
) void {
const NUM_MSGS = 1000;
const SIZE = 64 * 1024;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("large_burst", false, "pub connect");
return;
};
defer pub_client.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const sub_client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{
.sub_queue_size = 2048,
.reconnect = false,
},
) catch {
reportResult("large_burst", false, "sub connect");
return;
};
defer sub_client.deinit();
const sub = sub_client.subscribeSync(
"large.burst",
) catch {
reportResult("large_burst", false, "sub");
return;
};
defer sub.deinit();
const payload = allocator.alloc(u8, SIZE) catch {
reportResult("large_burst", false, "alloc");
return;
};
defer allocator.free(payload);
@memset(payload, 'L');
sub_io.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
for (0..NUM_MSGS) |_| {
pub_client.publish(
"large.burst",
payload,
) catch {
reportResult("large_burst", false, "pub");
return;
};
}
sub_io.io().sleep(
.fromMilliseconds(3000),
.awake,
) catch {};
var received: usize = 0;
for (0..NUM_MSGS) |_| {
if (sub.nextMsgTimeout(50) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
const threshold = NUM_MSGS * 100 / 100;
if (received >= threshold) {
reportResult("large_burst", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ received, NUM_MSGS },
) catch "count";
reportResult("large_burst", false, msg);
}
}
/// 5K different subjects with wildcard subscriber.
pub fn testManySubjectsPublish(
allocator: std.mem.Allocator,
) void {
const NUM = 5_000;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{
.sub_queue_size = 8192,
.reconnect = false,
},
) catch {
reportResult("many_subj_pub", false, "connect");
return;
};
defer client.deinit();
const sub = client.subscribeSync("many.>") catch {
reportResult("many_subj_pub", false, "sub");
return;
};
defer sub.deinit();
sub_io.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("many_subj_pub", false, "pub con");
return;
};
defer pub_client.deinit();
for (0..NUM) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"many.{d}",
.{i},
) catch continue;
pub_client.publish(subj, "m") catch {
reportResult("many_subj_pub", false, "pub");
return;
};
}
sub_io.io().sleep(
.fromMilliseconds(2000),
.awake,
) catch {};
var received: usize = 0;
for (0..NUM) |_| {
if (sub.nextMsgTimeout(10) catch null) |m| {
m.deinit();
received += 1;
} else break;
}
const threshold = NUM * 100 / 100;
if (received >= threshold) {
reportResult("many_subj_pub", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ received, NUM },
) catch "count";
reportResult("many_subj_pub", false, msg);
}
}
// --- E. Subscription Queue Pressure ---
/// Slow consumer: queue overflows, verify drops.
pub fn testSlowConsumer(
allocator: std.mem.Allocator,
) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const sub_client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .sub_queue_size = 64, .reconnect = false },
) catch {
reportResult("slow_consumer", false, "connect");
return;
};
defer sub_client.deinit();
const sub = sub_client.subscribeSync("slow") catch {
reportResult("slow_consumer", false, "sub");
return;
};
defer sub.deinit();
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("slow_consumer", false, "pub con");
return;
};
defer pub_client.deinit();
sub_io.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
for (0..200) |_| {
pub_client.publish("slow", "flood") catch {};
}
sub_io.io().sleep(
.fromMilliseconds(500),
.awake,
) catch {};
const drops = sub.dropped();
if (drops > 0) {
reportResult("slow_consumer", true, "");
} else {
reportResult(
"slow_consumer",
false,
"no drops",
);
}
}
/// Fill queue, drain, refill, verify recovery.
pub fn testQueueFillAndRecover(
allocator: std.mem.Allocator,
) void {
const Q_SIZE: usize = 128;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const sub_client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{
.sub_queue_size = @intCast(Q_SIZE),
.reconnect = false,
},
) catch {
reportResult("queue_recover", false, "connect");
return;
};
defer sub_client.deinit();
const sub = sub_client.subscribeSync("qr") catch {
reportResult("queue_recover", false, "sub");
return;
};
defer sub.deinit();
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const pub_client = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("queue_recover", false, "pub con");
return;
};
defer pub_client.deinit();
sub_io.io().sleep(
.fromMilliseconds(50),
.awake,
) catch {};
for (0..Q_SIZE) |_| {
pub_client.publish("qr", "fill1") catch {};
}
sub_io.io().sleep(
.fromMilliseconds(200),
.awake,
) catch {};
var batch1: usize = 0;
for (0..Q_SIZE) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
batch1 += 1;
} else break;
}
for (0..Q_SIZE) |_| {
pub_client.publish("qr", "fill2") catch {};
}
sub_io.io().sleep(
.fromMilliseconds(200),
.awake,
) catch {};
var batch2: usize = 0;
for (0..Q_SIZE) |_| {
if (sub.nextMsgTimeout(500) catch null) |m| {
m.deinit();
batch2 += 1;
} else break;
}
if (batch1 > 0 and batch2 > 0) {
reportResult("queue_recover", true, "");
} else {
var buf: [48]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"b1={d} b2={d}",
.{ batch1, batch2 },
) catch "count";
reportResult("queue_recover", false, msg);
}
}
// --- F. SidMap Stress ---
/// Tombstone accumulation stress test.
pub fn testSidMapTombstoneStress(
allocator: std.mem.Allocator,
) void {
const ROUNDS = 10;
const PER_ROUND = 200;
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .sub_queue_size = 64, .reconnect = false },
) catch {
reportResult("sidmap_tombstone", false, "connect");
return;
};
defer client.deinit();
for (0..ROUNDS) |round| {
var round_subs: [PER_ROUND]?*nats.Subscription =
[_]?*nats.Subscription{null} ** PER_ROUND;
for (0..PER_ROUND) |i| {
var sbuf: [48]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"tomb.{d}.{d}",
.{ round, i },
) catch continue;
round_subs[i] =
client.subscribeSync(subj) catch |e| {
for (&round_subs) |*s| {
if (s.*) |sub| sub.deinit();
}
var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"r={d} i={d} {s}",
.{ round, i, @errorName(e) },
) catch "sub";
reportResult(
"sidmap_tombstone",
false,
msg,
);
return;
};
}
for (&round_subs) |*s| {
if (s.*) |sub| sub.deinit();
}
// Flush UNSUB commands between rounds
}
// Final: subscribe 100 fresh, verify pub/sub
var final_subs: [100]?*nats.Subscription =
[_]?*nats.Subscription{null} ** 100;
defer for (&final_subs) |*s| {
if (s.*) |sub| sub.deinit();
};
for (0..100) |i| {
var sbuf: [32]u8 = undefined;
const subj = std.fmt.bufPrint(
&sbuf,
"tomb.final.{d}",
.{i},
) catch continue;
final_subs[i] = client.subscribeSync(
subj,
) catch {
reportResult(
"sidmap_tombstone",
false,
"final sub",
);
return;
};
}
client.publish("tomb.final.0", "ok") catch {
reportResult(
"sidmap_tombstone",
false,
"publish",
);
return;
};
if (final_subs[0]) |sub| {
if (sub.nextMsgTimeout(1000) catch null) |m| {
m.deinit();
reportResult("sidmap_tombstone", true, "");
} else {
reportResult(
"sidmap_tombstone",
false,
"no msg",
);
}
} else {
reportResult(
"sidmap_tombstone",
false,
"null sub",
);
}
}
/// Runs all stress subscription tests.
pub fn runAll(allocator: std.mem.Allocator) void {
// A. Massive Subscription Tests
testFiveThousandSubs(allocator);
testSubUnsubChurn(allocator);
testSubsThenResubscribe(allocator);
testWildcardFanOut(allocator);
// B. Multi-Client Tests
testTenClientsManySubs(allocator);
testMultiPubMultiSub(allocator);
// C. Message Size Edge Cases
testPayloadSizes(allocator);
testMaxPayload1MB(allocator);
testOverMaxPayload(allocator);
// D. Publishing Stress
testBurstPublish100K(allocator);
testLargePayloadBurst(allocator);
testManySubjectsPublish(allocator);
// E. Queue Pressure
testSlowConsumer(allocator);
testQueueFillAndRecover(allocator);
// F. SidMap Stress
testSidMapTombstoneStress(allocator);
}
================================================
FILE: src/testing/client/subscribe.zig
================================================
//! Subscribe Tests for NATS Client
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const test_token = utils.test_token;
const ServerManager = utils.ServerManager;
pub fn testClientManySubs(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_many_subs", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .sub_queue_size = 32, .reconnect = false },
) catch {
reportResult("client_many_subs", false, "connect failed");
return;
};
defer client.deinit();
const NUM_SUBS = 5;
var subs: [NUM_SUBS]*nats.Client.Sub = undefined;
var sub_buf: [NUM_SUBS][32]u8 = undefined;
var topics: [NUM_SUBS][]const u8 = undefined;
for (0..NUM_SUBS) |i| {
topics[i] = std.fmt.bufPrint(
&sub_buf[i],
"many.{d}",
.{i},
) catch "err";
subs[i] = client.subscribeSync(topics[i]) catch {
reportResult("client_many_subs", false, "sub failed");
return;
};
}
defer for (subs) |s| s.deinit();
client.flush(500_000_000) catch {};
for (topics) |t| {
publisher.publish(t, "hello") catch {};
}
var received: usize = 0;
for (subs) |s| {
var future = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{s},
);
defer if (future.cancel(sub_io.io())) |m| m.deinit() else |_| {};
if (future.await(sub_io.io())) |_| {
received += 1;
} else |_| {}
}
if (received == NUM_SUBS) {
reportResult("client_many_subs", true, "");
} else {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(
&buf,
"got {d}/{d}",
.{ received, NUM_SUBS },
) catch "e";
reportResult("client_many_subs", false, msg);
}
}
pub fn testClientWildcard(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_wildcard", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_wildcard", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("wild.*") catch {
reportResult("client_wildcard", false, "sub failed");
return;
};
defer sub.deinit();
sub_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
publisher.publish("wild.a", "msg-a") catch {
reportResult("client_wildcard", false, "pub a failed");
return;
};
publisher.publish("wild.b", "msg-b") catch {
reportResult("client_wildcard", false, "pub b failed");
return;
};
publisher.publish("wild.c", "msg-c") catch {
reportResult("client_wildcard", false, "pub c failed");
return;
};
const NUM_MSGS = 3;
var received: usize = 0;
for (0..NUM_MSGS) |_| {
var future = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(sub_io.io())) |m| m.deinit() else |_| {};
if (future.await(sub_io.io())) |_| {
received += 1;
} else |_| {}
}
if (received == NUM_MSGS) {
reportResult("client_wildcard", true, "");
} else {
var buf: [32]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "got {d}/3", .{received}) catch "e";
reportResult("client_wildcard", false, msg);
}
}
pub fn testClientDuplicateSubs(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_dup_subs", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_dup_subs", false, "connect failed");
return;
};
defer client.deinit();
const sub1 = client.subscribeSync("dup") catch {
reportResult("client_dup_subs", false, "sub1 failed");
return;
};
defer sub1.deinit();
const sub2 = client.subscribeSync("dup") catch {
reportResult("client_dup_subs", false, "sub2 failed");
return;
};
defer sub2.deinit();
sub_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
publisher.publish("dup", "hello") catch {};
var future1 = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub1},
);
defer if (future1.cancel(sub_io.io())) |m| m.deinit() else |_| {};
var future2 = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub2},
);
defer if (future2.cancel(sub_io.io())) |m| m.deinit() else |_| {};
const got1 = if (future1.await(sub_io.io())) |_| true else |_| false;
const got2 = if (future2.await(sub_io.io())) |_| true else |_| false;
if (got1 and got2) {
reportResult("client_dup_subs", true, "");
} else {
reportResult("client_dup_subs", false, "not both received");
}
}
pub fn testClientQueueGroup(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const pub_io = utils.newIo(allocator);
defer pub_io.deinit();
const publisher = nats.Client.connect(
allocator,
pub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_queue_group", false, "pub connect failed");
return;
};
defer publisher.deinit();
const sub_io = utils.newIo(allocator);
defer sub_io.deinit();
const client = nats.Client.connect(
allocator,
sub_io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("client_queue_group", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.queueSubscribeSync("qg", "workers") catch {
reportResult("client_queue_group", false, "sub failed");
return;
};
defer sub.deinit();
sub_io.io().sleep(.fromMilliseconds(50), .awake) catch {};
publisher.publish("qg", "task") catch {};
var future = sub_io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(sub_io.io())) |m| m.deinit() else |_| {};
if (future.await(sub_io.io())) |_| {
reportResult("client_queue_group", true, "");
return;
} else |_| {}
reportResult("client_queue_group", false, "no message");
}
pub fn testWildcardMatching(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wildcard_matching", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("wc.*") catch {
reportResult("wildcard_matching", false, "sub failed");
return;
};
defer sub.deinit();
client.publish("wc.test", "msg") catch {};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |_| {
reportResult("wildcard_matching", true, "");
return;
} else |_| {}
reportResult("wildcard_matching", false, "no match");
}
pub fn testWildcardGreater(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wildcard_greater", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("gt.>") catch {
reportResult("wildcard_greater", false, "sub failed");
return;
};
defer sub.deinit();
client.publish("gt.a.b.c", "msg") catch {};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |_| {
reportResult("wildcard_greater", true, "");
return;
} else |_| {}
reportResult("wildcard_greater", false, "no match");
}
pub fn testSubjectCaseSensitivity(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("subject_case", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("case.test") catch {
reportResult("subject_case", false, "sub failed");
return;
};
defer sub.deinit();
client.publish("case.test", "msg") catch {};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |_| {
reportResult("subject_case", true, "");
return;
} else |_| {}
reportResult("subject_case", false, "no match");
}
pub fn testUnsubscribeStopsDelivery(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("unsub_stops", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("unsub.test") catch {
reportResult("unsub_stops", false, "sub failed");
return;
};
sub.unsubscribe() catch {};
sub.deinit();
client.publish("unsub.test", "msg") catch {};
io.io().sleep(.fromMilliseconds(10), .awake) catch {};
if (client.isConnected()) {
reportResult("unsub_stops", true, "");
} else {
reportResult("unsub_stops", false, "disconnected");
}
}
pub fn testHierarchicalSubject(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("hierarchical", false, "connect failed");
return;
};
defer client.deinit();
const subject = "a.b.c.d.e.f.g.h";
const sub = client.subscribeSync(subject) catch {
reportResult("hierarchical", false, "sub failed");
return;
};
defer sub.deinit();
client.publish(subject, "deep") catch {
reportResult("hierarchical", false, "pub failed");
return;
};
var future = io.io().async(
nats.Client.Sub.nextMsg,
.{sub},
);
defer if (future.cancel(io.io())) |m| m.deinit() else |_| {};
if (future.await(io.io())) |_| {
reportResult("hierarchical", true, "");
return;
} else |_| {}
reportResult("hierarchical", false, "no message");
}
pub fn testUnsubscribeWithPending(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("unsub_with_pending", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("pending.test") catch {
reportResult("unsub_with_pending", false, "subscribe failed");
return;
};
defer sub.deinit();
for (0..5) |_| {
client.publish("pending.test", "msg") catch {};
}
io.io().sleep(.fromMilliseconds(50), .awake) catch {};
sub.unsubscribe() catch {
reportResult("unsub_with_pending", false, "unsubscribe failed");
return;
};
reportResult("unsub_with_pending", true, "");
}
pub fn testSubscribeAfterDisconnect(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("sub_after_disconnect", false, "connect failed");
return;
};
defer client.deinit();
_ = client.drain() catch {
reportResult("sub_after_disconnect", false, "drain failed");
return;
};
const result = client.subscribeSync("test.sub");
if (result) |sub| {
sub.deinit();
reportResult("sub_after_disconnect", false, "should have failed");
} else |_| {
reportResult("sub_after_disconnect", true, "");
}
}
pub fn testSubscriptionQueueCapacity(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("sub_queue_cap", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("qcap.test") catch {
reportResult("sub_queue_cap", false, "subscribe failed");
return;
};
defer sub.deinit();
const NUM_MSGS = 100;
for (0..NUM_MSGS) |_| {
client.publish("qcap.test", "qcap") catch {
reportResult("sub_queue_cap", false, "publish failed");
return;
};
}
var received: u32 = 0;
for (0..NUM_MSGS) |_| {
const msg = sub.nextMsgTimeout(200) catch break;
if (msg) |m| {
m.deinit();
received += 1;
} else break;
}
if (received == NUM_MSGS) {
reportResult("sub_queue_cap", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"got {d}/100",
.{received},
) catch "e";
reportResult("sub_queue_cap", false, detail);
}
}
pub fn runAll(allocator: std.mem.Allocator) void {
testClientManySubs(allocator);
testClientWildcard(allocator);
testClientDuplicateSubs(allocator);
testClientQueueGroup(allocator);
testWildcardMatching(allocator);
testWildcardGreater(allocator);
testSubjectCaseSensitivity(allocator);
testUnsubscribeStopsDelivery(allocator);
testHierarchicalSubject(allocator);
testUnsubscribeWithPending(allocator);
testSubscribeAfterDisconnect(allocator);
testSubscriptionQueueCapacity(allocator);
}
================================================
FILE: src/testing/client/tests.zig
================================================
//! Client Test Suite
//!
//! Re-exports all client test modules.
const std = @import("std");
const utils = @import("../test_utils.zig");
const ServerManager = utils.ServerManager;
pub const basic = @import("basic.zig");
pub const publish = @import("publish.zig");
pub const subscribe = @import("subscribe.zig");
pub const multi_client = @import("multi_client.zig");
pub const stats = @import("stats.zig");
pub const getters = @import("getters.zig");
pub const stress = @import("stress.zig");
pub const auth = @import("auth.zig");
pub const connection = @import("connection.zig");
pub const request_reply = @import("request_reply.zig");
pub const drain = @import("drain.zig");
pub const edge_cases = @import("edge_cases.zig");
pub const wildcard = @import("wildcard.zig");
pub const queue = @import("queue.zig");
pub const server = @import("server.zig");
pub const protocol = @import("protocol.zig");
pub const concurrency = @import("concurrency.zig");
pub const reconnect = @import("reconnect.zig");
pub const error_handling = @import("error_handling.zig");
pub const headers = @import("headers.zig");
pub const nkey = @import("nkey.zig");
pub const jwt = @import("jwt.zig");
pub const tls = @import("tls.zig");
pub const state_notifications = @import("state_notifications.zig");
pub const advanced = @import("advanced.zig");
pub const flush_confirmed = @import("flush_confirmed.zig");
pub const autoflush = @import("autoflush.zig");
pub const async_patterns = @import("async_patterns.zig");
pub const dynamic_jwt = @import("dynamic_jwt.zig");
pub const callback = @import("callback.zig");
pub const stress_subs = @import("stress_subs.zig");
pub const jetstream = @import("jetstream.zig");
pub const multithread = @import("multithread.zig");
pub const micro = @import("micro.zig");
/// Runs all client tests.
pub fn runAll(allocator: std.mem.Allocator, manager: *ServerManager) void {
basic.runAll(allocator);
publish.runAll(allocator);
subscribe.runAll(allocator);
multi_client.runAll(allocator);
stats.runAll(allocator);
getters.runAll(allocator);
stress.runAll(allocator);
auth.runAll(allocator);
connection.runAll(allocator, manager);
request_reply.runAll(allocator);
drain.runAll(allocator);
edge_cases.runAll(allocator);
wildcard.runAll(allocator);
queue.runAll(allocator);
server.runAll(allocator);
protocol.runAll(allocator);
concurrency.runAll(allocator);
nkey.runAll(allocator);
jwt.runAll(allocator);
tls.runAll(allocator, manager);
error_handling.runAll(allocator);
headers.runAll(allocator);
state_notifications.runAll(allocator);
advanced.runAll(allocator);
flush_confirmed.runAll(allocator);
autoflush.runAll(allocator, manager);
async_patterns.runAll(allocator, manager);
reconnect.runAll(allocator, manager);
dynamic_jwt.runAll(allocator, manager);
callback.runAll(allocator);
stress_subs.runAll(allocator);
jetstream.runAll(allocator, manager);
jetstream.runReconnectTests(allocator, manager);
multithread.runAll(allocator);
micro.runAll(allocator, manager);
}
================================================
FILE: src/testing/client/tls.zig
================================================
//! TLS Tests for NATS Client
//!
//! Tests TLS connection functionality including:
//! - TLS connection with CA certificate
//! - Insecure skip verify mode
//! - Pub/sub over TLS
//! - TLS reconnection
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatTlsUrl = utils.formatTlsUrl;
const tls_port = utils.tls_port;
const ServerManager = utils.ServerManager;
const TestServer = utils.server_manager.TestServer;
const Dir = std.Io.Dir;
const tls_plain_probe_port: u16 = 14240;
/// Returns absolute path to CA file. Caller owns returned memory.
fn getCaFilePath(allocator: std.mem.Allocator, io: std.Io) ?[:0]const u8 {
return Dir.realPathFileAlloc(.cwd(), io, utils.tls_ca_file, allocator) catch null;
}
pub fn testTlsConnection(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const ca_path = getCaFilePath(allocator, io) orelse {
reportResult("tls_connection", false, "CA file not found");
return;
};
defer allocator.free(ca_path);
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.tls_ca_file = ca_path,
}) catch |err| {
var err_buf: [64]u8 = undefined;
const err_msg = std.fmt.bufPrint(
&err_buf,
"connect failed: {}",
.{err},
) catch "connect failed";
reportResult("tls_connection", false, err_msg);
return;
};
defer client.deinit();
if (client.isConnected()) {
const info = client.serverInfo();
if (info != null and info.?.tls_required) {
reportResult("tls_connection", true, "");
} else {
reportResult("tls_connection", false, "server not TLS required");
}
} else {
reportResult("tls_connection", false, "not connected");
}
}
pub fn testTlsInsecureSkipVerify(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.tls_insecure_skip_verify = true,
}) catch |err| {
var err_buf: [64]u8 = undefined;
const err_msg = std.fmt.bufPrint(
&err_buf,
"connect failed: {}",
.{err},
) catch "connect failed";
reportResult("tls_insecure_skip_verify", false, err_msg);
return;
};
defer client.deinit();
if (client.isConnected()) {
reportResult("tls_insecure_skip_verify", true, "");
} else {
reportResult("tls_insecure_skip_verify", false, "not connected");
}
}
pub fn testTlsPubSub(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const ca_path = getCaFilePath(allocator, io) orelse {
reportResult("tls_pubsub", false, "CA file not found");
return;
};
defer allocator.free(ca_path);
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.tls_ca_file = ca_path,
}) catch {
reportResult("tls_pubsub", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("tls.test.subject") catch {
reportResult("tls_pubsub", false, "subscribe failed");
return;
};
defer sub.deinit();
const test_msg = "encrypted message over TLS";
client.publish("tls.test.subject", test_msg) catch {
reportResult("tls_pubsub", false, "publish failed");
return;
};
client.flush(500_000_000) catch {};
if (sub.nextMsgTimeout(1000) catch null) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.data, test_msg)) {
reportResult("tls_pubsub", true, "");
} else {
reportResult("tls_pubsub", false, "message mismatch");
}
} else {
reportResult("tls_pubsub", false, "no message received");
}
}
pub fn testTlsReconnect(
allocator: std.mem.Allocator,
manager: *ServerManager,
) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const ca_path = getCaFilePath(allocator, io) orelse {
reportResult("tls_reconnect", false, "CA file not found");
return;
};
defer allocator.free(ca_path);
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = true,
.max_reconnect_attempts = 10,
.reconnect_wait_ms = 100,
.reconnect_wait_max_ms = 1000,
.tls_ca_file = ca_path,
}) catch {
reportResult("tls_reconnect", false, "initial connect failed");
return;
};
defer client.deinit();
if (!client.isConnected()) {
reportResult("tls_reconnect", false, "not connected initially");
return;
}
// Find TLS server index (last started server)
const tls_server_idx = manager.count() - 1;
manager.stopServer(tls_server_idx, io);
io.sleep(.fromMilliseconds(200), .awake) catch {};
_ = manager.startServer(allocator, io, .{
.port = tls_port,
.config_file = utils.tls_config_file,
}) catch {
reportResult("tls_reconnect", false, "server restart failed");
return;
};
io.sleep(.fromMilliseconds(500), .awake) catch {};
client.publish("tls.reconnect.test", "ping") catch {
reportResult("tls_reconnect", false, "publish after restart failed");
return;
};
if (client.isConnected()) {
reportResult("tls_reconnect", true, "");
} else {
reportResult("tls_reconnect", false, "not reconnected");
}
}
pub fn testTlsServerInfo(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const ca_path = getCaFilePath(allocator, io) orelse {
reportResult("tls_server_info", false, "CA file not found");
return;
};
defer allocator.free(ca_path);
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.tls_ca_file = ca_path,
}) catch {
reportResult("tls_server_info", false, "connect failed");
return;
};
defer client.deinit();
const info = client.serverInfo();
if (info == null) {
reportResult("tls_server_info", false, "no server info");
return;
}
if (info.?.tls_required) {
reportResult("tls_server_info", true, "");
} else {
reportResult("tls_server_info", false, "tls_required not set");
}
}
pub fn testTlsMultipleMessages(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_port);
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
const ca_path = getCaFilePath(allocator, io) orelse {
reportResult("tls_multiple_msgs", false, "CA file not found");
return;
};
defer allocator.free(ca_path);
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.tls_ca_file = ca_path,
}) catch {
reportResult("tls_multiple_msgs", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("tls.multi.>") catch {
reportResult("tls_multiple_msgs", false, "subscribe failed");
return;
};
defer sub.deinit();
const msg_count: usize = 100;
for (0..msg_count) |i| {
var subject_buf: [32]u8 = undefined;
const subject = std.fmt.bufPrint(
&subject_buf,
"tls.multi.{d}",
.{i},
) catch "tls.multi.x";
client.publish(subject, "data") catch {
reportResult("tls_multiple_msgs", false, "publish failed");
return;
};
}
client.flush(500_000_000) catch {};
var received: usize = 0;
for (0..msg_count) |_| {
if (sub.nextMsgTimeout(100) catch null) |m| {
m.deinit();
received += 1;
} else {
break;
}
}
if (received == msg_count) {
reportResult("tls_multiple_msgs", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"{d}/{d} received",
.{ received, msg_count },
) catch "partial";
reportResult("tls_multiple_msgs", false, detail);
}
}
pub fn testTlsSchemeRejectsPlainServer(allocator: std.mem.Allocator) void {
const threaded = utils.newIo(allocator);
defer threaded.deinit();
const io = threaded.io();
var server = TestServer.start(allocator, io, .{
.port = tls_plain_probe_port,
}) catch {
reportResult(
"tls_scheme_rejects_plain_server",
false,
"plain server start failed",
);
return;
};
defer server.deinit(io);
var url_buf: [64]u8 = undefined;
const url = formatTlsUrl(&url_buf, tls_plain_probe_port);
const client = nats.Client.connect(allocator, io, url, .{
.reconnect = false,
.connect_timeout_ns = 500 * std.time.ns_per_ms,
});
if (client) |c| {
c.deinit();
reportResult(
"tls_scheme_rejects_plain_server",
false,
"tls:// connected without TLS",
);
} else |_| {
reportResult("tls_scheme_rejects_plain_server", true, "");
}
}
pub fn runAll(allocator: std.mem.Allocator, manager: *ServerManager) void {
testTlsConnection(allocator);
testTlsInsecureSkipVerify(allocator);
testTlsPubSub(allocator);
testTlsServerInfo(allocator);
testTlsMultipleMessages(allocator);
testTlsSchemeRejectsPlainServer(allocator);
testTlsReconnect(allocator, manager);
}
================================================
FILE: src/testing/client/wildcard.zig
================================================
//! Wildcard Tests for NATS Client
//!
//! Tests for wildcard subscriptions (* and >) and pattern matching.
const std = @import("std");
const utils = @import("../test_utils.zig");
const nats = utils.nats;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const test_port = utils.test_port;
pub fn testWildcardSubscribe(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wildcard_subscribe", false, "connect failed");
return;
};
defer client.deinit();
// Test * wildcard
const sub1 = client.subscribeSync("wild.*") catch {
reportResult("wildcard_subscribe", false, "* wildcard failed");
return;
};
defer sub1.deinit();
// Test > wildcard
const sub2 = client.subscribeSync("wild.>") catch {
reportResult("wildcard_subscribe", false, "> wildcard failed");
return;
};
defer sub2.deinit();
reportResult("wildcard_subscribe", true, "");
}
pub fn testWildcardMatching(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wildcard_matching", false, "connect failed");
return;
};
defer client.deinit();
// Subscribe to foo.*
const sub_star = client.subscribeSync("wtest.*") catch {
reportResult("wildcard_matching", false, "star sub failed");
return;
};
defer sub_star.deinit();
// Subscribe to foo.>
const sub_gt = client.subscribeSync("wtest.>") catch {
reportResult("wildcard_matching", false, "gt sub failed");
return;
};
defer sub_gt.deinit();
// Publish to wtest.bar (matches both)
client.publish("wtest.bar", "one") catch {
reportResult("wildcard_matching", false, "pub1 failed");
return;
};
// Publish to wtest.bar.baz (matches only >)
client.publish("wtest.bar.baz", "two") catch {
reportResult("wildcard_matching", false, "pub2 failed");
return;
};
// star should get 1 message
var star_count: u32 = 0;
while (true) {
const msg = sub_star.nextMsgTimeout(200) catch {
break;
};
if (msg) |m| {
m.deinit();
star_count += 1;
} else {
break;
}
}
// gt should get 2 messages
var gt_count: u32 = 0;
while (true) {
const msg = sub_gt.nextMsgTimeout(200) catch {
break;
};
if (msg) |m| {
m.deinit();
gt_count += 1;
} else {
break;
}
}
if (star_count == 1 and gt_count == 2) {
reportResult("wildcard_matching", true, "");
} else {
var buf: [64]u8 = undefined;
const detail = std.fmt.bufPrint(
&buf,
"star={d} gt={d}",
.{ star_count, gt_count },
) catch "count error";
reportResult("wildcard_matching", false, detail);
}
}
pub fn testWildcardPositions(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("wildcard_positions", false, "connect failed");
return;
};
defer client.deinit();
// Wildcard at beginning: *.bar
const sub1 = client.subscribeSync("*.middle.end") catch {
reportResult("wildcard_positions", false, "sub1 failed");
return;
};
defer sub1.deinit();
// Wildcard in middle: foo.*.baz
const sub2 = client.subscribeSync("start.*.end") catch {
reportResult("wildcard_positions", false, "sub2 failed");
return;
};
defer sub2.deinit();
// Publish matching messages
client.publish("foo.middle.end", "msg1") catch {};
client.publish("start.bar.end", "msg2") catch {};
var count: u32 = 0;
if (sub1.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (sub2.nextMsgTimeout(500) catch null) |m| {
m.deinit();
count += 1;
}
if (count == 2) {
reportResult("wildcard_positions", true, "");
} else {
var buf: [32]u8 = undefined;
const detail =
std.fmt.bufPrint(&buf, "got {d}/2", .{count}) catch "err";
reportResult("wildcard_positions", false, detail);
}
}
pub fn testMultipleWildcards(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("multi_wildcards", false, "connect failed");
return;
};
defer client.deinit();
// Subscribe with multiple * wildcards
const sub = client.subscribeSync("mw.*.middle.*") catch {
reportResult("multi_wildcards", false, "subscribe failed");
return;
};
defer sub.deinit();
// Publish matching subjects
client.publish("mw.foo.middle.bar", "hit1") catch {};
client.publish("mw.a.middle.b", "hit2") catch {};
client.publish("mw.xyz.other.abc", "miss") catch {}; // should not match
var count: u32 = 0;
for (0..4) |_| {
const msg = sub.nextMsgTimeout(200) catch break;
if (msg) |m| {
m.deinit();
count += 1;
} else break;
}
if (count == 2) {
reportResult("multi_wildcards", true, "");
} else {
var buf: [32]u8 = undefined;
const detail = std.fmt.bufPrint(&buf, "got {d}/2", .{count}) catch "e";
reportResult("multi_wildcards", false, detail);
}
}
pub fn testPublishSubscribe(allocator: std.mem.Allocator) void {
var url_buf: [64]u8 = undefined;
const url = formatUrl(&url_buf, test_port);
const io = utils.newIo(allocator);
defer io.deinit();
const client = nats.Client.connect(
allocator,
io.io(),
url,
.{ .reconnect = false },
) catch {
reportResult("publish_subscribe", false, "connect failed");
return;
};
defer client.deinit();
const sub = client.subscribeSync("roundtrip.test") catch {
reportResult("publish_subscribe", false, "subscribe failed");
return;
};
defer sub.deinit();
client.publish("roundtrip.test", "hello from zig") catch {
reportResult("publish_subscribe", false, "publish failed");
return;
};
// Receive message
const msg = sub.nextMsgTimeout(1000) catch {
reportResult("publish_subscribe", false, "nextWithTimeout failed");
return;
};
if (msg) |m| {
defer m.deinit();
if (std.mem.eql(u8, m.subject, "roundtrip.test") and
std.mem.eql(u8, m.data, "hello from zig"))
{
reportResult("publish_subscribe", true, "");
return;
}
}
reportResult("publish_subscribe", false, "message not received");
}
/// Runs all wildcard tests.
pub fn runAll(allocator: std.mem.Allocator) void {
testWildcardSubscribe(allocator);
testWildcardMatching(allocator);
testWildcardPositions(allocator);
testMultipleWildcards(allocator);
testPublishSubscribe(allocator);
}
================================================
FILE: src/testing/configs/TestUser.creds
================================================
-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJMN1dBT1hJU0tPSUZNM1QyNEhMQ09ENzJRT1czQkNVWEdETjRKVU1SSUtHTlQ3RzdZVFRRIiwiaWF0IjoxNjUxNzkwOTgyLCJpc3MiOiJBRFRRUzdaQ0ZWSk5XNTcyNkdPWVhXNVRTQ1pGTklRU0hLMlpHWVVCQ0Q1RDc3T1ROTE9PS1pPWiIsIm5hbWUiOiJUZXN0VXNlciIsInN1YiI6IlVBRkhHNkZVRDJVVTRTREZWQUZVTDVMREZPMlhNNFdZTTc2VU5YVFBKWUpLN0VFTVlSQkhUMlZFIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.bp2-Jsy33l4ayF7Ku1MNdJby4WiMKUrG-rSVYGBusAtV3xP4EdCa-zhSNUaBVIL3uYPPCQYCEoM1pCUdOnoJBg
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM
------END USER NKEY SEED------
*************************************************************
================================================
FILE: src/testing/configs/jwt.conf
================================================
// Operator "TestOperator"
operator: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJVWlhRWjVXR1VGSUY0S0ZKS1dFRUhSTU5PM0RQVUdZMzUyM1QyNk1IQU1KTENDVzVZVjdRIiwiaWF0IjoxNjUxNzkwOTcwLCJpc3MiOiJPQjVYQ0U0NVdFVkYyRjVWWUZOM1Q3NEpJRVpFT1JaVzYzSEJFSVdVWEpHWU9HS0VBSVVLM0FMTSIsIm5hbWUiOiJUZXN0T3BlcmF0b3IiLCJzdWIiOiJPQjVYQ0U0NVdFVkYyRjVWWUZOM1Q3NEpJRVpFT1JaVzYzSEJFSVdVWEpHWU9HS0VBSVVLM0FMTSIsIm5hdHMiOnsic3lzdGVtX2FjY291bnQiOiJBQlJFSk5ZQVNXR1MyQTVFVlhQVlhHR0NPSzJMSlhUN0taTllCQlpBWFVUVUJJMlZTTUFWN0RITiIsInR5cGUiOiJvcGVyYXRvciIsInZlcnNpb24iOjJ9fQ.RN7AkgTATcx9E_ykTQHI0wM3OE8BwKPb3aPj7ojLGiNpjIRP-ehvSiUUkfWPh6rcO709TKspfQgTxcRxLoq5Bg
// system_account: ADTQS7ZCFVJNW5726GOYXW5TSCZFNIQSHK2ZGYUBCD5D77OTNLOOKZOZ
resolver: MEMORY
resolver_preload: {
// Account "SYS"
ABREJNYASWGS2A5EVXPVXGGCOK2LJXT7KZNYBBZAXUTUBI2VSMAV7DHN: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI1RDMzSVBJRFJCTUg1VTdWTFlOMlk0VEpTV09aUzJaSTNPMlBMR1dORDdWN0MyQjJDNjZRIiwiaWF0IjoxNjUxNzkwOTcwLCJpc3MiOiJPQjVYQ0U0NVdFVkYyRjVWWUZOM1Q3NEpJRVpFT1JaVzYzSEJFSVdVWEpHWU9HS0VBSVVLM0FMTSIsIm5hbWUiOiJTWVMiLCJzdWIiOiJBQlJFSk5ZQVNXR1MyQTVFVlhQVlhHR0NPSzJMSlhUN0taTllCQlpBWFVUVUJJMlZTTUFWN0RITiIsIm5hdHMiOnsiZXhwb3J0cyI6W3sibmFtZSI6ImFjY291bnQtbW9uaXRvcmluZy1zdHJlYW1zIiwic3ViamVjdCI6IiRTWVMuQUNDT1VOVC4qLlx1MDAzZSIsInR5cGUiOiJzdHJlYW0iLCJhY2NvdW50X3Rva2VuX3Bvc2l0aW9uIjozLCJkZXNjcmlwdGlvbiI6IkFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzdHJlYW0iLCJpbmZvX3VybCI6Imh0dHBzOi8vZG9jcy5uYXRzLmlvL25hdHMtc2VydmVyL2NvbmZpZ3VyYXRpb24vc3lzX2FjY291bnRzIn0seyJuYW1lIjoiYWNjb3VudC1tb25pdG9yaW5nLXNlcnZpY2VzIiwic3ViamVjdCI6IiRTWVMuUkVRLkFDQ09VTlQuKi4qIiwidHlwZSI6InNlcnZpY2UiLCJyZXNwb25zZV90eXBlIjoiU3RyZWFtIiwiYWNjb3VudF90b2tlbl9wb3NpdGlvbiI6NCwiZGVzY3JpcHRpb24iOiJSZXF1ZXN0IGFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzZXJ2aWNlcyBmb3I6IFNVQlNaLCBDT05OWiwgTEVBRlosIEpTWiBhbmQgSU5GTyIsImluZm9fdXJsIjoiaHR0cHM6Ly9kb2NzLm5hdHMuaW8vbmF0cy1zZXJ2ZXIvY29uZmlndXJhdGlvbi9zeXNfYWNjb3VudHMifV0sImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFDUEpPQ0xZTTJHQzU0Nk1EMzRGVzVPMkRVQTYzTERIV0ZHU0JJSVpTSUZJQjJUWlVGREQ0TlBVIl0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sInR5cGUiOiJhY2NvdW50IiwidmVyc2lvbiI6Mn19.I_ybyL7Gm9gTH1IVrulNB596y-YmdYQ9QoyGEez3SviPJNFFD1vkmtl2wpzesUB1zaVYVyAhhN_jsEWElmUnBQ
// Account "TestAccount"
ADTQS7ZCFVJNW5726GOYXW5TSCZFNIQSHK2ZGYUBCD5D77OTNLOOKZOZ: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJSWDRVUzZWUURCTFNCSVNaRjRCWFBJREUyMlZRREJWWUsyTkYzUUNBRTdZWkE2NjNOSVBBIiwiaWF0IjoxNjUxNzkwOTc3LCJpc3MiOiJPQjVYQ0U0NVdFVkYyRjVWWUZOM1Q3NEpJRVpFT1JaVzYzSEJFSVdVWEpHWU9HS0VBSVVLM0FMTSIsIm5hbWUiOiJUZXN0QWNjb3VudCIsInN1YiI6IkFEVFFTN1pDRlZKTlc1NzI2R09ZWFc1VFNDWkZOSVFTSEsyWkdZVUJDRDVENzdPVE5MT09LWk9aIiwibmF0cyI6eyJsaW1pdHMiOnsic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwiaW1wb3J0cyI6LTEsImV4cG9ydHMiOi0xLCJ3aWxkY2FyZHMiOnRydWUsImNvbm4iOi0xLCJsZWFmIjotMX0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sInR5cGUiOiJhY2NvdW50IiwidmVyc2lvbiI6Mn19.R_SRlgJhdLFFmG0E_dScqrrKsCmVzTitB8-3HfKbo6gbcqu647O7SPGixH5BXHVZpOaOZJ0gzN36OebU5E5LAw
}
================================================
FILE: src/testing/configs/tls.conf
================================================
port: 14226
tls {
cert_file: "src/testing/certs/server-cert.pem"
key_file: "src/testing/certs/server-key.pem"
ca_file: "src/testing/certs/rootCA.pem"
# Generous handshake timeout so Debug builds (slow std.crypto)
# don't get killed mid-handshake by the server.
timeout: 60
}
================================================
FILE: src/testing/integration_test.zig
================================================
//! NATS Integration Tests
//!
//! Tests against a nats-server instance.
//! Run with: zig build test-integration
const std = @import("std");
const nats = @import("nats");
const utils = @import("test_utils.zig");
const client_tests = @import("client/tests.zig");
const ServerManager = utils.ServerManager;
const test_port = utils.test_port;
const auth_port = utils.auth_port;
const nkey_port = utils.nkey_port;
const jwt_port = utils.jwt_port;
const tls_port = utils.tls_port;
const test_token = utils.test_token;
const test_nkey_seed = utils.test_nkey_seed;
const jwt_config_file = utils.jwt_config_file;
const tls_config_file = utils.tls_config_file;
const reportResult = utils.reportResult;
const formatUrl = utils.formatUrl;
const formatAuthUrl = utils.formatAuthUrl;
const nkey_config_path = "/tmp/nats-nkey-test.conf";
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
utils.setProcessEnviron(init.minimal.environ);
// Use the io_backend selector instead of init.io so
// -Dio_backend=threaded|evented is exercised end-to-end by
// the test runner itself (server startup, file I/O, sleeps).
const test_io = utils.newIo(allocator);
defer test_io.deinit();
const io = test_io.io();
std.debug.print("\n=== NATS Integration Tests ===\n\n", .{});
var manager: ServerManager = .init(allocator);
defer manager.deinit(allocator, io);
std.debug.print("Starting primary server on port {d}...\n", .{test_port});
_ = manager.startServer(allocator, io, .{ .port = test_port }) catch |err| {
std.debug.print("Failed to start primary server: {}\n", .{err});
std.process.exit(1);
};
std.debug.print("Starting auth server on port {d}...\n", .{auth_port});
_ = manager.startServer(allocator, io, .{
.port = auth_port,
.auth_token = test_token,
}) catch |err| {
std.debug.print("Failed to start auth server: {}\n", .{err});
std.process.exit(1);
};
std.debug.print("Starting NKey server on port {d}...\n", .{nkey_port});
writeNKeyConfig(io) catch |err| {
std.debug.print("Failed to write NKey config: {}\n", .{err});
std.process.exit(1);
};
defer deleteNKeyConfig(io);
_ = manager.startServer(allocator, io, .{
.port = nkey_port,
.config_file = nkey_config_path,
}) catch |err| {
std.debug.print("Failed to start NKey server: {}\n", .{err});
std.process.exit(1);
};
std.debug.print("Starting JWT server on port {d}...\n", .{jwt_port});
_ = manager.startServer(allocator, io, .{
.port = jwt_port,
.config_file = jwt_config_file,
}) catch |err| {
std.debug.print("Failed to start JWT server: {}\n", .{err});
std.process.exit(1);
};
std.debug.print("Starting TLS server on port {d}...\n", .{tls_port});
_ = manager.startServer(allocator, io, .{
.port = tls_port,
.config_file = tls_config_file,
}) catch |err| {
std.debug.print("Failed to start TLS server: {}\n", .{err});
std.process.exit(1);
};
io.sleep(.fromMilliseconds(200), .awake) catch {};
std.debug.print("\nRunning tests...\n\n", .{});
client_tests.runAll(allocator, &manager);
const summary = utils.getSummary();
std.debug.print("\n=== Test Summary ===\n", .{});
std.debug.print("Passed: {d}\n", .{summary.passed});
std.debug.print("Failed: {d}\n", .{summary.failed});
std.debug.print("Total: {d}\n\n", .{summary.total});
if (summary.failed > 0) {
std.process.exit(1);
}
}
fn writeNKeyConfig(io: std.Io) !void {
const Dir = std.Io.Dir;
var kp = nats.auth.KeyPair.fromSeed(test_nkey_seed) catch {
return error.InvalidSeed;
};
defer kp.wipe();
var pubkey_buf: [56]u8 = undefined;
const pubkey = kp.publicKey(&pubkey_buf);
const file = try Dir.createFile(Dir.cwd(), io, nkey_config_path, .{});
defer file.close(io);
var buf: [256]u8 = undefined;
var writer = file.writer(io, &buf);
try writer.interface.print(
\\authorization {{
\\ users = [{{ nkey: "{s}" }}]
\\}}
\\
,
.{pubkey},
);
try writer.interface.flush();
}
fn deleteNKeyConfig(io: std.Io) void {
const Dir = std.Io.Dir;
Dir.deleteFile(Dir.cwd(), io, nkey_config_path) catch {};
}
================================================
FILE: src/testing/micro_integration_test.zig
================================================
//! Focused microservices integration tests.
//!
//! Run with: zig build test-integration-micro
const std = @import("std");
const utils = @import("test_utils.zig");
const micro_tests = @import("client/micro.zig");
const ServerManager = utils.ServerManager;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
utils.setProcessEnviron(init.minimal.environ);
const test_io = utils.newIo(allocator);
defer test_io.deinit();
const io = test_io.io();
std.debug.print("\n=== NATS Micro Integration Tests ===\n\n", .{});
var manager: ServerManager = .init(allocator);
defer manager.deinit(allocator, io);
std.debug.print("\nRunning micro tests...\n\n", .{});
micro_tests.runAll(allocator, &manager);
const summary = utils.getSummary();
std.debug.print("\n=== Micro Test Summary ===\n", .{});
std.debug.print("Passed: {d}\n", .{summary.passed});
std.debug.print("Failed: {d}\n", .{summary.failed});
std.debug.print("Total: {d}\n\n", .{summary.total});
if (summary.failed > 0) std.process.exit(1);
}
================================================
FILE: src/testing/server_manager.zig
================================================
//! NATS Test Server
//!
//! Self-contained nats-server for integration testing.
//! Returns by value - each test owns its servers.
//! Use `defer server.deinit(io)` for automatic cleanup.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Io = std.Io;
/// Configuration for a NATS server instance.
pub const ServerConfig = struct {
/// Port to listen on.
port: u16 = 4222,
/// Optional authentication token.
auth_token: ?[]const u8 = null,
/// Optional path to config file (-c option).
config_file: ?[]const u8 = null,
/// Enable debug/verbose output.
debug: bool = false,
/// Enable JetStream.
jetstream: bool = false,
/// Optional JetStream store directory. If omitted, tests use an
/// isolated per-port directory under /tmp and remove it on stop.
store_dir: ?[]const u8 = null,
};
/// A self-contained NATS server for testing.
/// Returns by value - no pointer stability issues.
/// Call deinit() when done (typically via defer).
pub const TestServer = struct {
process: ?std.process.Child = null,
config: ServerConfig,
port_buf: [8]u8 = undefined,
store_dir_buf: [128]u8 = undefined,
store_dir_len: u8 = 0,
/// Starts a test server. Returns owned instance.
/// Usage: `var server = TestServer.start(...) catch return;`
/// `defer server.deinit(io);`
pub fn start(allocator: Allocator, io: Io, config: ServerConfig) !TestServer {
assert(config.port > 0);
var server: TestServer = .{ .config = config };
const port_str = std.fmt.bufPrint(
&server.port_buf,
"{d}",
.{config.port},
) catch unreachable;
var args: std.ArrayList([]const u8) = .empty;
defer args.deinit(allocator);
try args.append(allocator, "nats-server");
try args.append(allocator, "-p");
try args.append(allocator, port_str);
if (config.auth_token) |token| {
try args.append(allocator, "--auth");
try args.append(allocator, token);
}
if (config.config_file) |config_file| {
try args.append(allocator, "-c");
try args.append(allocator, config_file);
}
if (config.jetstream) {
try args.append(allocator, "-js");
const store_dir = try server.prepareJetStreamStore(io);
try args.append(allocator, "-sd");
try args.append(allocator, store_dir);
}
if (config.debug) {
try args.append(allocator, "-DV");
}
server.process = try std.process.spawn(io, .{
.argv = args.items,
.stdout = .ignore,
.stderr = .ignore,
});
assert(server.process != null);
// Wait for server to become ready
try server.waitReady(io, 5000);
// Give server extra time to fully initialize after port is open
io.sleep(.fromMilliseconds(500), .awake) catch {};
return server;
}
/// Stops and cleans up. Safe to call multiple times (idempotent).
/// Typically called via defer: `defer server.deinit(io);`
pub fn deinit(self: *TestServer, io: Io) void {
self.stop(io);
}
/// Stops the server. Idempotent - safe if already stopped or process died.
pub fn stop(self: *TestServer, io: Io) void {
if (self.process) |*proc| {
std.debug.print(
"[SERVER] Killing server on port {d}...\n",
.{self.config.port},
);
if (proc.id != null) {
proc.kill(io);
}
self.process = null;
// Give OS time to fully terminate the process and close sockets
io.sleep(.fromMilliseconds(100), .awake) catch {};
self.cleanJetStreamStore(io);
std.debug.print("[SERVER] Server killed, waited 100ms\n", .{});
}
}
/// Returns true if the server process is running.
pub fn isRunning(self: *const TestServer) bool {
return self.process != null;
}
/// Waits for the server to become ready by probing the TCP port.
fn waitReady(self: *TestServer, io: Io, timeout_ms: u32) !void {
assert(self.process != null);
const max_attempts = timeout_ms / 50;
var attempts: u32 = 0;
while (attempts < max_attempts) : (attempts += 1) {
if (self.probePort(io)) {
return;
}
io.sleep(.fromMilliseconds(50), .awake) catch {};
}
return error.ServerStartTimeout;
}
/// Probes if the server port is accepting connections.
fn probePort(self: *TestServer, io: Io) bool {
const address = Io.net.IpAddress.parse(
"127.0.0.1",
self.config.port,
) catch return false;
const stream = Io.net.IpAddress.connect(
&address,
io,
.{
.mode = .stream,
.protocol = .tcp,
},
) catch return false;
stream.close(io);
return true;
}
fn prepareJetStreamStore(self: *TestServer, io: Io) ![]const u8 {
const dir = if (self.config.store_dir) |configured| blk: {
if (configured.len > self.store_dir_buf.len)
return error.NameTooLong;
@memcpy(self.store_dir_buf[0..configured.len], configured);
self.store_dir_len = @intCast(configured.len);
break :blk self.store_dir_buf[0..configured.len];
} else blk: {
const formatted = std.fmt.bufPrint(
&self.store_dir_buf,
"/tmp/nats-zig-js-{d}",
.{self.config.port},
) catch return error.NameTooLong;
self.store_dir_len = @intCast(formatted.len);
break :blk formatted;
};
// Start from a clean JetStream store so integration tests do not
// inherit state from an interrupted previous run on the same host.
std.Io.Dir.deleteTree(std.Io.Dir.cwd(), io, dir) catch {};
return dir;
}
fn cleanJetStreamStore(self: *TestServer, io: Io) void {
if (self.store_dir_len == 0) return;
const dir = self.store_dir_buf[0..self.store_dir_len];
std.Io.Dir.deleteTree(std.Io.Dir.cwd(), io, dir) catch {};
self.store_dir_len = 0;
}
};
// Legacy aliases for backward compatibility during migration
pub const ServerInstance = TestServer;
pub const ServerManager = struct {
servers: std.ArrayList(TestServer) = .empty,
/// Max servers expected in any test - pre-allocate to avoid reallocation
const MAX_SERVERS: usize = 16;
pub fn init(allocator: Allocator) ServerManager {
var mgr = ServerManager{};
// Pre-allocate to prevent reallocation (which invalidates pointers)
mgr.servers.ensureTotalCapacity(allocator, MAX_SERVERS) catch {};
return mgr;
}
pub fn deinit(self: *ServerManager, allocator: Allocator, io: Io) void {
self.stopAll(io);
self.servers.deinit(allocator);
}
pub fn startServer(
self: *ServerManager,
allocator: Allocator,
io: Io,
config: ServerConfig,
) !*TestServer {
const server = try TestServer.start(allocator, io, config);
try self.servers.ensureTotalCapacity(allocator, self.servers.items.len + 1);
try self.servers.append(allocator, server);
return &self.servers.items[self.servers.items.len - 1];
}
pub fn stopAll(self: *ServerManager, io: Io) void {
for (self.servers.items) |*server| {
server.stop(io);
}
io.sleep(.fromMilliseconds(500), .awake) catch {};
}
pub fn stopServer(self: *ServerManager, index: usize, io: Io) void {
if (index < self.servers.items.len) {
self.servers.items[index].stop(io);
}
io.sleep(.fromMilliseconds(500), .awake) catch {};
}
pub fn count(self: *const ServerManager) usize {
return self.servers.items.len;
}
};
test "server config defaults" {
const config: ServerConfig = .{};
try std.testing.expectEqual(@as(u16, 4222), config.port);
try std.testing.expect(config.auth_token == null);
try std.testing.expect(config.config_file == null);
try std.testing.expect(!config.debug);
}
test "test server init" {
var server: TestServer = .{ .config = .{ .port = 14222 } };
try std.testing.expectEqual(@as(u16, 14222), server.config.port);
try std.testing.expect(!server.isRunning());
}
================================================
FILE: src/testing/test_utils.zig
================================================
//! Shared test utilities for NATS integration tests.
const std = @import("std");
pub const nats = @import("nats");
pub const server_manager = @import("server_manager.zig");
pub const ServerManager = server_manager.ServerManager;
pub const ServerConfig = server_manager.ServerConfig;
pub const test_port: u16 = 14222;
pub const auth_port: u16 = 14223;
pub const nkey_port: u16 = 14224;
pub const jwt_port: u16 = 14225;
pub const test_token = "test-secret-token";
pub const test_nkey_seed =
"SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY";
pub const test_nkey_seed_file = "/tmp/nats-test-nkey.seed";
pub const jwt_config_file = "src/testing/configs/jwt.conf";
pub const test_creds_file = "src/testing/configs/TestUser.creds";
pub const test_jwt_seed =
"SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM";
// Dynamic JWT test constants
pub const dynamic_jwt_port: u16 = 14228;
pub const jetstream_port: u16 = 14229;
pub const micro_port: u16 = 14241;
// TLS test constants
pub const tls_port: u16 = 14226;
pub const tls_config_file = "src/testing/configs/tls.conf";
pub const tls_ca_file = "src/testing/certs/rootCA.pem";
pub const tls_server_cert = "src/testing/certs/server-cert.pem";
pub const tls_server_key = "src/testing/certs/server-key.pem";
pub const tls_client_cert = "src/testing/certs/client-cert.pem";
pub const tls_client_key = "src/testing/certs/client-key.pem";
pub var tests_passed: u32 = 0;
pub var tests_failed: u32 = 0;
/// Reports a test result and updates counters.
pub fn reportResult(name: []const u8, passed: bool, details: []const u8) void {
if (passed) {
tests_passed += 1;
std.debug.print("[PASS] {s}\n", .{name});
} else {
tests_failed += 1;
std.debug.print("[FAIL] {s}: {s}\n", .{ name, details });
}
}
/// Reports a failed test step with the Zig error name included.
pub fn reportError(name: []const u8, step: []const u8, err: anyerror) void {
var buf: [128]u8 = undefined;
const details = std.fmt.bufPrint(
&buf,
"{s}: {s}",
.{ step, @errorName(err) },
) catch step;
reportResult(name, false, details);
}
/// Formats a NATS URL for the given port.
pub fn formatUrl(buf: []u8, port: u16) []const u8 {
const fmt = "nats://127.0.0.1:{d}";
return std.fmt.bufPrint(buf, fmt, .{port}) catch "invalid";
}
/// Formats a NATS URL with auth token.
pub fn formatAuthUrl(buf: []u8, port: u16, token: []const u8) []const u8 {
return std.fmt.bufPrint(
buf,
"nats://{s}@127.0.0.1:{d}",
.{ token, port },
) catch "invalid";
}
/// Formats a TLS NATS URL for the given port.
/// Uses localhost since test certificates are issued for localhost.
pub fn formatTlsUrl(buf: []u8, port: u16) []const u8 {
const fmt = "tls://localhost:{d}";
return std.fmt.bufPrint(buf, fmt, .{port}) catch "invalid";
}
pub fn resetCounters() void {
tests_passed = 0;
tests_failed = 0;
}
pub fn getSummary() struct { passed: u32, failed: u32, total: u32 } {
return .{
.passed = tests_passed,
.failed = tests_failed,
.total = tests_passed + tests_failed,
};
}
const io_backend = @import("io_backend");
var process_environ: std.process.Environ = .empty;
/// Sets the environment used by test Io backends. Integration test entry
/// points call this once from their `std.process.Init` so child-process
/// lookups, such as `nats-server`, see the same PATH as the test runner.
pub fn setProcessEnviron(environ: std.process.Environ) void {
process_environ = environ;
}
/// Heap-allocated wrapper around `io_backend.Backend` for use by
/// integration tests. Each `newIo()` call returns a fresh
/// `*TestIo` that owns its backend; calling `deinit()` releases
/// both the backend and the wrapper itself.
///
/// Existing test code that does:
///
/// var io: std.Io.Threaded = .init(allocator, .{ .environ = .empty });
/// defer io.deinit();
///
/// becomes:
///
/// const io = utils.newIo(allocator);
/// defer io.deinit();
///
/// All `io.io()` and `io.deinit()` calls work unchanged through
/// the pointer because Zig auto-dereferences method calls when
/// the receiver matches.
pub const TestIo = struct {
backend: io_backend.Backend,
allocator: std.mem.Allocator,
/// Tears down the backend and frees the wrapper. Must be
/// called once per `newIo()` call (typically via `defer`).
pub fn deinit(self: *TestIo) void {
self.backend.deinit();
self.allocator.destroy(self);
}
/// Returns the abstract `std.Io` for passing to client APIs.
pub fn io(self: *TestIo) std.Io {
return self.backend.io();
}
};
/// Allocates and initializes a `TestIo` wrapper. Panics on
/// allocation or backend init failure — acceptable for tests
/// because every existing test function returns `void`, not
/// `!void`, and propagating an errorable here would force a
/// viral signature change across the entire suite.
pub fn newIo(allocator: std.mem.Allocator) *TestIo {
const t = allocator.create(TestIo) catch
@panic("OOM in test newIo");
t.allocator = allocator;
io_backend.initWithEnviron(&t.backend, allocator, process_environ) catch
@panic("io_backend init failed in test newIo");
return t;
}
================================================
FILE: src/testing/tls_integration_test.zig
================================================
//! Focused JWT + TLS integration tests to debug hangs around the TLS block.
//!
//! Run with: zig build test-integration-tls
const std = @import("std");
const utils = @import("test_utils.zig");
const jwt_tests = @import("client/jwt.zig");
const tls_tests = @import("client/tls.zig");
const ServerManager = utils.ServerManager;
const Dir = std.Io.Dir;
const jwt_port = utils.jwt_port;
const tls_port = utils.tls_port;
const jwt_config_file = utils.jwt_config_file;
const tls_config_file = utils.tls_config_file;
fn probeTlsPort(io: std.Io) bool {
const address = std.Io.net.IpAddress.parse("127.0.0.1", tls_port) catch return false;
const stream = std.Io.net.IpAddress.connect(&address, io, .{
.mode = .stream,
.protocol = .tcp,
}) catch return false;
stream.close(io);
return true;
}
fn probeCaLoad(allocator: std.mem.Allocator, io: std.Io) !void {
const ca_path = try Dir.realPathFileAlloc(.cwd(), io, utils.tls_ca_file, allocator);
defer allocator.free(ca_path);
var bundle: std.crypto.Certificate.Bundle = .empty;
defer bundle.deinit(allocator);
const now = std.Io.Clock.real.now(io);
try bundle.addCertsFromFilePathAbsolute(allocator, io, now, ca_path);
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
utils.setProcessEnviron(init.minimal.environ);
const test_io = utils.newIo(allocator);
defer test_io.deinit();
const io = test_io.io();
std.debug.print("\n=== NATS JWT/TLS Integration Tests ===\n\n", .{});
var manager: ServerManager = .init(allocator);
defer manager.deinit(allocator, io);
std.debug.print("Starting JWT server on port {d}...\n", .{jwt_port});
_ = manager.startServer(allocator, io, .{
.port = jwt_port,
.config_file = jwt_config_file,
}) catch |err| {
std.debug.print("Failed to start JWT server: {}\n", .{err});
std.process.exit(1);
};
std.debug.print("Starting TLS server on port {d}...\n", .{tls_port});
_ = manager.startServer(allocator, io, .{
.port = tls_port,
.config_file = tls_config_file,
}) catch |err| {
std.debug.print("Failed to start TLS server: {}\n", .{err});
std.process.exit(1);
};
io.sleep(.fromMilliseconds(200), .awake) catch {};
std.debug.print("TLS port probe before tests: {s}\n", .{
if (probeTlsPort(io)) "ok" else "failed",
});
std.debug.print("\nRunning JWT tests...\n\n", .{});
jwt_tests.runAll(allocator);
std.debug.print("\nRunning TLS tests...\n\n", .{});
std.debug.print("[RUN ] tls_insecure_skip_verify\n", .{});
tls_tests.testTlsInsecureSkipVerify(allocator);
std.debug.print("[RUN ] ca_bundle_load\n", .{});
probeCaLoad(allocator, io) catch |err| {
std.debug.print("[FAIL] ca_bundle_load: {}\n", .{err});
std.process.exit(1);
};
std.debug.print("[PASS] ca_bundle_load\n", .{});
std.debug.print("[RUN ] tls_connection\n", .{});
tls_tests.testTlsConnection(allocator);
std.debug.print("[RUN ] tls_pubsub\n", .{});
tls_tests.testTlsPubSub(allocator);
std.debug.print("[RUN ] tls_server_info\n", .{});
tls_tests.testTlsServerInfo(allocator);
std.debug.print("[RUN ] tls_multiple_msgs\n", .{});
tls_tests.testTlsMultipleMessages(allocator);
std.debug.print("[RUN ] tls_scheme_rejects_plain_server\n", .{});
tls_tests.testTlsSchemeRejectsPlainServer(allocator);
std.debug.print("[RUN ] tls_reconnect\n", .{});
tls_tests.testTlsReconnect(allocator, &manager);
const summary = utils.getSummary();
std.debug.print("\n=== JWT/TLS Test Summary ===\n", .{});
std.debug.print("Passed: {d}\n", .{summary.passed});
std.debug.print("Failed: {d}\n", .{summary.failed});
std.debug.print("Total: {d}\n\n", .{summary.total});
if (summary.failed > 0) std.process.exit(1);
}