@@ -199,7 +199,7 @@ public actor WebPushManager: Sendable {
199
199
public func send(
200
200
data message: some DataProtocol ,
201
201
to subscriber: some SubscriberProtocol ,
202
- expiration: VAPID . Configuration . Duration = . days ( 30 ) ,
202
+ expiration: Expiration = . recommendedMaximum ,
203
203
urgency: Urgency = . high
204
204
) async throws {
205
205
switch executor {
@@ -228,7 +228,7 @@ public actor WebPushManager: Sendable {
228
228
public func send(
229
229
string message: some StringProtocol ,
230
230
to subscriber: some SubscriberProtocol ,
231
- expiration: VAPID . Configuration . Duration = . days ( 30 ) ,
231
+ expiration: Expiration = . recommendedMaximum ,
232
232
urgency: Urgency = . high
233
233
) async throws {
234
234
try await routeMessage (
@@ -251,7 +251,7 @@ public actor WebPushManager: Sendable {
251
251
public func send(
252
252
json message: some Encodable & Sendable ,
253
253
to subscriber: some SubscriberProtocol ,
254
- expiration: VAPID . Configuration . Duration = . days ( 30 ) ,
254
+ expiration: Expiration = . recommendedMaximum ,
255
255
urgency: Urgency = . high
256
256
) async throws {
257
257
try await routeMessage (
@@ -271,7 +271,7 @@ public actor WebPushManager: Sendable {
271
271
func routeMessage(
272
272
message: _Message ,
273
273
to subscriber: some SubscriberProtocol ,
274
- expiration: VAPID . Configuration . Duration ,
274
+ expiration: Expiration ,
275
275
urgency: Urgency
276
276
) async throws {
277
277
switch executor {
@@ -304,7 +304,7 @@ public actor WebPushManager: Sendable {
304
304
httpClient: some HTTPClientProtocol ,
305
305
data message: some DataProtocol ,
306
306
subscriber: some SubscriberProtocol ,
307
- expiration: VAPID . Configuration . Duration ,
307
+ expiration: Expiration ,
308
308
urgency: Urgency
309
309
) async throws {
310
310
guard let signingKey = vapidKeyLookup [ subscriber. vapidKeyID]
@@ -377,13 +377,19 @@ public actor WebPushManager: Sendable {
377
377
/// Attach the header with our public key and salt, along with the authentication tag.
378
378
let requestContent = contentCodingHeader + encryptedRecord. ciphertext + encryptedRecord. tag
379
379
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
+
380
386
/// Add the VAPID authorization and corrent content encoding and type.
381
387
var request = HTTPClientRequest ( url: subscriber. endpoint. absoluteURL. absoluteString)
382
388
request. method = . POST
383
389
request. headers. add ( name: " Authorization " , value: authorization)
384
390
request. headers. add ( name: " Content-Encoding " , value: " aes128gcm " )
385
391
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) " )
387
393
request. headers. add ( name: " Urgency " , value: " \( urgency) " )
388
394
request. body = . bytes( ByteBuffer ( bytes: requestContent) )
389
395
@@ -424,6 +430,90 @@ extension WebPushManager: Service {
424
430
425
431
// MARK: - Public Types
426
432
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
+
427
517
extension WebPushManager {
428
518
public struct Urgency : Hashable , Comparable , Sendable , CustomStringConvertible {
429
519
let rawValue : String
@@ -495,7 +585,7 @@ extension WebPushManager {
495
585
case handler( @Sendable (
496
586
_ message: _Message ,
497
587
_ subscriber: Subscriber ,
498
- _ expiration: VAPID . Configuration . Duration ,
588
+ _ expiration: Expiration ,
499
589
_ urgency: Urgency
500
590
) async throws -> Void )
501
591
}
0 commit comments