init
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.build
|
||||
.git*
|
||||
.swiftpm
|
||||
Dockerfile
|
||||
LICENSE
|
||||
47
.github/ISSUE_TEMPLATE/defect.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/defect.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: Defect
|
||||
description: Report a defect, such as a bug or regression.
|
||||
labels:
|
||||
- defect
|
||||
body:
|
||||
- type: textarea
|
||||
id: observed
|
||||
attributes:
|
||||
label: Observed behavior
|
||||
description: Describe the unexpected behavior or performance regression you are observing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe the expected behavior or performance characteristics.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: versions
|
||||
attributes:
|
||||
label: Server and client version
|
||||
description: |-
|
||||
Provide the versions you were using when the detect was observed.
|
||||
For the server, use `nats-server --version`, check the startup log output, or the image tag pulled from Docker.
|
||||
For the CLI client, use `nats --version`.
|
||||
For language-specific clients, check the version downloaded by the language dependency manager.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Host environment
|
||||
description: |-
|
||||
Specify any relevant details about the host environment the server and/or client was running in,
|
||||
such as operating system, CPU architecture, container runtime, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide as many concrete steps to reproduce the defect.
|
||||
validations:
|
||||
required: false
|
||||
28
.github/ISSUE_TEMPLATE/proposal.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/proposal.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Proposal
|
||||
description: Propose an enhancement or new feature.
|
||||
labels:
|
||||
- proposal
|
||||
body:
|
||||
- type: textarea
|
||||
id: change
|
||||
attributes:
|
||||
label: Proposed change
|
||||
description: This could be a behavior change, enhanced API, or a new feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: usecase
|
||||
attributes:
|
||||
label: Use case
|
||||
description: What is the use case or general motivation for this proposal?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribution
|
||||
description: |-
|
||||
Are you intending or interested in contributing code for this proposal if accepted?
|
||||
validations:
|
||||
required: false
|
||||
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, release/*, feature/* ]
|
||||
pull_request:
|
||||
branches: [ main, release/* ]
|
||||
|
||||
jobs:
|
||||
macos:
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install nats-server
|
||||
run: curl --fail https://binaries.nats.dev/nats-io/nats-server/v2@latest | PREFIX='/usr/local/bin' sh
|
||||
- name: Check nats-server version
|
||||
run: nats-server -v
|
||||
- name: List schemes
|
||||
run: xcodebuild -list
|
||||
- name: Build
|
||||
run: xcodebuild build -scheme Nats -destination 'platform=macOS,arch=x86_64'
|
||||
- name: Test
|
||||
run: swift test
|
||||
ios:
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: xcodebuild build -scheme Nats -destination generic/platform=ios
|
||||
check-linter:
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install swift-format
|
||||
run: brew install swift-format
|
||||
- name: Run check
|
||||
run: swift-format lint --configuration .swift-format -r --strict Sources Tests
|
||||
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
Nats.xcodeproj
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
Package.resolved
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
/.swiftpm
|
||||
.DS_Store
|
||||
.vscode
|
||||
61
.swift-format
Normal file
61
.swift-format
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"version": 1,
|
||||
"fileScopedDeclarationPrivacy": {
|
||||
"accessLevel": "private"
|
||||
},
|
||||
"indentation": {
|
||||
"spaces": 4
|
||||
},
|
||||
"lineLength": 100,
|
||||
"maximumBlankLines": 1,
|
||||
"noAssignmentInExpressions": {
|
||||
"allowedFunctions": [
|
||||
"XCTAssertNoThrow"
|
||||
]
|
||||
},
|
||||
"prioritizeKeepingFunctionOutputTogether": true,
|
||||
"respectsExistingLineBreaks": true,
|
||||
"rules": {
|
||||
"AllPublicDeclarationsHaveDocumentation": false,
|
||||
"AlwaysUseLiteralForEmptyCollectionInit": false,
|
||||
"AlwaysUseLowerCamelCase": true,
|
||||
"AmbiguousTrailingClosureOverload": true,
|
||||
"BeginDocumentationCommentWithOneLineSummary": false,
|
||||
"DoNotUseSemicolons": true,
|
||||
"DontRepeatTypeInStaticProperties": true,
|
||||
"FileScopedDeclarationPrivacy": true,
|
||||
"FullyIndirectEnum": true,
|
||||
"GroupNumericLiterals": true,
|
||||
"IdentifiersMustBeASCII": true,
|
||||
"NeverForceUnwrap": false,
|
||||
"NeverUseForceTry": false,
|
||||
"NeverUseImplicitlyUnwrappedOptionals": false,
|
||||
"NoAccessLevelOnExtensionDeclaration": true,
|
||||
"NoAssignmentInExpressions": true,
|
||||
"NoBlockComments": true,
|
||||
"NoCasesWithOnlyFallthrough": true,
|
||||
"NoEmptyTrailingClosureParentheses": true,
|
||||
"NoLabelsInCasePatterns": true,
|
||||
"NoLeadingUnderscores": false,
|
||||
"NoParensAroundConditions": true,
|
||||
"NoPlaygroundLiterals": true,
|
||||
"NoVoidReturnOnFunctionSignature": true,
|
||||
"OmitExplicitReturns": false,
|
||||
"OneCasePerLine": true,
|
||||
"OneVariableDeclarationPerLine": true,
|
||||
"OnlyOneTrailingClosureArgument": true,
|
||||
"OrderedImports": true,
|
||||
"ReplaceForEachWithForLoop": true,
|
||||
"ReturnVoidInsteadOfEmptyTuple": true,
|
||||
"TypeNamesShouldBeCapitalized": true,
|
||||
"UseEarlyExits": false,
|
||||
"UseLetInEveryBoundCaseVariable": true,
|
||||
"UseShorthandTypeNames": true,
|
||||
"UseSingleLinePropertyGetter": true,
|
||||
"UseSynthesizedInitializer": true,
|
||||
"UseTripleSlashForDocumentationComments": true,
|
||||
"UseWhereClausesInForLoops": false,
|
||||
"ValidateDocumentationComments": false
|
||||
},
|
||||
"spacesAroundRangeFormationOperators": false
|
||||
}
|
||||
3
CODE-OF-CONDUCT.md
Normal file
3
CODE-OF-CONDUCT.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Community Code of Conduct
|
||||
|
||||
The NATS Swift follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
38
CONTRIBUTING.md
Normal file
38
CONTRIBUTING.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing! This document contains `nats-io/nats.swift` specific contributing details. If you are a first-time contributor, please refer to the general [NATS Contributor Guide](https://nats.io/contributing/) to get a comprehensive overview of contributing to the NATS project.
|
||||
|
||||
## Getting started
|
||||
|
||||
There are three general ways you can contribute to this repo:
|
||||
|
||||
- Proposing an enhancement or new feature
|
||||
- Reporting a bug or regression
|
||||
- Contributing changes to the source code
|
||||
|
||||
For the first two, refer to the [GitHub Issues](https://github.com/nats-io/nats.swift/issues/new/choose) which guides you through the available options along with the needed information to collect.
|
||||
|
||||
## Contributing Changes
|
||||
|
||||
_Prior to opening a pull request, it is recommended to open an issue first to ensure the maintainers can review intended changes. Exceptions to this rule include fixing non-functional source such as code comments, documentation or other supporting files._
|
||||
|
||||
Proposing source code changes is done through GitHub's standard pull request workflow.
|
||||
|
||||
If your branch is a work-in-progress then please start by creating your pull requests as draft, by clicking the down-arrow next to the `Create pull request` button and instead selecting `Create draft pull request`.
|
||||
|
||||
This will defer the automatic process of requesting a review from the NATS Swift team and significantly reduces noise until you are ready. Once you are happy with your PR, you can click the `Ready for review` button.
|
||||
|
||||
### Guidelines
|
||||
|
||||
A good pull request includes:
|
||||
|
||||
- A high-level description of the changes, including links to any issues that are related by adding comments like `Resolves #NNN` to your description. See [Linking a Pull Request to an Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) for more information.
|
||||
- An up-to-date parent commit. Please make sure you are pulling in the latest `main` branch and rebasing your work on top of it, i.e. `git rebase main`.
|
||||
- Unit tests where appropriate. Bug fixes will benefit from the addition of regression tests. New features will not be accepted without suitable test coverage!
|
||||
- No more commits than necessary. Sometimes having multiple commits is useful for telling a story or isolating changes from one another, but please squash down any unnecessary commits that may just be for clean-up, comments or small changes.
|
||||
- No additional external dependencies that aren't absolutely essential. Please do everything you can to avoid pulling in additional libraries/dependencies as we will be very critical of these.
|
||||
|
||||
## Get Help
|
||||
|
||||
If you have questions about the contribution process, please start a [GitHub discussion](https://github.com/nats-io/nats.swift/discussions), join the [NATS Slack](https://slack.nats.io/), or send your question to the [NATS Google Group](https://groups.google.com/forum/#!forum/natsio).
|
||||
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
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 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 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 those 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
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2019 The NATS 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.
|
||||
68
Package.swift
Normal file
68
Package.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
// swift-tools-version:5.7
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "nats-swift",
|
||||
platforms: [
|
||||
.macOS(.v13),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
.library(name: "Nats", targets: ["Nats"]),
|
||||
.library(name: "JetStream", targets: ["JetStream"]),
|
||||
.library(name: "NatsServer", targets: ["NatsServer"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.2"),
|
||||
.package(url: "https://github.com/nats-io/nkeys.swift.git", from: "0.1.2"),
|
||||
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/Jarema/swift-nuid.git", from: "0.2.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Nats",
|
||||
dependencies: [
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "NIOSSL", package: "swift-nio-ssl"),
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "NIOFoundationCompat", package: "swift-nio"),
|
||||
.product(name: "NIOHTTP1", package: "swift-nio"),
|
||||
.product(name: "NIOWebSocket", package: "swift-nio"),
|
||||
.product(name: "NKeys", package: "nkeys.swift"),
|
||||
.product(name: "Nuid", package: "swift-nuid"),
|
||||
]),
|
||||
.target(
|
||||
name: "JetStream",
|
||||
dependencies: [
|
||||
"Nats",
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
]),
|
||||
.target(
|
||||
name: "NatsServer",
|
||||
dependencies: [
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
]),
|
||||
|
||||
.testTarget(
|
||||
name: "NatsTests",
|
||||
dependencies: ["Nats", "NatsServer"],
|
||||
resources: [
|
||||
.process("Integration/Resources")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "JetStreamTests",
|
||||
dependencies: ["Nats", "JetStream", "NatsServer"],
|
||||
resources: [
|
||||
.process("Integration/Resources")
|
||||
]
|
||||
),
|
||||
.executableTarget(name: "bench", dependencies: ["Nats"]),
|
||||
.executableTarget(name: "Benchmark", dependencies: ["Nats"]),
|
||||
.executableTarget(name: "BenchmarkPubSub", dependencies: ["Nats"]),
|
||||
.executableTarget(name: "BenchmarkSub", dependencies: ["Nats"]),
|
||||
.executableTarget(name: "Example", dependencies: ["Nats"]),
|
||||
]
|
||||
)
|
||||
198
README.md
Normal file
198
README.md
Normal file
@@ -0,0 +1,198 @@
|
||||

|
||||
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
[](https://swiftpackageindex.com/nats-io/nats.swift)
|
||||
[](https://swiftpackageindex.com/nats-io/nats.swift)
|
||||
|
||||
|
||||
|
||||
|
||||
# NATS Swift Client
|
||||
|
||||
Welcome to the [Swift](https://www.swift.org) Client for [NATS](https://nats.io),
|
||||
your gateway to asynchronous messaging in Swift applications. This client library
|
||||
is designed to provide Swift developers with a seamless interface to NATS
|
||||
messaging, enabling swift and efficient communication across distributed systems.
|
||||
|
||||
## Features
|
||||
|
||||
Currently, the client supports **Core NATS** with auth, TLS, lame duck mode and more.
|
||||
|
||||
JetStream, KV, Object Store, Service API are on the roadmap.
|
||||
|
||||
## Support
|
||||
|
||||
Join the [#swift](https://natsio.slack.com/channels/swift) channel on nats.io Slack.
|
||||
We'll do our best to help quickly. You can also just drop by and say hello. We're looking forward to developing the community.
|
||||
|
||||
## Installation via Swift Package Manager
|
||||
|
||||
Include this package as a dependency in your project's `Package.swift` file and add the package name to your target as shown in the following example:
|
||||
|
||||
```swift
|
||||
// swift-tools-version:5.7
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "YourApp",
|
||||
products: [
|
||||
.executable(name: "YourApp", targets: ["YourApp"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Nats", url: "https://github.com/nats-io/nats.swift.git", from: "0.1")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "YourApp", dependencies: ["Nats"]),
|
||||
]
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### Xcode Package Dependencies
|
||||
|
||||
Open the project inspector in Xcode and select your project. It is important to select the **project** and not a target!
|
||||
Click on the third tab `Package Dependencies` and add the git url `https://github.com/nats-io/nats.swift.git` by selecting the little `+`-sign at the end of the package list.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Here is a quick start example to see everything at a glance:
|
||||
|
||||
```swift
|
||||
import Nats
|
||||
|
||||
// create the client
|
||||
let nats = NatsClientOptions().url(URL(string: "nats://localhost:4222")!).build()
|
||||
|
||||
// connect to the server
|
||||
try await nats.connect()
|
||||
|
||||
// subscribe to a subject
|
||||
let subscription = try await nats.subscribe(subject: "events.>")
|
||||
|
||||
// publish a message
|
||||
try await nats.publish("my event".data(using: .utf8)!, subject: "events.example")
|
||||
|
||||
// receive published messages
|
||||
for await msg in subscriptions {
|
||||
print( "Received: \(String(data:msg.payload!, encoding: .utf8)!)")
|
||||
}
|
||||
```
|
||||
|
||||
### Connecting to a NATS Server
|
||||
|
||||
The first step is establishing a connection to a NATS server.
|
||||
This example demonstrates how to connect to a NATS server using the default settings, which assume the server is
|
||||
running locally on the default port (4222). You can also customize your connection by specifying additional options:
|
||||
|
||||
```swift
|
||||
let nats = NatsClientOptions()
|
||||
.url(URL(string: "nats://localhost:4222")!)
|
||||
.build()
|
||||
|
||||
try await nats.connect()
|
||||
```
|
||||
|
||||
### Publishing Messages
|
||||
|
||||
Once you've established a connection to a NATS server, the next step is to publish messages.
|
||||
Publishing messages to a subject allows any subscribed clients to receive these messages
|
||||
asynchronously. This example shows how to publish a simple text message to a specific subject.
|
||||
|
||||
```swift
|
||||
let data = "message text".data(using: .utf8)!
|
||||
try await nats.publish(data, subject: "foo.msg")
|
||||
```
|
||||
|
||||
In more complex scenarios, you might want to include additional metadata with your messages in
|
||||
the form of headers. Headers allow you to pass key-value pairs along with your message, providing
|
||||
extra context or instructions for the subscriber. This example shows how to publish a
|
||||
message with headers:
|
||||
|
||||
```swift
|
||||
let data = "message text".data(using: .utf8)!
|
||||
|
||||
var headers = NatsHeaderMap()
|
||||
headers.append(try! NatsHeaderName("X-Example"), NatsHeaderValue("example value"))
|
||||
|
||||
try await nats.publish(data, subject: "foo.msg.1", headers: headers)
|
||||
```
|
||||
|
||||
### Subscribing to Subjects
|
||||
|
||||
After establishing a connection and publishing messages to a NATS server, the next crucial step is
|
||||
subscribing to subjects. Subscriptions enable your client to listen for messages published to
|
||||
specific subjects, facilitating asynchronous communication patterns. This example
|
||||
will guide you through creating a subscription to a subject, allowing your application to process
|
||||
incoming messages as they are received.
|
||||
|
||||
```swift
|
||||
let subscription = try await nats.subscribe(subject: "foo.>")
|
||||
|
||||
for try await msg in subscription {
|
||||
|
||||
if msg.subject == "foo.done" {
|
||||
break
|
||||
}
|
||||
|
||||
if let payload = msg.payload {
|
||||
print("received \(msg.subject): \(String(data: payload, encoding: .utf8) ?? "")")
|
||||
}
|
||||
|
||||
if let headers = msg.headers {
|
||||
if let headerValue = headers.get(try! NatsHeaderName("X-Example")) {
|
||||
print(" header: X-Example: \(headerValue.description)")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the subject `foo.>` uses a special wildcard syntax, allowing for subscription
|
||||
to a hierarchy of subjects. For more detailed information, please refer to the [NATS documentation
|
||||
on _Subject-Based Messaging_](https://docs.nats.io/nats-concepts/subjects).
|
||||
|
||||
### Setting Log Levels
|
||||
|
||||
The default log level is `.info`. You can set it to see more or less verbose messages. Possible values are `.debug`, `.info`, `.error` or `.critical`.
|
||||
|
||||
```swift
|
||||
// TODO
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
You can also monitor when your app connects, disconnects, or encounters an error using events:
|
||||
|
||||
```swift
|
||||
let nats = NatsClientOptions()
|
||||
.url(URL(string: "nats://localhost:4222")!)
|
||||
.build()
|
||||
|
||||
nats.on(.connected) { event in
|
||||
print("event: connected")
|
||||
}
|
||||
```
|
||||
|
||||
### AppDelegate or SceneDelegate Integration
|
||||
|
||||
In order to make sure the connection is managed properly in your
|
||||
AppDelegate.swift or SceneDelegate.swift, integrate the NatsClient connection
|
||||
management as follows:
|
||||
|
||||
```swift
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
Task {
|
||||
try await self.natsClient.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
Task {
|
||||
try await self.natsClient.suspend()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Attribution
|
||||
|
||||
This library is based on excellent work in https://github.com/aus-der-Technik/SwiftyNats
|
||||
BIN
Resources/Logo.afdesign
Normal file
BIN
Resources/Logo.afdesign
Normal file
Binary file not shown.
BIN
Resources/Logo@256.png
Normal file
BIN
Resources/Logo@256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
40
Sources/Benchmark/main.swift
Normal file
40
Sources/Benchmark/main.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
let nats = NatsClientOptions()
|
||||
.url(URL(string: "nats://localhost:4222")!)
|
||||
.build()
|
||||
print("Connecting...")
|
||||
try! await nats.connect()
|
||||
print("Connected!")
|
||||
|
||||
let data = "foo".data(using: .utf8)!
|
||||
// Warmup
|
||||
print("Warming up...")
|
||||
for _ in 0..<10_000 {
|
||||
try! await nats.publish(data, subject: "foo")
|
||||
}
|
||||
print("Starting benchmark...")
|
||||
let now = DispatchTime.now()
|
||||
let numMsgs = 1_000_000
|
||||
for _ in 0..<numMsgs {
|
||||
try! await nats.publish(data, subject: "foo")
|
||||
}
|
||||
try! await nats.flush()
|
||||
let elapsed = DispatchTime.now().uptimeNanoseconds - now.uptimeNanoseconds
|
||||
let msgsPerSec: Double = Double(numMsgs) / (Double(elapsed) / 1_000_000_000)
|
||||
print("Elapsed: \(elapsed / 1_000_000)ms")
|
||||
print("\(msgsPerSec) msgs/s")
|
||||
82
Sources/BenchmarkPubSub/main.swift
Normal file
82
Sources/BenchmarkPubSub/main.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
let nats = NatsClientOptions().urls([URL(string: "nats://localhost:4222")!]).build()
|
||||
print("Connecting...")
|
||||
try! await nats.connect()
|
||||
print("Connected!")
|
||||
|
||||
let data = "foo".data(using: .utf8)!
|
||||
// Warmup
|
||||
print("Warming up...")
|
||||
for _ in 0..<10_000 {
|
||||
try! await nats.publish(data, subject: "foo")
|
||||
}
|
||||
print("Starting benchmark...")
|
||||
let now = DispatchTime.now()
|
||||
let numMsgs = 1_000_000
|
||||
let sub = try await nats.subscribe(subject: "foo")
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
|
||||
hm.insert(try! NatsHeaderName("another"), NatsHeaderValue("one"))
|
||||
var i = 0
|
||||
for try await msg in sub {
|
||||
let payload = msg.payload!
|
||||
if String(data: payload, encoding: .utf8) != "\(i)" {
|
||||
let emptyString = ""
|
||||
print(
|
||||
"invalid payload; expected: \(i); got: \(String(data: payload, encoding: .utf8) ?? emptyString)"
|
||||
)
|
||||
}
|
||||
guard let headers = msg.headers else {
|
||||
print("empty headers!")
|
||||
continue
|
||||
}
|
||||
if headers != hm {
|
||||
print("invalid headers; expected: \(hm); got: \(headers)")
|
||||
}
|
||||
if i % 1000 == 0 {
|
||||
print("received \(i) msgs")
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
|
||||
hm.insert(try! NatsHeaderName("another"), NatsHeaderValue("one"))
|
||||
for i in 0..<numMsgs {
|
||||
try await nats.publish("\(i)".data(using: .utf8)!, subject: "foo", headers: hm)
|
||||
if i % 1000 == 0 {
|
||||
print("published \(i) msgs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all tasks in the group to complete
|
||||
try await group.waitForAll()
|
||||
}
|
||||
|
||||
try! await nats.flush()
|
||||
let elapsed = DispatchTime.now().uptimeNanoseconds - now.uptimeNanoseconds
|
||||
let msgsPerSec: Double = Double(numMsgs) / (Double(elapsed) / 1_000_000_000)
|
||||
print("Elapsed: \(elapsed / 1_000_000)ms")
|
||||
print("\(msgsPerSec) msgs/s")
|
||||
60
Sources/BenchmarkSub/main.swift
Normal file
60
Sources/BenchmarkSub/main.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
let nats = NatsClientOptions().url(URL(string: "nats://localhost:4222")!).build()
|
||||
print("Connecting...")
|
||||
try! await nats.connect()
|
||||
print("Connected!")
|
||||
|
||||
let data = "foo".data(using: .utf8)!
|
||||
// Warmup
|
||||
print("Starting benchmark...")
|
||||
print("Waiting for first message...")
|
||||
var now = DispatchTime.now()
|
||||
let numMsgs = 1_000_000
|
||||
let sub = try await nats.subscribe(subject: "foo").makeAsyncIterator()
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
|
||||
for i in 1...numMsgs {
|
||||
let msg = try await sub.next()
|
||||
if i == 0 {
|
||||
print("Received first message! Starting the timer")
|
||||
now = DispatchTime.now()
|
||||
}
|
||||
guard let payload = msg?.payload else {
|
||||
print("empty payload!")
|
||||
continue
|
||||
}
|
||||
if String(data: payload, encoding: .utf8) != "\(i)" {
|
||||
let emptyString = ""
|
||||
print(
|
||||
"invalid payload; expected: \(i); got: \(String(data: payload, encoding: .utf8) ?? emptyString)"
|
||||
)
|
||||
}
|
||||
guard msg?.headers != nil else {
|
||||
print("empty headers!")
|
||||
continue
|
||||
}
|
||||
if i % 1000 == 0 {
|
||||
print("received \(i) msgs")
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = DispatchTime.now().uptimeNanoseconds - now.uptimeNanoseconds
|
||||
let msgsPerSec: Double = Double(numMsgs) / (Double(elapsed) / 1_000_000_000)
|
||||
print("Elapsed: \(elapsed / 1_000_000)ms")
|
||||
print("\(msgsPerSec) msgs/s")
|
||||
76
Sources/Example/main.swift
Executable file
76
Sources/Example/main.swift
Executable file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
print("\n### Setup NATS Connection")
|
||||
|
||||
let nats = NatsClientOptions()
|
||||
.url(URL(string: "nats://localhost:4222")!)
|
||||
.build()
|
||||
|
||||
nats.on(.connected) { event in
|
||||
print("event: connected")
|
||||
}
|
||||
|
||||
print("connecting...")
|
||||
try await nats.connect()
|
||||
|
||||
print("\n### Publish / Subscribe")
|
||||
|
||||
print("subscribing...")
|
||||
let sub = try await nats.subscribe(subject: "foo.>")
|
||||
|
||||
let loop = Task {
|
||||
print("starting message loop...")
|
||||
|
||||
for try await msg in sub {
|
||||
|
||||
if msg.subject == "foo.done" {
|
||||
break
|
||||
}
|
||||
|
||||
if let payload = msg.payload {
|
||||
print("received \(msg.subject): \(String(data: payload, encoding: .utf8) ?? "")")
|
||||
}
|
||||
|
||||
if let headers = msg.headers {
|
||||
if let headerValue = headers.get(try! NatsHeaderName("X-Example")) {
|
||||
print(" header: X-Example: \(headerValue.description)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("message loop done...")
|
||||
}
|
||||
|
||||
print("publishing data...")
|
||||
for i in 1...3 {
|
||||
var headers = NatsHeaderMap()
|
||||
headers.append(try! NatsHeaderName("X-Example"), NatsHeaderValue("example value"))
|
||||
|
||||
if let data = "data\(i)".data(using: .utf8) {
|
||||
try await nats.publish(data, subject: "foo.\(i)", headers: headers)
|
||||
}
|
||||
}
|
||||
|
||||
print("signalling done...")
|
||||
try await nats.publish(Data(), subject: "foo.done")
|
||||
|
||||
try await loop.value
|
||||
|
||||
print("closing...")
|
||||
try await nats.close()
|
||||
|
||||
print("bye")
|
||||
220
Sources/JetStream/Consumer+Pull.swift
Normal file
220
Sources/JetStream/Consumer+Pull.swift
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
import Nuid
|
||||
|
||||
/// Extension to ``Consumer`` adding pull consumer capabilities.
|
||||
extension Consumer {
|
||||
|
||||
/// Retrieves up to a provided number of messages from a stream.
|
||||
/// This method will send a single request and deliver requested messages unless time out is met earlier.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - batch: maximum number of messages to be retrieved
|
||||
/// - expires: timeout of a pull request
|
||||
/// - idleHeartbeat: interval in which server should send heartbeat messages (if no user messages are available).
|
||||
///
|
||||
/// - Returns: ``FetchResult`` which implements ``AsyncSequence`` allowing iteration over messages.
|
||||
///
|
||||
/// - Throws:
|
||||
/// - ``JetStreamError/FetchError`` if there was an error while fetching messages
|
||||
public func fetch(
|
||||
batch: Int, expires: TimeInterval = 30, idleHeartbeat: TimeInterval? = nil
|
||||
) async throws -> FetchResult {
|
||||
var request: PullRequest
|
||||
if let idleHeartbeat {
|
||||
request = PullRequest(
|
||||
batch: batch, expires: NanoTimeInterval(expires),
|
||||
heartbeat: NanoTimeInterval(idleHeartbeat))
|
||||
} else {
|
||||
request = PullRequest(batch: batch, expires: NanoTimeInterval(expires))
|
||||
}
|
||||
|
||||
let subject = ctx.apiSubject("CONSUMER.MSG.NEXT.\(info.stream).\(info.name)")
|
||||
let inbox = ctx.client.newInbox()
|
||||
let sub = try await ctx.client.subscribe(subject: inbox)
|
||||
try await self.ctx.client.publish(
|
||||
JSONEncoder().encode(request), subject: subject, reply: inbox)
|
||||
return FetchResult(ctx: ctx, sub: sub, idleHeartbeat: idleHeartbeat, batch: batch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to iterate over results of ``Consumer/fetch(batch:expires:idleHeartbeat:)``
|
||||
public class FetchResult: AsyncSequence {
|
||||
public typealias Element = JetStreamMessage
|
||||
public typealias AsyncIterator = FetchIterator
|
||||
|
||||
private let ctx: JetStreamContext
|
||||
private let sub: NatsSubscription
|
||||
private let idleHeartbeat: TimeInterval?
|
||||
private let batch: Int
|
||||
|
||||
init(ctx: JetStreamContext, sub: NatsSubscription, idleHeartbeat: TimeInterval?, batch: Int) {
|
||||
self.ctx = ctx
|
||||
self.sub = sub
|
||||
self.idleHeartbeat = idleHeartbeat
|
||||
self.batch = batch
|
||||
}
|
||||
|
||||
public func makeAsyncIterator() -> FetchIterator {
|
||||
return FetchIterator(
|
||||
ctx: ctx,
|
||||
sub: self.sub, idleHeartbeat: self.idleHeartbeat, remainingMessages: self.batch)
|
||||
}
|
||||
|
||||
public struct FetchIterator: AsyncIteratorProtocol {
|
||||
private let ctx: JetStreamContext
|
||||
private let sub: NatsSubscription
|
||||
private let idleHeartbeat: TimeInterval?
|
||||
private var remainingMessages: Int
|
||||
private var subIterator: NatsSubscription.AsyncIterator
|
||||
|
||||
init(
|
||||
ctx: JetStreamContext, sub: NatsSubscription, idleHeartbeat: TimeInterval?,
|
||||
remainingMessages: Int
|
||||
) {
|
||||
self.ctx = ctx
|
||||
self.sub = sub
|
||||
self.idleHeartbeat = idleHeartbeat
|
||||
self.remainingMessages = remainingMessages
|
||||
self.subIterator = sub.makeAsyncIterator()
|
||||
}
|
||||
|
||||
public mutating func next() async throws -> JetStreamMessage? {
|
||||
if remainingMessages <= 0 {
|
||||
try await sub.unsubscribe()
|
||||
return nil
|
||||
}
|
||||
|
||||
while true {
|
||||
let message: NatsMessage?
|
||||
|
||||
if let idleHeartbeat = idleHeartbeat {
|
||||
let timeout = idleHeartbeat * 2
|
||||
message = try await nextWithTimeout(timeout, subIterator)
|
||||
} else {
|
||||
message = try await subIterator.next()
|
||||
}
|
||||
|
||||
guard let message else {
|
||||
// the subscription has ended
|
||||
try await sub.unsubscribe()
|
||||
return nil
|
||||
}
|
||||
|
||||
let status = message.status ?? .ok
|
||||
|
||||
switch status {
|
||||
case .timeout:
|
||||
try await sub.unsubscribe()
|
||||
return nil
|
||||
case .idleHeartbeat:
|
||||
// in case of idle heartbeat error, we want to
|
||||
// wait for next message on subscription
|
||||
continue
|
||||
case .notFound:
|
||||
try await sub.unsubscribe()
|
||||
return nil
|
||||
case .ok:
|
||||
remainingMessages -= 1
|
||||
return JetStreamMessage(message: message, client: ctx.client)
|
||||
case .badRequest:
|
||||
try await sub.unsubscribe()
|
||||
throw JetStreamError.FetchError.badRequest
|
||||
case .noResponders:
|
||||
try await sub.unsubscribe()
|
||||
throw JetStreamError.FetchError.noResponders
|
||||
case .requestTerminated:
|
||||
try await sub.unsubscribe()
|
||||
guard let description = message.description else {
|
||||
throw JetStreamError.FetchError.invalidResponse
|
||||
}
|
||||
|
||||
let descLower = description.lowercased()
|
||||
if descLower.contains("message size exceeds maxbytes") {
|
||||
return nil
|
||||
} else if descLower.contains("leadership changed") {
|
||||
throw JetStreamError.FetchError.leadershipChanged
|
||||
} else if descLower.contains("consumer deleted") {
|
||||
throw JetStreamError.FetchError.consumerDeleted
|
||||
} else if descLower.contains("consumer is push based") {
|
||||
throw JetStreamError.FetchError.consumerIsPush
|
||||
}
|
||||
default:
|
||||
throw JetStreamError.FetchError.unknownStatus(status, message.description)
|
||||
}
|
||||
|
||||
if remainingMessages == 0 {
|
||||
try await sub.unsubscribe()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func nextWithTimeout(
|
||||
_ timeout: TimeInterval, _ subIterator: NatsSubscription.AsyncIterator
|
||||
) async throws -> NatsMessage? {
|
||||
try await withThrowingTaskGroup(of: NatsMessage?.self) { group in
|
||||
group.addTask {
|
||||
return try await subIterator.next()
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
||||
try await sub.unsubscribe()
|
||||
return nil
|
||||
}
|
||||
defer {
|
||||
group.cancelAll()
|
||||
}
|
||||
for try await result in group {
|
||||
if let msg = result {
|
||||
return msg
|
||||
} else {
|
||||
throw JetStreamError.FetchError.noHeartbeatReceived
|
||||
}
|
||||
}
|
||||
// this should not be reachable
|
||||
throw JetStreamError.FetchError.noHeartbeatReceived
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal struct PullRequest: Codable {
|
||||
let batch: Int
|
||||
let expires: NanoTimeInterval
|
||||
let maxBytes: Int?
|
||||
let noWait: Bool?
|
||||
let heartbeat: NanoTimeInterval?
|
||||
|
||||
internal init(
|
||||
batch: Int, expires: NanoTimeInterval, maxBytes: Int? = nil, noWait: Bool? = nil,
|
||||
heartbeat: NanoTimeInterval? = nil
|
||||
) {
|
||||
self.batch = batch
|
||||
self.expires = expires
|
||||
self.maxBytes = maxBytes
|
||||
self.noWait = noWait
|
||||
self.heartbeat = heartbeat
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case batch
|
||||
case expires
|
||||
case maxBytes = "max_bytes"
|
||||
case noWait = "no_wait"
|
||||
case heartbeat = "idle_heartbeat"
|
||||
}
|
||||
}
|
||||
379
Sources/JetStream/Consumer.swift
Normal file
379
Sources/JetStream/Consumer.swift
Normal file
@@ -0,0 +1,379 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Nuid
|
||||
|
||||
public class Consumer {
|
||||
|
||||
private static var rdigits: [UInt8] = Array(
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".utf8)
|
||||
|
||||
/// Contains information about the consumer.
|
||||
/// Note that this may be out of date and reading it does not query the server.
|
||||
/// For up-to-date stream info use ``Consumer/info()``
|
||||
public internal(set) var info: ConsumerInfo
|
||||
internal let ctx: JetStreamContext
|
||||
|
||||
init(ctx: JetStreamContext, info: ConsumerInfo) {
|
||||
self.ctx = ctx
|
||||
self.info = info
|
||||
}
|
||||
|
||||
/// Retrieves information about the consumer
|
||||
/// This also refreshes ``Consumer/info``.
|
||||
///
|
||||
/// - Returns ``ConsumerInfo`` from the server.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamRequestError`` if the request was unsuccessful.
|
||||
/// > - ``JetStreamError`` if the server responded with an API error.
|
||||
public func info() async throws -> ConsumerInfo {
|
||||
let subj = "CONSUMER.INFO.\(info.stream).\(info.name)"
|
||||
let info: Response<ConsumerInfo> = try await ctx.request(subj)
|
||||
switch info {
|
||||
case .success(let info):
|
||||
self.info = info
|
||||
return info
|
||||
case .error(let apiResponse):
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
internal static func validate(name: String) throws {
|
||||
guard !name.isEmpty else {
|
||||
throw JetStreamError.StreamError.nameRequired
|
||||
}
|
||||
|
||||
let invalidChars = CharacterSet(charactersIn: ">*. /\\")
|
||||
if name.rangeOfCharacter(from: invalidChars) != nil {
|
||||
throw JetStreamError.StreamError.invalidStreamName(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func generateConsumerName() -> String {
|
||||
let name = nextNuid()
|
||||
|
||||
let hash = SHA256.hash(data: Data(name.utf8))
|
||||
let hashData = Data(hash)
|
||||
|
||||
// Convert the first 8 bytes of the hash to the required format.
|
||||
let base: UInt8 = 36
|
||||
|
||||
var result = [UInt8]()
|
||||
for i in 0..<8 {
|
||||
let index = Int(hashData[i] % base)
|
||||
result.append(Consumer.rdigits[index])
|
||||
}
|
||||
|
||||
// Convert the result array to a string and return it.
|
||||
return String(bytes: result, encoding: .utf8)!
|
||||
}
|
||||
}
|
||||
|
||||
/// `ConsumerInfo` is the detailed information about a JetStream consumer.
|
||||
public struct ConsumerInfo: Codable {
|
||||
/// The name of the stream that the consumer is bound to.
|
||||
public let stream: String
|
||||
|
||||
/// The unique identifier for the consumer.
|
||||
public let name: String
|
||||
|
||||
/// The timestamp when the consumer was created.
|
||||
public let created: String
|
||||
|
||||
/// The configuration settings of the consumer, set when creating or updating the consumer.
|
||||
public let config: ConsumerConfig
|
||||
|
||||
/// Information about the most recently delivered message, including its sequence numbers and timestamp.
|
||||
public let delivered: SequenceInfo
|
||||
|
||||
/// Indicates the message before the first unacknowledged message.
|
||||
public let ackFloor: SequenceInfo
|
||||
|
||||
/// The number of messages that have been delivered but not yet acknowledged.
|
||||
public let numAckPending: Int
|
||||
|
||||
/// The number of messages that have been redelivered and not yet acknowledged.
|
||||
public let numRedelivered: Int
|
||||
|
||||
/// The count of active pull requests (relevant for pull-based consumers).
|
||||
public let numWaiting: Int
|
||||
|
||||
/// The number of messages that match the consumer's filter but have not been delivered yet.
|
||||
public let numPending: UInt64
|
||||
|
||||
/// Information about the cluster to which this consumer belongs (if applicable).
|
||||
public let cluster: ClusterInfo?
|
||||
|
||||
/// Indicates whether at least one subscription exists for the delivery subject of this consumer (only for push-based consumers).
|
||||
public let pushBound: Bool?
|
||||
|
||||
/// The timestamp indicating when this information was gathered by the server.
|
||||
public let timeStamp: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case stream = "stream_name"
|
||||
case name
|
||||
case created
|
||||
case config
|
||||
case delivered
|
||||
case ackFloor = "ack_floor"
|
||||
case numAckPending = "num_ack_pending"
|
||||
case numRedelivered = "num_redelivered"
|
||||
case numWaiting = "num_waiting"
|
||||
case numPending = "num_pending"
|
||||
case cluster
|
||||
case pushBound = "push_bound"
|
||||
case timeStamp = "ts"
|
||||
}
|
||||
}
|
||||
|
||||
/// `ConsumerConfig` is the configuration of a JetStream consumer.
|
||||
public struct ConsumerConfig: Codable, Equatable {
|
||||
/// Optional name for the consumer.
|
||||
public var name: String?
|
||||
|
||||
/// Optional durable name for the consumer.
|
||||
public var durable: String?
|
||||
|
||||
/// Optional description of the consumer.
|
||||
public var description: String?
|
||||
|
||||
/// Defines from which point to start delivering messages from the stream.
|
||||
public var deliverPolicy: DeliverPolicy
|
||||
|
||||
/// Optional sequence number from which to start message delivery.
|
||||
public var optStartSeq: UInt64?
|
||||
|
||||
/// Optional time from which to start message delivery.
|
||||
public var optStartTime: String?
|
||||
|
||||
/// Defines the acknowledgment policy for the consumer.
|
||||
public var ackPolicy: AckPolicy
|
||||
|
||||
/// Defines how long the server will wait for an acknowledgment before resending a message.
|
||||
public var ackWait: NanoTimeInterval?
|
||||
|
||||
/// Defines the maximum number of delivery attempts for a message.
|
||||
public var maxDeliver: Int?
|
||||
|
||||
/// Specifies the optional back-off intervals for retrying message delivery after a failed acknowledgment.
|
||||
public var backOff: [NanoTimeInterval]?
|
||||
|
||||
/// Can be used to filter messages delivered from the stream by given subject.
|
||||
/// It is exclusive with ``ConsumerConfig/filterSubjects``
|
||||
public var filterSubject: String?
|
||||
|
||||
/// Can be used to filter messages delivered from the stream by given subjects.
|
||||
/// It is exclusive with ``ConsumerConfig/filterSubject``
|
||||
public var filterSubjects: [String]?
|
||||
|
||||
/// Defines the rate at which messages are sent to the consumer.
|
||||
public var replayPolicy: ReplayPolicy
|
||||
|
||||
/// Specifies an optional maximum rate of message delivery in bits per second.
|
||||
public var rateLimit: UInt64?
|
||||
|
||||
/// Optional frequency for sampling acknowledgments for observability.
|
||||
public var sampleFrequency: String?
|
||||
|
||||
/// Maximum number of pull requests waiting to be fulfilled.
|
||||
public var maxWaiting: Int?
|
||||
|
||||
/// Maximum number of outstanding unacknowledged messages.
|
||||
public var maxAckPending: Int?
|
||||
|
||||
/// Indicates whether only headers of messages should be sent (and no payload).
|
||||
public var headersOnly: Bool?
|
||||
|
||||
/// Optional maximum batch size a single pull request can make.
|
||||
public var maxRequestBatch: Int?
|
||||
|
||||
/// Maximum duration a single pull request will wait for messages to be available to pull.
|
||||
public var maxRequestExpires: NanoTimeInterval?
|
||||
|
||||
/// Optional maximum total bytes that can be requested in a given batch.
|
||||
public var maxRequestMaxBytes: Int?
|
||||
|
||||
/// Duration which instructs the server to clean up the consumer if it has been inactive for the specified duration.
|
||||
public var inactiveThreshold: NanoTimeInterval?
|
||||
|
||||
/// Number of replicas for the consumer's state.
|
||||
public var replicas: Int
|
||||
|
||||
/// Flag to force the consumer to use memory storage rather than inherit the storage type from the stream.
|
||||
public var memoryStorage: Bool?
|
||||
|
||||
/// A set of application-defined key-value pairs for associating metadata on the consumer.
|
||||
public var metadata: [String: String]?
|
||||
|
||||
public init(
|
||||
name: String? = nil,
|
||||
durable: String? = nil,
|
||||
description: String? = nil,
|
||||
deliverPolicy: DeliverPolicy = .all,
|
||||
optStartSeq: UInt64? = nil,
|
||||
optStartTime: String? = nil,
|
||||
ackPolicy: AckPolicy = .explicit,
|
||||
ackWait: NanoTimeInterval? = nil,
|
||||
maxDeliver: Int? = nil,
|
||||
backOff: [NanoTimeInterval]? = nil,
|
||||
filterSubject: String? = nil,
|
||||
filterSubjects: [String]? = nil,
|
||||
replayPolicy: ReplayPolicy = .instant,
|
||||
rateLimit: UInt64? = nil,
|
||||
sampleFrequency: String? = nil,
|
||||
maxWaiting: Int? = nil,
|
||||
maxAckPending: Int? = nil,
|
||||
headersOnly: Bool? = nil,
|
||||
maxRequestBatch: Int? = nil,
|
||||
maxRequestExpires: NanoTimeInterval? = nil,
|
||||
maxRequestMaxBytes: Int? = nil,
|
||||
inactiveThreshold: NanoTimeInterval? = nil,
|
||||
replicas: Int = 1,
|
||||
memoryStorage: Bool? = nil,
|
||||
metadata: [String: String]? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.durable = durable
|
||||
self.description = description
|
||||
self.deliverPolicy = deliverPolicy
|
||||
self.optStartSeq = optStartSeq
|
||||
self.optStartTime = optStartTime
|
||||
self.ackPolicy = ackPolicy
|
||||
self.ackWait = ackWait
|
||||
self.maxDeliver = maxDeliver
|
||||
self.backOff = backOff
|
||||
self.filterSubject = filterSubject
|
||||
self.replayPolicy = replayPolicy
|
||||
self.rateLimit = rateLimit
|
||||
self.sampleFrequency = sampleFrequency
|
||||
self.maxWaiting = maxWaiting
|
||||
self.maxAckPending = maxAckPending
|
||||
self.headersOnly = headersOnly
|
||||
self.maxRequestBatch = maxRequestBatch
|
||||
self.maxRequestExpires = maxRequestExpires
|
||||
self.maxRequestMaxBytes = maxRequestMaxBytes
|
||||
self.inactiveThreshold = inactiveThreshold
|
||||
self.replicas = replicas
|
||||
self.memoryStorage = memoryStorage
|
||||
self.filterSubjects = filterSubjects
|
||||
self.metadata = metadata
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case durable = "durable_name"
|
||||
case description
|
||||
case deliverPolicy = "deliver_policy"
|
||||
case optStartSeq = "opt_start_seq"
|
||||
case optStartTime = "opt_start_time"
|
||||
case ackPolicy = "ack_policy"
|
||||
case ackWait = "ack_wait"
|
||||
case maxDeliver = "max_deliver"
|
||||
case backOff = "backoff"
|
||||
case filterSubject = "filter_subject"
|
||||
case replayPolicy = "replay_policy"
|
||||
case rateLimit = "rate_limit_bps"
|
||||
case sampleFrequency = "sample_freq"
|
||||
case maxWaiting = "max_waiting"
|
||||
case maxAckPending = "max_ack_pending"
|
||||
case headersOnly = "headers_only"
|
||||
case maxRequestBatch = "max_batch"
|
||||
case maxRequestExpires = "max_expires"
|
||||
case maxRequestMaxBytes = "max_bytes"
|
||||
case inactiveThreshold = "inactive_threshold"
|
||||
case replicas = "num_replicas"
|
||||
case memoryStorage = "mem_storage"
|
||||
case filterSubjects = "filter_subjects"
|
||||
case metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// `SequenceInfo` has both the consumer and the stream sequence and last activity.
|
||||
public struct SequenceInfo: Codable, Equatable {
|
||||
/// Consumer sequence number.
|
||||
public let consumer: UInt64
|
||||
|
||||
/// Stream sequence number.
|
||||
public let stream: UInt64
|
||||
|
||||
/// Last activity timestamp.
|
||||
public let last: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case consumer = "consumer_seq"
|
||||
case stream = "stream_seq"
|
||||
case last = "last_active"
|
||||
}
|
||||
}
|
||||
|
||||
/// `DeliverPolicy` determines from which point to start delivering messages.
|
||||
public enum DeliverPolicy: String, Codable {
|
||||
/// DeliverAllPolicy starts delivering messages from the very beginning of stream. This is the default.
|
||||
case all
|
||||
|
||||
/// DeliverLastPolicy will start the consumer with the last received.
|
||||
case last
|
||||
|
||||
/// DeliverNewPolicy will only deliver new messages that are sent after consumer is created.
|
||||
case new
|
||||
|
||||
/// DeliverByStartSequencePolicy will deliver messages starting from a sequence configured with OptStartSeq in ConsumerConfig.
|
||||
case byStartSequence = "by_start_sequence"
|
||||
|
||||
/// DeliverByStartTimePolicy will deliver messages starting from a given configured with OptStartTime in ConsumerConfig.
|
||||
case byStartTime = "by_start_time"
|
||||
|
||||
/// DeliverLastPerSubjectPolicy will start the consumer with the last for all subjects received.
|
||||
case lastPerSubject = "last_per_subject"
|
||||
}
|
||||
|
||||
/// `AckPolicy` determines how the consumer should acknowledge delivered messages.
|
||||
public enum AckPolicy: String, Codable {
|
||||
/// AckNonePolicy requires no acks for delivered messages./
|
||||
case none
|
||||
|
||||
/// AckAllPolicy when acking a sequence number, this implicitly acks sequences below this one as well.
|
||||
case all
|
||||
|
||||
/// AckExplicitPolicy requires ack or nack for all messages.
|
||||
case explicit
|
||||
}
|
||||
|
||||
/// `ReplayPolicy` determines how the consumer should replay messages it already has queued in the stream.
|
||||
public enum ReplayPolicy: String, Codable {
|
||||
/// ReplayInstantPolicy will replay messages as fast as possible./
|
||||
case instant
|
||||
|
||||
/// ReplayOriginalPolicy will maintain the same timing as the messages received.
|
||||
case original
|
||||
}
|
||||
|
||||
internal struct CreateConsumerRequest: Codable {
|
||||
internal let stream: String
|
||||
internal let config: ConsumerConfig
|
||||
internal let action: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case stream = "stream_name"
|
||||
case config
|
||||
case action
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsumerDeleteResponse: Codable {
|
||||
let success: Bool
|
||||
}
|
||||
322
Sources/JetStream/JetStreamContext+Consumer.swift
Normal file
322
Sources/JetStream/JetStreamContext+Consumer.swift
Normal file
@@ -0,0 +1,322 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension JetStreamContext {
|
||||
|
||||
/// Creates a consumer with the specified configuration.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: name of the stream the consumer will be created on
|
||||
/// - cfg: consumer config
|
||||
///
|
||||
/// - Returns: ``Consumer`` object containing ``ConsumerConfig`` and exposing operations on the consumer
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError``: if there was am error creating the consumer. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/ConsumerError/invalidConfig(_:)``: if the provided configuration is not valid.
|
||||
/// > - ``JetStreamError/ConsumerError/consumerNameExist(_:)``: if attempting to overwrite an existing consumer (with different configuration)
|
||||
/// > - ``JetStreamError/ConsumerError/maximumConsumersLimit(_:)``: if a max number of consumers (specified on stream/account level) has been reached.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func createConsumer(stream: String, cfg: ConsumerConfig) async throws -> Consumer {
|
||||
try Stream.validate(name: stream)
|
||||
return try await upsertConsumer(stream: stream, cfg: cfg, action: "create")
|
||||
}
|
||||
|
||||
/// Updates an existing consumer using specified config.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: name of the stream the consumer will be updated on
|
||||
/// - cfg: consumer config
|
||||
///
|
||||
/// - Returns: ``Consumer`` object containing ``ConsumerConfig`` and exposing operations on the consumer
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError``: if there was am error updating the consumer. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/ConsumerError/invalidConfig(_:)``: if the provided configuration is not valid or atteppting to update an illegal property
|
||||
/// > - ``JetStreamError/ConsumerError/consumerDoesNotExist(_:)``: if attempting to update a non-existing consumer
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func updateConsumer(stream: String, cfg: ConsumerConfig) async throws -> Consumer {
|
||||
try Stream.validate(name: stream)
|
||||
return try await upsertConsumer(stream: stream, cfg: cfg, action: "update")
|
||||
}
|
||||
|
||||
/// Creates a consumer with the specified configuration or updates an existing consumer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: name of the stream the consumer will be created on
|
||||
/// - cfg: consumer config
|
||||
///
|
||||
/// - Returns: ``Consumer`` object containing ``ConsumerConfig`` and exposing operations on the consumer
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError``: if there was am error creating or updatig the consumer. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/ConsumerError/invalidConfig(_:)``: if the provided configuration is not valid or atteppting to update an illegal property
|
||||
/// > - ``JetStreamError/ConsumerError/maximumConsumersLimit(_:)``: if a max number of consumers (specified on stream/account level) has been reached.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func createOrUpdateConsumer(stream: String, cfg: ConsumerConfig) async throws -> Consumer
|
||||
{
|
||||
try Stream.validate(name: stream)
|
||||
return try await upsertConsumer(stream: stream, cfg: cfg)
|
||||
}
|
||||
|
||||
/// Retrieves a consumer with given name from a stream.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: name of the stream the consumer is retrieved from
|
||||
/// - name: name of the stream
|
||||
///
|
||||
/// - Returns a ``Stream`` object containing ``StreamInfo`` and exposing operations on the stream or nil if stream with given name does not exist.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError/consumerNotFound(_:)``: if the consumer with given name does not exist on a given stream.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func getConsumer(stream: String, name: String) async throws -> Consumer? {
|
||||
try Stream.validate(name: stream)
|
||||
try Consumer.validate(name: name)
|
||||
|
||||
let subj = "CONSUMER.INFO.\(stream).\(name)"
|
||||
let info: Response<ConsumerInfo> = try await request(subj)
|
||||
switch info {
|
||||
case .success(let info):
|
||||
return Consumer(ctx: self, info: info)
|
||||
case .error(let apiResponse):
|
||||
if apiResponse.error.errorCode == .consumerNotFound {
|
||||
return nil
|
||||
}
|
||||
if let consumerError = JetStreamError.ConsumerError(from: apiResponse.error) {
|
||||
throw consumerError
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a consumer from a stream.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: name of the stream the consumer will be created on
|
||||
/// - name: consumer name
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError/consumerNotFound(_:)``: if the consumer with given name does not exist on a given stream.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func deleteConsumer(stream: String, name: String) async throws {
|
||||
try Stream.validate(name: stream)
|
||||
try Consumer.validate(name: name)
|
||||
|
||||
let subject = "CONSUMER.DELETE.\(stream).\(name)"
|
||||
let resp: Response<ConsumerDeleteResponse> = try await request(subject)
|
||||
|
||||
switch resp {
|
||||
case .success(_):
|
||||
return
|
||||
case .error(let apiResponse):
|
||||
if let streamErr = JetStreamError.ConsumerError(from: apiResponse.error) {
|
||||
throw streamErr
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
internal func upsertConsumer(
|
||||
stream: String, cfg: ConsumerConfig, action: String? = nil
|
||||
) async throws -> Consumer {
|
||||
let consumerName = cfg.name ?? cfg.durable ?? Consumer.generateConsumerName()
|
||||
|
||||
try Consumer.validate(name: consumerName)
|
||||
|
||||
let createReq = CreateConsumerRequest(stream: stream, config: cfg, action: action)
|
||||
let req = try! JSONEncoder().encode(createReq)
|
||||
|
||||
var subject: String
|
||||
if let filterSubject = cfg.filterSubject {
|
||||
subject = "CONSUMER.CREATE.\(stream).\(consumerName).\(filterSubject)"
|
||||
} else {
|
||||
subject = "CONSUMER.CREATE.\(stream).\(consumerName)"
|
||||
}
|
||||
|
||||
let info: Response<ConsumerInfo> = try await request(subject, message: req)
|
||||
|
||||
switch info {
|
||||
case .success(let info):
|
||||
return Consumer(ctx: self, info: info)
|
||||
case .error(let apiResponse):
|
||||
if let consumerError = JetStreamError.ConsumerError(from: apiResponse.error) {
|
||||
throw consumerError
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to list consumer names.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: the name of the strem to list the consumers from.
|
||||
///
|
||||
/// - Returns ``Consumers`` which implements AsyncSequence allowing iteration over stream infos.
|
||||
public func consumers(stream: String) async -> Consumers {
|
||||
return Consumers(ctx: self, stream: stream)
|
||||
}
|
||||
|
||||
/// Used to list consumer names.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: the name of the strem to list the consumers from.
|
||||
///
|
||||
/// - Returns ``ConsumerNames`` which implements AsyncSequence allowing iteration over consumer names.
|
||||
public func consumerNames(stream: String) async -> ConsumerNames {
|
||||
return ConsumerNames(ctx: self, stream: stream)
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ConsumersPagedRequest: Codable {
|
||||
let offset: Int
|
||||
}
|
||||
|
||||
/// Used to iterate over consumer names when using ``JetStreamContext/consumerNames(stream:)``
|
||||
public struct ConsumerNames: AsyncSequence {
|
||||
public typealias Element = String
|
||||
public typealias AsyncIterator = ConsumerNamesIterator
|
||||
|
||||
private let ctx: JetStreamContext
|
||||
private let stream: String
|
||||
private var buffer: [String]
|
||||
private var offset: Int
|
||||
private var total: Int?
|
||||
|
||||
private struct ConsumerNamesPage: Codable {
|
||||
let total: Int
|
||||
let consumers: [String]?
|
||||
}
|
||||
|
||||
init(ctx: JetStreamContext, stream: String) {
|
||||
self.stream = stream
|
||||
self.ctx = ctx
|
||||
self.buffer = []
|
||||
self.offset = 0
|
||||
}
|
||||
|
||||
public func makeAsyncIterator() -> ConsumerNamesIterator {
|
||||
return ConsumerNamesIterator(seq: self)
|
||||
}
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
if let consumer = buffer.first {
|
||||
buffer.removeFirst()
|
||||
return consumer
|
||||
}
|
||||
|
||||
if let total = self.total, self.offset >= total {
|
||||
return nil
|
||||
}
|
||||
|
||||
// poll consumers
|
||||
let request = ConsumersPagedRequest(offset: offset)
|
||||
|
||||
let res: Response<ConsumerNamesPage> = try await ctx.request(
|
||||
"CONSUMER.NAMES.\(self.stream)", message: JSONEncoder().encode(request))
|
||||
switch res {
|
||||
case .success(let names):
|
||||
guard let consumers = names.consumers else {
|
||||
return nil
|
||||
}
|
||||
self.offset += consumers.count
|
||||
self.total = names.total
|
||||
buffer.append(contentsOf: consumers)
|
||||
return try await self.next()
|
||||
case .error(let err):
|
||||
throw err.error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct ConsumerNamesIterator: AsyncIteratorProtocol {
|
||||
var seq: ConsumerNames
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
try await seq.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to iterate over consumers when listing consumer infos using ``JetStreamContext/consumers(stream:)``
|
||||
public struct Consumers: AsyncSequence {
|
||||
public typealias Element = ConsumerInfo
|
||||
public typealias AsyncIterator = ConsumersIterator
|
||||
|
||||
private let ctx: JetStreamContext
|
||||
private let stream: String
|
||||
private var buffer: [ConsumerInfo]
|
||||
private var offset: Int
|
||||
private var total: Int?
|
||||
|
||||
private struct ConsumersPage: Codable {
|
||||
let total: Int
|
||||
let consumers: [ConsumerInfo]?
|
||||
}
|
||||
|
||||
init(ctx: JetStreamContext, stream: String) {
|
||||
self.stream = stream
|
||||
self.ctx = ctx
|
||||
self.buffer = []
|
||||
self.offset = 0
|
||||
}
|
||||
|
||||
public func makeAsyncIterator() -> ConsumersIterator {
|
||||
return ConsumersIterator(seq: self)
|
||||
}
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
if let consumer = buffer.first {
|
||||
buffer.removeFirst()
|
||||
return consumer
|
||||
}
|
||||
|
||||
if let total = self.total, self.offset >= total {
|
||||
return nil
|
||||
}
|
||||
|
||||
// poll consumers
|
||||
let request = ConsumersPagedRequest(offset: offset)
|
||||
|
||||
let res: Response<ConsumersPage> = try await ctx.request(
|
||||
"CONSUMER.LIST.\(self.stream)", message: JSONEncoder().encode(request))
|
||||
switch res {
|
||||
case .success(let infos):
|
||||
guard let consumers = infos.consumers else {
|
||||
return nil
|
||||
}
|
||||
self.offset += consumers.count
|
||||
self.total = infos.total
|
||||
buffer.append(contentsOf: consumers)
|
||||
return try await self.next()
|
||||
case .error(let err):
|
||||
throw err.error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct ConsumersIterator: AsyncIteratorProtocol {
|
||||
var seq: Consumers
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
try await seq.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
291
Sources/JetStream/JetStreamContext+Stream.swift
Normal file
291
Sources/JetStream/JetStreamContext+Stream.swift
Normal file
@@ -0,0 +1,291 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Extension to `JetStreamContext` adding stream management functionalities.
|
||||
extension JetStreamContext {
|
||||
|
||||
/// Creates a stream with the specified configuration.
|
||||
///
|
||||
/// - Parameter cfg: stream config
|
||||
///
|
||||
/// - Returns: ``Stream`` object containing ``StreamInfo`` and exposing operations on the stream
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/StreamError``: if there was am error creating the stream. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/StreamError/nameRequired``: if the provided stream name is empty.
|
||||
/// > - ``JetStreamError/StreamError/invalidStreamName(_:)``: if the provided stream name is not valid.
|
||||
/// > - ``JetStreamError/StreamError/streamNameExist(_:)``: if a stream with provided name exists and has different configuration.
|
||||
/// > - ``JetStreamError/StreamError/invalidConfig(_:)``: if the stream config is not valid.
|
||||
/// > - ``JetStreamError/StreamError/maximumStreamsLimit(_:)``: if the maximum number of streams has been reached.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func createStream(cfg: StreamConfig) async throws -> Stream {
|
||||
try Stream.validate(name: cfg.name)
|
||||
let req = try! JSONEncoder().encode(cfg)
|
||||
let subj = "STREAM.CREATE.\(cfg.name)"
|
||||
let info: Response<StreamInfo> = try await request(subj, message: req)
|
||||
switch info {
|
||||
case .success(let info):
|
||||
return Stream(ctx: self, info: info)
|
||||
case .error(let apiResponse):
|
||||
if let streamErr = JetStreamError.StreamError(from: apiResponse.error) {
|
||||
throw streamErr
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a stream by its name.
|
||||
///
|
||||
/// - Parameter name: name of the stream
|
||||
///
|
||||
/// - Returns a ``Stream`` object containing ``StreamInfo`` and exposing operations on the stream or nil if stream with given name does not exist.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/StreamError/nameRequired`` if the provided stream name is empty.
|
||||
/// > - ``JetStreamError/StreamError/invalidStreamName(_:)`` if the provided stream name is not valid.
|
||||
/// > - ``JetStreamError/RequestError`` if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError`` if there was a different JetStreamError returned from JetStream.
|
||||
public func getStream(name: String) async throws -> Stream? {
|
||||
try Stream.validate(name: name)
|
||||
let subj = "STREAM.INFO.\(name)"
|
||||
let info: Response<StreamInfo> = try await request(subj)
|
||||
switch info {
|
||||
case .success(let info):
|
||||
return Stream(ctx: self, info: info)
|
||||
case .error(let apiResponse):
|
||||
if apiResponse.error.errorCode == .streamNotFound {
|
||||
return nil
|
||||
}
|
||||
if let streamErr = JetStreamError.StreamError(from: apiResponse.error) {
|
||||
throw streamErr
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing stream with new configuration.
|
||||
///
|
||||
/// - Parameter: cfg: stream config
|
||||
///
|
||||
/// - Returns: ``Stream`` object containing ``StreamInfo`` and exposing operations on the stream
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/StreamError`` if there was am error updating the stream.
|
||||
/// > There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/StreamError/nameRequired`` if the provided stream name is empty.
|
||||
/// > - ``JetStreamError/StreamError/invalidStreamName(_:)`` if the provided stream name is not valid.
|
||||
/// > - ``JetStreamError/StreamError/streamNotFound(_:)`` if a stream with provided name exists and has different configuration.
|
||||
/// > - ``JetStreamError/StreamError/invalidConfig(_:)`` if the stream config is not valid or user attempts to update non-updatable properties.
|
||||
/// > - ``JetStreamError/RequestError`` if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError`` if there was a different API error returned from JetStream.
|
||||
public func updateStream(cfg: StreamConfig) async throws -> Stream {
|
||||
try Stream.validate(name: cfg.name)
|
||||
let req = try! JSONEncoder().encode(cfg)
|
||||
let subj = "STREAM.UPDATE.\(cfg.name)"
|
||||
let info: Response<StreamInfo> = try await request(subj, message: req)
|
||||
switch info {
|
||||
case .success(let info):
|
||||
return Stream(ctx: self, info: info)
|
||||
case .error(let apiResponse):
|
||||
if let streamErr = JetStreamError.StreamError(from: apiResponse.error) {
|
||||
throw streamErr
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a stream by its name.
|
||||
///
|
||||
/// - Parameter name: name of the stream to be deleted.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/StreamError/nameRequired`` if the provided stream name is empty.
|
||||
/// > - ``JetStreamError/StreamError/invalidStreamName(_:)`` if the provided stream name is not valid.
|
||||
/// > - ``JetStreamError/RequestError`` if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError`` if there was a different JetStreamError returned from JetStream.
|
||||
public func deleteStream(name: String) async throws {
|
||||
try Stream.validate(name: name)
|
||||
let subj = "STREAM.DELETE.\(name)"
|
||||
let info: Response<StreamDeleteResponse> = try await request(subj)
|
||||
switch info {
|
||||
case .success(_):
|
||||
return
|
||||
case .error(let apiResponse):
|
||||
if let streamErr = JetStreamError.StreamError(from: apiResponse.error) {
|
||||
throw streamErr
|
||||
}
|
||||
throw apiResponse.error
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamDeleteResponse: Codable {
|
||||
let success: Bool
|
||||
}
|
||||
|
||||
/// Used to list stream infos.
|
||||
///
|
||||
/// - Returns ``Streams`` which implements AsyncSequence allowing iteration over streams.
|
||||
///
|
||||
/// - Parameter subject: if provided will be used to filter out returned streams
|
||||
public func streams(subject: String? = nil) async -> Streams {
|
||||
return Streams(ctx: self, subject: subject)
|
||||
}
|
||||
|
||||
/// Used to list stream names.
|
||||
///
|
||||
/// - Returns ``StreamNames`` which implements AsyncSequence allowing iteration over stream names.
|
||||
///
|
||||
/// - Parameter subject: if provided will be used to filter out returned stream names
|
||||
public func streamNames(subject: String? = nil) async -> StreamNames {
|
||||
return StreamNames(ctx: self, subject: subject)
|
||||
}
|
||||
}
|
||||
|
||||
internal struct StreamsPagedRequest: Codable {
|
||||
let offset: Int
|
||||
let subject: String?
|
||||
}
|
||||
|
||||
/// Used to iterate over streams when listing stream infos using ``JetStreamContext/streams(subject:)``
|
||||
public struct Streams: AsyncSequence {
|
||||
public typealias Element = StreamInfo
|
||||
public typealias AsyncIterator = StreamsIterator
|
||||
|
||||
private let ctx: JetStreamContext
|
||||
private let subject: String?
|
||||
private var buffer: [StreamInfo]
|
||||
private var offset: Int
|
||||
private var total: Int?
|
||||
|
||||
private struct StreamsInfoPage: Codable {
|
||||
let total: Int
|
||||
let streams: [StreamInfo]?
|
||||
}
|
||||
|
||||
init(ctx: JetStreamContext, subject: String?) {
|
||||
self.ctx = ctx
|
||||
self.subject = subject
|
||||
self.buffer = []
|
||||
self.offset = 0
|
||||
}
|
||||
|
||||
public func makeAsyncIterator() -> StreamsIterator {
|
||||
return StreamsIterator(seq: self)
|
||||
}
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
if let stream = buffer.first {
|
||||
buffer.removeFirst()
|
||||
return stream
|
||||
}
|
||||
|
||||
if let total = self.total, self.offset >= total {
|
||||
return nil
|
||||
}
|
||||
|
||||
// poll streams
|
||||
let request = StreamsPagedRequest(offset: offset, subject: subject)
|
||||
|
||||
let res: Response<StreamsInfoPage> = try await ctx.request(
|
||||
"STREAM.LIST", message: JSONEncoder().encode(request))
|
||||
switch res {
|
||||
case .success(let infos):
|
||||
guard let streams = infos.streams else {
|
||||
return nil
|
||||
}
|
||||
self.offset += streams.count
|
||||
self.total = infos.total
|
||||
buffer.append(contentsOf: streams)
|
||||
return try await self.next()
|
||||
case .error(let err):
|
||||
throw err.error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct StreamsIterator: AsyncIteratorProtocol {
|
||||
var seq: Streams
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
try await seq.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct StreamNames: AsyncSequence {
|
||||
public typealias Element = String
|
||||
public typealias AsyncIterator = StreamNamesIterator
|
||||
|
||||
private let ctx: JetStreamContext
|
||||
private let subject: String?
|
||||
private var buffer: [String]
|
||||
private var offset: Int
|
||||
private var total: Int?
|
||||
|
||||
private struct StreamNamesPage: Codable {
|
||||
let total: Int
|
||||
let streams: [String]?
|
||||
}
|
||||
|
||||
init(ctx: JetStreamContext, subject: String?) {
|
||||
self.ctx = ctx
|
||||
self.subject = subject
|
||||
self.buffer = []
|
||||
self.offset = 0
|
||||
}
|
||||
|
||||
public func makeAsyncIterator() -> StreamNamesIterator {
|
||||
return StreamNamesIterator(seq: self)
|
||||
}
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
if let stream = buffer.first {
|
||||
buffer.removeFirst()
|
||||
return stream
|
||||
}
|
||||
|
||||
if let total = self.total, self.offset >= total {
|
||||
return nil
|
||||
}
|
||||
|
||||
// poll streams
|
||||
let request = StreamsPagedRequest(offset: offset, subject: subject)
|
||||
|
||||
let res: Response<StreamNamesPage> = try await ctx.request(
|
||||
"STREAM.NAMES", message: JSONEncoder().encode(request))
|
||||
switch res {
|
||||
case .success(let names):
|
||||
guard let streams = names.streams else {
|
||||
return nil
|
||||
}
|
||||
self.offset += streams.count
|
||||
self.total = names.total
|
||||
buffer.append(contentsOf: streams)
|
||||
return try await self.next()
|
||||
case .error(let err):
|
||||
throw err.error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct StreamNamesIterator: AsyncIteratorProtocol {
|
||||
var seq: StreamNames
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
try await seq.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
234
Sources/JetStream/JetStreamContext.swift
Normal file
234
Sources/JetStream/JetStreamContext.swift
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Nats
|
||||
import Nuid
|
||||
|
||||
/// A context which can perform jetstream scoped requests.
|
||||
public class JetStreamContext {
|
||||
internal var client: NatsClient
|
||||
private var prefix: String = "$JS.API"
|
||||
private var timeout: TimeInterval = 5.0
|
||||
|
||||
/// Creates a JetStreamContext from ``NatsClient`` with optional custom prefix and timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - client: NATS client connection.
|
||||
/// - prefix: Used to comfigure a prefix for JetStream API requests.
|
||||
/// - timeout: Used to configure a timeout for JetStream API operations.
|
||||
public init(client: NatsClient, prefix: String = "$JS.API", timeout: TimeInterval = 5.0) {
|
||||
self.client = client
|
||||
self.prefix = prefix
|
||||
self.timeout = timeout
|
||||
}
|
||||
|
||||
/// Creates a JetStreamContext from ``NatsClient`` with custom domain and timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - client: NATS client connection.
|
||||
/// - domain: Used to comfigure a domain for JetStream API requests.
|
||||
/// - timeout: Used to configure a timeout for JetStream API operations.
|
||||
public init(client: NatsClient, domain: String, timeout: TimeInterval = 5.0) {
|
||||
self.client = client
|
||||
self.prefix = "$JS.\(domain).API"
|
||||
self.timeout = timeout
|
||||
}
|
||||
|
||||
/// Creates a JetStreamContext from ``NatsClient``
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - client: NATS client connection.
|
||||
public init(client: NatsClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// Sets a custom timeout for JetStream API requests.
|
||||
public func setTimeout(_ timeout: TimeInterval) {
|
||||
self.timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
extension JetStreamContext {
|
||||
|
||||
/// Publishes a message on a stream subjec without waiting for acknowledgment from the server that the message has been successfully delivered.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - subject: Subject on which the message will be published.
|
||||
/// - message: NATS message payload.
|
||||
/// - headers:Optional set of message headers.
|
||||
///
|
||||
/// - Returns: ``AckFuture`` allowing to await for the ack from the server.
|
||||
public func publish(
|
||||
_ subject: String, message: Data, headers: NatsHeaderMap? = nil
|
||||
) async throws -> AckFuture {
|
||||
// TODO(pp): add stream header options (expected seq etc)
|
||||
let inbox = client.newInbox()
|
||||
let sub = try await self.client.subscribe(subject: inbox)
|
||||
try await self.client.publish(message, subject: subject, reply: inbox, headers: headers)
|
||||
return AckFuture(sub: sub, timeout: self.timeout)
|
||||
}
|
||||
|
||||
internal func request<T: Codable>(
|
||||
_ subject: String, message: Data? = nil
|
||||
) async throws -> Response<T> {
|
||||
let data = message ?? Data()
|
||||
do {
|
||||
let response = try await self.client.request(
|
||||
data, subject: apiSubject(subject), timeout: self.timeout)
|
||||
let decoder = JSONDecoder()
|
||||
guard let payload = response.payload else {
|
||||
throw JetStreamError.RequestError.emptyResponsePayload
|
||||
}
|
||||
return try decoder.decode(Response<T>.self, from: payload)
|
||||
} catch let err as NatsError.RequestError {
|
||||
switch err {
|
||||
case .noResponders:
|
||||
throw JetStreamError.RequestError.noResponders
|
||||
case .timeout:
|
||||
throw JetStreamError.RequestError.timeout
|
||||
case .permissionDenied:
|
||||
throw JetStreamError.RequestError.permissionDenied(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func request(_ subject: String, message: Data? = nil) async throws -> NatsMessage {
|
||||
let data = message ?? Data()
|
||||
do {
|
||||
return try await self.client.request(
|
||||
data, subject: apiSubject(subject), timeout: self.timeout)
|
||||
} catch let err as NatsError.RequestError {
|
||||
switch err {
|
||||
case .noResponders:
|
||||
throw JetStreamError.RequestError.noResponders
|
||||
case .timeout:
|
||||
throw JetStreamError.RequestError.timeout
|
||||
case .permissionDenied:
|
||||
throw JetStreamError.RequestError.permissionDenied(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func apiSubject(_ subject: String) -> String {
|
||||
return "\(self.prefix).\(subject)"
|
||||
}
|
||||
}
|
||||
|
||||
public struct JetStreamAPIResponse: Codable {
|
||||
public let type: String
|
||||
public let error: JetStreamError.APIError
|
||||
}
|
||||
|
||||
/// Used to await for response from ``JetStreamContext/publish(_:message:headers:)``
|
||||
public struct AckFuture {
|
||||
let sub: NatsSubscription
|
||||
let timeout: TimeInterval
|
||||
|
||||
/// Waits for an ACK from JetStream server.
|
||||
///
|
||||
/// - Returns: Acknowledgement object returned by the server.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/RequestError`` if the request timed out (client did not receive the ack in time) or
|
||||
public func wait() async throws -> Ack {
|
||||
let response = try await withThrowingTaskGroup(
|
||||
of: NatsMessage?.self,
|
||||
body: { group in
|
||||
group.addTask {
|
||||
return try await sub.makeAsyncIterator().next()
|
||||
}
|
||||
|
||||
// task for the timeout
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(self.timeout * 1_000_000_000))
|
||||
return nil
|
||||
}
|
||||
|
||||
for try await result in group {
|
||||
// if the result is not empty, return it (or throw status error)
|
||||
if let msg = result {
|
||||
group.cancelAll()
|
||||
return msg
|
||||
} else {
|
||||
group.cancelAll()
|
||||
try await sub.unsubscribe()
|
||||
// if result is empty, time out
|
||||
throw JetStreamError.RequestError.timeout
|
||||
}
|
||||
}
|
||||
|
||||
// this should not be reachable
|
||||
throw NatsError.ClientError.internalError("error waiting for response")
|
||||
})
|
||||
if response.status == StatusCode.noResponders {
|
||||
throw JetStreamError.PublishError.streamNotFound
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
guard let payload = response.payload else {
|
||||
throw JetStreamError.RequestError.emptyResponsePayload
|
||||
}
|
||||
|
||||
let ack = try decoder.decode(Response<Ack>.self, from: payload)
|
||||
switch ack {
|
||||
case .success(let ack):
|
||||
return ack
|
||||
case .error(let err):
|
||||
if let publishErr = JetStreamError.PublishError(from: err.error) {
|
||||
throw publishErr
|
||||
} else {
|
||||
throw err.error
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public struct Ack: Codable {
|
||||
public let stream: String
|
||||
public let seq: UInt64
|
||||
public let domain: String?
|
||||
public let duplicate: Bool
|
||||
|
||||
// Custom CodingKeys to map JSON keys to Swift property names
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case stream
|
||||
case seq
|
||||
case domain
|
||||
case duplicate
|
||||
}
|
||||
|
||||
// Custom initializer from Decoder
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
// Decode `stream` and `seq` as they are required
|
||||
stream = try container.decode(String.self, forKey: .stream)
|
||||
seq = try container.decode(UInt64.self, forKey: .seq)
|
||||
|
||||
// Decode `domain` as optional since it may not be present
|
||||
domain = try container.decodeIfPresent(String.self, forKey: .domain)
|
||||
|
||||
// Decode `duplicate` and provide a default value of `false` if not present
|
||||
duplicate = try container.decodeIfPresent(Bool.self, forKey: .duplicate) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
/// contains info about the `JetStream` usage from the current account.
|
||||
public struct AccountInfo: Codable {
|
||||
public let memory: Int64
|
||||
public let storage: Int64
|
||||
public let streams: Int64
|
||||
public let consumers: Int64
|
||||
}
|
||||
863
Sources/JetStream/JetStreamError.swift
Normal file
863
Sources/JetStream/JetStreamError.swift
Normal file
@@ -0,0 +1,863 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
public protocol JetStreamErrorProtocol: Error, CustomStringConvertible {}
|
||||
|
||||
public enum JetStreamError {
|
||||
public struct APIError: Codable, Error {
|
||||
public var code: UInt
|
||||
public var errorCode: ErrorCode
|
||||
public var description: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code = "code"
|
||||
case errorCode = "err_code"
|
||||
case description = "description"
|
||||
}
|
||||
}
|
||||
|
||||
public enum RequestError: JetStreamErrorProtocol {
|
||||
case noResponders
|
||||
case timeout
|
||||
case emptyResponsePayload
|
||||
case permissionDenied(String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .noResponders:
|
||||
return "nats: no responders available for request"
|
||||
case .timeout:
|
||||
return "nats: request timed out"
|
||||
case .emptyResponsePayload:
|
||||
return "nats: empty response payload"
|
||||
case .permissionDenied(let subject):
|
||||
return "nats: permission denied on subject \(subject)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageMetadataError: JetStreamErrorProtocol {
|
||||
case noReplyInMessage
|
||||
case invalidPrefix
|
||||
case invalidTokenNum
|
||||
case invalidTokenValue
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .noReplyInMessage:
|
||||
return "nats: did not fund reply subject in message"
|
||||
case .invalidPrefix:
|
||||
return "nats: invalid reply subject prefix"
|
||||
case .invalidTokenNum:
|
||||
return "nats: invalid token count"
|
||||
case .invalidTokenValue:
|
||||
return "nats: invalid token value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FetchError: JetStreamErrorProtocol {
|
||||
case noHeartbeatReceived
|
||||
case consumerDeleted
|
||||
case badRequest
|
||||
case noResponders
|
||||
case consumerIsPush
|
||||
case invalidResponse
|
||||
case leadershipChanged
|
||||
case unknownStatus(StatusCode, String?)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .noHeartbeatReceived:
|
||||
return "nats: no heartbeat received"
|
||||
case .consumerDeleted:
|
||||
return "nats: consumer deleted"
|
||||
case .badRequest:
|
||||
return "nats: bad request"
|
||||
case .noResponders:
|
||||
return "nats: no responders"
|
||||
case .consumerIsPush:
|
||||
return "nats: consumer is push based"
|
||||
case .invalidResponse:
|
||||
return "nats: no description in status response"
|
||||
case .leadershipChanged:
|
||||
return "nats: leadership changed"
|
||||
case .unknownStatus(let status, let description):
|
||||
if let description {
|
||||
return "nats: unknown response status: \(status): \(description)"
|
||||
} else {
|
||||
return "nats: unknown response status: \(status)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum AckError: JetStreamErrorProtocol {
|
||||
case noReplyInMessage
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .noReplyInMessage:
|
||||
return "nats: did not fund reply subject in message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StreamError: JetStreamErrorProtocol {
|
||||
case nameRequired
|
||||
case invalidStreamName(String)
|
||||
case streamNotFound(APIError)
|
||||
case streamNameExist(APIError)
|
||||
case streamMessageExceedsMaximum(APIError)
|
||||
case streamDelete(APIError)
|
||||
case streamUpdate(APIError)
|
||||
case streamInvalidExternalDeliverySubject(APIError)
|
||||
case streamMirrorNotUpdatable(APIError)
|
||||
case streamLimitsExceeded(APIError)
|
||||
case invalidConfig(APIError)
|
||||
case maximumStreamsLimit(APIError)
|
||||
case streamSealed(APIError)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .nameRequired:
|
||||
return "nats: stream name is required"
|
||||
case .invalidStreamName(let name):
|
||||
return "nats: invalid stream name: \(name)"
|
||||
case .streamNotFound(let err),
|
||||
.streamNameExist(let err),
|
||||
.streamMessageExceedsMaximum(let err),
|
||||
.streamDelete(let err),
|
||||
.streamUpdate(let err),
|
||||
.streamInvalidExternalDeliverySubject(let err),
|
||||
.streamMirrorNotUpdatable(let err),
|
||||
.streamLimitsExceeded(let err),
|
||||
.invalidConfig(let err),
|
||||
.maximumStreamsLimit(let err),
|
||||
.streamSealed(let err):
|
||||
return "nats: \(err.description)"
|
||||
}
|
||||
}
|
||||
|
||||
internal init?(from err: APIError) {
|
||||
switch err.errorCode {
|
||||
case ErrorCode.streamNotFound:
|
||||
self = .streamNotFound(err)
|
||||
case ErrorCode.streamNameExist:
|
||||
self = .streamNameExist(err)
|
||||
case ErrorCode.streamMessageExceedsMaximum:
|
||||
self = .streamMessageExceedsMaximum(err)
|
||||
case ErrorCode.streamDelete:
|
||||
self = .streamDelete(err)
|
||||
case ErrorCode.streamUpdate:
|
||||
self = .streamUpdate(err)
|
||||
case ErrorCode.streamInvalidExternalDeliverySubject:
|
||||
self = .streamInvalidExternalDeliverySubject(err)
|
||||
case ErrorCode.streamMirrorNotUpdatable:
|
||||
self = .streamMirrorNotUpdatable(err)
|
||||
case ErrorCode.streamLimits:
|
||||
self = .streamLimitsExceeded(err)
|
||||
case ErrorCode.mirrorWithSources,
|
||||
ErrorCode.streamSubjectOverlap,
|
||||
ErrorCode.streamExternalDeletePrefixOverlaps,
|
||||
ErrorCode.mirrorMaxMessageSizeTooBig,
|
||||
ErrorCode.sourceMaxMessageSizeTooBig,
|
||||
ErrorCode.streamInvalidConfig,
|
||||
ErrorCode.mirrorWithSubjects,
|
||||
ErrorCode.streamExternalApiOverlap,
|
||||
ErrorCode.mirrorWithStartSequenceAndTime,
|
||||
ErrorCode.mirrorWithSubjectFilters,
|
||||
ErrorCode.streamReplicasNotSupported,
|
||||
ErrorCode.streamReplicasNotUpdatable,
|
||||
ErrorCode.streamMaxBytesRequired,
|
||||
ErrorCode.streamMaxStreamBytesExceeded,
|
||||
ErrorCode.streamNameContainsPathSeparators,
|
||||
ErrorCode.replicasCountCannotBeNegative,
|
||||
ErrorCode.sourceDuplicateDetected,
|
||||
ErrorCode.sourceInvalidStreamName,
|
||||
ErrorCode.mirrorInvalidStreamName,
|
||||
ErrorCode.sourceMultipleFiltersNotAllowed,
|
||||
ErrorCode.sourceInvalidSubjectFilter,
|
||||
ErrorCode.sourceInvalidTransformDestination,
|
||||
ErrorCode.sourceOverlappingSubjectFilters,
|
||||
ErrorCode.streamExternalDeletePrefixOverlaps:
|
||||
self = .invalidConfig(err)
|
||||
case ErrorCode.maximumStreamsLimit:
|
||||
self = .maximumStreamsLimit(err)
|
||||
case ErrorCode.streamSealed:
|
||||
self = .streamSealed(err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PublishError: JetStreamErrorProtocol {
|
||||
case streamWrongLastSequence(APIError)
|
||||
case streamWrongLastMessageId(APIError)
|
||||
case streamNotMatch(APIError)
|
||||
case streamNotFound
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .streamWrongLastSequence(let err),
|
||||
.streamWrongLastMessageId(let err),
|
||||
.streamNotMatch(let err):
|
||||
return "nats: \(err.description)"
|
||||
case .streamNotFound:
|
||||
return "nats: stream not found"
|
||||
}
|
||||
}
|
||||
|
||||
internal init?(from err: APIError) {
|
||||
switch err.errorCode {
|
||||
case ErrorCode.streamWrongLastSequence:
|
||||
self = .streamWrongLastSequence(err)
|
||||
case ErrorCode.streamWrongLastMessageId:
|
||||
self = .streamWrongLastMessageId(err)
|
||||
case ErrorCode.streamNotMatch:
|
||||
self = .streamNotMatch(err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConsumerError: JetStreamErrorProtocol {
|
||||
case consumerNotFound(APIError)
|
||||
case maximumConsumersLimit(APIError)
|
||||
case consumerNameExist(APIError)
|
||||
case consumerDoesNotExist(APIError)
|
||||
case invalidConfig(APIError)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .consumerNotFound(let err),
|
||||
.maximumConsumersLimit(let err),
|
||||
.consumerNameExist(let err),
|
||||
.consumerDoesNotExist(let err),
|
||||
.invalidConfig(let err):
|
||||
return "nats: \(err.description)"
|
||||
}
|
||||
}
|
||||
|
||||
internal init?(from err: APIError) {
|
||||
switch err.errorCode {
|
||||
case ErrorCode.consumerNotFound:
|
||||
self = .consumerNotFound(err)
|
||||
case ErrorCode.maximumConsumersLimit:
|
||||
self = .maximumConsumersLimit(err)
|
||||
case ErrorCode.consumerNameExist,
|
||||
ErrorCode.consumerExistingActive,
|
||||
ErrorCode.consumerAlreadyExists:
|
||||
self = .consumerNameExist(err)
|
||||
case ErrorCode.consumerDoesNotExist:
|
||||
self = .consumerDoesNotExist(err)
|
||||
case ErrorCode.consumerDeliverToWildcards,
|
||||
ErrorCode.consumerPushMaxWaiting,
|
||||
ErrorCode.consumerDeliverCycle,
|
||||
ErrorCode.consumerMaxPendingAckPolicyRequired,
|
||||
ErrorCode.consumerSmallHeartbeat,
|
||||
ErrorCode.consumerPullRequiresAck,
|
||||
ErrorCode.consumerPullNotDurable,
|
||||
ErrorCode.consumerPullWithRateLimit,
|
||||
ErrorCode.consumerPullNotDurable,
|
||||
ErrorCode.consumerMaxWaitingNegative,
|
||||
ErrorCode.consumerHeartbeatRequiresPush,
|
||||
ErrorCode.consumerFlowControlRequiresPush,
|
||||
ErrorCode.consumerDirectRequiresPush,
|
||||
ErrorCode.consumerDirectRequiresEphemeral,
|
||||
ErrorCode.consumerOnMapped,
|
||||
ErrorCode.consumerFilterNotSubset,
|
||||
ErrorCode.consumerInvalidPolicy,
|
||||
ErrorCode.consumerInvalidSampling,
|
||||
ErrorCode.consumerWithFlowControlNeedsHeartbeats,
|
||||
ErrorCode.consumerWqRequiresExplicitAck,
|
||||
ErrorCode.consumerWqMultipleUnfiltered,
|
||||
ErrorCode.consumerWqConsumerNotUnique,
|
||||
ErrorCode.consumerWqConsumerNotDeliverAll,
|
||||
ErrorCode.consumerNameTooLong,
|
||||
ErrorCode.consumerBadDurableName,
|
||||
ErrorCode.consumerDescriptionTooLong,
|
||||
ErrorCode.consumerInvalidDeliverSubject,
|
||||
ErrorCode.consumerMaxRequestBatchNegative,
|
||||
ErrorCode.consumerMaxRequestExpiresToSmall,
|
||||
ErrorCode.consumerMaxDeliverBackoff,
|
||||
ErrorCode.consumerMaxPendingAckExcess,
|
||||
ErrorCode.consumerMaxRequestBatchExceeded,
|
||||
ErrorCode.consumerReplicasExceedsStream,
|
||||
ErrorCode.consumerNameContainsPathSeparators,
|
||||
ErrorCode.consumerCreateFilterSubjectMismatch,
|
||||
ErrorCode.consumerCreateDurableAndNameMismatch,
|
||||
ErrorCode.replicasCountCannotBeNegative,
|
||||
ErrorCode.consumerReplicasShouldMatchStream,
|
||||
ErrorCode.consumerMetadataLength,
|
||||
ErrorCode.consumerDuplicateFilterSubjects,
|
||||
ErrorCode.consumerMultipleFiltersNotAllowed,
|
||||
ErrorCode.consumerOverlappingSubjectFilters,
|
||||
ErrorCode.consumerEmptyFilter,
|
||||
ErrorCode.mirrorMultipleFiltersNotAllowed,
|
||||
ErrorCode.mirrorInvalidSubjectFilter,
|
||||
ErrorCode.mirrorOverlappingSubjectFilters,
|
||||
ErrorCode.consumerInactiveThresholdExcess:
|
||||
self = .invalidConfig(err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StreamMessageError: JetStreamErrorProtocol {
|
||||
case deleteSequenceNotFound(APIError)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .deleteSequenceNotFound(let err):
|
||||
return "nats: \(err.description)"
|
||||
}
|
||||
}
|
||||
|
||||
internal init?(from err: APIError) {
|
||||
switch err.errorCode {
|
||||
case ErrorCode.sequenceNotFound:
|
||||
self = .deleteSequenceNotFound(err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DirectGetError: JetStreamErrorProtocol {
|
||||
case invalidResponse(String)
|
||||
case errorResponse(StatusCode, String?)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidResponse(let cause):
|
||||
return "invalid response: \(cause)"
|
||||
case .errorResponse(let code, let description):
|
||||
if let description {
|
||||
return "unable to get message: \(code) \(description)"
|
||||
} else {
|
||||
return "unable to get message: \(code)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ErrorCode: Codable, Equatable {
|
||||
public let rawValue: UInt64
|
||||
/// Peer not a member
|
||||
public static let clusterPeerNotMember = ErrorCode(rawValue: 10040)
|
||||
|
||||
/// Consumer expected to be ephemeral but detected a durable name set in subject
|
||||
public static let consumerEphemeralWithDurable = ErrorCode(rawValue: 10019)
|
||||
|
||||
/// Stream external delivery prefix overlaps with stream subject
|
||||
public static let streamExternalDeletePrefixOverlaps = ErrorCode(rawValue: 10022)
|
||||
|
||||
/// Resource limits exceeded for account
|
||||
public static let accountResourcesExceeded = ErrorCode(rawValue: 10002)
|
||||
|
||||
/// Jetstream system temporarily unavailable
|
||||
public static let clusterNotAvailable = ErrorCode(rawValue: 10008)
|
||||
|
||||
/// Subjects overlap with an existing stream
|
||||
public static let streamSubjectOverlap = ErrorCode(rawValue: 10065)
|
||||
|
||||
/// Wrong last sequence
|
||||
public static let streamWrongLastSequence = ErrorCode(rawValue: 10071)
|
||||
|
||||
/// Template name in subject does not match request
|
||||
public static let nameNotMatchSubject = ErrorCode(rawValue: 10073)
|
||||
|
||||
/// No suitable peers for placement
|
||||
public static let clusterNoPeers = ErrorCode(rawValue: 10005)
|
||||
|
||||
/// Consumer expected to be ephemeral but a durable name was set in request
|
||||
public static let consumerEphemeralWithDurableName = ErrorCode(rawValue: 10020)
|
||||
|
||||
/// Insufficient resources
|
||||
public static let insufficientResources = ErrorCode(rawValue: 10023)
|
||||
|
||||
/// Stream mirror must have max message size >= source
|
||||
public static let mirrorMaxMessageSizeTooBig = ErrorCode(rawValue: 10030)
|
||||
|
||||
/// Generic error from stream deletion operation
|
||||
public static let streamTemplateDeleteFailed = ErrorCode(rawValue: 10067)
|
||||
|
||||
/// Bad request
|
||||
public static let badRequest = ErrorCode(rawValue: 10003)
|
||||
|
||||
/// Not currently supported in clustered mode
|
||||
public static let notSupportedInClusterMode = ErrorCode(rawValue: 10036)
|
||||
|
||||
/// Consumer not found
|
||||
public static let consumerNotFound = ErrorCode(rawValue: 10014)
|
||||
|
||||
/// Stream source must have max message size >= target
|
||||
public static let sourceMaxMessageSizeTooBig = ErrorCode(rawValue: 10046)
|
||||
|
||||
/// Generic error when stream operation fails.
|
||||
public static let streamAssignment = ErrorCode(rawValue: 10048)
|
||||
|
||||
/// Message size exceeds maximum allowed
|
||||
public static let streamMessageExceedsMaximum = ErrorCode(rawValue: 10054)
|
||||
|
||||
/// Generic error for stream creation error with a string
|
||||
public static let streamCreateTemplate = ErrorCode(rawValue: 10066)
|
||||
|
||||
/// Invalid JSON
|
||||
public static let invalidJson = ErrorCode(rawValue: 10025)
|
||||
|
||||
/// Stream external delivery prefix must not contain wildcards
|
||||
public static let streamInvalidExternalDeliverySubject = ErrorCode(rawValue: 10024)
|
||||
|
||||
/// Restore failed
|
||||
public static let streamRestore = ErrorCode(rawValue: 10062)
|
||||
|
||||
/// Incomplete results
|
||||
public static let clusterIncomplete = ErrorCode(rawValue: 10004)
|
||||
|
||||
/// Account not found
|
||||
public static let noAccount = ErrorCode(rawValue: 10035)
|
||||
|
||||
/// General RAFT error
|
||||
public static let raftGeneral = ErrorCode(rawValue: 10041)
|
||||
|
||||
/// Jetstream unable to subscribe to restore snapshot
|
||||
public static let restoreSubscribeFailed = ErrorCode(rawValue: 10042)
|
||||
|
||||
/// Stream deletion failed
|
||||
public static let streamDelete = ErrorCode(rawValue: 10050)
|
||||
|
||||
/// Stream external api prefix must not overlap
|
||||
public static let streamExternalApiOverlap = ErrorCode(rawValue: 10021)
|
||||
|
||||
/// Stream mirrors can not contain subjects
|
||||
public static let mirrorWithSubjects = ErrorCode(rawValue: 10034)
|
||||
|
||||
/// Jetstream not enabled
|
||||
public static let jetstreamNotEnabled = ErrorCode(rawValue: 10076)
|
||||
|
||||
/// Jetstream not enabled for account
|
||||
public static let jetstreamNotEnabledForAccount = ErrorCode(rawValue: 10039)
|
||||
|
||||
/// Sequence not found
|
||||
public static let sequenceNotFound = ErrorCode(rawValue: 10043)
|
||||
|
||||
/// Stream mirror configuration can not be updated
|
||||
public static let streamMirrorNotUpdatable = ErrorCode(rawValue: 10055)
|
||||
|
||||
/// Expected stream sequence does not match
|
||||
public static let streamSequenceNotMatch = ErrorCode(rawValue: 10063)
|
||||
|
||||
/// Wrong last msg id
|
||||
public static let streamWrongLastMessageId = ErrorCode(rawValue: 10070)
|
||||
|
||||
/// Jetstream unable to open temp storage for restore
|
||||
public static let tempStorageFailed = ErrorCode(rawValue: 10072)
|
||||
|
||||
/// Insufficient storage resources available
|
||||
public static let storageResourcesExceeded = ErrorCode(rawValue: 10047)
|
||||
|
||||
/// Stream name in subject does not match request
|
||||
public static let streamMismatch = ErrorCode(rawValue: 10056)
|
||||
|
||||
/// Expected stream does not match
|
||||
public static let streamNotMatch = ErrorCode(rawValue: 10060)
|
||||
|
||||
/// Setting up consumer mirror failed
|
||||
public static let mirrorConsumerSetupFailed = ErrorCode(rawValue: 10029)
|
||||
|
||||
/// Expected an empty request payload
|
||||
public static let notEmptyRequest = ErrorCode(rawValue: 10038)
|
||||
|
||||
/// Stream name already in use with a different configuration
|
||||
public static let streamNameExist = ErrorCode(rawValue: 10058)
|
||||
|
||||
/// Tags placement not supported for operation
|
||||
public static let clusterTags = ErrorCode(rawValue: 10011)
|
||||
|
||||
/// Maximum consumers limit reached
|
||||
public static let maximumConsumersLimit = ErrorCode(rawValue: 10026)
|
||||
|
||||
/// General source consumer setup failure
|
||||
public static let sourceConsumerSetupFailed = ErrorCode(rawValue: 10045)
|
||||
|
||||
/// Consumer creation failed
|
||||
public static let consumerCreate = ErrorCode(rawValue: 10012)
|
||||
|
||||
/// Consumer expected to be durable but no durable name set in subject
|
||||
public static let consumerDurableNameNotInSubject = ErrorCode(rawValue: 10016)
|
||||
|
||||
/// Stream limits error
|
||||
public static let streamLimits = ErrorCode(rawValue: 10053)
|
||||
|
||||
/// Replicas configuration can not be updated
|
||||
public static let streamReplicasNotUpdatable = ErrorCode(rawValue: 10061)
|
||||
|
||||
/// Template not found
|
||||
public static let streamTemplateNotFound = ErrorCode(rawValue: 10068)
|
||||
|
||||
/// Jetstream cluster not assigned to this server
|
||||
public static let clusterNotAssigned = ErrorCode(rawValue: 10007)
|
||||
|
||||
/// Jetstream cluster can't handle request
|
||||
public static let clusterNotLeader = ErrorCode(rawValue: 10009)
|
||||
|
||||
/// Consumer name already in use
|
||||
public static let consumerNameExist = ErrorCode(rawValue: 10013)
|
||||
|
||||
/// Stream mirrors can't also contain other sources
|
||||
public static let mirrorWithSources = ErrorCode(rawValue: 10031)
|
||||
|
||||
/// Stream not found
|
||||
public static let streamNotFound = ErrorCode(rawValue: 10059)
|
||||
|
||||
/// Jetstream clustering support required
|
||||
public static let clusterRequired = ErrorCode(rawValue: 10010)
|
||||
|
||||
/// Consumer expected to be durable but a durable name was not set
|
||||
public static let consumerDurableNameNotSet = ErrorCode(rawValue: 10018)
|
||||
|
||||
/// Maximum number of streams reached
|
||||
public static let maximumStreamsLimit = ErrorCode(rawValue: 10027)
|
||||
|
||||
/// Stream mirrors can not have both start seq and start time configured
|
||||
public static let mirrorWithStartSequenceAndTime = ErrorCode(rawValue: 10032)
|
||||
|
||||
/// Stream snapshot failed
|
||||
public static let streamSnapshot = ErrorCode(rawValue: 10064)
|
||||
|
||||
/// Stream update failed
|
||||
public static let streamUpdate = ErrorCode(rawValue: 10069)
|
||||
|
||||
/// Jetstream not in clustered mode
|
||||
public static let clusterNotActive = ErrorCode(rawValue: 10006)
|
||||
|
||||
/// Consumer name in subject does not match durable name in request
|
||||
public static let consumerDurableNameNotMatchSubject = ErrorCode(rawValue: 10017)
|
||||
|
||||
/// Insufficient memory resources available
|
||||
public static let memoryResourcesExceeded = ErrorCode(rawValue: 10028)
|
||||
|
||||
/// Stream mirrors can not contain filtered subjects
|
||||
public static let mirrorWithSubjectFilters = ErrorCode(rawValue: 10033)
|
||||
|
||||
/// Stream create failed with a string
|
||||
public static let streamCreate = ErrorCode(rawValue: 10049)
|
||||
|
||||
/// Server is not a member of the cluster
|
||||
public static let clusterServerNotMember = ErrorCode(rawValue: 10044)
|
||||
|
||||
/// No message found
|
||||
public static let noMessageFound = ErrorCode(rawValue: 10037)
|
||||
|
||||
/// Deliver subject not valid
|
||||
public static let snapshotDeliverSubjectInvalid = ErrorCode(rawValue: 10015)
|
||||
|
||||
/// General stream failure
|
||||
public static let streamGeneralError = ErrorCode(rawValue: 10051)
|
||||
|
||||
/// Invalid stream config
|
||||
public static let streamInvalidConfig = ErrorCode(rawValue: 10052)
|
||||
|
||||
/// Replicas > 1 not supported in non-clustered mode
|
||||
public static let streamReplicasNotSupported = ErrorCode(rawValue: 10074)
|
||||
|
||||
/// Stream message delete failed
|
||||
public static let streamMessageDeleteFailed = ErrorCode(rawValue: 10057)
|
||||
|
||||
/// Peer remap failed
|
||||
public static let peerRemap = ErrorCode(rawValue: 10075)
|
||||
|
||||
/// Stream store failed
|
||||
public static let streamStoreFailed = ErrorCode(rawValue: 10077)
|
||||
|
||||
/// Consumer config required
|
||||
public static let consumerConfigRequired = ErrorCode(rawValue: 10078)
|
||||
|
||||
/// Consumer deliver subject has wildcards
|
||||
public static let consumerDeliverToWildcards = ErrorCode(rawValue: 10079)
|
||||
|
||||
/// Consumer in push mode can not set max waiting
|
||||
public static let consumerPushMaxWaiting = ErrorCode(rawValue: 10080)
|
||||
|
||||
/// Consumer deliver subject forms a cycle
|
||||
public static let consumerDeliverCycle = ErrorCode(rawValue: 10081)
|
||||
|
||||
/// Consumer requires ack policy for max ack pending
|
||||
public static let consumerMaxPendingAckPolicyRequired = ErrorCode(rawValue: 10082)
|
||||
|
||||
/// Consumer idle heartbeat needs to be >= 100ms
|
||||
public static let consumerSmallHeartbeat = ErrorCode(rawValue: 10083)
|
||||
|
||||
/// Consumer in pull mode requires ack policy
|
||||
public static let consumerPullRequiresAck = ErrorCode(rawValue: 10084)
|
||||
|
||||
/// Consumer in pull mode requires a durable name
|
||||
public static let consumerPullNotDurable = ErrorCode(rawValue: 10085)
|
||||
|
||||
/// Consumer in pull mode can not have rate limit set
|
||||
public static let consumerPullWithRateLimit = ErrorCode(rawValue: 10086)
|
||||
|
||||
/// Consumer max waiting needs to be positive
|
||||
public static let consumerMaxWaitingNegative = ErrorCode(rawValue: 10087)
|
||||
|
||||
/// Consumer idle heartbeat requires a push based consumer
|
||||
public static let consumerHeartbeatRequiresPush = ErrorCode(rawValue: 10088)
|
||||
|
||||
/// Consumer flow control requires a push based consumer
|
||||
public static let consumerFlowControlRequiresPush = ErrorCode(rawValue: 10089)
|
||||
|
||||
/// Consumer direct requires a push based consumer
|
||||
public static let consumerDirectRequiresPush = ErrorCode(rawValue: 10090)
|
||||
|
||||
/// Consumer direct requires an ephemeral consumer
|
||||
public static let consumerDirectRequiresEphemeral = ErrorCode(rawValue: 10091)
|
||||
|
||||
/// Consumer direct on a mapped consumer
|
||||
public static let consumerOnMapped = ErrorCode(rawValue: 10092)
|
||||
|
||||
/// Consumer filter subject is not a valid subset of the interest subjects
|
||||
public static let consumerFilterNotSubset = ErrorCode(rawValue: 10093)
|
||||
|
||||
/// Invalid consumer policy
|
||||
public static let consumerInvalidPolicy = ErrorCode(rawValue: 10094)
|
||||
|
||||
/// Failed to parse consumer sampling configuration
|
||||
public static let consumerInvalidSampling = ErrorCode(rawValue: 10095)
|
||||
|
||||
/// Stream not valid
|
||||
public static let streamInvalid = ErrorCode(rawValue: 10096)
|
||||
|
||||
/// Workqueue stream requires explicit ack
|
||||
public static let consumerWqRequiresExplicitAck = ErrorCode(rawValue: 10098)
|
||||
|
||||
/// Multiple non-filtered consumers not allowed on workqueue stream
|
||||
public static let consumerWqMultipleUnfiltered = ErrorCode(rawValue: 10099)
|
||||
|
||||
/// Filtered consumer not unique on workqueue stream
|
||||
public static let consumerWqConsumerNotUnique = ErrorCode(rawValue: 10100)
|
||||
|
||||
/// Consumer must be deliver all on workqueue stream
|
||||
public static let consumerWqConsumerNotDeliverAll = ErrorCode(rawValue: 10101)
|
||||
|
||||
/// Consumer name is too long
|
||||
public static let consumerNameTooLong = ErrorCode(rawValue: 10102)
|
||||
|
||||
/// Durable name can not contain token separators and wildcards
|
||||
public static let consumerBadDurableName = ErrorCode(rawValue: 10103)
|
||||
|
||||
/// Error creating store for consumer
|
||||
public static let consumerStoreFailed = ErrorCode(rawValue: 10104)
|
||||
|
||||
/// Consumer already exists and is still active
|
||||
public static let consumerExistingActive = ErrorCode(rawValue: 10105)
|
||||
|
||||
/// Consumer replacement durable config not the same
|
||||
public static let consumerReplacementWithDifferentName = ErrorCode(rawValue: 10106)
|
||||
|
||||
/// Consumer description is too long
|
||||
public static let consumerDescriptionTooLong = ErrorCode(rawValue: 10107)
|
||||
|
||||
/// Header size exceeds maximum allowed of 64k
|
||||
public static let streamHeaderExceedsMaximum = ErrorCode(rawValue: 10097)
|
||||
|
||||
/// Consumer with flow control also needs heartbeats
|
||||
public static let consumerWithFlowControlNeedsHeartbeats = ErrorCode(rawValue: 10108)
|
||||
|
||||
/// Invalid operation on sealed stream
|
||||
public static let streamSealed = ErrorCode(rawValue: 10109)
|
||||
|
||||
/// Stream purge failed
|
||||
public static let streamPurgeFailed = ErrorCode(rawValue: 10110)
|
||||
|
||||
/// Stream rollup failed
|
||||
public static let streamRollupFailed = ErrorCode(rawValue: 10111)
|
||||
|
||||
/// Invalid push consumer deliver subject
|
||||
public static let consumerInvalidDeliverSubject = ErrorCode(rawValue: 10112)
|
||||
|
||||
/// Account requires a stream config to have max bytes set
|
||||
public static let streamMaxBytesRequired = ErrorCode(rawValue: 10113)
|
||||
|
||||
/// Consumer max request batch needs to be > 0
|
||||
public static let consumerMaxRequestBatchNegative = ErrorCode(rawValue: 10114)
|
||||
|
||||
/// Consumer max request expires needs to be >= 1ms
|
||||
public static let consumerMaxRequestExpiresToSmall = ErrorCode(rawValue: 10115)
|
||||
|
||||
/// Max deliver is required to be > length of backoff values
|
||||
public static let consumerMaxDeliverBackoff = ErrorCode(rawValue: 10116)
|
||||
|
||||
/// Subject details would exceed maximum allowed
|
||||
public static let streamInfoMaxSubjects = ErrorCode(rawValue: 10117)
|
||||
|
||||
/// Stream is offline
|
||||
public static let streamOffline = ErrorCode(rawValue: 10118)
|
||||
|
||||
/// Consumer is offline
|
||||
public static let consumerOffline = ErrorCode(rawValue: 10119)
|
||||
|
||||
/// No jetstream default or applicable tiered limit present
|
||||
public static let noLimits = ErrorCode(rawValue: 10120)
|
||||
|
||||
/// Consumer max ack pending exceeds system limit
|
||||
public static let consumerMaxPendingAckExcess = ErrorCode(rawValue: 10121)
|
||||
|
||||
/// Stream max bytes exceeds account limit max stream bytes
|
||||
public static let streamMaxStreamBytesExceeded = ErrorCode(rawValue: 10122)
|
||||
|
||||
/// Can not move and scale a stream in a single update
|
||||
public static let streamMoveAndScale = ErrorCode(rawValue: 10123)
|
||||
|
||||
/// Stream move already in progress
|
||||
public static let streamMoveInProgress = ErrorCode(rawValue: 10124)
|
||||
|
||||
/// Consumer max request batch exceeds server limit
|
||||
public static let consumerMaxRequestBatchExceeded = ErrorCode(rawValue: 10125)
|
||||
|
||||
/// Consumer config replica count exceeds parent stream
|
||||
public static let consumerReplicasExceedsStream = ErrorCode(rawValue: 10126)
|
||||
|
||||
/// Consumer name can not contain path separators
|
||||
public static let consumerNameContainsPathSeparators = ErrorCode(rawValue: 10127)
|
||||
|
||||
/// Stream name can not contain path separators
|
||||
public static let streamNameContainsPathSeparators = ErrorCode(rawValue: 10128)
|
||||
|
||||
/// Stream move not in progress
|
||||
public static let streamMoveNotInProgress = ErrorCode(rawValue: 10129)
|
||||
|
||||
/// Stream name already in use, cannot restore
|
||||
public static let streamNameExistRestoreFailed = ErrorCode(rawValue: 10130)
|
||||
|
||||
/// Consumer create request did not match filtered subject from create subject
|
||||
public static let consumerCreateFilterSubjectMismatch = ErrorCode(rawValue: 10131)
|
||||
|
||||
/// Consumer durable and name have to be equal if both are provided
|
||||
public static let consumerCreateDurableAndNameMismatch = ErrorCode(rawValue: 10132)
|
||||
|
||||
/// Replicas count cannot be negative
|
||||
public static let replicasCountCannotBeNegative = ErrorCode(rawValue: 10133)
|
||||
|
||||
/// Consumer config replicas must match interest retention stream's replicas
|
||||
public static let consumerReplicasShouldMatchStream = ErrorCode(rawValue: 10134)
|
||||
|
||||
/// Consumer metadata exceeds maximum size
|
||||
public static let consumerMetadataLength = ErrorCode(rawValue: 10135)
|
||||
|
||||
/// Consumer cannot have both filter_subject and filter_subjects specified
|
||||
public static let consumerDuplicateFilterSubjects = ErrorCode(rawValue: 10136)
|
||||
|
||||
/// Consumer with multiple subject filters cannot use subject based api
|
||||
public static let consumerMultipleFiltersNotAllowed = ErrorCode(rawValue: 10137)
|
||||
|
||||
/// Consumer subject filters cannot overlap
|
||||
public static let consumerOverlappingSubjectFilters = ErrorCode(rawValue: 10138)
|
||||
|
||||
/// Consumer filter in filter_subjects cannot be empty
|
||||
public static let consumerEmptyFilter = ErrorCode(rawValue: 10139)
|
||||
|
||||
/// Duplicate source configuration detected
|
||||
public static let sourceDuplicateDetected = ErrorCode(rawValue: 10140)
|
||||
|
||||
/// Sourced stream name is invalid
|
||||
public static let sourceInvalidStreamName = ErrorCode(rawValue: 10141)
|
||||
|
||||
/// Mirrored stream name is invalid
|
||||
public static let mirrorInvalidStreamName = ErrorCode(rawValue: 10142)
|
||||
|
||||
/// Source with multiple subject transforms cannot also have a single subject filter
|
||||
public static let sourceMultipleFiltersNotAllowed = ErrorCode(rawValue: 10144)
|
||||
|
||||
/// Source subject filter is invalid
|
||||
public static let sourceInvalidSubjectFilter = ErrorCode(rawValue: 10145)
|
||||
|
||||
/// Source transform destination is invalid
|
||||
public static let sourceInvalidTransformDestination = ErrorCode(rawValue: 10146)
|
||||
|
||||
/// Source filters cannot overlap
|
||||
public static let sourceOverlappingSubjectFilters = ErrorCode(rawValue: 10147)
|
||||
|
||||
/// Consumer already exists
|
||||
public static let consumerAlreadyExists = ErrorCode(rawValue: 10148)
|
||||
|
||||
/// Consumer does not exist
|
||||
public static let consumerDoesNotExist = ErrorCode(rawValue: 10149)
|
||||
|
||||
/// Mirror with multiple subject transforms cannot also have a single subject filter
|
||||
public static let mirrorMultipleFiltersNotAllowed = ErrorCode(rawValue: 10150)
|
||||
|
||||
/// Mirror subject filter is invalid
|
||||
public static let mirrorInvalidSubjectFilter = ErrorCode(rawValue: 10151)
|
||||
|
||||
/// Mirror subject filters cannot overlap
|
||||
public static let mirrorOverlappingSubjectFilters = ErrorCode(rawValue: 10152)
|
||||
|
||||
/// Consumer inactive threshold exceeds system limit
|
||||
public static let consumerInactiveThresholdExcess = ErrorCode(rawValue: 10153)
|
||||
|
||||
}
|
||||
|
||||
extension ErrorCode {
|
||||
// Encoding
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
|
||||
// Decoding
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let decodedValue = try container.decode(UInt64.self)
|
||||
self = ErrorCode(rawValue: decodedValue)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Response<T: Codable>: Codable {
|
||||
case success(T)
|
||||
case error(JetStreamAPIResponse)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// Try to decode the expected success type T first
|
||||
if let successResponse = try? container.decode(T.self) {
|
||||
self = .success(successResponse)
|
||||
return
|
||||
}
|
||||
|
||||
// If that fails, try to decode ErrorResponse
|
||||
let errorResponse = try container.decode(JetStreamAPIResponse.self)
|
||||
self = .error(errorResponse)
|
||||
return
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .success(let successData):
|
||||
try container.encode(successData)
|
||||
case .error(let errorData):
|
||||
try container.encode(errorData)
|
||||
}
|
||||
}
|
||||
}
|
||||
193
Sources/JetStream/JetStreamMessage.swift
Normal file
193
Sources/JetStream/JetStreamMessage.swift
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
/// Representation of NATS message in the context of JetStream.
|
||||
/// It exposes message properties (payload, headers etc.) and various methods for acknowledging delivery.
|
||||
/// It also allows for checking message metadata.
|
||||
public struct JetStreamMessage {
|
||||
private let message: NatsMessage
|
||||
|
||||
/// Message payload.
|
||||
public var payload: Data? { message.payload }
|
||||
|
||||
/// Message headers.
|
||||
public var headers: NatsHeaderMap? { message.headers }
|
||||
|
||||
/// The subject the message was published on.
|
||||
public var subject: String { message.subject }
|
||||
|
||||
/// Reply subject used for acking a message.
|
||||
public var reply: String? { message.replySubject }
|
||||
|
||||
internal let client: NatsClient
|
||||
|
||||
private let emptyPayload = "".data(using: .utf8)!
|
||||
|
||||
internal init(message: NatsMessage, client: NatsClient) {
|
||||
self.message = message
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// Sends an acknowledgement of given kind to the server.
|
||||
///
|
||||
/// - Parameter ackType: the type of acknowledgement being sent (defaults to ``AckKind/ack``. For details, see ``AckKind``.
|
||||
/// - Throws:
|
||||
/// - ``JetStreamError/AckError`` if there was an error sending the acknowledgement.
|
||||
public func ack(ackType: AckKind = .ack) async throws {
|
||||
guard let subject = message.replySubject else {
|
||||
throw JetStreamError.AckError.noReplyInMessage
|
||||
}
|
||||
try await client.publish(ackType.payload(), subject: subject)
|
||||
}
|
||||
|
||||
/// Parses the reply subject of the message, exposing JetStream message metadata.
|
||||
///
|
||||
/// - Returns ``MessageMetadata``
|
||||
///
|
||||
/// - Throws:
|
||||
/// - ``JetStreamError/MessageMetadataError`` when there is an error parsing metadata.
|
||||
public func metadata() throws -> MessageMetadata {
|
||||
let prefix = "$JS.ACK."
|
||||
guard let subject = message.replySubject else {
|
||||
throw JetStreamError.MessageMetadataError.noReplyInMessage
|
||||
}
|
||||
if !subject.starts(with: prefix) {
|
||||
throw JetStreamError.MessageMetadataError.invalidPrefix
|
||||
}
|
||||
|
||||
let startIndex = subject.index(subject.startIndex, offsetBy: prefix.count)
|
||||
let parts = subject[startIndex...].split(separator: ".")
|
||||
|
||||
return try MessageMetadata(tokens: parts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents various types of JetStream message acknowledgement.
|
||||
public enum AckKind {
|
||||
/// Normal acknowledgemnt
|
||||
case ack
|
||||
/// Negative ack, message will be redelivered (immediately or after given delay)
|
||||
case nak(delay: TimeInterval? = nil)
|
||||
/// Marks the message as being processed, resets ack wait timer delaying evential redelivery.
|
||||
case inProgress
|
||||
/// Marks the message as terminated, it will never be redelivered.
|
||||
case term(reason: String? = nil)
|
||||
|
||||
func payload() -> Data {
|
||||
switch self {
|
||||
case .ack:
|
||||
return "+ACK".data(using: .utf8)!
|
||||
case .nak(let delay):
|
||||
if let delay {
|
||||
let delayStr = String(Int64(delay * 1_000_000_000))
|
||||
return "-NAK {\"delay\":\(delayStr)}".data(using: .utf8)!
|
||||
} else {
|
||||
return "-NAK".data(using: .utf8)!
|
||||
}
|
||||
case .inProgress:
|
||||
return "+WPI".data(using: .utf8)!
|
||||
case .term(let reason):
|
||||
if let reason {
|
||||
return "+TERM \(reason)".data(using: .utf8)!
|
||||
} else {
|
||||
return "+TERM".data(using: .utf8)!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata of a JetStream message.
|
||||
public struct MessageMetadata {
|
||||
/// The domain this message was received on.
|
||||
public let domain: String?
|
||||
|
||||
/// Optional account hash, present in servers post-ADR-15.
|
||||
public let accountHash: String?
|
||||
|
||||
/// Name of the stream the message is delivered from.
|
||||
public let stream: String
|
||||
|
||||
/// Name of the consumer the mesasge is delivered from.
|
||||
public let consumer: String
|
||||
|
||||
/// Number of delivery attempts of this message.
|
||||
public let delivered: UInt64
|
||||
|
||||
/// Stream sequence associated with this message.
|
||||
public let streamSequence: UInt64
|
||||
|
||||
/// Consumer sequence associated with this message.
|
||||
public let consumerSequence: UInt64
|
||||
|
||||
/// The time this message was received by the server from the publisher.
|
||||
public let timestamp: String
|
||||
|
||||
/// The number of messages known by the server to be pending to this consumer.
|
||||
public let pending: UInt64
|
||||
|
||||
private let v1TokenCount = 7
|
||||
private let v2TokenCount = 9
|
||||
|
||||
init(tokens: [Substring]) throws {
|
||||
if tokens.count >= v2TokenCount {
|
||||
self.domain = String(tokens[0])
|
||||
self.accountHash = String(tokens[1])
|
||||
self.stream = String(tokens[2])
|
||||
self.consumer = String(tokens[3])
|
||||
guard let delivered = UInt64(tokens[4]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.delivered = delivered
|
||||
guard let sseq = UInt64(tokens[5]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.streamSequence = sseq
|
||||
guard let cseq = UInt64(tokens[6]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.consumerSequence = cseq
|
||||
self.timestamp = String(tokens[7])
|
||||
guard let pending = UInt64(tokens[8]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.pending = pending
|
||||
} else if tokens.count == v1TokenCount {
|
||||
self.domain = nil
|
||||
self.accountHash = nil
|
||||
self.stream = String(tokens[0])
|
||||
self.consumer = String(tokens[1])
|
||||
guard let delivered = UInt64(tokens[2]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.delivered = delivered
|
||||
guard let sseq = UInt64(tokens[3]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.streamSequence = sseq
|
||||
guard let cseq = UInt64(tokens[4]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.consumerSequence = cseq
|
||||
self.timestamp = String(tokens[5])
|
||||
guard let pending = UInt64(tokens[6]) else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenValue
|
||||
}
|
||||
self.pending = pending
|
||||
} else {
|
||||
throw JetStreamError.MessageMetadataError.invalidTokenNum
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Sources/JetStream/NanoTimeInterval.swift
Normal file
39
Sources/JetStream/NanoTimeInterval.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// `NanoTimeInterval` represents a time interval in nanoseconds, facilitating high precision time measurements.
|
||||
public struct NanoTimeInterval: Codable, Equatable {
|
||||
/// The value of the time interval in seconds.
|
||||
var value: TimeInterval
|
||||
|
||||
public init(_ timeInterval: TimeInterval) {
|
||||
self.value = timeInterval
|
||||
}
|
||||
|
||||
/// Initializes a `NanoTimeInterval` from a decoder, assuming the encoded value is in nanoseconds.
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let nanoseconds = try container.decode(Double.self)
|
||||
self.value = nanoseconds / 1_000_000_000.0
|
||||
}
|
||||
|
||||
/// Encodes this `NanoTimeInterval` into a given encoder, converting the time interval from seconds to nanoseconds.
|
||||
/// This method allows `NanoTimeInterval` to be serialized directly into a format that stores time in nanoseconds.
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
let nanoseconds = self.value * 1_000_000_000.0
|
||||
try container.encode(nanoseconds)
|
||||
}
|
||||
}
|
||||
118
Sources/JetStream/Stream+Consumer.swift
Normal file
118
Sources/JetStream/Stream+Consumer.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Stream {
|
||||
|
||||
/// Creates a consumer with the specified configuration.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - cfg: consumer config
|
||||
///
|
||||
/// - Returns: ``Consumer`` object containing ``ConsumerConfig`` and exposing operations on the consumer
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError``: if there was am error creating the stream. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/ConsumerError/invalidConfig(_:)``: if the provided configuration is not valid.
|
||||
/// > - ``JetStreamError/ConsumerError/consumerNameExist(_:)``: if attempting to overwrite an existing consumer (with different configuration)
|
||||
/// > - ``JetStreamError/ConsumerError/maximumConsumersLimit(_:)``: if a max number of consumers (specified on stream/account level) has been reached.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func createConsumer(cfg: ConsumerConfig) async throws -> Consumer {
|
||||
return try await ctx.upsertConsumer(stream: info.config.name, cfg: cfg, action: "create")
|
||||
}
|
||||
|
||||
/// Updates an existing consumer using specified config.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - cfg: consumer config
|
||||
///
|
||||
/// - Returns: ``Consumer`` object containing ``ConsumerConfig`` and exposing operations on the consumer
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError``: if there was am error creating the stream. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/ConsumerError/invalidConfig(_:)``: if the provided configuration is not valid or atteppting to update an illegal property
|
||||
/// > - ``JetStreamError/ConsumerError/consumerDoesNotExist(_:)``: if attempting to update a non-existing consumer
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func updateConsumer(cfg: ConsumerConfig) async throws -> Consumer {
|
||||
return try await ctx.upsertConsumer(stream: info.config.name, cfg: cfg, action: "update")
|
||||
}
|
||||
|
||||
/// Creates a consumer with the specified configuration or updates an existing consumer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: name of the stream the consumer will be created on
|
||||
/// - cfg: consumer config
|
||||
///
|
||||
/// - Returns: ``Consumer`` object containing ``ConsumerConfig`` and exposing operations on the consumer
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError``: if there was am error creating the stream. There are several errors which may occur, most common being:
|
||||
/// > - ``JetStreamError/ConsumerError/invalidConfig(_:)``: if the provided configuration is not valid or atteppting to update an illegal property
|
||||
/// > - ``JetStreamError/ConsumerError/maximumConsumersLimit(_:)``: if a max number of consumers (specified on stream/account level) has been reached.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func createOrUpdateConsumer(cfg: ConsumerConfig) async throws -> Consumer {
|
||||
return try await ctx.upsertConsumer(stream: info.config.name, cfg: cfg)
|
||||
}
|
||||
|
||||
/// Retrieves a consumer with given name from a stream.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: name of the stream
|
||||
///
|
||||
/// - Returns a ``Stream`` object containing ``StreamInfo`` and exposing operations on the stream or nil if stream with given name does not exist.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError/consumerNotFound(_:)``: if the consumer with given name does not exist on a given stream.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func getConsumer(name: String) async throws -> Consumer? {
|
||||
return try await ctx.getConsumer(stream: info.config.name, name: name)
|
||||
}
|
||||
|
||||
/// Deletes a consumer from a stream.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: consumer name
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``JetStreamError/ConsumerError/consumerNotFound(_:)``: if the consumer with given name does not exist on a given stream.
|
||||
/// > - ``JetStreamError/RequestError``: if the request fails if e.g. JetStream is not enabled.
|
||||
/// > - ``JetStreamError/APIError``: if there was a different API error returned from JetStream.
|
||||
public func deleteConsumer(name: String) async throws {
|
||||
try await ctx.deleteConsumer(stream: info.config.name, name: name)
|
||||
}
|
||||
|
||||
/// Used to list consumer names.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: the name of the strem to list the consumers from.
|
||||
///
|
||||
/// - Returns ``Consumers`` which implements AsyncSequence allowing iteration over stream infos.
|
||||
public func consumers() async -> Consumers {
|
||||
return Consumers(ctx: ctx, stream: info.config.name)
|
||||
}
|
||||
|
||||
/// Used to list consumer names.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - stream: the name of the strem to list the consumers from.
|
||||
///
|
||||
/// - Returns ``ConsumerNames`` which implements AsyncSequence allowing iteration over consumer names.
|
||||
public func consumerNames() async -> ConsumerNames {
|
||||
return ConsumerNames(ctx: ctx, stream: info.config.name)
|
||||
}
|
||||
}
|
||||
1031
Sources/JetStream/Stream.swift
Normal file
1031
Sources/JetStream/Stream.swift
Normal file
File diff suppressed because it is too large
Load Diff
128
Sources/Nats/BatchBuffer.swift
Normal file
128
Sources/Nats/BatchBuffer.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import NIO
|
||||
import NIOConcurrencyHelpers
|
||||
|
||||
extension BatchBuffer {
|
||||
struct State {
|
||||
private var buffer: ByteBuffer
|
||||
private var allocator: ByteBufferAllocator
|
||||
var waitingPromises: [(ClientOp, UnsafeContinuation<Void, Error>)] = []
|
||||
var isWriteInProgress: Bool = false
|
||||
|
||||
internal init(allocator: ByteBufferAllocator, batchSize: Int = 16 * 1024) {
|
||||
self.allocator = allocator
|
||||
self.buffer = allocator.buffer(capacity: batchSize)
|
||||
}
|
||||
|
||||
var readableBytes: Int {
|
||||
return self.buffer.readableBytes
|
||||
}
|
||||
|
||||
mutating func clear() {
|
||||
buffer.clear()
|
||||
}
|
||||
|
||||
mutating func getWriteBuffer() -> ByteBuffer {
|
||||
var writeBuffer = allocator.buffer(capacity: buffer.readableBytes)
|
||||
writeBuffer.writeBytes(buffer.readableBytesView)
|
||||
buffer.clear()
|
||||
|
||||
return writeBuffer
|
||||
}
|
||||
|
||||
mutating func writeMessage(_ message: ClientOp) {
|
||||
self.buffer.writeClientOp(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class BatchBuffer {
|
||||
private let batchSize: Int
|
||||
private let channel: Channel
|
||||
private let state: NIOLockedValueBox<State>
|
||||
|
||||
init(channel: Channel, batchSize: Int = 16 * 1024) {
|
||||
self.batchSize = batchSize
|
||||
self.channel = channel
|
||||
self.state = .init(
|
||||
State(allocator: channel.allocator)
|
||||
)
|
||||
}
|
||||
|
||||
func writeMessage(_ message: ClientOp) async throws {
|
||||
#if SWIFT_NATS_BATCH_BUFFER_DISABLED
|
||||
let b = channel.allocator.buffer(bytes: data)
|
||||
try await channel.writeAndFlush(b)
|
||||
#else
|
||||
// Batch writes and if we have more than the batch size
|
||||
// already in the buffer await until buffer is flushed
|
||||
// to handle any back pressure
|
||||
try await withUnsafeThrowingContinuation { continuation in
|
||||
self.state.withLockedValue { state in
|
||||
guard state.readableBytes < self.batchSize else {
|
||||
state.waitingPromises.append((message, continuation))
|
||||
return
|
||||
}
|
||||
|
||||
state.writeMessage(message)
|
||||
self.flushWhenIdle(state: &state)
|
||||
continuation.resume()
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func flushWhenIdle(state: inout State) {
|
||||
// The idea is to keep writing to the buffer while a writeAndFlush() is
|
||||
// in progress, so we can batch as many messages as possible.
|
||||
guard !state.isWriteInProgress else {
|
||||
return
|
||||
}
|
||||
// We need a separate write buffer so we can free the message buffer for more
|
||||
// messages to be collected.
|
||||
let writeBuffer = state.getWriteBuffer()
|
||||
state.isWriteInProgress = true
|
||||
|
||||
let writePromise = self.channel.eventLoop.makePromise(of: Void.self)
|
||||
writePromise.futureResult.whenComplete { result in
|
||||
self.state.withLockedValue { state in
|
||||
state.isWriteInProgress = false
|
||||
switch result {
|
||||
case .success:
|
||||
for (message, continuation) in state.waitingPromises {
|
||||
state.writeMessage(message)
|
||||
continuation.resume()
|
||||
}
|
||||
state.waitingPromises.removeAll()
|
||||
case .failure(let error):
|
||||
for (_, continuation) in state.waitingPromises {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
state.waitingPromises.removeAll()
|
||||
state.clear()
|
||||
}
|
||||
|
||||
// Check if there are any pending flushes
|
||||
if state.readableBytes > 0 {
|
||||
self.flushWhenIdle(state: &state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.channel.writeAndFlush(writeBuffer, promise: writePromise)
|
||||
}
|
||||
}
|
||||
32
Sources/Nats/ConcurrentQueue.swift
Normal file
32
Sources/Nats/ConcurrentQueue.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import NIOConcurrencyHelpers
|
||||
|
||||
internal class ConcurrentQueue<T> {
|
||||
private var elements: [T] = []
|
||||
private let lock = NIOLock()
|
||||
|
||||
func enqueue(_ element: T) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
elements.append(element)
|
||||
}
|
||||
|
||||
func dequeue() -> T? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
guard !elements.isEmpty else { return nil }
|
||||
return elements.removeFirst()
|
||||
}
|
||||
}
|
||||
87
Sources/Nats/Extensions/ByteBuffer+Writer.swift
Normal file
87
Sources/Nats/Extensions/ByteBuffer+Writer.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import NIO
|
||||
|
||||
extension ByteBuffer {
|
||||
mutating func writeClientOp(_ op: ClientOp) {
|
||||
switch op {
|
||||
case .publish((let subject, let reply, let payload, let headers)):
|
||||
if let payload = payload {
|
||||
self.reserveCapacity(
|
||||
minimumWritableBytes: payload.count + subject.utf8.count
|
||||
+ NatsOperation.publish.rawValue.count + 12)
|
||||
if headers != nil {
|
||||
self.writeBytes(NatsOperation.hpublish.rawBytes)
|
||||
} else {
|
||||
self.writeBytes(NatsOperation.publish.rawBytes)
|
||||
}
|
||||
self.writeString(" ")
|
||||
self.writeString(subject)
|
||||
self.writeString(" ")
|
||||
if let reply = reply {
|
||||
self.writeString("\(reply) ")
|
||||
}
|
||||
if let headers = headers {
|
||||
let headers = headers.toBytes()
|
||||
let totalLen = headers.count + payload.count
|
||||
let headersLen = headers.count
|
||||
self.writeString("\(headersLen) \(totalLen)\r\n")
|
||||
self.writeData(headers)
|
||||
} else {
|
||||
self.writeString("\(payload.count)\r\n")
|
||||
}
|
||||
self.writeData(payload)
|
||||
self.writeString("\r\n")
|
||||
} else {
|
||||
self.reserveCapacity(
|
||||
minimumWritableBytes: subject.utf8.count + NatsOperation.publish.rawValue.count
|
||||
+ 12)
|
||||
self.writeBytes(NatsOperation.publish.rawBytes)
|
||||
self.writeString(" ")
|
||||
self.writeString(subject)
|
||||
if let reply = reply {
|
||||
self.writeString("\(reply) ")
|
||||
}
|
||||
self.writeString("\r\n")
|
||||
}
|
||||
|
||||
case .subscribe((let sid, let subject, let queue)):
|
||||
if let queue {
|
||||
self.writeString(
|
||||
"\(NatsOperation.subscribe.rawValue) \(subject) \(queue) \(sid)\r\n")
|
||||
} else {
|
||||
self.writeString("\(NatsOperation.subscribe.rawValue) \(subject) \(sid)\r\n")
|
||||
}
|
||||
|
||||
case .unsubscribe((let sid, let max)):
|
||||
if let max {
|
||||
self.writeString("\(NatsOperation.unsubscribe.rawValue) \(sid) \(max)\r\n")
|
||||
} else {
|
||||
self.writeString("\(NatsOperation.unsubscribe.rawValue) \(sid)\r\n")
|
||||
}
|
||||
case .connect(let info):
|
||||
// This encode can't actually fail
|
||||
let json = try! JSONEncoder().encode(info)
|
||||
self.reserveCapacity(minimumWritableBytes: json.count + 5)
|
||||
self.writeString("\(NatsOperation.connect.rawValue) ")
|
||||
self.writeData(json)
|
||||
self.writeString("\r\n")
|
||||
case .ping:
|
||||
self.writeString("\(NatsOperation.ping.rawValue)\r\n")
|
||||
case .pong:
|
||||
self.writeString("\(NatsOperation.pong.rawValue)\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Sources/Nats/Extensions/Data+Base64.swift
Normal file
24
Sources/Nats/Extensions/Data+Base64.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
/// Swift does not provide a way to encode data to base64 without padding in URL safe way.
|
||||
func base64EncodedURLSafeNotPadded() -> String {
|
||||
return self.base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.trimmingCharacters(in: CharacterSet(charactersIn: "="))
|
||||
}
|
||||
}
|
||||
184
Sources/Nats/Extensions/Data+Parser.swift
Normal file
184
Sources/Nats/Extensions/Data+Parser.swift
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
private static let cr = UInt8(ascii: "\r")
|
||||
private static let lf = UInt8(ascii: "\n")
|
||||
private static let crlf = Data([cr, lf])
|
||||
private static var currentNum = 0
|
||||
private static var errored = false
|
||||
internal static let versionLinePrefix = "NATS/1.0"
|
||||
|
||||
func removePrefix(_ prefix: Data) -> Data {
|
||||
guard self.starts(with: prefix) else { return self }
|
||||
return self.dropFirst(prefix.count)
|
||||
}
|
||||
|
||||
func split(
|
||||
separator: Data, maxSplits: Int = .max, omittingEmptySubsequences: Bool = true
|
||||
)
|
||||
-> [Data]
|
||||
{
|
||||
var chunks: [Data] = []
|
||||
var start = startIndex
|
||||
var end = startIndex
|
||||
var splitsCount = 0
|
||||
|
||||
while end < count {
|
||||
if splitsCount >= maxSplits {
|
||||
break
|
||||
}
|
||||
if self[start..<end].elementsEqual(separator) {
|
||||
if !omittingEmptySubsequences || start != end {
|
||||
chunks.append(self[start..<end])
|
||||
}
|
||||
start = index(end, offsetBy: separator.count)
|
||||
end = start
|
||||
splitsCount += 1
|
||||
continue
|
||||
}
|
||||
end = index(after: end)
|
||||
}
|
||||
|
||||
if start <= endIndex {
|
||||
if !omittingEmptySubsequences || start != endIndex {
|
||||
chunks.append(self[start..<endIndex])
|
||||
}
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
func getMessageType() -> NatsOperation? {
|
||||
guard self.count > 2 else { return nil }
|
||||
for operation in NatsOperation.allOperations() {
|
||||
if self.starts(with: operation.rawBytes) {
|
||||
return operation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func starts(with bytes: [UInt8]) -> Bool {
|
||||
guard self.count >= bytes.count else { return false }
|
||||
return self.prefix(bytes.count).elementsEqual(bytes)
|
||||
}
|
||||
|
||||
internal mutating func prepend(_ other: Data) {
|
||||
self = other + self
|
||||
}
|
||||
|
||||
internal func parseOutMessages() throws -> (ops: [ServerOp], remainder: Data?) {
|
||||
var serverOps = [ServerOp]()
|
||||
var startIndex = self.startIndex
|
||||
var remainder: Data?
|
||||
|
||||
while startIndex < self.endIndex {
|
||||
var nextLineStartIndex: Int
|
||||
var lineData: Data
|
||||
if let range = self[startIndex...].range(of: Data.crlf) {
|
||||
let lineEndIndex = range.lowerBound
|
||||
nextLineStartIndex =
|
||||
self.index(range.upperBound, offsetBy: 0, limitedBy: self.endIndex)
|
||||
?? self.endIndex
|
||||
lineData = self[startIndex..<lineEndIndex]
|
||||
} else {
|
||||
remainder = self[startIndex..<self.endIndex]
|
||||
break
|
||||
}
|
||||
if lineData.count == 0 {
|
||||
startIndex = nextLineStartIndex
|
||||
continue
|
||||
}
|
||||
|
||||
let serverOp = try ServerOp.parse(from: lineData)
|
||||
|
||||
// if it's a message, get the full payload and add to returned data
|
||||
if case .message(var msg) = serverOp {
|
||||
if msg.length == 0 {
|
||||
serverOps.append(serverOp)
|
||||
} else {
|
||||
var payload = Data()
|
||||
let payloadEndIndex = nextLineStartIndex + msg.length
|
||||
let payloadStartIndex = nextLineStartIndex
|
||||
// include crlf in the expected payload leangth
|
||||
if payloadEndIndex + Data.crlf.count > endIndex {
|
||||
remainder = self[startIndex..<self.endIndex]
|
||||
break
|
||||
}
|
||||
payload.append(self[payloadStartIndex..<payloadEndIndex])
|
||||
msg.payload = payload
|
||||
startIndex =
|
||||
self.index(
|
||||
payloadEndIndex, offsetBy: Data.crlf.count, limitedBy: self.endIndex)
|
||||
?? self.endIndex
|
||||
serverOps.append(.message(msg))
|
||||
continue
|
||||
}
|
||||
//TODO(jrm): Add HMSG handling here too.
|
||||
} else if case .hMessage(var msg) = serverOp {
|
||||
if msg.length == 0 {
|
||||
serverOps.append(serverOp)
|
||||
} else {
|
||||
let headersStartIndex = nextLineStartIndex
|
||||
let headersEndIndex = nextLineStartIndex + msg.headersLength
|
||||
let payloadStartIndex = headersEndIndex
|
||||
let payloadEndIndex = nextLineStartIndex + msg.length
|
||||
|
||||
var payload: Data?
|
||||
if msg.length > msg.headersLength {
|
||||
payload = Data()
|
||||
}
|
||||
var headers = NatsHeaderMap()
|
||||
|
||||
// if the whole msg length (including training crlf) is longer
|
||||
// than the remaining chunk, break and return the remainder
|
||||
if payloadEndIndex + Data.crlf.count > endIndex {
|
||||
remainder = self[startIndex..<self.endIndex]
|
||||
break
|
||||
}
|
||||
|
||||
let headersData = self[headersStartIndex..<headersEndIndex]
|
||||
if let headersString = String(data: headersData, encoding: .utf8) {
|
||||
headers = try NatsHeaderMap(from: headersString)
|
||||
}
|
||||
msg.status = headers.status
|
||||
msg.description = headers.description
|
||||
msg.headers = headers
|
||||
|
||||
if var payload = payload {
|
||||
payload.append(self[payloadStartIndex..<payloadEndIndex])
|
||||
msg.payload = payload
|
||||
}
|
||||
|
||||
startIndex =
|
||||
self.index(
|
||||
payloadEndIndex, offsetBy: Data.crlf.count, limitedBy: self.endIndex)
|
||||
?? self.endIndex
|
||||
serverOps.append(.hMessage(msg))
|
||||
continue
|
||||
}
|
||||
|
||||
} else {
|
||||
// otherwise, just add this server op to the result
|
||||
serverOps.append(serverOp)
|
||||
}
|
||||
startIndex = nextLineStartIndex
|
||||
|
||||
}
|
||||
|
||||
return (serverOps, remainder)
|
||||
}
|
||||
}
|
||||
24
Sources/Nats/Extensions/Data+String.swift
Normal file
24
Sources/Nats/Extensions/Data+String.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import NIOPosix
|
||||
|
||||
extension Data {
|
||||
func toString() -> String? {
|
||||
if let str = String(data: self, encoding: .utf8) {
|
||||
return str
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
38
Sources/Nats/Extensions/String+Utilities.swift
Normal file
38
Sources/Nats/Extensions/String+Utilities.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
private static let charactersToTrim: CharacterSet = .whitespacesAndNewlines.union(
|
||||
CharacterSet(charactersIn: "'"))
|
||||
|
||||
static func hash() -> String {
|
||||
let uuid = String.uuid()
|
||||
return uuid[0...7]
|
||||
}
|
||||
|
||||
func trimWhitespacesAndApostrophes() -> String {
|
||||
return self.trimmingCharacters(in: String.charactersToTrim)
|
||||
}
|
||||
|
||||
static func uuid() -> String {
|
||||
return UUID().uuidString.trimmingCharacters(in: .punctuationCharacters)
|
||||
}
|
||||
|
||||
subscript(bounds: CountableClosedRange<Int>) -> String {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
return String(self[start...end])
|
||||
}
|
||||
}
|
||||
182
Sources/Nats/HTTPUpgradeRequestHandler.swift
Normal file
182
Sources/Nats/HTTPUpgradeRequestHandler.swift
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import NIO
|
||||
import NIOHTTP1
|
||||
import NIOWebSocket
|
||||
|
||||
// Adapted from https://github.com/vapor/websocket-kit/blob/main/Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
|
||||
internal final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHandler {
|
||||
typealias InboundIn = HTTPClientResponsePart
|
||||
typealias OutboundOut = HTTPClientRequestPart
|
||||
|
||||
let host: String
|
||||
let path: String
|
||||
let query: String?
|
||||
let headers: HTTPHeaders
|
||||
let upgradePromise: EventLoopPromise<Void>
|
||||
|
||||
private var requestSent = false
|
||||
|
||||
init(
|
||||
host: String, path: String, query: String?, headers: HTTPHeaders,
|
||||
upgradePromise: EventLoopPromise<Void>
|
||||
) {
|
||||
self.host = host
|
||||
self.path = path
|
||||
self.query = query
|
||||
self.headers = headers
|
||||
self.upgradePromise = upgradePromise
|
||||
}
|
||||
|
||||
func channelActive(context: ChannelHandlerContext) {
|
||||
self.sendRequest(context: context)
|
||||
context.fireChannelActive()
|
||||
}
|
||||
|
||||
func handlerAdded(context: ChannelHandlerContext) {
|
||||
if context.channel.isActive {
|
||||
self.sendRequest(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendRequest(context: ChannelHandlerContext) {
|
||||
if self.requestSent {
|
||||
// we might run into this handler twice, once in handlerAdded and once in channelActive.
|
||||
return
|
||||
}
|
||||
self.requestSent = true
|
||||
|
||||
var headers = self.headers
|
||||
headers.add(name: "Host", value: self.host)
|
||||
|
||||
var uri: String
|
||||
if self.path.hasPrefix("/") || self.path.hasPrefix("ws://") || self.path.hasPrefix("wss://")
|
||||
{
|
||||
uri = self.path
|
||||
} else {
|
||||
uri = "/" + self.path
|
||||
}
|
||||
|
||||
if let query = self.query {
|
||||
uri += "?\(query)"
|
||||
}
|
||||
|
||||
let requestHead = HTTPRequestHead(
|
||||
version: HTTPVersion(major: 1, minor: 1),
|
||||
method: .GET,
|
||||
uri: uri,
|
||||
headers: headers
|
||||
)
|
||||
context.write(self.wrapOutboundOut(.head(requestHead)), promise: nil)
|
||||
|
||||
let emptyBuffer = context.channel.allocator.buffer(capacity: 0)
|
||||
let body = HTTPClientRequestPart.body(.byteBuffer(emptyBuffer))
|
||||
context.write(self.wrapOutboundOut(body), promise: nil)
|
||||
|
||||
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
|
||||
}
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
// `NIOHTTPClientUpgradeHandler` should consume the first response in the success case,
|
||||
// any response we see here indicates a failure. Report the failure and tidy up at the end of the response.
|
||||
let clientResponse = self.unwrapInboundIn(data)
|
||||
switch clientResponse {
|
||||
case .head(let responseHead):
|
||||
self.upgradePromise.fail(
|
||||
NatsError.ClientError.invalidConnection("ws error \(responseHead)"))
|
||||
case .body: break
|
||||
case .end:
|
||||
context.close(promise: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
||||
self.upgradePromise.fail(error)
|
||||
context.close(promise: nil)
|
||||
}
|
||||
}
|
||||
|
||||
internal final class WebSocketByteBufferCodec: ChannelDuplexHandler {
|
||||
typealias InboundIn = WebSocketFrame
|
||||
typealias InboundOut = ByteBuffer
|
||||
typealias OutboundIn = ByteBuffer
|
||||
typealias OutboundOut = WebSocketFrame
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
let frame = unwrapInboundIn(data)
|
||||
|
||||
switch frame.opcode {
|
||||
case .binary:
|
||||
context.fireChannelRead(wrapInboundOut(frame.data))
|
||||
case .text:
|
||||
preconditionFailure("We will never receive a text frame")
|
||||
case .continuation:
|
||||
preconditionFailure("We will never receive a continuation frame")
|
||||
case .pong:
|
||||
break
|
||||
case .ping:
|
||||
if frame.fin {
|
||||
var frameData = frame.data
|
||||
let maskingKey = frame.maskKey
|
||||
if let maskingKey = maskingKey {
|
||||
frameData.webSocketUnmask(maskingKey)
|
||||
}
|
||||
let bb = context.channel.allocator.buffer(bytes: frameData.readableBytesView)
|
||||
self.send(
|
||||
bb,
|
||||
context: context,
|
||||
opcode: .pong
|
||||
)
|
||||
} else {
|
||||
context.close(promise: nil)
|
||||
}
|
||||
default:
|
||||
// We ignore all other frames.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
let buffer = unwrapOutboundIn(data)
|
||||
let frame = WebSocketFrame(
|
||||
fin: true,
|
||||
opcode: .binary,
|
||||
maskKey: self.makeMaskKey(),
|
||||
data: buffer
|
||||
)
|
||||
context.write(wrapOutboundOut(frame), promise: promise)
|
||||
}
|
||||
|
||||
public func send(
|
||||
_ data: ByteBuffer,
|
||||
context: ChannelHandlerContext,
|
||||
opcode: WebSocketOpcode = .binary,
|
||||
fin: Bool = true,
|
||||
promise: EventLoopPromise<Void>? = nil
|
||||
) {
|
||||
let frame = WebSocketFrame(
|
||||
fin: fin,
|
||||
opcode: opcode,
|
||||
maskKey: self.makeMaskKey(),
|
||||
data: data
|
||||
)
|
||||
context.writeAndFlush(wrapOutboundOut(frame), promise: promise)
|
||||
}
|
||||
|
||||
func makeMaskKey() -> WebSocketMaskingKey? {
|
||||
/// See https://github.com/apple/swift/issues/66099
|
||||
var generator = SystemRandomNumberGenerator()
|
||||
return WebSocketMaskingKey.random(using: &generator)
|
||||
}
|
||||
}
|
||||
57
Sources/Nats/NatsClient/NatsClient+Events.swift
Normal file
57
Sources/Nats/NatsClient/NatsClient+Events.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension NatsClient {
|
||||
|
||||
/// Registers a callback for given event types.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - events: an array of ``NatsEventKind`` for which the handler will be invoked.
|
||||
/// - handler: a callback invoked upon triggering a specific event.
|
||||
///
|
||||
/// - Returns an ID of the registered listener which can be used to disable it.
|
||||
@discardableResult
|
||||
public func on(_ events: [NatsEventKind], _ handler: @escaping (NatsEvent) -> Void) -> String {
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
return ""
|
||||
}
|
||||
return connectionHandler.addListeners(for: events, using: handler)
|
||||
}
|
||||
|
||||
/// Registers a callback for given event type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - events: a ``NatsEventKind`` for which the handler will be invoked.
|
||||
/// - handler: a callback invoked upon triggering a specific event.
|
||||
///
|
||||
/// - Returns an ID of the registered listener which can be used to disable it.
|
||||
@discardableResult
|
||||
public func on(_ event: NatsEventKind, _ handler: @escaping (NatsEvent) -> Void) -> String {
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
return ""
|
||||
}
|
||||
return connectionHandler.addListeners(for: [event], using: handler)
|
||||
}
|
||||
|
||||
/// Disables the event listener.
|
||||
///
|
||||
/// - Parameter id: an ID of a listener to be disabled (returned when creating it).
|
||||
public func off(_ id: String) {
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
return
|
||||
}
|
||||
connectionHandler.removeListener(id)
|
||||
}
|
||||
}
|
||||
352
Sources/Nats/NatsClient/NatsClient.swift
Executable file
352
Sources/Nats/NatsClient/NatsClient.swift
Executable file
@@ -0,0 +1,352 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
import Nuid
|
||||
|
||||
public var logger = Logger(label: "Nats")
|
||||
|
||||
/// NatsClient connection states
|
||||
public enum NatsState {
|
||||
case pending
|
||||
case connecting
|
||||
case connected
|
||||
case disconnected
|
||||
case closed
|
||||
case suspended
|
||||
}
|
||||
|
||||
public struct Auth {
|
||||
var user: String?
|
||||
var password: String?
|
||||
var token: String?
|
||||
var credentialsPath: URL?
|
||||
var nkeyPath: URL?
|
||||
var nkey: String?
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
init(user: String, password: String) {
|
||||
self.user = user
|
||||
self.password = password
|
||||
}
|
||||
init(token: String) {
|
||||
self.token = token
|
||||
}
|
||||
static func fromCredentials(_ credentials: URL) -> Auth {
|
||||
var auth = Auth()
|
||||
auth.credentialsPath = credentials
|
||||
return auth
|
||||
}
|
||||
static func fromNkey(_ nkey: URL) -> Auth {
|
||||
var auth = Auth()
|
||||
auth.nkeyPath = nkey
|
||||
return auth
|
||||
}
|
||||
static func fromNkey(_ nkey: String) -> Auth {
|
||||
var auth = Auth()
|
||||
auth.nkey = nkey
|
||||
return auth
|
||||
}
|
||||
}
|
||||
|
||||
public class NatsClient {
|
||||
public var connectedUrl: URL? {
|
||||
connectionHandler?.connectedUrl
|
||||
}
|
||||
internal let allocator = ByteBufferAllocator()
|
||||
internal var buffer: ByteBuffer
|
||||
internal var connectionHandler: ConnectionHandler?
|
||||
internal var inboxPrefix: String = "_INBOX."
|
||||
|
||||
internal init() {
|
||||
self.buffer = allocator.buffer(capacity: 1024)
|
||||
}
|
||||
|
||||
/// Returns a new inbox subject using the configured prefix and a generated NUID.
|
||||
public func newInbox() -> String {
|
||||
return inboxPrefix + nextNuid()
|
||||
}
|
||||
}
|
||||
|
||||
extension NatsClient {
|
||||
|
||||
/// Connects to a NATS server using configuration provided via ``NatsClientOptions``.
|
||||
/// If ``NatsClientOptions/retryOnfailedConnect()`` is used, `connect()`
|
||||
/// will not wait until the connection is established but rather return immediatelly.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``NatsError/ConnectError/invalidConfig(_:)`` if the provided configuration is invalid
|
||||
/// > - ``NatsError/ConnectError/tlsFailure(_:)`` if upgrading to TLS connection fails
|
||||
/// > - ``NatsError/ConnectError/timeout`` if there was a timeout waiting to establish TCP connection
|
||||
/// > - ``NatsError/ConnectError/dns(_:)`` if there was an error during dns lookup
|
||||
/// > - ``NatsError/ConnectError/io`` if there was other error establishing connection
|
||||
/// > - ``NatsError/ServerError/autorization(_:)`` if connection could not be established due to invalid/missing/expired auth
|
||||
/// > - ``NatsError/ServerError/other(_:)`` if the server responds to client connection with a different error (e.g. max connections exceeded)
|
||||
public func connect() async throws {
|
||||
logger.debug("connect")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
|
||||
// Check if already connected or in invalid state for connect()
|
||||
let currentState = connectionHandler.currentState
|
||||
switch currentState {
|
||||
case .connected, .connecting:
|
||||
throw NatsError.ClientError.alreadyConnected
|
||||
case .closed:
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
case .suspended:
|
||||
throw NatsError.ClientError.invalidConnection(
|
||||
"connection is suspended, use resume() instead")
|
||||
case .pending, .disconnected:
|
||||
// These states allow connection/reconnection
|
||||
break
|
||||
}
|
||||
|
||||
// Set state to connecting immediately to prevent concurrent connect() calls
|
||||
connectionHandler.setState(.connecting)
|
||||
|
||||
do {
|
||||
if !connectionHandler.retryOnFailedConnect {
|
||||
try await connectionHandler.connect()
|
||||
connectionHandler.setState(.connected)
|
||||
connectionHandler.fire(.connected)
|
||||
} else {
|
||||
connectionHandler.handleReconnect()
|
||||
}
|
||||
} catch {
|
||||
// Reset state on connection failure
|
||||
connectionHandler.setState(.disconnected)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes a connection to NATS server.
|
||||
///
|
||||
/// - Throws ``NatsError/ClientError/connectionClosed`` if the conneciton is already closed.
|
||||
public func close() async throws {
|
||||
logger.debug("close")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
try await connectionHandler.close()
|
||||
}
|
||||
|
||||
/// Suspends a connection to NATS server.
|
||||
/// A suspended connection does not receive messages on subscriptions.
|
||||
/// It can be resumed using ``resume()`` which restores subscriptions on successful reconnect.
|
||||
///
|
||||
/// - Throws ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
public func suspend() async throws {
|
||||
logger.debug("suspend")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
try await connectionHandler.suspend()
|
||||
}
|
||||
|
||||
/// Resumes a suspended connection.
|
||||
/// ``resume()`` will not wait for successful reconnection but rather trigger a reconnect process and return.
|
||||
/// Register ``NatsEvent`` using ``NatsClient/on()`` to wait for successful reconnection.
|
||||
///
|
||||
/// - Throws ``NatsError/ClientError`` if the conneciton is not in suspended state.
|
||||
public func resume() async throws {
|
||||
logger.debug("resume")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
try await connectionHandler.resume()
|
||||
}
|
||||
|
||||
/// Forces a reconnect attempt to the server.
|
||||
/// This is a non-blocking operation and will start the process without waiting for it to complete.
|
||||
///
|
||||
/// - Throws ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
public func reconnect() async throws {
|
||||
logger.debug("resume")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
try await connectionHandler.reconnect()
|
||||
}
|
||||
|
||||
/// Publishes a message on a given subject.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - payload: data to be published.
|
||||
/// - subject: a NATS subject on which the message will be published.
|
||||
/// - reply: optional reply subject when publishing a request.
|
||||
/// - headers: optional message headers.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
/// > - ``NatsError/ClientError/io(_:)`` if there is an error writing message to a TCP socket (e.g. bloken pipe).
|
||||
public func publish(
|
||||
_ payload: Data, subject: String, reply: String? = nil, headers: NatsHeaderMap? = nil
|
||||
) async throws {
|
||||
logger.debug("publish")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
try await connectionHandler.write(
|
||||
operation: ClientOp.publish((subject, reply, payload, headers)))
|
||||
}
|
||||
|
||||
/// Sends a blocking request on a given subject.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - payload: data to be published in the request.
|
||||
/// - subject: a NATS subject on which the request will be published.
|
||||
/// - headers: optional request headers.
|
||||
/// - timeout: request timeout - defaults to 5 seconds.
|
||||
///
|
||||
/// - Returns a ``NatsMessage`` containing the response.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
/// > - ``NatsError/ClientError/io(_:)`` if there is an error writing message to a TCP socket (e.g. bloken pipe).
|
||||
/// > - ``NatsError/RequestError/noResponders`` if there are no responders available for the request.
|
||||
/// > - ``NatsError/RequestError/timeout`` if there was a timeout waiting for the response.
|
||||
public func request(
|
||||
_ payload: Data, subject: String, headers: NatsHeaderMap? = nil, timeout: TimeInterval = 5
|
||||
) async throws -> NatsMessage {
|
||||
logger.debug("request")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
let inbox = newInbox()
|
||||
|
||||
let sub = try await connectionHandler.subscribe(inbox)
|
||||
try await sub.unsubscribe(after: 1)
|
||||
try await connectionHandler.write(
|
||||
operation: ClientOp.publish((subject, inbox, payload, headers)))
|
||||
|
||||
return try await withThrowingTaskGroup(
|
||||
of: NatsMessage?.self
|
||||
) { group in
|
||||
group.addTask {
|
||||
do {
|
||||
return try await sub.makeAsyncIterator().next()
|
||||
} catch NatsError.SubscriptionError.permissionDenied {
|
||||
throw NatsError.RequestError.permissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
// task for the timeout
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
||||
return nil
|
||||
}
|
||||
|
||||
for try await result in group {
|
||||
// if the result is not empty, return it (or throw status error)
|
||||
if let msg = result {
|
||||
group.cancelAll()
|
||||
if let status = msg.status, status == StatusCode.noResponders {
|
||||
throw NatsError.RequestError.noResponders
|
||||
}
|
||||
return msg
|
||||
} else {
|
||||
try await sub.unsubscribe()
|
||||
group.cancelAll()
|
||||
throw NatsError.RequestError.timeout
|
||||
}
|
||||
}
|
||||
|
||||
// this should not be reachable
|
||||
throw NatsError.ClientError.internalError("error waiting for response")
|
||||
}
|
||||
}
|
||||
|
||||
/// Flushes the internal buffer ensuring that all messages are sent.
|
||||
///
|
||||
/// - Throws ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
public func flush() async throws {
|
||||
logger.debug("flush")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
connectionHandler.channel?.flush()
|
||||
}
|
||||
|
||||
/// Subscribes to a subject to receive messages.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - subject:a subject the client want's to subscribe to.
|
||||
/// - queue: optional queue group name.
|
||||
///
|
||||
/// - Returns a ``NatsSubscription`` allowing iteration over incoming messages.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
/// > - ``NatsError/ClientError/io(_:)`` if there is an error sending the SUB request to the server.
|
||||
/// > - ``NatsError/SubscriptionError/invalidSubject`` if the provided subject is invalid.
|
||||
/// > - ``NatsError/SubscriptionError/invalidQueue`` if the provided queue group is invalid.
|
||||
public func subscribe(subject: String, queue: String? = nil) async throws -> NatsSubscription {
|
||||
logger.info("subscribe to subject \(subject)")
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
return try await connectionHandler.subscribe(subject, queue: queue)
|
||||
}
|
||||
|
||||
/// Sends a PING to the server, returning the time it took for the server to respond.
|
||||
///
|
||||
/// - Returns rtt of the request.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
/// > - ``NatsError/ClientError/io(_:)`` if there is an error sending the SUB request to the server.
|
||||
public func rtt() async throws -> TimeInterval {
|
||||
guard let connectionHandler = self.connectionHandler else {
|
||||
throw NatsError.ClientError.internalError("empty connection handler")
|
||||
}
|
||||
if case .closed = connectionHandler.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
let ping = RttCommand.makeFrom(channel: connectionHandler.channel)
|
||||
await connectionHandler.sendPing(ping)
|
||||
return try await ping.getRoundTripTime()
|
||||
}
|
||||
}
|
||||
202
Sources/Nats/NatsClient/NatsClientOptions.swift
Normal file
202
Sources/Nats/NatsClient/NatsClientOptions.swift
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
|
||||
public class NatsClientOptions {
|
||||
private var urls: [URL] = []
|
||||
private var pingInterval: TimeInterval = 60.0
|
||||
private var reconnectWait: TimeInterval = 2.0
|
||||
private var maxReconnects: Int?
|
||||
private var initialReconnect = false
|
||||
private var noRandomize = false
|
||||
private var auth: Auth? = nil
|
||||
private var withTls = false
|
||||
private var tlsFirst = false
|
||||
private var rootCertificate: URL? = nil
|
||||
private var clientCertificate: URL? = nil
|
||||
private var clientKey: URL? = nil
|
||||
private var inboxPrefix: String = "_INBOX."
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Sets the prefix for inbox subjects used for request/reply.
|
||||
/// Defaults to "_INBOX."
|
||||
public func inboxPrefix(_ prefix: String) -> NatsClientOptions {
|
||||
if prefix.isEmpty {
|
||||
self.inboxPrefix = "_INBOX."
|
||||
return self
|
||||
}
|
||||
if prefix.last != "." {
|
||||
self.inboxPrefix = prefix + "."
|
||||
return self
|
||||
}
|
||||
self.inboxPrefix = prefix
|
||||
return self
|
||||
}
|
||||
|
||||
/// A list of server urls that a client can connect to.
|
||||
public func urls(_ urls: [URL]) -> NatsClientOptions {
|
||||
self.urls = urls
|
||||
return self
|
||||
}
|
||||
|
||||
/// A single url that the client can connect to.
|
||||
public func url(_ url: URL) -> NatsClientOptions {
|
||||
self.urls = [url]
|
||||
return self
|
||||
}
|
||||
|
||||
/// The interval with which the client will send pings to NATS server.
|
||||
/// Defaults to 60s.
|
||||
public func pingInterval(_ pingInterval: TimeInterval) -> NatsClientOptions {
|
||||
self.pingInterval = pingInterval
|
||||
return self
|
||||
}
|
||||
|
||||
/// Wait time between reconnect attempts.
|
||||
/// Defaults to 2s.
|
||||
public func reconnectWait(_ reconnectWait: TimeInterval) -> NatsClientOptions {
|
||||
self.reconnectWait = reconnectWait
|
||||
return self
|
||||
}
|
||||
|
||||
/// Maximum number of reconnect attempts after each disconnect.
|
||||
/// Defaults to unlimited.
|
||||
public func maxReconnects(_ maxReconnects: Int) -> NatsClientOptions {
|
||||
self.maxReconnects = maxReconnects
|
||||
return self
|
||||
}
|
||||
|
||||
/// Username and password used to connect to the server.
|
||||
public func usernameAndPassword(_ username: String, _ password: String) -> NatsClientOptions {
|
||||
if self.auth == nil {
|
||||
self.auth = Auth(user: username, password: password)
|
||||
} else {
|
||||
self.auth?.user = username
|
||||
self.auth?.password = password
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Token used for token auth to NATS server.
|
||||
public func token(_ token: String) -> NatsClientOptions {
|
||||
if self.auth == nil {
|
||||
self.auth = Auth(token: token)
|
||||
} else {
|
||||
self.auth?.token = token
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// The location of a credentials file containing user JWT and Nkey seed.
|
||||
public func credentialsFile(_ credentials: URL) -> NatsClientOptions {
|
||||
if self.auth == nil {
|
||||
self.auth = Auth.fromCredentials(credentials)
|
||||
} else {
|
||||
self.auth?.credentialsPath = credentials
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// The location of a public nkey file.
|
||||
/// This and ``NatsClientOptions/nkey(_:)`` are mutually exclusive.
|
||||
public func nkeyFile(_ nkey: URL) -> NatsClientOptions {
|
||||
if self.auth == nil {
|
||||
self.auth = Auth.fromNkey(nkey)
|
||||
} else {
|
||||
self.auth?.nkeyPath = nkey
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Public nkey.
|
||||
/// This and ``NatsClientOptions/nkeyFile(_:)`` are mutually exclusive.
|
||||
public func nkey(_ nkey: String) -> NatsClientOptions {
|
||||
if self.auth == nil {
|
||||
self.auth = Auth.fromNkey(nkey)
|
||||
} else {
|
||||
self.auth?.nkey = nkey
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Indicates whether the client requires an SSL connection.
|
||||
public func requireTls() -> NatsClientOptions {
|
||||
self.withTls = true
|
||||
return self
|
||||
}
|
||||
|
||||
/// Indicates whether the client will attempt to perform a TLS handshake first, that is
|
||||
/// before receiving the INFO protocol. This requires the server to also be
|
||||
/// configured with such option, otherwise the connection will fail.
|
||||
public func withTlsFirst() -> NatsClientOptions {
|
||||
self.tlsFirst = true
|
||||
return self
|
||||
}
|
||||
|
||||
/// The location of a root CAs file.
|
||||
public func rootCertificates(_ rootCertificate: URL) -> NatsClientOptions {
|
||||
self.rootCertificate = rootCertificate
|
||||
return self
|
||||
}
|
||||
|
||||
/// The location of a client cert file.
|
||||
public func clientCertificate(_ clientCertificate: URL, _ clientKey: URL) -> NatsClientOptions {
|
||||
self.clientCertificate = clientCertificate
|
||||
self.clientKey = clientKey
|
||||
return self
|
||||
}
|
||||
|
||||
/// Indicates whether the client will retain the order of URLs to connect to provided in ``NatsClientOptions/urls(_:)``
|
||||
/// If not set, the client will randomize the server pool.
|
||||
public func retainServersOrder() -> NatsClientOptions {
|
||||
self.noRandomize = true
|
||||
return self
|
||||
}
|
||||
|
||||
/// By default, ``NatsClient/connect()`` will return an error if
|
||||
/// the connection to the server cannot be established.
|
||||
///
|
||||
/// Setting `retryOnfailedConnect()` makes the client
|
||||
/// establish the connection in the background even if the initial connect fails.
|
||||
public func retryOnfailedConnect() -> NatsClientOptions {
|
||||
self.initialReconnect = true
|
||||
return self
|
||||
}
|
||||
|
||||
public func build() -> NatsClient {
|
||||
let client = NatsClient()
|
||||
client.inboxPrefix = inboxPrefix
|
||||
client.connectionHandler = ConnectionHandler(
|
||||
inputBuffer: client.buffer,
|
||||
urls: urls,
|
||||
reconnectWait: reconnectWait,
|
||||
maxReconnects: maxReconnects,
|
||||
retainServersOrder: noRandomize,
|
||||
pingInterval: pingInterval,
|
||||
auth: auth,
|
||||
requireTls: withTls,
|
||||
tlsFirst: tlsFirst,
|
||||
clientCertificate: clientCertificate,
|
||||
clientKey: clientKey,
|
||||
rootCertificate: rootCertificate,
|
||||
retryOnFailedConnect: initialReconnect
|
||||
)
|
||||
return client
|
||||
}
|
||||
}
|
||||
1098
Sources/Nats/NatsConnection.swift
Normal file
1098
Sources/Nats/NatsConnection.swift
Normal file
File diff suppressed because it is too large
Load Diff
244
Sources/Nats/NatsError.swift
Normal file
244
Sources/Nats/NatsError.swift
Normal file
@@ -0,0 +1,244 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol NatsErrorProtocol: Error, CustomStringConvertible {}
|
||||
|
||||
public enum NatsError {
|
||||
public enum ServerError: NatsErrorProtocol, Equatable {
|
||||
case staleConnection
|
||||
case maxConnectionsExceeded
|
||||
case authorizationViolation
|
||||
case authenticationExpired
|
||||
case authenticationRevoked
|
||||
case authenticationTimeout
|
||||
case permissionsViolation(Operation, String, String?)
|
||||
case proto(String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .staleConnection:
|
||||
return "nats: stale connection"
|
||||
case .maxConnectionsExceeded:
|
||||
return "nats: maximum connections exceeded"
|
||||
case .authorizationViolation:
|
||||
return "nats: authorization violation"
|
||||
case .authenticationExpired:
|
||||
return "nats: authentication expired"
|
||||
case .authenticationRevoked:
|
||||
return "nats: authentication revoked"
|
||||
case .authenticationTimeout:
|
||||
return "nats: authentication timeout"
|
||||
case .permissionsViolation(let operation, let subject, let queue):
|
||||
if let queue {
|
||||
return
|
||||
"nats: permissions violation for operation \"\(operation)\" on subject \"\(subject)\" using queue \"\(queue)\""
|
||||
} else {
|
||||
return
|
||||
"nats: permissions violation for operation \"\(operation)\" on subject \"\(subject)\""
|
||||
}
|
||||
case .proto(let error):
|
||||
return "nats: \(error)"
|
||||
}
|
||||
}
|
||||
var normalizedError: String {
|
||||
return description.trimWhitespacesAndApostrophes().lowercased()
|
||||
}
|
||||
init(_ error: String) {
|
||||
let normalizedError = error.trimWhitespacesAndApostrophes().lowercased()
|
||||
if normalizedError.contains("stale connection") {
|
||||
self = .staleConnection
|
||||
} else if normalizedError.contains("maximum connections exceeded") {
|
||||
self = .maxConnectionsExceeded
|
||||
} else if normalizedError.contains("authorization violation") {
|
||||
self = .authorizationViolation
|
||||
} else if normalizedError.contains("authentication expired") {
|
||||
self = .authenticationExpired
|
||||
} else if normalizedError.contains("authentication revoked") {
|
||||
self = .authenticationRevoked
|
||||
} else if normalizedError.contains("authentication timeout") {
|
||||
self = .authenticationTimeout
|
||||
} else if normalizedError.contains("permissions violation") {
|
||||
if let (operation, subject, queue) = NatsError.ServerError.parsePermissions(
|
||||
error: error)
|
||||
{
|
||||
self = .permissionsViolation(operation, subject, queue)
|
||||
} else {
|
||||
self = .proto(error)
|
||||
}
|
||||
} else {
|
||||
self = .proto(error)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Operation: String, Equatable {
|
||||
case publish = "Publish"
|
||||
case subscribe = "Subscription"
|
||||
}
|
||||
|
||||
internal static func parsePermissions(error: String) -> (Operation, String, String?)? {
|
||||
let pattern = "(Publish|Subscription) to \"(\\S+)\""
|
||||
let regex = try! NSRegularExpression(pattern: pattern)
|
||||
let matches = regex.matches(
|
||||
in: error, options: [], range: NSRange(location: 0, length: error.utf16.count))
|
||||
|
||||
guard let match = matches.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var operation: Operation?
|
||||
if let operationRange = Range(match.range(at: 1), in: error) {
|
||||
let operationString = String(error[operationRange])
|
||||
operation = Operation(rawValue: operationString)
|
||||
}
|
||||
|
||||
var subject: String?
|
||||
if let subjectRange = Range(match.range(at: 2), in: error) {
|
||||
subject = String(error[subjectRange])
|
||||
}
|
||||
|
||||
let queuePattern = "using queue \"(\\S+)\""
|
||||
let queueRegex = try! NSRegularExpression(pattern: queuePattern)
|
||||
let queueMatches = queueRegex.matches(
|
||||
in: error, options: [], range: NSRange(location: 0, length: error.utf16.count))
|
||||
|
||||
var queue: String?
|
||||
if let match = queueMatches.first, let queueRange = Range(match.range(at: 1), in: error)
|
||||
{
|
||||
queue = String(error[queueRange])
|
||||
}
|
||||
|
||||
if let operation, let subject {
|
||||
return (operation, subject, queue)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ProtocolError: NatsErrorProtocol, Equatable {
|
||||
case invalidOperation(String)
|
||||
case parserFailure(String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidOperation(let op):
|
||||
return "nats: unknown server operation: \(op)"
|
||||
case .parserFailure(let cause):
|
||||
return "nats: parser failure: \(cause)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ClientError: NatsErrorProtocol {
|
||||
case internalError(String)
|
||||
case maxReconnects
|
||||
case connectionClosed
|
||||
case io(Error)
|
||||
case invalidConnection(String)
|
||||
case cancelled
|
||||
case alreadyConnected
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .internalError(let error):
|
||||
return "nats: internal error: \(error)"
|
||||
case .maxReconnects:
|
||||
return "nats: max reconnects exceeded"
|
||||
case .connectionClosed:
|
||||
return "nats: connection is closed"
|
||||
case .io(let error):
|
||||
return "nats: IO error: \(error)"
|
||||
case .invalidConnection(let error):
|
||||
return "nats: \(error)"
|
||||
case .cancelled:
|
||||
return "nats: operation cancelled"
|
||||
case .alreadyConnected:
|
||||
return "nats: client is already connected or connecting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConnectError: NatsErrorProtocol {
|
||||
case invalidConfig(String)
|
||||
case tlsFailure(Error)
|
||||
case timeout
|
||||
case dns(Error)
|
||||
case io(Error)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidConfig(let error):
|
||||
return "nats: invalid client configuration: \(error)"
|
||||
case .tlsFailure(let error):
|
||||
return "nats: TLS error: \(error)"
|
||||
case .timeout:
|
||||
return "nats: timed out waiting for connection"
|
||||
case .dns(let error):
|
||||
return "nats: DNS lookup error: \(error)"
|
||||
case .io(let error):
|
||||
return "nats: error establishing connection: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum RequestError: NatsErrorProtocol, Equatable {
|
||||
case noResponders
|
||||
case timeout
|
||||
case permissionDenied
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .noResponders:
|
||||
return "nats: no responders available for request"
|
||||
case .timeout:
|
||||
return "nats: request timed out"
|
||||
case .permissionDenied:
|
||||
return "nats: permission denied"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SubscriptionError: NatsErrorProtocol, Equatable {
|
||||
case invalidSubject
|
||||
case invalidQueue
|
||||
case permissionDenied
|
||||
case subscriptionClosed
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidSubject:
|
||||
return "nats: invalid subject name"
|
||||
case .invalidQueue:
|
||||
return "nats: invalid queue group name"
|
||||
case .permissionDenied:
|
||||
return "nats: permission denied"
|
||||
case .subscriptionClosed:
|
||||
return "nats: subscription closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ParseHeaderError: NatsErrorProtocol, Equatable {
|
||||
case invalidCharacter
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidCharacter:
|
||||
return
|
||||
"nats: invalid header name (name cannot contain non-ascii alphanumeric characters other than '-')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
Sources/Nats/NatsHeaders.swift
Normal file
159
Sources/Nats/NatsHeaders.swift
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
// Represents NATS header field value in Swift.
|
||||
public struct NatsHeaderValue: Equatable, CustomStringConvertible {
|
||||
private var inner: String
|
||||
|
||||
public init(_ value: String) {
|
||||
self.inner = value
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return inner
|
||||
}
|
||||
}
|
||||
|
||||
// Custom header representation in Swift
|
||||
public struct NatsHeaderName: Equatable, Hashable, CustomStringConvertible {
|
||||
private var inner: String
|
||||
|
||||
public init(_ value: String) throws {
|
||||
if value.contains(where: { $0 == ":" || $0.asciiValue! < 33 || $0.asciiValue! > 126 }) {
|
||||
throw NatsError.ParseHeaderError.invalidCharacter
|
||||
}
|
||||
self.inner = value
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return inner
|
||||
}
|
||||
|
||||
// Example of standard headers
|
||||
public static let natsStream = try! NatsHeaderName("Nats-Stream")
|
||||
public static let natsSequence = try! NatsHeaderName("Nats-Sequence")
|
||||
public static let natsTimestamp = try! NatsHeaderName("Nats-Time-Stamp")
|
||||
public static let natsSubject = try! NatsHeaderName("Nats-Subject")
|
||||
// Add other standard headers as needed...
|
||||
}
|
||||
|
||||
// Represents a NATS header map in Swift.
|
||||
public struct NatsHeaderMap: Equatable {
|
||||
private var inner: [NatsHeaderName: [NatsHeaderValue]]
|
||||
internal var status: StatusCode? = nil
|
||||
internal var description: String? = nil
|
||||
|
||||
public init() {
|
||||
self.inner = [:]
|
||||
}
|
||||
|
||||
public init(from headersString: String) throws {
|
||||
self.inner = [:]
|
||||
let headersArray = headersString.split(separator: "\r\n")
|
||||
let versionLine = headersArray[0]
|
||||
guard versionLine.hasPrefix(Data.versionLinePrefix) else {
|
||||
throw NatsError.ProtocolError.parserFailure(
|
||||
"header version line does not begin with `NATS/1.0`")
|
||||
}
|
||||
let versionLineSuffix =
|
||||
versionLine
|
||||
.dropFirst(Data.versionLinePrefix.count)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// handle inlines status and description
|
||||
if versionLineSuffix.count > 0 {
|
||||
let statusAndDesc = versionLineSuffix.split(
|
||||
separator: " ", maxSplits: 1)
|
||||
guard let status = StatusCode(statusAndDesc[0]) else {
|
||||
throw NatsError.ProtocolError.parserFailure("could not parse status parameter")
|
||||
}
|
||||
self.status = status
|
||||
if statusAndDesc.count > 1 {
|
||||
self.description = String(statusAndDesc[1])
|
||||
}
|
||||
}
|
||||
|
||||
for header in headersArray.dropFirst() {
|
||||
let headerParts = header.split(separator: ":", maxSplits: 1)
|
||||
if headerParts.count == 2 {
|
||||
self.append(
|
||||
try NatsHeaderName(String(headerParts[0])),
|
||||
NatsHeaderValue(String(headerParts[1]).trimmingCharacters(in: .whitespaces)))
|
||||
} else {
|
||||
logger.error("Error parsing header: \(header)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return inner.isEmpty
|
||||
}
|
||||
|
||||
public mutating func insert(_ name: NatsHeaderName, _ value: NatsHeaderValue) {
|
||||
self.inner[name] = [value]
|
||||
}
|
||||
|
||||
public mutating func append(_ name: NatsHeaderName, _ value: NatsHeaderValue) {
|
||||
if inner[name] != nil {
|
||||
inner[name]?.append(value)
|
||||
} else {
|
||||
insert(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
public func get(_ name: NatsHeaderName) -> NatsHeaderValue? {
|
||||
return inner[name]?.first
|
||||
}
|
||||
|
||||
public func getAll(_ name: NatsHeaderName) -> [NatsHeaderValue] {
|
||||
return inner[name] ?? []
|
||||
}
|
||||
|
||||
//TODO(jrm): can we use unsafe methods here? Probably yes.
|
||||
func toBytes() -> [UInt8] {
|
||||
var bytes: [UInt8] = []
|
||||
bytes.append(contentsOf: "NATS/1.0\r\n".utf8)
|
||||
for (name, values) in inner {
|
||||
for value in values {
|
||||
bytes.append(contentsOf: name.description.utf8)
|
||||
bytes.append(contentsOf: ":".utf8)
|
||||
bytes.append(contentsOf: value.description.utf8)
|
||||
bytes.append(contentsOf: "\r\n".utf8)
|
||||
}
|
||||
}
|
||||
bytes.append(contentsOf: "\r\n".utf8)
|
||||
return bytes
|
||||
}
|
||||
|
||||
// Implementing the == operator to exclude status and desc internal properties
|
||||
public static func == (lhs: NatsHeaderMap, rhs: NatsHeaderMap) -> Bool {
|
||||
return lhs.inner == rhs.inner
|
||||
}
|
||||
}
|
||||
|
||||
extension NatsHeaderMap {
|
||||
public subscript(name: NatsHeaderName) -> NatsHeaderValue? {
|
||||
get {
|
||||
return get(name)
|
||||
}
|
||||
set {
|
||||
if let value = newValue {
|
||||
insert(name, value)
|
||||
} else {
|
||||
inner[name] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Sources/Nats/NatsJwtUtils.swift
Normal file
69
Sources/Nats/NatsJwtUtils.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
class JwtUtils {
|
||||
// This regular expression is equivalent to the one used in Rust.
|
||||
static let userConfigRE: NSRegularExpression = {
|
||||
do {
|
||||
return try NSRegularExpression(
|
||||
pattern:
|
||||
"\\s*(?:(?:-{3,}.*-{3,}\\r?\\n)([\\w\\-.=]+)(?:\\r?\\n-{3,}.*-{3,}\\r?\\n))",
|
||||
options: [])
|
||||
} catch {
|
||||
fatalError("Invalid regular expression: \(error)")
|
||||
}
|
||||
}()
|
||||
|
||||
/// Parses a credentials file and returns its user JWT.
|
||||
static func parseDecoratedJWT(contents: String) -> String? {
|
||||
let matches = userConfigRE.matches(
|
||||
in: contents, options: [], range: NSRange(contents.startIndex..., in: contents))
|
||||
if let match = matches.first, let range = Range(match.range(at: 1), in: contents) {
|
||||
return String(contents[range])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
/// Parses a credentials file and returns its user JWT.
|
||||
static func parseDecoratedJWT(contents: Data) -> Data? {
|
||||
guard let contentsString = String(data: contents, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
if let match = parseDecoratedJWT(contents: contentsString) {
|
||||
return match.data(using: .utf8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Parses a credentials file and returns its nkey.
|
||||
static func parseDecoratedNKey(contents: String) -> String? {
|
||||
let matches = userConfigRE.matches(
|
||||
in: contents, options: [], range: NSRange(contents.startIndex..., in: contents))
|
||||
if matches.count > 1, let range = Range(matches[1].range(at: 1), in: contents) {
|
||||
return String(contents[range])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Parses a credentials file and returns its nkey.
|
||||
static func parseDecoratedNKey(contents: Data) -> Data? {
|
||||
guard let contentsString = String(data: contents, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
if let match = parseDecoratedNKey(contents: contentsString) {
|
||||
return match.data(using: .utf8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
60
Sources/Nats/NatsMessage.swift
Normal file
60
Sources/Nats/NatsMessage.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NatsMessage {
|
||||
public let payload: Data?
|
||||
public let subject: String
|
||||
public let replySubject: String?
|
||||
public let length: Int
|
||||
public let headers: NatsHeaderMap?
|
||||
public let status: StatusCode?
|
||||
public let description: String?
|
||||
}
|
||||
|
||||
public struct StatusCode: Equatable {
|
||||
public static let idleHeartbeat = StatusCode(value: 100)
|
||||
public static let ok = StatusCode(value: 200)
|
||||
public static let badRequest = StatusCode(value: 400)
|
||||
public static let notFound = StatusCode(value: 404)
|
||||
public static let timeout = StatusCode(value: 408)
|
||||
public static let noResponders = StatusCode(value: 503)
|
||||
public static let requestTerminated = StatusCode(value: 409)
|
||||
|
||||
let value: UInt16
|
||||
|
||||
// non-optional initializer for static status codes
|
||||
private init(value: UInt16) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init?(_ value: UInt16) {
|
||||
if !(100..<1000 ~= value) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init?(_ value: any StringProtocol) {
|
||||
guard let status = UInt16(value) else {
|
||||
return nil
|
||||
}
|
||||
if !(100..<1000 ~= status) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.value = status
|
||||
}
|
||||
}
|
||||
341
Sources/Nats/NatsProto.swift
Normal file
341
Sources/Nats/NatsProto.swift
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import NIO
|
||||
|
||||
internal struct NatsOperation: RawRepresentable, Hashable {
|
||||
let rawValue: String
|
||||
|
||||
static let connect = NatsOperation(rawValue: "CONNECT")
|
||||
static let subscribe = NatsOperation(rawValue: "SUB")
|
||||
static let unsubscribe = NatsOperation(rawValue: "UNSUB")
|
||||
static let publish = NatsOperation(rawValue: "PUB")
|
||||
static let hpublish = NatsOperation(rawValue: "HPUB")
|
||||
static let message = NatsOperation(rawValue: "MSG")
|
||||
static let hmessage = NatsOperation(rawValue: "HMSG")
|
||||
static let info = NatsOperation(rawValue: "INFO")
|
||||
static let ok = NatsOperation(rawValue: "+OK")
|
||||
static let error = NatsOperation(rawValue: "-ERR")
|
||||
static let ping = NatsOperation(rawValue: "PING")
|
||||
static let pong = NatsOperation(rawValue: "PONG")
|
||||
|
||||
var rawBytes: String.UTF8View {
|
||||
self.rawValue.utf8
|
||||
}
|
||||
|
||||
static func allOperations() -> [NatsOperation] {
|
||||
return [
|
||||
.connect, .subscribe, .unsubscribe, .publish, .message, .hmessage, .info, .ok, .error,
|
||||
.ping, .pong,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerOp {
|
||||
case ok
|
||||
case info(ServerInfo)
|
||||
case ping
|
||||
case pong
|
||||
case error(NatsError.ServerError)
|
||||
case message(MessageInbound)
|
||||
case hMessage(HMessageInbound)
|
||||
|
||||
static func parse(from msg: Data) throws -> ServerOp {
|
||||
guard msg.count > 2 else {
|
||||
throw NatsError.ProtocolError.parserFailure(
|
||||
"unable to parse inbound message: \(String(data: msg, encoding: .utf8)!)")
|
||||
}
|
||||
guard let msgType = msg.getMessageType() else {
|
||||
throw NatsError.ProtocolError.invalidOperation(String(data: msg, encoding: .utf8)!)
|
||||
}
|
||||
switch msgType {
|
||||
case .message:
|
||||
return try message(MessageInbound.parse(data: msg))
|
||||
case .hmessage:
|
||||
return try hMessage(HMessageInbound.parse(data: msg))
|
||||
case .info:
|
||||
return try info(ServerInfo.parse(data: msg))
|
||||
case .ok:
|
||||
return ok
|
||||
case .error:
|
||||
if let errMsg = msg.removePrefix(Data(NatsOperation.error.rawBytes)).toString() {
|
||||
return error(NatsError.ServerError(errMsg))
|
||||
}
|
||||
return error(NatsError.ServerError("unexpected error"))
|
||||
case .ping:
|
||||
return ping
|
||||
case .pong:
|
||||
return pong
|
||||
default:
|
||||
throw NatsError.ProtocolError.invalidOperation(
|
||||
"unknown server op: \(String(data: msg, encoding: .utf8)!)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal struct HMessageInbound: Equatable {
|
||||
private static let newline = UInt8(ascii: "\n")
|
||||
private static let space = UInt8(ascii: " ")
|
||||
var subject: String
|
||||
var sid: UInt64
|
||||
var reply: String?
|
||||
var payload: Data?
|
||||
var headers: NatsHeaderMap
|
||||
var headersLength: Int
|
||||
var length: Int
|
||||
var status: StatusCode?
|
||||
var description: String?
|
||||
|
||||
// Parse the operation syntax: HMSG <subject> <sid> [reply-to]
|
||||
internal static func parse(data: Data) throws -> HMessageInbound {
|
||||
let protoComponents =
|
||||
data
|
||||
.dropFirst(NatsOperation.hmessage.rawValue.count) // Assuming msg starts with "HMSG "
|
||||
.split(separator: space)
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
let parseArgs: ((Data, Data, Data?, Data, Data) throws -> HMessageInbound) = {
|
||||
subjectData, sidData, replyData, lengthHeaders, lengthData in
|
||||
let subject = String(decoding: subjectData, as: UTF8.self)
|
||||
guard let sid = UInt64(String(decoding: sidData, as: UTF8.self)) else {
|
||||
throw NatsError.ProtocolError.parserFailure(
|
||||
"unable to parse subscription ID as number")
|
||||
}
|
||||
var replySubject: String? = nil
|
||||
if let replyData = replyData {
|
||||
replySubject = String(decoding: replyData, as: UTF8.self)
|
||||
}
|
||||
let headersLength = Int(String(decoding: lengthHeaders, as: UTF8.self)) ?? 0
|
||||
let length = Int(String(decoding: lengthData, as: UTF8.self)) ?? 0
|
||||
return HMessageInbound(
|
||||
subject: subject, sid: sid, reply: replySubject, payload: nil,
|
||||
headers: NatsHeaderMap(),
|
||||
headersLength: headersLength, length: length)
|
||||
}
|
||||
|
||||
var msg: HMessageInbound
|
||||
switch protoComponents.count {
|
||||
case 4:
|
||||
msg = try parseArgs(
|
||||
protoComponents[0], protoComponents[1], nil, protoComponents[2],
|
||||
protoComponents[3])
|
||||
case 5:
|
||||
msg = try parseArgs(
|
||||
protoComponents[0], protoComponents[1], protoComponents[2], protoComponents[3],
|
||||
protoComponents[4])
|
||||
default:
|
||||
throw NatsError.ProtocolError.parserFailure("unable to parse inbound message header")
|
||||
}
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(pp): add headers and HMSG parsing
|
||||
internal struct MessageInbound: Equatable {
|
||||
private static let newline = UInt8(ascii: "\n")
|
||||
private static let space = UInt8(ascii: " ")
|
||||
var subject: String
|
||||
var sid: UInt64
|
||||
var reply: String?
|
||||
var payload: Data?
|
||||
var length: Int
|
||||
|
||||
// Parse the operation syntax: MSG <subject> <sid> [reply-to]
|
||||
internal static func parse(data: Data) throws -> MessageInbound {
|
||||
let protoComponents =
|
||||
data
|
||||
.dropFirst(NatsOperation.message.rawValue.count) // Assuming msg starts with "MSG "
|
||||
.split(separator: space)
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
let parseArgs: ((Data, Data, Data?, Data) throws -> MessageInbound) = {
|
||||
subjectData, sidData, replyData, lengthData in
|
||||
let subject = String(decoding: subjectData, as: UTF8.self)
|
||||
guard let sid = UInt64(String(decoding: sidData, as: UTF8.self)) else {
|
||||
throw NatsError.ProtocolError.parserFailure(
|
||||
"unable to parse subscription ID as number")
|
||||
}
|
||||
var replySubject: String? = nil
|
||||
if let replyData = replyData {
|
||||
replySubject = String(decoding: replyData, as: UTF8.self)
|
||||
}
|
||||
let length = Int(String(decoding: lengthData, as: UTF8.self)) ?? 0
|
||||
return MessageInbound(
|
||||
subject: subject, sid: sid, reply: replySubject, payload: nil, length: length)
|
||||
}
|
||||
|
||||
var msg: MessageInbound
|
||||
switch protoComponents.count {
|
||||
case 3:
|
||||
msg = try parseArgs(protoComponents[0], protoComponents[1], nil, protoComponents[2])
|
||||
case 4:
|
||||
msg = try parseArgs(
|
||||
protoComponents[0], protoComponents[1], protoComponents[2], protoComponents[3])
|
||||
default:
|
||||
throw NatsError.ProtocolError.parserFailure("unable to parse inbound message header")
|
||||
}
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing server information in NATS.
|
||||
struct ServerInfo: Codable, Equatable {
|
||||
/// The unique identifier of the NATS server.
|
||||
let serverId: String
|
||||
/// Generated Server Name.
|
||||
let serverName: String
|
||||
/// The host specified in the cluster parameter/options.
|
||||
let host: String
|
||||
/// The port number specified in the cluster parameter/options.
|
||||
let port: UInt16
|
||||
/// The version of the NATS server.
|
||||
let version: String
|
||||
/// If this is set, then the server should try to authenticate upon connect.
|
||||
let authRequired: Bool?
|
||||
/// If this is set, then the server must authenticate using TLS.
|
||||
let tlsRequired: Bool?
|
||||
/// Maximum payload size that the server will accept.
|
||||
let maxPayload: UInt
|
||||
/// The protocol version in use.
|
||||
let proto: Int8
|
||||
/// The server-assigned client ID. This may change during reconnection.
|
||||
let clientId: UInt64?
|
||||
/// The version of golang the NATS server was built with.
|
||||
let go: String
|
||||
/// The nonce used for nkeys.
|
||||
let nonce: String?
|
||||
/// A list of server urls that a client can connect to.
|
||||
let connectUrls: [String]?
|
||||
/// The client IP as known by the server.
|
||||
let clientIp: String
|
||||
/// Whether the server supports headers.
|
||||
let headers: Bool
|
||||
/// Whether server goes into lame duck
|
||||
private let _lameDuckMode: Bool?
|
||||
var lameDuckMode: Bool {
|
||||
return _lameDuckMode ?? false
|
||||
}
|
||||
|
||||
private static let prefix = NatsOperation.info.rawValue.data(using: .utf8)!
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case serverId = "server_id"
|
||||
case serverName = "server_name"
|
||||
case host
|
||||
case port
|
||||
case version
|
||||
case authRequired = "auth_required"
|
||||
case tlsRequired = "tls_required"
|
||||
case maxPayload = "max_payload"
|
||||
case proto
|
||||
case clientId = "client_id"
|
||||
case go
|
||||
case nonce
|
||||
case connectUrls = "connect_urls"
|
||||
case clientIp = "client_ip"
|
||||
case headers
|
||||
case _lameDuckMode = "ldm"
|
||||
}
|
||||
|
||||
internal static func parse(data: Data) throws -> ServerInfo {
|
||||
let info = data.removePrefix(prefix)
|
||||
return try JSONDecoder().decode(self, from: info)
|
||||
}
|
||||
}
|
||||
|
||||
enum ClientOp {
|
||||
case publish((subject: String, reply: String?, payload: Data?, headers: NatsHeaderMap?))
|
||||
case subscribe((sid: UInt64, subject: String, queue: String?))
|
||||
case unsubscribe((sid: UInt64, max: UInt64?))
|
||||
case connect(ConnectInfo)
|
||||
case ping
|
||||
case pong
|
||||
}
|
||||
|
||||
/// Info to construct a CONNECT message.
|
||||
struct ConnectInfo: Encodable {
|
||||
/// Turns on +OK protocol acknowledgments.
|
||||
var verbose: Bool
|
||||
/// Turns on additional strict format checking, e.g. for properly formed
|
||||
/// subjects.
|
||||
var pedantic: Bool
|
||||
/// User's JWT.
|
||||
var userJwt: String?
|
||||
/// Public nkey.
|
||||
var nkey: String
|
||||
/// Signed nonce, encoded to Base64URL.
|
||||
var signature: String?
|
||||
/// Optional client name.
|
||||
var name: String
|
||||
/// If set to `true`, the server (version 1.2.0+) will not send originating
|
||||
/// messages from this connection to its own subscriptions. Clients should
|
||||
/// set this to `true` only for server supporting this feature, which is
|
||||
/// when proto in the INFO protocol is set to at least 1.
|
||||
var echo: Bool
|
||||
/// The implementation language of the client.
|
||||
var lang: String
|
||||
/// The version of the client.
|
||||
var version: String
|
||||
/// Sending 0 (or absent) indicates client supports original protocol.
|
||||
/// Sending 1 indicates that the client supports dynamic reconfiguration
|
||||
/// of cluster topology changes by asynchronously receiving INFO messages
|
||||
/// with known servers it can reconnect to.
|
||||
var natsProtocol: NatsProtocol
|
||||
/// Indicates whether the client requires an SSL connection.
|
||||
var tlsRequired: Bool
|
||||
/// Connection username (if `auth_required` is set)
|
||||
var user: String
|
||||
/// Connection password (if auth_required is set)
|
||||
var pass: String
|
||||
/// Client authorization token (if auth_required is set)
|
||||
var authToken: String
|
||||
/// Whether the client supports the usage of headers.
|
||||
var headers: Bool
|
||||
/// Whether the client supports no_responders.
|
||||
var noResponders: Bool
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case verbose
|
||||
case pedantic
|
||||
case userJwt = "jwt"
|
||||
case nkey
|
||||
case signature = "sig" // Custom key name for JSON
|
||||
case name
|
||||
case echo
|
||||
case lang
|
||||
case version
|
||||
case natsProtocol = "protocol"
|
||||
case tlsRequired = "tls_required"
|
||||
case user
|
||||
case pass
|
||||
case authToken = "auth_token"
|
||||
case headers
|
||||
case noResponders = "no_responders"
|
||||
}
|
||||
}
|
||||
|
||||
enum NatsProtocol: Encodable {
|
||||
case original
|
||||
case dynamic
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .original:
|
||||
try container.encode(0)
|
||||
case .dynamic:
|
||||
try container.encode(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
185
Sources/Nats/NatsSubscription.swift
Normal file
185
Sources/Nats/NatsSubscription.swift
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
|
||||
// TODO(pp): Implement slow consumer
|
||||
public class NatsSubscription: AsyncSequence {
|
||||
public typealias Element = NatsMessage
|
||||
public typealias AsyncIterator = SubscriptionIterator
|
||||
|
||||
public let subject: String
|
||||
public let queue: String?
|
||||
internal var max: UInt64?
|
||||
internal var delivered: UInt64 = 0
|
||||
internal let sid: UInt64
|
||||
|
||||
private var buffer: [Result<Element, NatsError.SubscriptionError>]
|
||||
private let capacity: UInt64
|
||||
private var closed = false
|
||||
private var continuation:
|
||||
CheckedContinuation<Result<Element, NatsError.SubscriptionError>?, Never>?
|
||||
private let lock = NSLock()
|
||||
private let conn: ConnectionHandler
|
||||
|
||||
private static let defaultSubCapacity: UInt64 = 512 * 1024
|
||||
|
||||
convenience init(sid: UInt64, subject: String, queue: String?, conn: ConnectionHandler) throws {
|
||||
try self.init(
|
||||
sid: sid, subject: subject, queue: queue, capacity: NatsSubscription.defaultSubCapacity,
|
||||
conn: conn)
|
||||
}
|
||||
|
||||
init(
|
||||
sid: UInt64, subject: String, queue: String?, capacity: UInt64, conn: ConnectionHandler
|
||||
) throws {
|
||||
if !NatsSubscription.validSubject(subject) {
|
||||
throw NatsError.SubscriptionError.invalidSubject
|
||||
}
|
||||
if let queue, !NatsSubscription.validQueue(queue) {
|
||||
throw NatsError.SubscriptionError.invalidQueue
|
||||
}
|
||||
self.sid = sid
|
||||
self.subject = subject
|
||||
self.queue = queue
|
||||
self.capacity = capacity
|
||||
self.buffer = []
|
||||
self.conn = conn
|
||||
}
|
||||
|
||||
public func makeAsyncIterator() -> SubscriptionIterator {
|
||||
return SubscriptionIterator(subscription: self)
|
||||
}
|
||||
|
||||
func receiveMessage(_ message: NatsMessage) {
|
||||
lock.withLock {
|
||||
if let continuation = self.continuation {
|
||||
// Immediately use the continuation if it exists
|
||||
self.continuation = nil
|
||||
continuation.resume(returning: .success(message))
|
||||
} else if buffer.count < capacity {
|
||||
// Only append to buffer if no continuation is available
|
||||
// TODO(pp): Hadndle SlowConsumer as subscription event
|
||||
buffer.append(.success(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiveError(_ error: NatsError.SubscriptionError) {
|
||||
lock.withLock {
|
||||
if let continuation = self.continuation {
|
||||
// Immediately use the continuation if it exists
|
||||
self.continuation = nil
|
||||
continuation.resume(returning: .failure(error))
|
||||
} else {
|
||||
buffer.append(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func complete() {
|
||||
lock.withLock {
|
||||
closed = true
|
||||
if let continuation {
|
||||
self.continuation = nil
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncIterator implementation
|
||||
public class SubscriptionIterator: AsyncIteratorProtocol {
|
||||
private var subscription: NatsSubscription
|
||||
|
||||
init(subscription: NatsSubscription) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
public func next() async throws -> Element? {
|
||||
try await subscription.nextMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private func nextMessage() async throws -> Element? {
|
||||
let result: Result<Element, NatsError.SubscriptionError>? = await withCheckedContinuation {
|
||||
continuation in
|
||||
lock.withLock {
|
||||
if closed {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
delivered += 1
|
||||
if let message = buffer.first {
|
||||
buffer.removeFirst()
|
||||
continuation.resume(returning: message)
|
||||
} else {
|
||||
self.continuation = continuation
|
||||
}
|
||||
}
|
||||
}
|
||||
if let max, delivered >= max {
|
||||
conn.removeSub(sub: self)
|
||||
}
|
||||
switch result {
|
||||
case .success(let msg):
|
||||
return msg
|
||||
case .failure(let error):
|
||||
throw error
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsubscribes from subscription.
|
||||
///
|
||||
/// - Parameter after: If set, unsubscribe will be performed after reaching given number of messages.
|
||||
/// If it already reached or surpassed the passed value, it will immediately stop.
|
||||
///
|
||||
/// > **Throws:**
|
||||
/// > - ``NatsError/ClientError/connectionClosed`` if the conneciton is closed.
|
||||
/// > - ``NatsError/SubscriptionError/subscriptionClosed`` if the subscription is already closed
|
||||
public func unsubscribe(after: UInt64? = nil) async throws {
|
||||
logger.info("unsubscribe from subject \(subject)")
|
||||
if case .closed = self.conn.currentState {
|
||||
throw NatsError.ClientError.connectionClosed
|
||||
}
|
||||
if self.closed {
|
||||
throw NatsError.SubscriptionError.subscriptionClosed
|
||||
}
|
||||
return try await self.conn.unsubscribe(sub: self, max: after)
|
||||
}
|
||||
|
||||
// validateSubject will do a basic subject validation.
|
||||
// Spaces are not allowed and all tokens should be > 0 in length.
|
||||
private static func validSubject(_ subj: String) -> Bool {
|
||||
let whitespaceCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
if subj.rangeOfCharacter(from: whitespaceCharacterSet) != nil {
|
||||
return false
|
||||
}
|
||||
let tokens = subj.split(separator: ".")
|
||||
for token in tokens {
|
||||
if token.isEmpty {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// validQueue will check a queue name for whitespaces.
|
||||
private static func validQueue(_ queue: String) -> Bool {
|
||||
let whitespaceCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
return queue.rangeOfCharacter(from: whitespaceCharacterSet) == nil
|
||||
}
|
||||
}
|
||||
39
Sources/Nats/RttCommand.swift
Normal file
39
Sources/Nats/RttCommand.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
internal class RttCommand {
|
||||
let startTime = DispatchTime.now()
|
||||
let promise: EventLoopPromise<TimeInterval>?
|
||||
|
||||
static func makeFrom(channel: Channel?) -> RttCommand {
|
||||
RttCommand(promise: channel?.eventLoop.makePromise(of: TimeInterval.self))
|
||||
}
|
||||
|
||||
private init(promise: EventLoopPromise<TimeInterval>?) {
|
||||
self.promise = promise
|
||||
}
|
||||
|
||||
func setRoundTripTime() {
|
||||
let now = DispatchTime.now()
|
||||
let nanoTime = now.uptimeNanoseconds - startTime.uptimeNanoseconds
|
||||
let rtt = TimeInterval(nanoTime) / 1_000_000_000 // Convert nanos to seconds
|
||||
promise?.succeed(rtt)
|
||||
}
|
||||
|
||||
func getRoundTripTime() async throws -> TimeInterval {
|
||||
try await promise?.futureResult.get() ?? 0
|
||||
}
|
||||
}
|
||||
206
Sources/NatsServer/NatsServer.swift
Normal file
206
Sources/NatsServer/NatsServer.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
public class NatsServer {
|
||||
public var port: Int? { return natsServerPort }
|
||||
public var clientURL: String {
|
||||
let scheme = tlsEnabled ? "tls://" : "nats://"
|
||||
if let natsServerPort {
|
||||
return "\(scheme)localhost:\(natsServerPort)"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public var clientWebsocketURL: String {
|
||||
let scheme = tlsEnabled ? "wss://" : "ws://"
|
||||
if let natsWebsocketPort {
|
||||
return "\(scheme)localhost:\(natsWebsocketPort)"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private var process: Process?
|
||||
private var natsServerPort: Int?
|
||||
private var natsWebsocketPort: Int?
|
||||
private var tlsEnabled = false
|
||||
private var pidFile: URL?
|
||||
|
||||
public init() {}
|
||||
|
||||
// TODO: When implementing JetStream, creating and deleting store dir should be handled in start/stop methods
|
||||
public func start(
|
||||
port: Int = -1, cfg: String? = nil, file: StaticString = #file, line: UInt = #line
|
||||
) {
|
||||
XCTAssertNil(
|
||||
self.process, "nats-server is already running on port \(port)", file: file, line: line)
|
||||
let process = Process()
|
||||
let pipe = Pipe()
|
||||
|
||||
let fileManager = FileManager.default
|
||||
pidFile = fileManager.temporaryDirectory.appendingPathComponent("nats-server.pid")
|
||||
|
||||
let tempDir = FileManager.default.temporaryDirectory.appending(component: UUID().uuidString)
|
||||
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
process.arguments = [
|
||||
"nats-server", "-p", "\(port)", "-P", pidFile!.path, "--store_dir",
|
||||
"\(tempDir.absoluteString)",
|
||||
]
|
||||
if let cfg {
|
||||
process.arguments?.append(contentsOf: ["-c", cfg])
|
||||
}
|
||||
process.standardError = pipe
|
||||
process.standardOutput = pipe
|
||||
|
||||
let outputHandle = pipe.fileHandleForReading
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var lineCount = 0
|
||||
let maxLines = 100
|
||||
var serverError: String?
|
||||
var outputBuffer = Data()
|
||||
|
||||
outputHandle.readabilityHandler = { fileHandle in
|
||||
let data = fileHandle.availableData
|
||||
guard data.count > 0 else { return }
|
||||
outputBuffer.append(data)
|
||||
|
||||
guard let output = String(data: outputBuffer, encoding: .utf8) else { return }
|
||||
|
||||
let lines = output.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
let completedLines = lines.dropLast()
|
||||
|
||||
for lineSequence in completedLines {
|
||||
let line = String(lineSequence)
|
||||
lineCount += 1
|
||||
|
||||
let errorLine = self.extracErrorMessage(from: line)
|
||||
|
||||
if let port = self.extractPort(from: line, for: "client connections") {
|
||||
self.natsServerPort = port
|
||||
}
|
||||
|
||||
if let port = self.extractPort(from: line, for: "websocket clients") {
|
||||
self.natsWebsocketPort = port
|
||||
}
|
||||
|
||||
let ready = line.contains("Server is ready")
|
||||
|
||||
if !self.tlsEnabled && self.isTLS(from: line) {
|
||||
self.tlsEnabled = true
|
||||
}
|
||||
|
||||
if ready || errorLine != nil || lineCount >= maxLines {
|
||||
serverError = errorLine
|
||||
semaphore.signal()
|
||||
outputHandle.readabilityHandler = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if output.hasSuffix("\n") {
|
||||
outputBuffer.removeAll()
|
||||
} else {
|
||||
if let lastLine = lines.last, let incompleteLine = lastLine.data(using: .utf8) {
|
||||
outputBuffer = incompleteLine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertNoThrow(
|
||||
try process.run(), "error starting nats-server on port \(port)", file: file, line: line)
|
||||
|
||||
let result = semaphore.wait(timeout: .now() + .seconds(10))
|
||||
|
||||
XCTAssertFalse(
|
||||
result == .timedOut, "timeout waiting for server to be ready", file: file, line: line)
|
||||
XCTAssertNil(
|
||||
serverError, "error starting nats-server: \(serverError!)", file: file, line: line)
|
||||
|
||||
self.process = process
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if process == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.process?.terminate()
|
||||
process?.waitUntilExit()
|
||||
process = nil
|
||||
natsServerPort = port
|
||||
tlsEnabled = false
|
||||
}
|
||||
|
||||
public func sendSignal(_ signal: Signal, file: StaticString = #file, line: UInt = #line) {
|
||||
let process = Process()
|
||||
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
process.arguments = ["nats-server", "--signal", "\(signal.rawValue)=\(self.pidFile!.path)"]
|
||||
|
||||
XCTAssertNoThrow(
|
||||
try process.run(), "error setting signal", file: file, line: line)
|
||||
self.process = nil
|
||||
}
|
||||
|
||||
private func extractPort(from string: String, for phrase: String) -> Int? {
|
||||
// Listening for websocket clients on
|
||||
// Listening for client connections on
|
||||
let pattern = "Listening for \(phrase) on .*?:(\\d+)$"
|
||||
|
||||
let regex = try! NSRegularExpression(pattern: pattern)
|
||||
let nsrange = NSRange(string.startIndex..<string.endIndex, in: string)
|
||||
|
||||
if let match = regex.firstMatch(in: string, options: [], range: nsrange) {
|
||||
let portRange = match.range(at: 1)
|
||||
if let swiftRange = Range(portRange, in: string) {
|
||||
let portString = String(string[swiftRange])
|
||||
return Int(portString)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func extracErrorMessage(from logLine: String) -> String? {
|
||||
if logLine.contains("nats-server: No such file or directory") {
|
||||
return "nats-server not found - make sure nats-server can be found in PATH"
|
||||
}
|
||||
guard let range = logLine.range(of: "[FTL]") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let messageStartIndex = range.upperBound
|
||||
let message = logLine[messageStartIndex...]
|
||||
|
||||
return String(message).trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
private func isTLS(from logLine: String) -> Bool {
|
||||
return logLine.contains("TLS required for client connections")
|
||||
|| logLine.contains("websocket clients on wss://")
|
||||
}
|
||||
|
||||
deinit {
|
||||
stop()
|
||||
}
|
||||
|
||||
public enum Signal: String {
|
||||
case lameDuckMode = "ldm"
|
||||
case reload = "reload"
|
||||
}
|
||||
}
|
||||
65
Sources/bench/main.swift
Normal file
65
Sources/bench/main.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Nats
|
||||
|
||||
if CommandLine.arguments.count != 4 {
|
||||
exit(usage())
|
||||
}
|
||||
|
||||
let cmd = CommandLine.arguments[1]
|
||||
|
||||
if cmd != "pub" {
|
||||
exit(usage())
|
||||
}
|
||||
|
||||
let subject = CommandLine.arguments[2]
|
||||
guard let msgs = Int(CommandLine.arguments[3]) else {
|
||||
exit(usage())
|
||||
}
|
||||
|
||||
let nats = NatsClientOptions()
|
||||
.url(URL(string: ProcessInfo.processInfo.environment["NATS_URL"] ?? "nats://localhost:4222")!)
|
||||
.build()
|
||||
|
||||
print("connect")
|
||||
try await nats.connect()
|
||||
|
||||
let data = Data(repeating: 0, count: 128)
|
||||
|
||||
print("start")
|
||||
let now = DispatchTime.now()
|
||||
|
||||
if cmd == "pub" {
|
||||
try await pub()
|
||||
}
|
||||
|
||||
let elapsed = DispatchTime.now().uptimeNanoseconds - now.uptimeNanoseconds
|
||||
let msgsPerSec: Double = Double(msgs) / (Double(elapsed) / 1_000_000_000)
|
||||
print("elapsed: \(elapsed / 1_000_000)ms ~ \(msgsPerSec) msgs/s")
|
||||
|
||||
func pub() async throws {
|
||||
print("publish")
|
||||
for _ in 1...msgs {
|
||||
try await nats.publish(data, subject: subject)
|
||||
}
|
||||
|
||||
print("flush")
|
||||
_ = try await nats.rtt()
|
||||
}
|
||||
|
||||
func usage() -> Int32 {
|
||||
print("Usage: bench pub <subject> <msgs>")
|
||||
return 2
|
||||
}
|
||||
418
Tests/JetStreamTests/Integration/ConsumerTests.swift
Normal file
418
Tests/JetStreamTests/Integration/ConsumerTests.swift
Normal file
@@ -0,0 +1,418 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import JetStream
|
||||
import Logging
|
||||
import Nats
|
||||
import NatsServer
|
||||
import XCTest
|
||||
|
||||
class ConsumerTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testFetchWithDefaultOptions", testFetchWithDefaultOptions),
|
||||
("testFetchConsumerDeleted", testFetchConsumerDeleted),
|
||||
("testFetchExpires", testFetchExpires),
|
||||
("testFetchInvalidIdleHeartbeat", testFetchInvalidIdleHeartbeat),
|
||||
("testAck", testAck),
|
||||
("testNak", testNak),
|
||||
("testNakWithDelay", testNakWithDelay),
|
||||
("testTerm", testTerm),
|
||||
]
|
||||
|
||||
var natsServer = NatsServer()
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
natsServer.stop()
|
||||
}
|
||||
|
||||
func testFetchWithDefaultOptions() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
||||
|
||||
let payload = "hello".data(using: .utf8)!
|
||||
// publish some messages on stream
|
||||
for _ in 1...100 {
|
||||
let ack = try await ctx.publish("foo.A", message: payload)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 100)
|
||||
|
||||
let batch = try await consumer.fetch(batch: 30)
|
||||
|
||||
var i = 0
|
||||
for try await msg in batch {
|
||||
try await msg.ack()
|
||||
XCTAssertEqual(msg.payload, payload)
|
||||
i += 1
|
||||
}
|
||||
XCTAssertEqual(i, 30)
|
||||
}
|
||||
|
||||
func testFetchConsumerDeleted() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
||||
|
||||
let payload = "hello".data(using: .utf8)!
|
||||
// publish some messages on stream
|
||||
for _ in 1...10 {
|
||||
let ack = try await ctx.publish("foo.A", message: payload)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 10)
|
||||
|
||||
let batch = try await consumer.fetch(batch: 30)
|
||||
|
||||
sleep(1)
|
||||
try await stream.deleteConsumer(name: "cons")
|
||||
var i = 0
|
||||
do {
|
||||
for try await msg in batch {
|
||||
try await msg.ack()
|
||||
XCTAssertEqual(msg.payload, payload)
|
||||
i += 1
|
||||
}
|
||||
} catch JetStreamError.FetchError.consumerDeleted {
|
||||
XCTAssertEqual(i, 10)
|
||||
return
|
||||
}
|
||||
XCTFail("should get consumer deleted")
|
||||
}
|
||||
|
||||
func testFetchExpires() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
||||
|
||||
let payload = "hello".data(using: .utf8)!
|
||||
// publish some messages on stream
|
||||
for _ in 1...10 {
|
||||
let ack = try await ctx.publish("foo.A", message: payload)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 10)
|
||||
|
||||
let batch = try await consumer.fetch(batch: 30, expires: 1)
|
||||
|
||||
var i = 0
|
||||
for try await msg in batch {
|
||||
try await msg.ack()
|
||||
XCTAssertEqual(msg.payload, payload)
|
||||
i += 1
|
||||
}
|
||||
XCTAssertEqual(i, 10)
|
||||
}
|
||||
|
||||
func testFetchInvalidIdleHeartbeat() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
||||
|
||||
let batch = try await consumer.fetch(batch: 30, expires: 1, idleHeartbeat: 2)
|
||||
|
||||
do {
|
||||
for try await _ in batch {}
|
||||
} catch JetStreamError.FetchError.badRequest {
|
||||
// success
|
||||
return
|
||||
}
|
||||
XCTFail("should get bad request")
|
||||
}
|
||||
|
||||
func testFetchMissingHeartbeat() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
||||
|
||||
let payload = "hello".data(using: .utf8)!
|
||||
// publish some messages on stream
|
||||
for _ in 1...10 {
|
||||
let ack = try await ctx.publish("foo.A", message: payload)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 10)
|
||||
|
||||
try await stream.deleteConsumer(name: "cons")
|
||||
|
||||
let batch = try await consumer.fetch(batch: 30, idleHeartbeat: 1)
|
||||
|
||||
do {
|
||||
for try await _ in batch {}
|
||||
} catch JetStreamError.FetchError.noHeartbeatReceived {
|
||||
return
|
||||
} catch JetStreamError.FetchError.noResponders {
|
||||
// This is also expected when the consumer has been deleted
|
||||
return
|
||||
}
|
||||
XCTFail("should get missing heartbeats or no responders error")
|
||||
}
|
||||
|
||||
func testAck() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
// create a consumer with 500ms ack wait
|
||||
let consumer = try await stream.createConsumer(
|
||||
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
|
||||
|
||||
// publish some messages on stream
|
||||
for i in 0..<100 {
|
||||
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 100)
|
||||
|
||||
var batch = try await consumer.fetch(batch: 10)
|
||||
|
||||
var i = 0
|
||||
for try await msg in batch {
|
||||
try await msg.ack()
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(i)")
|
||||
i += 1
|
||||
}
|
||||
XCTAssertEqual(i, 10)
|
||||
|
||||
// now wait 1 second and make sure the messages are not re-delivered
|
||||
sleep(1)
|
||||
|
||||
batch = try await consumer.fetch(batch: 10)
|
||||
|
||||
for try await msg in batch {
|
||||
try await msg.ack()
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(i)")
|
||||
i += 1
|
||||
}
|
||||
XCTAssertEqual(i, 20)
|
||||
}
|
||||
|
||||
func testNak() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
// create a consumer with 500ms ack wait
|
||||
let consumer = try await stream.createConsumer(
|
||||
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
|
||||
|
||||
// publish some messages on stream
|
||||
for i in 0..<10 {
|
||||
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 10)
|
||||
|
||||
var batch = try await consumer.fetch(batch: 1)
|
||||
var iter = batch.makeAsyncIterator()
|
||||
var msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
||||
var meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 1)
|
||||
XCTAssertEqual(meta.consumerSequence, 1)
|
||||
try await msg.ack(ackType: .nak())
|
||||
|
||||
// now fetch the message again, it should be redelivered
|
||||
batch = try await consumer.fetch(batch: 1)
|
||||
iter = batch.makeAsyncIterator()
|
||||
msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
||||
meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 1)
|
||||
XCTAssertEqual(meta.consumerSequence, 2)
|
||||
try await msg.ack()
|
||||
}
|
||||
|
||||
func testNakWithDelay() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
// create a consumer with 500ms ack wait
|
||||
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
|
||||
|
||||
// publish some messages on stream
|
||||
for i in 0..<10 {
|
||||
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 10)
|
||||
|
||||
var batch = try await consumer.fetch(batch: 1)
|
||||
var iter = batch.makeAsyncIterator()
|
||||
var msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
||||
var meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 1)
|
||||
XCTAssertEqual(meta.consumerSequence, 1)
|
||||
try await msg.ack(ackType: .nak(delay: 0.5))
|
||||
|
||||
// now fetch the next message immediately, it should be the next message
|
||||
batch = try await consumer.fetch(batch: 1)
|
||||
iter = batch.makeAsyncIterator()
|
||||
msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(1)")
|
||||
meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 2)
|
||||
XCTAssertEqual(meta.consumerSequence, 2)
|
||||
try await msg.ack()
|
||||
|
||||
// wait a second, the first message should be redelivered at this point
|
||||
sleep(1)
|
||||
batch = try await consumer.fetch(batch: 1)
|
||||
iter = batch.makeAsyncIterator()
|
||||
msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
||||
meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 1)
|
||||
XCTAssertEqual(meta.consumerSequence, 3)
|
||||
}
|
||||
|
||||
func testTerm() async throws {
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
|
||||
let stream = try await ctx.createStream(cfg: streamCfg)
|
||||
|
||||
// create a consumer with 500ms ack wait
|
||||
let consumer = try await stream.createConsumer(
|
||||
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
|
||||
|
||||
// publish some messages on stream
|
||||
for i in 0..<10 {
|
||||
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
|
||||
_ = try await ack.wait()
|
||||
}
|
||||
let info = try await stream.info()
|
||||
XCTAssertEqual(info.state.messages, 10)
|
||||
|
||||
var batch = try await consumer.fetch(batch: 1)
|
||||
var iter = batch.makeAsyncIterator()
|
||||
var msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
|
||||
var meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 1)
|
||||
XCTAssertEqual(meta.consumerSequence, 1)
|
||||
try await msg.ack(ackType: .term())
|
||||
|
||||
// wait 1s, the first message should not be redelivered (even though we are past ack wait)
|
||||
sleep(1)
|
||||
batch = try await consumer.fetch(batch: 1)
|
||||
iter = batch.makeAsyncIterator()
|
||||
msg = try await iter.next()!
|
||||
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(1)")
|
||||
meta = try msg.metadata()
|
||||
XCTAssertEqual(meta.streamSequence, 2)
|
||||
XCTAssertEqual(meta.consumerSequence, 2)
|
||||
try await msg.ack()
|
||||
}
|
||||
}
|
||||
1033
Tests/JetStreamTests/Integration/JetStreamTests.swift
Normal file
1033
Tests/JetStreamTests/Integration/JetStreamTests.swift
Normal file
File diff suppressed because it is too large
Load Diff
69
Tests/JetStreamTests/Integration/RequestTests.swift
Normal file
69
Tests/JetStreamTests/Integration/RequestTests.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Piotr Piotrowski on 05/06/2024.
|
||||
//
|
||||
|
||||
import Logging
|
||||
import NIO
|
||||
import Nats
|
||||
import NatsServer
|
||||
import XCTest
|
||||
|
||||
@testable import JetStream
|
||||
|
||||
class RequestTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testRequest", testRequest)
|
||||
]
|
||||
|
||||
var natsServer = NatsServer()
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
natsServer.stop()
|
||||
}
|
||||
|
||||
func testRequest() async throws {
|
||||
|
||||
let bundle = Bundle.module
|
||||
natsServer.start(
|
||||
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let ctx = JetStreamContext(client: client)
|
||||
|
||||
let stream = """
|
||||
{
|
||||
"name": "FOO",
|
||||
"subjects": ["foo"]
|
||||
}
|
||||
"""
|
||||
let data = stream.data(using: .utf8)!
|
||||
|
||||
_ = try await client.request(data, subject: "$JS.API.STREAM.CREATE.FOO")
|
||||
|
||||
let info: Response<AccountInfo> = try await ctx.request("INFO", message: Data())
|
||||
|
||||
guard case .success(let info) = info else {
|
||||
XCTFail("request should be successful")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(info.streams, 1)
|
||||
let badInfo: Response<AccountInfo> = try await ctx.request(
|
||||
"STREAM.INFO.BAD", message: Data())
|
||||
guard case .error(let jetStreamAPIResponse) = badInfo else {
|
||||
XCTFail("should get error")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(ErrorCode.streamNotFound, jetStreamAPIResponse.error.errorCode)
|
||||
|
||||
}
|
||||
}
|
||||
3
Tests/JetStreamTests/Integration/Resources/domain.conf
Normal file
3
Tests/JetStreamTests/Integration/Resources/domain.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
jetstream: {
|
||||
domain: ABC
|
||||
}
|
||||
25
Tests/JetStreamTests/Integration/Resources/jetstream.conf
Normal file
25
Tests/JetStreamTests/Integration/Resources/jetstream.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
jetstream: {
|
||||
max_mem_store: 64MiB,
|
||||
max_file_store: 10GiB
|
||||
}
|
||||
|
||||
no_auth_user: pp
|
||||
accounts {
|
||||
JS {
|
||||
jetstream: enabled
|
||||
users: [
|
||||
{
|
||||
user: pp,
|
||||
password: foo
|
||||
}
|
||||
]
|
||||
}
|
||||
NO_JS {
|
||||
users: [
|
||||
{
|
||||
user: nojs,
|
||||
password: foo
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
Tests/JetStreamTests/Integration/Resources/prefix.conf
Normal file
17
Tests/JetStreamTests/Integration/Resources/prefix.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
jetstream: enabled
|
||||
accounts: {
|
||||
A: {
|
||||
users: [ {user: a, password: a} ]
|
||||
jetstream: enabled
|
||||
exports: [
|
||||
{service: '$JS.API.>' }
|
||||
]
|
||||
},
|
||||
I: {
|
||||
jetstream: disabled
|
||||
users: [ {user: i, password: i} ]
|
||||
imports: [
|
||||
{service: {account: A, subject: '$JS.API.>'}, to: 'fromA.>' }
|
||||
]
|
||||
}
|
||||
}
|
||||
119
Tests/JetStreamTests/Unit/MessageTests.swift
Normal file
119
Tests/JetStreamTests/Unit/MessageTests.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import JetStream
|
||||
@testable import Nats
|
||||
|
||||
class JetStreamMessageTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testValidOldFormatMessage", testValidOldFormatMessage),
|
||||
("testValidNewFormatMessage", testValidNewFormatMessage),
|
||||
("testMissingTokens", testMissingTokens),
|
||||
("testInvalidTokenValues", testInvalidTokenValues),
|
||||
("testInvalidPrefix", testInvalidPrefix),
|
||||
("testNoReplySubject", testNoReplySubject),
|
||||
]
|
||||
|
||||
func testValidOldFormatMessage() async throws {
|
||||
let replySubject = "$JS.ACK.myStream.myConsumer.10.20.30.1234567890.5"
|
||||
let natsMessage = NatsMessage(
|
||||
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
|
||||
status: nil, description: nil)
|
||||
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
|
||||
|
||||
let metadata = try jetStreamMessage.metadata()
|
||||
|
||||
XCTAssertNil(metadata.domain)
|
||||
XCTAssertNil(metadata.accountHash)
|
||||
XCTAssertEqual(metadata.stream, "myStream")
|
||||
XCTAssertEqual(metadata.consumer, "myConsumer")
|
||||
XCTAssertEqual(metadata.delivered, 10)
|
||||
XCTAssertEqual(metadata.streamSequence, 20)
|
||||
XCTAssertEqual(metadata.consumerSequence, 30)
|
||||
XCTAssertEqual(metadata.timestamp, "1234567890")
|
||||
XCTAssertEqual(metadata.pending, 5)
|
||||
}
|
||||
|
||||
func testValidNewFormatMessage() async throws {
|
||||
let replySubject = "$JS.ACK.domain.accountHash123.myStream.myConsumer.10.20.30.1234567890.5"
|
||||
let natsMessage = NatsMessage(
|
||||
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
|
||||
status: nil, description: nil)
|
||||
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
|
||||
let metadata = try jetStreamMessage.metadata()
|
||||
|
||||
XCTAssertEqual(metadata.domain, "domain")
|
||||
XCTAssertEqual(metadata.accountHash, "accountHash123")
|
||||
XCTAssertEqual(metadata.stream, "myStream")
|
||||
XCTAssertEqual(metadata.consumer, "myConsumer")
|
||||
XCTAssertEqual(metadata.delivered, 10)
|
||||
XCTAssertEqual(metadata.streamSequence, 20)
|
||||
XCTAssertEqual(metadata.consumerSequence, 30)
|
||||
XCTAssertEqual(metadata.timestamp, "1234567890")
|
||||
XCTAssertEqual(metadata.pending, 5)
|
||||
}
|
||||
|
||||
func testMissingTokens() async throws {
|
||||
let replySubject = "$JS.ACK.myStream.myConsumer"
|
||||
let natsMessage = NatsMessage(
|
||||
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
|
||||
status: nil, description: nil)
|
||||
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
|
||||
do {
|
||||
_ = try jetStreamMessage.metadata()
|
||||
} catch JetStreamError.MessageMetadataError.invalidTokenNum {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testInvalidTokenValues() async throws {
|
||||
let replySubject = "$JS.ACK.myStream.myConsumer.invalid.20.30.1234567890.5"
|
||||
let natsMessage = NatsMessage(
|
||||
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
|
||||
status: nil, description: nil)
|
||||
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
|
||||
do {
|
||||
_ = try jetStreamMessage.metadata()
|
||||
} catch JetStreamError.MessageMetadataError.invalidTokenValue {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testInvalidPrefix() async throws {
|
||||
let replySubject = "$JS.WRONG.myStream.myConsumer.10.20.30.1234567890.5"
|
||||
let natsMessage = NatsMessage(
|
||||
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
|
||||
status: nil, description: nil)
|
||||
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
|
||||
do {
|
||||
_ = try jetStreamMessage.metadata()
|
||||
} catch JetStreamError.MessageMetadataError.invalidPrefix {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testNoReplySubject() async throws {
|
||||
let natsMessage = NatsMessage(
|
||||
payload: nil, subject: "", replySubject: nil, length: 0, headers: nil, status: nil,
|
||||
description: nil)
|
||||
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
|
||||
do {
|
||||
_ = try jetStreamMessage.metadata()
|
||||
} catch JetStreamError.MessageMetadataError.noReplyInMessage {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
1286
Tests/NatsTests/Integration/ConnectionTests.swift
Executable file
1286
Tests/NatsTests/Integration/ConnectionTests.swift
Executable file
File diff suppressed because it is too large
Load Diff
103
Tests/NatsTests/Integration/EventsTests.swift
Normal file
103
Tests/NatsTests/Integration/EventsTests.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Logging
|
||||
import Nats
|
||||
import NatsServer
|
||||
import XCTest
|
||||
|
||||
class TestNatsEvents: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testClientConnectedEvent", testClientConnectedEvent),
|
||||
("testClientClosedEvent", testClientClosedEvent),
|
||||
("testClientReconnectEvent", testClientReconnectEvent),
|
||||
]
|
||||
|
||||
var natsServer = NatsServer()
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
natsServer.stop()
|
||||
}
|
||||
|
||||
func testClientConnectedEvent() async throws {
|
||||
natsServer.start()
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
|
||||
let expectation = XCTestExpectation(
|
||||
description: "client was not notified of connection established event")
|
||||
client.on(.connected) { event in
|
||||
XCTAssertEqual(event.kind(), NatsEventKind.connected)
|
||||
expectation.fulfill()
|
||||
}
|
||||
try await client.connect()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1.0)
|
||||
try await client.close()
|
||||
}
|
||||
|
||||
func testClientClosedEvent() async throws {
|
||||
natsServer.start()
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
|
||||
let expectation = XCTestExpectation(
|
||||
description: "client was not notified of connection closed event")
|
||||
client.on(.closed) { event in
|
||||
XCTAssertEqual(event.kind(), NatsEventKind.closed)
|
||||
expectation.fulfill()
|
||||
}
|
||||
try await client.connect()
|
||||
|
||||
try await client.close()
|
||||
await fulfillment(of: [expectation], timeout: 1.0)
|
||||
}
|
||||
|
||||
func testClientReconnectEvent() async throws {
|
||||
natsServer.start()
|
||||
let port = natsServer.port!
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions()
|
||||
.url(URL(string: natsServer.clientURL)!)
|
||||
.reconnectWait(1)
|
||||
.build()
|
||||
|
||||
let disconnected = XCTestExpectation(
|
||||
description: "client was not notified of disconnection event")
|
||||
client.on(.disconnected) { event in
|
||||
XCTAssertEqual(event.kind(), NatsEventKind.disconnected)
|
||||
disconnected.fulfill()
|
||||
}
|
||||
try await client.connect()
|
||||
natsServer.stop()
|
||||
|
||||
let reconnected = XCTestExpectation(
|
||||
description: "client was not notified of reconnection event")
|
||||
client.on(.connected) { event in
|
||||
XCTAssertEqual(event.kind(), NatsEventKind.connected)
|
||||
reconnected.fulfill()
|
||||
}
|
||||
await fulfillment(of: [disconnected], timeout: 5.0)
|
||||
|
||||
natsServer.start(port: port)
|
||||
await fulfillment(of: [reconnected], timeout: 5.0)
|
||||
|
||||
try await client.close()
|
||||
}
|
||||
}
|
||||
55
Tests/NatsTests/Integration/MessageWithHeadersTests.swift
Normal file
55
Tests/NatsTests/Integration/MessageWithHeadersTests.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import Logging
|
||||
import Nats
|
||||
import NatsServer
|
||||
import XCTest
|
||||
|
||||
class TestMessageWithHeadersTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testMessageWithHeaders", testMessageWithHeaders)
|
||||
]
|
||||
|
||||
var natsServer = NatsServer()
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
natsServer.stop()
|
||||
}
|
||||
|
||||
func testMessageWithHeaders() async throws {
|
||||
natsServer.start()
|
||||
logger.logLevel = .critical
|
||||
|
||||
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
|
||||
try await client.connect()
|
||||
|
||||
let sub = try await client.subscribe(subject: "foo")
|
||||
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
|
||||
hm.insert(try! NatsHeaderName("another"), NatsHeaderValue("one"))
|
||||
|
||||
try await client.publish(
|
||||
"hello".data(using: .utf8)!, subject: "foo", reply: nil, headers: hm)
|
||||
|
||||
let iter = sub.makeAsyncIterator()
|
||||
let msg = try await iter.next()
|
||||
XCTAssertEqual(msg!.headers, hm)
|
||||
|
||||
}
|
||||
}
|
||||
13
Tests/NatsTests/Integration/Resources/TestUser.creds
Normal file
13
Tests/NatsTests/Integration/Resources/TestUser.creds
Normal file
@@ -0,0 +1,13 @@
|
||||
-----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------
|
||||
|
||||
*************************************************************
|
||||
86
Tests/NatsTests/Integration/Resources/certs/client-all.pem
Normal file
86
Tests/NatsTests/Integration/Resources/certs/client-all.pem
Normal file
@@ -0,0 +1,86 @@
|
||||
-----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-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIExDCCAyygAwIBAgIQEdLeZgsrEsLe37gR/voylTANBgkqhkiG9w0BAQsFADCB
|
||||
szEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMUQwQgYDVQQLDDtwaW90
|
||||
cnBpb3Ryb3dza2lAUGlvdHJzLU1hY0Jvb2stUHJvLmxvY2FsIChQaW90ciBQaW90
|
||||
cm93c2tpKTFLMEkGA1UEAwxCbWtjZXJ0IHBpb3RycGlvdHJvd3NraUBQaW90cnMt
|
||||
TWFjQm9vay1Qcm8ubG9jYWwgKFBpb3RyIFBpb3Ryb3dza2kpMB4XDTIzMDUxMjEw
|
||||
MTYyOFoXDTI1MDgxMjEwMTYyOFowbzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3Bt
|
||||
ZW50IGNlcnRpZmljYXRlMUQwQgYDVQQLDDtwaW90cnBpb3Ryb3dza2lAUGlvdHJz
|
||||
LU1hY0Jvb2stUHJvLmxvY2FsIChQaW90ciBQaW90cm93c2tpKTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAJqlzdmIcPNu8ad7TlrJA2SdAtaxYJJK8lRs
|
||||
oAdq+PO7JeWE8NyEPSEFpXclWsEvG49gS6AueLPRuVT4WqbIDVqm5Rvcx/a1K39i
|
||||
6Ik3qpLerGY8vPngIhXoU4CUjNrEyJy22bhCPidJtPRnfkVv/eI6LSA2jWsikGNH
|
||||
Iqnbu6KtUIKbnLuuH0NR8ycYaqeiOdaCMV6STSXmM5S96qH7h7NexGC08b+aersw
|
||||
CLelFR04J0RV/cax7U1pgsWKKv8icnjiB4tq5IbYuEZE0g/uJZ3BsB5DXKq+WjNI
|
||||
uRDJJUWyzrWhBPNIW/1I2zesEXSKCvDcUVAuUceYxiEokR4+pv0CAwEAAaOBljCB
|
||||
kzAOBgNVHQ8BAf8EBAMCBaAwJwYDVR0lBCAwHgYIKwYBBQUHAwIGCCsGAQUFBwMB
|
||||
BggrBgEFBQcDBDAfBgNVHSMEGDAWgBQbD6YymnmaX19FroClM52B8doDIDA3BgNV
|
||||
HREEMDAugglsb2NhbGhvc3SBD2VtYWlsQGxvY2FsaG9zdIcQAAAAAAAAAAAAAAAA
|
||||
AAAAATANBgkqhkiG9w0BAQsFAAOCAYEAJuFrQ0KdmwEc7UyaoTygW59f1JSJGbZa
|
||||
Ii5EuMtpSon5DX5NaI5aRE350UtimNrQIu8LAPx1UGwSRuPkzvuNAA/l0HAJrqh3
|
||||
gEorH6fbsRkqkDUvmNiqTfs+So6R0s2+6yVG6t8+NT1OBH616eQ9efvthwRO0AAL
|
||||
L8LGJJdYMveEJv+GB/+Zs75MQUxniJ+ip/YxF8bcaRjVS/tb3J52yZ1Eb2UU18kN
|
||||
uAlFOxiKnwvb2csFcZ6zc4Fpm0LfCrpzPCwGF5y6bsjzpqVej87ea6roG9BJ7vbX
|
||||
xjbwGfchJZmDsG/g9MeoQoIifYqupQmtaQtlKUUD5MRjDhpOVUEJ4tsXDoZEz9DB
|
||||
kviE+VlIGU2QJ5l9KU2rIdxfh95rrIaqCt5xsT6wUjNtv0wAfbhMannUhjLv+h+G
|
||||
tIbMIEo0GFA/uY1eXLO4PTgF+EojqFfpUUM17Z3kubsOSvepxkwyipA5eI2fkThu
|
||||
Yu5Oyyq9X9Y3vnDMvHKJfkzA56Sp19Oy
|
||||
-----END CERTIFICATE-----
|
||||
27
Tests/NatsTests/Integration/Resources/certs/client-cert.pem
Normal file
27
Tests/NatsTests/Integration/Resources/certs/client-cert.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----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-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCapc3ZiHDzbvGn
|
||||
e05ayQNknQLWsWCSSvJUbKAHavjzuyXlhPDchD0hBaV3JVrBLxuPYEugLniz0blU
|
||||
+FqmyA1apuUb3Mf2tSt/YuiJN6qS3qxmPLz54CIV6FOAlIzaxMicttm4Qj4nSbT0
|
||||
Z35Fb/3iOi0gNo1rIpBjRyKp27uirVCCm5y7rh9DUfMnGGqnojnWgjFekk0l5jOU
|
||||
veqh+4ezXsRgtPG/mnq7MAi3pRUdOCdEVf3Gse1NaYLFiir/InJ44geLauSG2LhG
|
||||
RNIP7iWdwbAeQ1yqvlozSLkQySVFss61oQTzSFv9SNs3rBF0igrw3FFQLlHHmMYh
|
||||
KJEePqb9AgMBAAECggEAMypWT/mPfUsgksv+IZVOFRTJoqSvEdfQE1SZIbsnwOQT
|
||||
Zru0QRFTdEB8/U2TmETwtmAixU16y+vAiLderr2ThYGgXbaPRjWsvYnI69VKDyuz
|
||||
GGRSFc4tGNh0AB+l9p+SzB7HK+pmy/Lb9tzi7zBdbGLZGUZTRbX61Y3sjwxPKUPw
|
||||
mrTindOvSH6FbVevAC6UCl92R9vk4ugS/oDZWKPntHeJX8NzXM6MfZ68+oPKeGXE
|
||||
DTQ98nZDfZMRDvyyClaAVfstsPV1pxYNYIWaB1w0saL2NZz08zZeGnkqFt621Q4V
|
||||
gsE9t9Gjg1o5paq0MEm5vBoJo7VyCoR7w/sfEzQaAQKBgQDAk/yOY2yxGcjZjE8q
|
||||
ozXq/EtbC/ldKcgngm1KYtA7ZyzRt7gGuAuv6sbmri4wpHL1D2UeKS8QPsywkXvM
|
||||
Oto3NdJraXbC26ObCP+njwWHWD1Zh3BD2O2mYOFUsTWzsxaZJbsy6Sh4cLg+0gVc
|
||||
UYzqOnUY5EJh6hCnGSZnK4v4fQKBgQDNk/QbK4RrRH33qaAly/Ihw5ZI76s3hc4Y
|
||||
RcsGi05iAV/jiE9HVF8vWytp1EdoLsO0BPrA9RPP8SYZdHCWh0KFYJtFzU+o8+1W
|
||||
ThtCIPdmOmAtQnoj52TMmwc+x/WbNIBvBrKQHIbTX9JHUiGFM9NqSuVjNhVDOzvM
|
||||
/o2D38swgQKBgBRzou68QF7OjjYMYJv2mVNLV/VjYCg0t7z6bQDpXZPxcSEUkcak
|
||||
5RjZpiX5eY5Q6KR97g818HmZMcPOr4cQ+PvEC4S8vpATI1zjp8LzvXKSPHG1oIaU
|
||||
EykIQOXtq/ZZnpzFFQxjFpkz311MkKUtQ/ncG3N5SlN7uCkG0r1CMqtBAoGBAL/z
|
||||
myVXb9Bc5qW+a7t+/7oJDyVRK/Su6m39lQGqR2j5UZh5qVS38hycqx+ox3f+2lsX
|
||||
ny9WZsZtq55u+8WBzFoPh0wY1X2zLXO9gHQxpe99KFp6TOODZropMw2q1aiy0A1b
|
||||
GpW3HSj2urg/du8SIiCIiEEnuZjKER9qu6Zb6zSBAoGBALxqe9jb7WLArV/eMEtx
|
||||
zg7V/FZfFyqEGEbLMM9njM6uiSq0u17H5bsvmgi+dAot16BbDPKWdOw01zQDhphe
|
||||
GbchPMuNOPNyBm3MIJ5zXi4pQcc5W+Z5z54X9BCBJwIHEp+Tt9VJ6J9/RkSoTXp9
|
||||
iq9elhb5bfMSA/KliX3cBTge
|
||||
-----END PRIVATE KEY-----
|
||||
28
Tests/NatsTests/Integration/Resources/certs/client-key.pem
Normal file
28
Tests/NatsTests/Integration/Resources/certs/client-key.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----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-----
|
||||
27
Tests/NatsTests/Integration/Resources/certs/ip-ca.pem
Normal file
27
Tests/NatsTests/Integration/Resources/certs/ip-ca.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----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-----
|
||||
99
Tests/NatsTests/Integration/Resources/certs/ip-cert.pem
Normal file
99
Tests/NatsTests/Integration/Resources/certs/ip-cert.pem
Normal file
@@ -0,0 +1,99 @@
|
||||
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-----
|
||||
28
Tests/NatsTests/Integration/Resources/certs/ip-key.pem
Normal file
28
Tests/NatsTests/Integration/Resources/certs/ip-key.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----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-----
|
||||
40
Tests/NatsTests/Integration/Resources/certs/rootCA-key.pem
Normal file
40
Tests/NatsTests/Integration/Resources/certs/rootCA-key.pem
Normal file
@@ -0,0 +1,40 @@
|
||||
-----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-----
|
||||
28
Tests/NatsTests/Integration/Resources/certs/rootCA.pem
Normal file
28
Tests/NatsTests/Integration/Resources/certs/rootCA.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----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-----
|
||||
26
Tests/NatsTests/Integration/Resources/certs/server-cert.pem
Normal file
26
Tests/NatsTests/Integration/Resources/certs/server-cert.pem
Normal file
@@ -0,0 +1,26 @@
|
||||
-----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-----
|
||||
28
Tests/NatsTests/Integration/Resources/certs/server-key.pem
Normal file
28
Tests/NatsTests/Integration/Resources/certs/server-key.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----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-----
|
||||
5
Tests/NatsTests/Integration/Resources/creds.conf
Normal file
5
Tests/NatsTests/Integration/Resources/creds.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
authorization {
|
||||
user: derek
|
||||
password: s3cr3t
|
||||
}
|
||||
|
||||
15
Tests/NatsTests/Integration/Resources/jwt.conf
Normal file
15
Tests/NatsTests/Integration/Resources/jwt.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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
|
||||
|
||||
}
|
||||
1
Tests/NatsTests/Integration/Resources/nkey
Normal file
1
Tests/NatsTests/Integration/Resources/nkey
Normal file
@@ -0,0 +1 @@
|
||||
SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM
|
||||
5
Tests/NatsTests/Integration/Resources/nkey.conf
Normal file
5
Tests/NatsTests/Integration/Resources/nkey.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
authorization: {
|
||||
users: [
|
||||
{ nkey: UAFHG6FUD2UU4SDFVAFUL5LDFO2XM4WYM76UNXTPJYJK7EEMYRBHT2VE }
|
||||
]
|
||||
}
|
||||
15
Tests/NatsTests/Integration/Resources/permissions.conf
Normal file
15
Tests/NatsTests/Integration/Resources/permissions.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
no_auth_user: pp
|
||||
|
||||
accounts {
|
||||
ACC {
|
||||
users: [
|
||||
{
|
||||
user: pp,
|
||||
password: foo
|
||||
permissions: {
|
||||
subscribe: { %@: ["%@"] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
10
Tests/NatsTests/Integration/Resources/tls.conf
Normal file
10
Tests/NatsTests/Integration/Resources/tls.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
# this needs to be here for testing localhost tls.
|
||||
listen: localhost:4222
|
||||
|
||||
tls {
|
||||
cert_file: "%@"
|
||||
key_file: "%@"
|
||||
ca_file: "%@"
|
||||
verify : true
|
||||
timeout: 2
|
||||
}
|
||||
11
Tests/NatsTests/Integration/Resources/tls_first.conf
Normal file
11
Tests/NatsTests/Integration/Resources/tls_first.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
# this needs to be here for testing localhost tls.
|
||||
listen: localhost:4222
|
||||
|
||||
tls {
|
||||
cert_file: "%@"
|
||||
key_file: "%@"
|
||||
ca_file: "%@"
|
||||
verify : true
|
||||
timeout: 2
|
||||
handshake_first: true
|
||||
}
|
||||
11
Tests/NatsTests/Integration/Resources/tls_first_auto.conf
Normal file
11
Tests/NatsTests/Integration/Resources/tls_first_auto.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
# this needs to be here for testing localhost tls.
|
||||
listen: localhost:4545
|
||||
|
||||
tls {
|
||||
cert_file: "./Tests/NatsTests/Integration/Resources/certs/server-cert.pem"
|
||||
key_file: "./Tests/NatsTests/Integration/Resources/certs/server-key.pem"
|
||||
ca_file: "./Tests/NatsTests/Integration/Resources/certs/rootCA.pem"
|
||||
verify : true
|
||||
timeout: 2
|
||||
handshake_first: 300ms
|
||||
}
|
||||
4
Tests/NatsTests/Integration/Resources/token.conf
Normal file
4
Tests/NatsTests/Integration/Resources/token.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
authorization {
|
||||
token: s3cr3t
|
||||
}
|
||||
|
||||
4
Tests/NatsTests/Integration/Resources/ws.conf
Normal file
4
Tests/NatsTests/Integration/Resources/ws.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
websocket {
|
||||
port: -1
|
||||
no_tls: true
|
||||
}
|
||||
10
Tests/NatsTests/Integration/Resources/wss.conf
Normal file
10
Tests/NatsTests/Integration/Resources/wss.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
websocket {
|
||||
port: -1
|
||||
tls {
|
||||
cert_file: "%@"
|
||||
key_file: "%@"
|
||||
ca_file: "%@"
|
||||
verify : true
|
||||
timeout: 2
|
||||
}
|
||||
}
|
||||
42
Tests/NatsTests/Unit/ErrorsTests.swift
Normal file
42
Tests/NatsTests/Unit/ErrorsTests.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import Nats
|
||||
|
||||
class ErrorsTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testServerErrorPermissionsDenied", testServerErrorPermissionsDenied)
|
||||
]
|
||||
|
||||
func testServerErrorPermissionsDenied() {
|
||||
var err = NatsError.ServerError(
|
||||
"Permissions Violation for Subscription to \"events.A.B.*\"]")
|
||||
XCTAssertEqual(
|
||||
err, NatsError.ServerError.permissionsViolation(.subscribe, "events.A.B.*", nil))
|
||||
|
||||
err = NatsError.ServerError("Permissions Violation for Publish to \"events.A.B.*\"")
|
||||
XCTAssertEqual(
|
||||
err, NatsError.ServerError.permissionsViolation(.publish, "events.A.B.*", nil))
|
||||
|
||||
err = NatsError.ServerError(
|
||||
"Permissions Violation for Publish to \"events.A.B.*\" using queue \"q\"")
|
||||
XCTAssertEqual(
|
||||
err, NatsError.ServerError.permissionsViolation(.publish, "events.A.B.*", "q"))
|
||||
|
||||
err = NatsError.ServerError("Some other error")
|
||||
XCTAssertEqual(err, NatsError.ServerError.proto("Some other error"))
|
||||
}
|
||||
}
|
||||
97
Tests/NatsTests/Unit/HeadersTests.swift
Normal file
97
Tests/NatsTests/Unit/HeadersTests.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import Nats
|
||||
|
||||
class HeadersTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testAppend", testAppend),
|
||||
("testSubscript", testSubscript),
|
||||
("testInsert", testInsert),
|
||||
("testSerialize", testSerialize),
|
||||
("testValidNatsHeaderName", testValidNatsHeaderName),
|
||||
("testDollarNatsHeaderName", testDollarNatsHeaderName),
|
||||
("testInvalidNatsHeaderName", testInvalidNatsHeaderName),
|
||||
(
|
||||
"testInvalidNatsHeaderNameWithSpecialCharacters",
|
||||
testInvalidNatsHeaderNameWithSpecialCharacters
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
func testAppend() {
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
|
||||
XCTAssertEqual(
|
||||
hm.getAll(try! NatsHeaderName("foo")), [NatsHeaderValue("bar"), NatsHeaderValue("baz")])
|
||||
}
|
||||
|
||||
func testInsert() {
|
||||
var hm = NatsHeaderMap()
|
||||
hm.insert(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
XCTAssertEqual(hm.getAll(try! NatsHeaderName("foo")), [NatsHeaderValue("bar")])
|
||||
}
|
||||
|
||||
func testSerialize() {
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
|
||||
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
|
||||
hm.insert(try! NatsHeaderName("bar"), NatsHeaderValue("foo"))
|
||||
|
||||
let expected = [
|
||||
"NATS/1.0\r\nfoo:bar\r\nfoo:baz\r\nbar:foo\r\n\r\n",
|
||||
"NATS/1.0\r\nbar:foo\r\nfoo:bar\r\nfoo:baz\r\n\r\n",
|
||||
]
|
||||
|
||||
XCTAssertTrue(expected.contains(String(bytes: hm.toBytes(), encoding: .utf8)!))
|
||||
}
|
||||
|
||||
func testValidNatsHeaderName() {
|
||||
XCTAssertNoThrow(try NatsHeaderName("X-Custom-Header"))
|
||||
}
|
||||
|
||||
func testDollarNatsHeaderName() {
|
||||
XCTAssertNoThrow(try NatsHeaderName("$Dollar"))
|
||||
}
|
||||
|
||||
func testInvalidNatsHeaderName() {
|
||||
XCTAssertThrowsError(try NatsHeaderName("Invalid Header Name"))
|
||||
}
|
||||
|
||||
func testInvalidNatsHeaderNameWithSpecialCharacters() {
|
||||
XCTAssertThrowsError(try NatsHeaderName("Invalid:Header:Name"))
|
||||
}
|
||||
|
||||
func testSubscript() {
|
||||
var hm = NatsHeaderMap()
|
||||
|
||||
// Test setting a value
|
||||
hm[try! NatsHeaderName("foo")] = NatsHeaderValue("bar")
|
||||
XCTAssertEqual(hm[try! NatsHeaderName("foo")], NatsHeaderValue("bar"))
|
||||
|
||||
// Test updating existing value
|
||||
hm[try! NatsHeaderName("foo")] = NatsHeaderValue("baz")
|
||||
XCTAssertEqual(hm[try! NatsHeaderName("foo")], NatsHeaderValue("baz"))
|
||||
|
||||
// Test retrieving non-existing value (should be nil or default)
|
||||
XCTAssertNil(hm[try! NatsHeaderName("non-existing")])
|
||||
|
||||
// Test removal of a value
|
||||
hm[try! NatsHeaderName("foo")] = nil
|
||||
XCTAssertNil(hm[try! NatsHeaderName("foo")])
|
||||
}
|
||||
}
|
||||
43
Tests/NatsTests/Unit/JwtTests.swift
Normal file
43
Tests/NatsTests/Unit/JwtTests.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Nats
|
||||
|
||||
class JwtTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testParseCredentialsFile", testParseCredentialsFile)
|
||||
]
|
||||
|
||||
func testParseCredentialsFile() async throws {
|
||||
logger.logLevel = .critical
|
||||
let currentFile = URL(fileURLWithPath: #file)
|
||||
let testDir = currentFile.deletingLastPathComponent().deletingLastPathComponent()
|
||||
let resourceURL = testDir.appendingPathComponent("Integration/Resources/TestUser.creds")
|
||||
let credsData = try await URLSession.shared.data(from: resourceURL).0
|
||||
|
||||
let nkey = String(data: JwtUtils.parseDecoratedNKey(contents: credsData)!, encoding: .utf8)
|
||||
let expectedNkey = "SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM"
|
||||
|
||||
XCTAssertEqual(nkey, expectedNkey)
|
||||
|
||||
let jwt = String(data: JwtUtils.parseDecoratedJWT(contents: credsData)!, encoding: .utf8)
|
||||
let expectedJWT =
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJMN1dBT1hJU0tPSUZNM1QyNEhMQ09ENzJRT1czQkNVWEdETjRKVU1SSUtHTlQ3RzdZVFRRIiwiaWF0IjoxNjUxNzkwOTgyLCJpc3MiOiJBRFRRUzdaQ0ZWSk5XNTcyNkdPWVhXNVRTQ1pGTklRU0hLMlpHWVVCQ0Q1RDc3T1ROTE9PS1pPWiIsIm5hbWUiOiJUZXN0VXNlciIsInN1YiI6IlVBRkhHNkZVRDJVVTRTREZWQUZVTDVMREZPMlhNNFdZTTc2VU5YVFBKWUpLN0VFTVlSQkhUMlZFIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.bp2-Jsy33l4ayF7Ku1MNdJby4WiMKUrG-rSVYGBusAtV3xP4EdCa-zhSNUaBVIL3uYPPCQYCEoM1pCUdOnoJBg"
|
||||
|
||||
XCTAssertEqual(jwt, expectedJWT)
|
||||
}
|
||||
}
|
||||
39
Tests/NatsTests/Unit/NatsClientOptionsTests.swift
Normal file
39
Tests/NatsTests/Unit/NatsClientOptionsTests.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import Nats
|
||||
|
||||
class NatsClientOptionsTests: XCTestCase {
|
||||
static var allTests = [
|
||||
("testDefaultInboxPrefix", testDefaultInboxPrefix),
|
||||
("testCustomInboxPrefix", testCustomInboxPrefix),
|
||||
]
|
||||
|
||||
func testDefaultInboxPrefix() {
|
||||
let client = NatsClientOptions().build()
|
||||
let inbox = client.newInbox()
|
||||
XCTAssertTrue(inbox.hasPrefix("_INBOX."), "Default inbox prefix should be '_INBOX.'")
|
||||
XCTAssertEqual(inbox.count, "_INBOX.".count + 22, "Inbox should have prefix plus NUID")
|
||||
}
|
||||
|
||||
func testCustomInboxPrefix() {
|
||||
let customPrefix = "_INBOX_abc123."
|
||||
let client = NatsClientOptions().inboxPrefix(customPrefix).build()
|
||||
let inbox = client.newInbox()
|
||||
XCTAssertTrue(inbox.hasPrefix(customPrefix), "Inbox should use custom prefix")
|
||||
XCTAssertEqual(
|
||||
inbox.count, customPrefix.count + 22, "Inbox should have custom prefix plus NUID")
|
||||
}
|
||||
}
|
||||
253
Tests/NatsTests/Unit/ParserTests.swift
Normal file
253
Tests/NatsTests/Unit/ParserTests.swift
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright 2024 The NATS 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.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import Nats
|
||||
|
||||
class ParserTests: XCTestCase {
|
||||
|
||||
static var allTests = [
|
||||
("testParseOutMessages", testParseOutMessages)
|
||||
]
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testParseOutMessages() {
|
||||
struct TestCase {
|
||||
let name: String
|
||||
let givenChunks: [String]
|
||||
let expectedOps: [ServerOp]
|
||||
}
|
||||
|
||||
let fail: ((Int, String) -> String) = { index, name in
|
||||
return "Test case: \(index)\n Input: \(name)"
|
||||
}
|
||||
var hm = NatsHeaderMap()
|
||||
hm.append(try! NatsHeaderName("h1"), NatsHeaderValue("X"))
|
||||
hm.append(try! NatsHeaderName("h1"), NatsHeaderValue("Y"))
|
||||
hm.append(try! NatsHeaderName("h2"), NatsHeaderValue("Z"))
|
||||
|
||||
let testCases = [
|
||||
TestCase(
|
||||
name: "Single chunk, different operations",
|
||||
givenChunks: ["MSG foo 1 5\r\nhello\r\n+OK\r\nPONG\r\n"],
|
||||
expectedOps: [
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.ok,
|
||||
.pong,
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "Chunked messages",
|
||||
givenChunks: [
|
||||
"MSG foo 1 5\r\nhello\r\nMSG foo 1 5\r\nwo",
|
||||
"rld\r\nMSG f",
|
||||
"oo 1 5\r\nhello\r\nMSG foo 1 5\r\nworld",
|
||||
"\r\nMSG foo 1 5\r\nhello\r",
|
||||
"\nMSG foo 1 5\r\nworld\r\nMSG foo 1 5\r\n",
|
||||
"hello\r\n",
|
||||
],
|
||||
expectedOps: [
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "Message with headers only",
|
||||
givenChunks: [
|
||||
"HMSG foo 1 30 30\r\nNATS/1.0\r\nh1:X\r\nh1:Y\r\nh2:Z\r\n\r\n\r\n"
|
||||
],
|
||||
expectedOps: [
|
||||
.hMessage(
|
||||
HMessageInbound(
|
||||
subject: "foo", sid: 1, payload: nil, headers: hm, headersLength: 30,
|
||||
length: 30)
|
||||
)
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "Message with headers and payload",
|
||||
givenChunks: [
|
||||
"HMSG foo 1 30 35\r\nNATS/1.0\r\nh1:X\r\nh1:Y\r\nh2:Z\r\n\r\nhello\r\n"
|
||||
],
|
||||
expectedOps: [
|
||||
.hMessage(
|
||||
HMessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!,
|
||||
headers: hm, headersLength: 30, length: 35)
|
||||
)
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "Message with status and no other headers",
|
||||
givenChunks: [
|
||||
"HMSG foo 1 30 30\r\nNATS/1.0 503 no responders\r\n\r\n\r\n"
|
||||
],
|
||||
expectedOps: [
|
||||
.hMessage(
|
||||
HMessageInbound(
|
||||
subject: "foo", sid: 1, payload: nil, headers: NatsHeaderMap(),
|
||||
headersLength: 30, length: 30, status: StatusCode.noResponders,
|
||||
description: "no responders"
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "Message with status, headers and payload",
|
||||
givenChunks: [
|
||||
"HMSG foo 1 48 53\r\nNATS/1.0 503 no responders\r\nh1:X\r\nh1:Y\r\nh2:Z\r\n\r\nhello\r\n"
|
||||
],
|
||||
expectedOps: [
|
||||
.hMessage(
|
||||
HMessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!,
|
||||
headers: hm, headersLength: 48, length: 53,
|
||||
status: StatusCode.noResponders,
|
||||
description: "no responders")
|
||||
)
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "With empty lines",
|
||||
givenChunks: [
|
||||
"MSG foo 1 5\r\nhello\r\nMSG foo 1 5\r\nwo",
|
||||
"",
|
||||
"",
|
||||
"rld\r\nMSG f",
|
||||
"oo 1 5\r\nhello\r\n",
|
||||
],
|
||||
expectedOps: [
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
|
||||
),
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
name: "With crlf in payload",
|
||||
givenChunks: [
|
||||
"MSG foo 1 7\r\nhe\r\nllo\r\n"
|
||||
],
|
||||
expectedOps: [
|
||||
.message(
|
||||
MessageInbound(
|
||||
subject: "foo", sid: 1, payload: Data("he\r\nllo".utf8), length: 7))
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
for (tn, tc) in testCases.enumerated() {
|
||||
logger.logLevel = .critical
|
||||
var ops = [ServerOp]()
|
||||
var prevRemainder: Data?
|
||||
for chunk in tc.givenChunks {
|
||||
var chunkData = Data(chunk.utf8)
|
||||
if let prevRemainder {
|
||||
chunkData.prepend(prevRemainder)
|
||||
}
|
||||
let res = try! chunkData.parseOutMessages()
|
||||
prevRemainder = res.remainder
|
||||
ops.append(contentsOf: res.ops)
|
||||
}
|
||||
XCTAssertEqual(ops.count, tc.expectedOps.count)
|
||||
for (i, op) in ops.enumerated() {
|
||||
switch op {
|
||||
case .ok:
|
||||
if case .ok = tc.expectedOps[i] {
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
case .info(let info):
|
||||
if case .info(let expectedInfo) = tc.expectedOps[i] {
|
||||
XCTAssertEqual(info, expectedInfo, fail(tn, tc.name))
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
|
||||
case .ping:
|
||||
if case .ping = tc.expectedOps[i] {
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
case .pong:
|
||||
if case .pong = tc.expectedOps[i] {
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
case .error(_):
|
||||
if case .error(_) = tc.expectedOps[i] {
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
case .message(let msg):
|
||||
if case .message(let expectedMessage) = tc.expectedOps[i] {
|
||||
XCTAssertEqual(msg, expectedMessage, fail(tn, tc.name))
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
case .hMessage(let msg):
|
||||
if case .hMessage(let expectedMessage) = tc.expectedOps[i] {
|
||||
XCTAssertEqual(msg, expectedMessage, fail(tn, tc.name))
|
||||
} else {
|
||||
XCTFail(fail(tn, tc.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user