p2p-swift / Sources / IrdP2PSignaling / Mailbox / MailboxTypes.swift
649 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import Foundation

/// A mailbox sequence number. The node returns it as a decimal string because it
/// packs a millisecond clock into its high bits, past what a JSON number
/// survives in JavaScript. Compare as an opaque ordered token, never as a float.
public struct MailboxSeq: Sendable, Hashable, Comparable, CustomStringConvertible, Codable {
    public let raw: String

    public init(_ raw: String) { self.raw = raw }

    public init(from decoder: any Decoder) throws {
        let c = try decoder.singleValueContainer()
        if let s = try? c.decode(String.self) { raw = s } else { raw = String(try c.decode(UInt64.self)) }
    }

    public func encode(to encoder: any Encoder) throws {
        var c = encoder.singleValueContainer()
        try c.encode(raw)
    }

    public static func < (lhs: MailboxSeq, rhs: MailboxSeq) -> Bool {
        if let a = UInt64(lhs.raw), let b = UInt64(rhs.raw) { return a < b }
        if lhs.raw.count != rhs.raw.count { return lhs.raw.count < rhs.raw.count }
        return lhs.raw < rhs.raw
    }

    public var description: String { raw }
}

/// One replayed message; `payload` is the raw bytes that were deposited.
public struct MailboxMessage: Sendable, Equatable {
    public var seq: MailboxSeq
    public var id: String
    public var timestampMs: Int64
    public var payload: Data

    public init(seq: MailboxSeq, id: String, timestampMs: Int64, payload: Data) {
        self.seq = seq
        self.id = id
        self.timestampMs = timestampMs
        self.payload = payload
    }

    public var date: Date { Date(timeIntervalSince1970: Double(timestampMs) / 1000) }
}

/// One page of a replay.
public struct ReplayPage: Sendable, Equatable {
    public var messages: [MailboxMessage]
    public var hasMore: Bool
    public var oldestSeq: MailboxSeq?
    public var latestSeq: MailboxSeq?

    public init(messages: [MailboxMessage], hasMore: Bool, oldestSeq: MailboxSeq?, latestSeq: MailboxSeq?) {
        self.messages = messages
        self.hasMore = hasMore
        self.oldestSeq = oldestSeq
        self.latestSeq = latestSeq
    }
}

/// The result of a deposit; `deduped` means this exact payload was already stored.
public struct DepositResult: Sendable, Equatable {
    public var seq: MailboxSeq
    public var id: String
    public var expiresAt: Date
    public var deduped: Bool

    public init(seq: MailboxSeq, id: String, expiresAt: Date, deduped: Bool) {
        self.seq = seq
        self.id = id
        self.expiresAt = expiresAt
        self.deduped = deduped
    }
}

/// A mailbox bearer token and where it is valid.
public struct MailboxToken: Sendable, Equatable {
    public var token: String
    public var expiresAt: Date
    /// The room the token is scoped to; `nil` for an app-wide token.
    public var room: String?
    /// The mailbox base URL the token is meant for, when the mint told us.
    public var endpoint: URL?

    public init(token: String, expiresAt: Date, room: String? = nil, endpoint: URL? = nil) {
        self.token = token
        self.expiresAt = expiresAt
        self.room = room
        self.endpoint = endpoint
    }
}

// MARK: wire decoding

struct MintResponse: Decodable {
    var token: String
    var ttl: Int?
    var expires_at: Double
    var endpoint: URL?
    var room: String?
}

struct DepositResponse: Decodable {
    var seq: MailboxSeq
    var id: String
    var expires_at: Double
    var deduped: Bool?
}

struct ReplayResponse: Decodable {
    struct Item: Decodable { var seq: MailboxSeq; var id: String; var ts: Int64; var payload: String }
    var messages: [Item]
    var has_more: Bool?
    var oldest_seq: MailboxSeq?
    var latest_seq: MailboxSeq?
}

struct ErrorBody: Decodable { var error: String }

enum MailboxWire {
    /// Percent-encodes a room id as ONE path segment (slashes included), so ids
    /// like `/orbitdb/zdpu...` survive; `.urlPathAllowed` would not escape `/`.
    static func encodeSegment(_ s: String) -> String {
        var allowed = CharacterSet.alphanumerics
        allowed.insert(charactersIn: "-._~")
        return s.addingPercentEncoding(withAllowedCharacters: allowed) ?? s
    }

    static func messagesURL(endpoint: URL, room: String) -> URL {
        var base = endpoint.absoluteString
        while base.hasSuffix("/") { base.removeLast() }
        return URL(string: "\(base)/rooms/\(encodeSegment(room))/messages")!
    }

    static func validateRoom(_ room: String) throws {
        guard !room.trimmingCharacters(in: .whitespaces).isEmpty else { throw P2PError.config("mailbox room id is blank") }
        guard room.utf8.count <= 4096 else { throw P2PError.config("mailbox room id exceeds 4096 bytes") }
    }

    /// Maps a non-2xx response to `P2PError.http`, reading `{"error":"<code>"}` when present.
    static func error(_ response: HTTPResponse) -> P2PError {
        let code = (try? JSONDecoder().decode(ErrorBody.self, from: response.body))?.error
        return .http(status: response.status, code: code, body: response.body)
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git