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