Skip to content

Commit 5b447ff

Browse files
Added an explicit Expiration type to the WebPushManager
1 parent 466c14e commit 5b447ff

File tree

2 files changed

+98
-8
lines changed

2 files changed

+98
-8
lines changed

Sources/WebPush/WebPushManager.swift

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public actor WebPushManager: Sendable {
199199
public func send(
200200
data message: some DataProtocol,
201201
to subscriber: some SubscriberProtocol,
202-
expiration: VAPID.Configuration.Duration = .days(30),
202+
expiration: Expiration = .recommendedMaximum,
203203
urgency: Urgency = .high
204204
) async throws {
205205
switch executor {
@@ -228,7 +228,7 @@ public actor WebPushManager: Sendable {
228228
public func send(
229229
string message: some StringProtocol,
230230
to subscriber: some SubscriberProtocol,
231-
expiration: VAPID.Configuration.Duration = .days(30),
231+
expiration: Expiration = .recommendedMaximum,
232232
urgency: Urgency = .high
233233
) async throws {
234234
try await routeMessage(
@@ -251,7 +251,7 @@ public actor WebPushManager: Sendable {
251251
public func send(
252252
json message: some Encodable&Sendable,
253253
to subscriber: some SubscriberProtocol,
254-
expiration: VAPID.Configuration.Duration = .days(30),
254+
expiration: Expiration = .recommendedMaximum,
255255
urgency: Urgency = .high
256256
) async throws {
257257
try await routeMessage(
@@ -271,7 +271,7 @@ public actor WebPushManager: Sendable {
271271
func routeMessage(
272272
message: _Message,
273273
to subscriber: some SubscriberProtocol,
274-
expiration: VAPID.Configuration.Duration,
274+
expiration: Expiration,
275275
urgency: Urgency
276276
) async throws {
277277
switch executor {
@@ -304,7 +304,7 @@ public actor WebPushManager: Sendable {
304304
httpClient: some HTTPClientProtocol,
305305
data message: some DataProtocol,
306306
subscriber: some SubscriberProtocol,
307-
expiration: VAPID.Configuration.Duration,
307+
expiration: Expiration,
308308
urgency: Urgency
309309
) async throws {
310310
guard let signingKey = vapidKeyLookup[subscriber.vapidKeyID]
@@ -377,13 +377,19 @@ public actor WebPushManager: Sendable {
377377
/// Attach the header with our public key and salt, along with the authentication tag.
378378
let requestContent = contentCodingHeader + encryptedRecord.ciphertext + encryptedRecord.tag
379379

380+
if expiration < Expiration.dropIfUndeliverable {
381+
logger.error("The message expiration must be greater than or equal to \(Expiration.dropIfUndeliverable) seconds.", metadata: ["expiration": "\(expiration)"])
382+
} else if expiration >= Expiration.recommendedMaximum {
383+
logger.warning("The message expiration should be less than \(Expiration.recommendedMaximum) seconds.", metadata: ["expiration": "\(expiration)"])
384+
}
385+
380386
/// Add the VAPID authorization and corrent content encoding and type.
381387
var request = HTTPClientRequest(url: subscriber.endpoint.absoluteURL.absoluteString)
382388
request.method = .POST
383389
request.headers.add(name: "Authorization", value: authorization)
384390
request.headers.add(name: "Content-Encoding", value: "aes128gcm")
385391
request.headers.add(name: "Content-Type", value: "application/octet-stream")
386-
request.headers.add(name: "TTL", value: "\(expiration.seconds)")
392+
request.headers.add(name: "TTL", value: "\(max(expiration, .dropIfUndeliverable).seconds)")
387393
request.headers.add(name: "Urgency", value: "\(urgency)")
388394
request.body = .bytes(ByteBuffer(bytes: requestContent))
389395

@@ -424,6 +430,90 @@ extension WebPushManager: Service {
424430

425431
// MARK: - Public Types
426432

433+
extension WebPushManager {
434+
/// A duration in seconds used to express when push messages will expire.
435+
///
436+
/// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP §5.2. Push Message Time-To-Live](https://datatracker.ietf.org/doc/html/rfc8030#section-5.2)
437+
public struct Expiration: Hashable, Comparable, Codable, ExpressibleByIntegerLiteral, AdditiveArithmetic, Sendable {
438+
/// The number of seconds represented by this expiration.
439+
public let seconds: Int
440+
441+
/// The recommended maximum expiration duration push services are expected to support.
442+
///
443+
/// - Note: A time of 30 days was chosen to match the maximum Apple Push Notification Services (APNS) accepts, but there is otherwise no recommended value here. Note that other services are instead limited to 4 weeks, or 28 days.
444+
public static let recommendedMaximum: Self = .days(30)
445+
446+
/// The message will be delivered immediately, otherwise it'll be dropped.
447+
///
448+
/// A Push message with a zero TTL is immediately delivered if the user agent is available to receive the message. After delivery, the push service is permitted to immediately remove a push message with a zero TTL. This might occur before the user agent acknowledges receipt of the message by performing an HTTP DELETE on the push message resource. Consequently, an application server cannot rely on receiving acknowledgement receipts for zero TTL push messages.
449+
///
450+
/// If the user agent is unavailable, a push message with a zero TTL expires and is never delivered.
451+
///
452+
/// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP §5.2. Push Message Time-To-Live](https://datatracker.ietf.org/doc/html/rfc8030#section-5.2)
453+
public static let dropIfUndeliverable: Self = .zero
454+
455+
/// Initialize an expiration with a number of seconds.
456+
@inlinable
457+
public init(seconds: Int) {
458+
self.seconds = seconds
459+
}
460+
461+
@inlinable
462+
public static func < (lhs: Self, rhs: Self) -> Bool {
463+
lhs.seconds < rhs.seconds
464+
}
465+
466+
public init(from decoder: Decoder) throws {
467+
let container = try decoder.singleValueContainer()
468+
self.seconds = try container.decode(Int.self)
469+
}
470+
471+
public func encode(to encoder: any Encoder) throws {
472+
var container = encoder.singleValueContainer()
473+
try container.encode(self.seconds)
474+
}
475+
476+
@inlinable
477+
public init(integerLiteral value: Int) {
478+
self.seconds = value
479+
}
480+
481+
@inlinable
482+
public static func - (lhs: Self, rhs: Self) -> Self {
483+
Self(seconds: lhs.seconds - rhs.seconds)
484+
}
485+
486+
@inlinable
487+
public static func + (lhs: Self, rhs: Self) -> Self {
488+
Self(seconds: lhs.seconds + rhs.seconds)
489+
}
490+
491+
/// Make an expiration with a number of seconds.
492+
@inlinable
493+
public static func seconds(_ seconds: Int) -> Self {
494+
Self(seconds: seconds)
495+
}
496+
497+
/// Make an expiration with a number of minutes.
498+
@inlinable
499+
public static func minutes(_ minutes: Int) -> Self {
500+
.seconds(minutes*60)
501+
}
502+
503+
/// Make an expiration with a number of hours.
504+
@inlinable
505+
public static func hours(_ hours: Int) -> Self {
506+
.minutes(hours*60)
507+
}
508+
509+
/// Make an expiration with a number of days.
510+
@inlinable
511+
public static func days(_ days: Int) -> Self {
512+
.hours(days*24)
513+
}
514+
}
515+
}
516+
427517
extension WebPushManager {
428518
public struct Urgency: Hashable, Comparable, Sendable, CustomStringConvertible {
429519
let rawValue: String
@@ -495,7 +585,7 @@ extension WebPushManager {
495585
case handler(@Sendable (
496586
_ message: _Message,
497587
_ subscriber: Subscriber,
498-
_ expiration: VAPID.Configuration.Duration,
588+
_ expiration: Expiration,
499589
_ urgency: Urgency
500590
) async throws -> Void)
501591
}

Sources/WebPushTesting/WebPushManager+Testing.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ extension WebPushManager {
2828
messageHandler: @escaping @Sendable (
2929
_ message: Message,
3030
_ subscriber: Subscriber,
31-
_ expiration: VAPID.Configuration.Duration,
31+
_ expiration: Expiration,
3232
_ urgency: Urgency
3333
) async throws -> Void
3434
) -> WebPushManager {

0 commit comments

Comments
 (0)