194 lines
7.0 KiB
Swift
194 lines
7.0 KiB
Swift
// 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
|
|
}
|
|
}
|
|
}
|