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