Skip to content

Commit a547dbb

Browse files
Added push manager skeleton
1 parent 02f27b6 commit a547dbb

File tree

5 files changed

+225
-4
lines changed

5 files changed

+225
-4
lines changed

Package.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,26 @@ let package = Package(
1616
],
1717
dependencies: [
1818
.package(url: "https://github.com/apple/swift-crypto.git", "3.10.0"..<"5.0.0"),
19+
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
20+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.77.0"),
21+
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.24.0"),
22+
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.2"),
1923
],
2024
targets: [
2125
.target(
2226
name: "WebPush",
2327
dependencies: [
28+
.product(name: "AsyncHTTPClient", package: "async-http-client"),
2429
.product(name: "Crypto", package: "swift-crypto"),
30+
.product(name: "Logging", package: "swift-log"),
31+
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
32+
.product(name: "NIOCore", package: "swift-nio"),
2533
]
2634
),
27-
.testTarget(name: "WebPushTests", dependencies: ["WebPush"]),
35+
.testTarget(name: "WebPushTests", dependencies: [
36+
.product(name: "Logging", package: "swift-log"),
37+
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
38+
.target(name: "WebPush"),
39+
]),
2840
]
2941
)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//
2+
// PrintLogHandler.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-06.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import Logging
11+
12+
struct PrintLogHandler: LogHandler {
13+
private let label: String
14+
15+
var logLevel: Logger.Level = .info
16+
var metadataProvider: Logger.MetadataProvider?
17+
18+
init(
19+
label: String,
20+
logLevel: Logger.Level = .info,
21+
metadataProvider: Logger.MetadataProvider? = nil
22+
) {
23+
self.label = label
24+
self.logLevel = logLevel
25+
self.metadataProvider = metadataProvider
26+
}
27+
28+
private var prettyMetadata: String?
29+
var metadata = Logger.Metadata() {
30+
didSet {
31+
self.prettyMetadata = self.prettify(self.metadata)
32+
}
33+
}
34+
35+
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
36+
get {
37+
self.metadata[metadataKey]
38+
}
39+
set {
40+
self.metadata[metadataKey] = newValue
41+
}
42+
}
43+
44+
func log(
45+
level: Logger.Level,
46+
message: Logger.Message,
47+
metadata explicitMetadata: Logger.Metadata?,
48+
source: String,
49+
file: String,
50+
function: String,
51+
line: UInt
52+
) {
53+
let effectiveMetadata = Self.prepareMetadata(
54+
base: self.metadata,
55+
provider: self.metadataProvider,
56+
explicit: explicitMetadata
57+
)
58+
59+
let prettyMetadata: String?
60+
if let effectiveMetadata = effectiveMetadata {
61+
prettyMetadata = self.prettify(effectiveMetadata)
62+
} else {
63+
prettyMetadata = self.prettyMetadata
64+
}
65+
66+
print("\(self.timestamp()) [\(level)] \(self.label):\(prettyMetadata.map { " \($0)" } ?? "") [\(source)] \(message)")
67+
}
68+
69+
internal static func prepareMetadata(
70+
base: Logger.Metadata,
71+
provider: Logger.MetadataProvider?,
72+
explicit: Logger.Metadata?
73+
) -> Logger.Metadata? {
74+
var metadata = base
75+
76+
let provided = provider?.get() ?? [:]
77+
78+
guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else {
79+
// all per-log-statement values are empty
80+
return nil
81+
}
82+
83+
if !provided.isEmpty {
84+
metadata.merge(provided, uniquingKeysWith: { _, provided in provided })
85+
}
86+
87+
if let explicit = explicit, !explicit.isEmpty {
88+
metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
89+
}
90+
91+
return metadata
92+
}
93+
94+
private func prettify(_ metadata: Logger.Metadata) -> String? {
95+
if metadata.isEmpty {
96+
return nil
97+
} else {
98+
return metadata.lazy.sorted(by: { $0.key < $1.key }).map { "\($0)=\($1)" }.joined(separator: " ")
99+
}
100+
}
101+
102+
private func timestamp() -> String {
103+
Date().formatted(date: .numeric, time: .complete)
104+
}
105+
}

