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

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

View File

@@ -0,0 +1,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()
}
}
}