Files
nats.swift/Sources/JetStream/JetStreamMessage.swift
wenzuhuai d7bdb4f378
Some checks failed
ci / macos (push) Has been cancelled
ci / ios (push) Has been cancelled
ci / check-linter (push) Has been cancelled
init
2026-01-12 18:29:52 +08:00

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