init
Some checks failed
ci / macos (push) Has been cancelled
ci / ios (push) Has been cancelled
ci / check-linter (push) Has been cancelled

This commit is contained in:
wenzuhuai
2026-01-12 18:29:52 +08:00
commit d7bdb4f378
87 changed files with 12664 additions and 0 deletions

View 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"
}
}

View 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
}

View 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()
}
}
}

View 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()
}
}
}

View 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
}

View 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)
}
}
}

View 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
}
}
}

View 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)
}
}

View 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)
}
}

File diff suppressed because it is too large Load Diff