init
This commit is contained in:
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
Reference in New Issue
Block a user