Sources/WebPush/WebPushManager.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,67 @@
66
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
77
//
88

9+
import AsyncHTTPClient
10+
import Foundation
11+
import Logging
12+
import NIOCore
13+
import ServiceLifecycle
14+
15+
actor WebPushManager: Service, Sendable {
16+
public let vapidConfiguration: VAPID.Configuration
17+
18+
nonisolated let logger: Logger
19+
let httpClient: HTTPClient
20+
21+
let vapidKeyLookup: [VAPID.Key.ID : VAPID.Key]
22+
var vapidAuthorizationCache: [String : (authorization: String, expiration: Date)] = [:]
23+
24+
public init(
25+
vapidConfiguration: VAPID.Configuration,
26+
// TODO: Add networkConfiguration for proxy, number of simultaneous pushes, etc…
27+
logger: Logger? = nil,
28+
eventLoopGroupProvider: NIOEventLoopGroupProvider = .shared(.singletonNIOTSEventLoopGroup)
29+
) {
30+
self.vapidConfiguration = vapidConfiguration
31+
let allKeys = vapidConfiguration.keys + Array(vapidConfiguration.deprecatedKeys ?? [])
32+
self.vapidKeyLookup = Dictionary(
33+
allKeys.map { ($0.id, $0) },
34+
uniquingKeysWith: { first, _ in first }
35+
)
36+
37+
self.logger = Logger(label: "WebPushManager", factory: { logger?.handler ?? PrintLogHandler(label: $0, metadataProvider: $1) })
38+
39+
var httpClientConfiguration = HTTPClient.Configuration()
40+
httpClientConfiguration.httpVersion = .automatic
41+
42+
switch eventLoopGroupProvider {
43+
case .shared(let eventLoopGroup):
44+
self.httpClient = HTTPClient(
45+
eventLoopGroupProvider: .shared(eventLoopGroup),
46+
configuration: httpClientConfiguration,
47+
backgroundActivityLogger: self.logger
48+
)
49+
case .createNew:
50+
self.httpClient = HTTPClient(
51+
configuration: httpClientConfiguration,
52+
backgroundActivityLogger: self.logger
53+
)
54+
}
55+
}
56+
57+
public func run() async throws {
58+
logger.info("Starting up WebPushManager")
59+
try await withTaskCancellationOrGracefulShutdownHandler {
60+
try await gracefulShutdown()
61+
} onCancelOrGracefulShutdown: { [self] in
62+
logger.info("Shutting down WebPushManager")
63+
do {
64+
try httpClient.syncShutdown()
65+
} catch {
66+
logger.error("Graceful Shutdown Failed", metadata: [
67+
"error": "\(error)"
68+
])
69+
}
70+
}
71+
}
72+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// VAPIDConfiguration+Testing.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-06.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import WebPush
11+
12+
extension VAPID.Configuration {
13+
static func makeTesting() -> VAPID.Configuration {
14+
VAPID.Configuration(
15+
key: VAPID.Key(),
16+
contactInformation: .url(URL(string: "https://example.com/contact")!)
17+
)
18+
}
19+
}

Tests/WebPushTests/WebPushTests.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,31 @@
66
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
77
//
88

9-
9+
import Logging
10+
import ServiceLifecycle
1011
import Testing
1112
@testable import WebPush
1213

13-
@Test func example() async throws {
14-
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
14+
@Test func webPushManagerInitializesOnItsOwn() async throws {
15+
let manager = WebPushManager(vapidConfiguration: .makeTesting())
16+
await withThrowingTaskGroup(of: Void.self) { group in
17+
group.addTask {
18+
try await manager.run()
19+
}
20+
group.cancelAll()
21+
}
22+
}
23+
24+
@Test func webPushManagerInitializesAsService() async throws {
25+
let logger = Logger(label: "ServiceLogger", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })
26+
let manager = WebPushManager(
27+
vapidConfiguration: .makeTesting(),
28+
logger: logger
29+
)
30+
await withThrowingTaskGroup(of: Void.self) { group in
31+
group.addTask {
32+
try await ServiceGroup(services: [manager], logger: logger).run()
33+
}
34+
group.cancelAll()
35+
}
1536
}

0 commit comments

Comments
 (0)