Skip to content

Commit

Permalink
Added push manager skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitribouniol committed Dec 6, 2024
1 parent 02f27b6 commit a547dbb
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 4 deletions.
14 changes: 13 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,26 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", "3.10.0"..<"5.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.77.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.24.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.2"),
],
targets: [
.target(
name: "WebPush",
dependencies: [
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "Logging", package: "swift-log"),
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.product(name: "NIOCore", package: "swift-nio"),
]
),
.testTarget(name: "WebPushTests", dependencies: ["WebPush"]),
.testTarget(name: "WebPushTests", dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.target(name: "WebPush"),
]),
]
)
105 changes: 105 additions & 0 deletions Sources/WebPush/Helpers/PrintLogHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// PrintLogHandler.swift
// swift-webpush
//
// Created by Dimitri Bouniol on 2024-12-06.
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//

import Foundation
import Logging

struct PrintLogHandler: LogHandler {
private let label: String

var logLevel: Logger.Level = .info
var metadataProvider: Logger.MetadataProvider?

init(
label: String,
logLevel: Logger.Level = .info,
metadataProvider: Logger.MetadataProvider? = nil
) {
self.label = label
self.logLevel = logLevel
self.metadataProvider = metadataProvider
}

private var prettyMetadata: String?
var metadata = Logger.Metadata() {
didSet {
self.prettyMetadata = self.prettify(self.metadata)
}
}

subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
get {
self.metadata[metadataKey]
}
set {
self.metadata[metadataKey] = newValue
}
}

func log(
level: Logger.Level,
message: Logger.Message,
metadata explicitMetadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt
) {
let effectiveMetadata = Self.prepareMetadata(
base: self.metadata,
provider: self.metadataProvider,
explicit: explicitMetadata
)

let prettyMetadata: String?
if let effectiveMetadata = effectiveMetadata {
prettyMetadata = self.prettify(effectiveMetadata)
} else {
prettyMetadata = self.prettyMetadata
}

print("\(self.timestamp()) [\(level)] \(self.label):\(prettyMetadata.map { " \($0)" } ?? "") [\(source)] \(message)")
}

internal static func prepareMetadata(
base: Logger.Metadata,
provider: Logger.MetadataProvider?,
explicit: Logger.Metadata?
) -> Logger.Metadata? {
var metadata = base

let provided = provider?.get() ?? [:]

guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else {
// all per-log-statement values are empty
return nil
}

if !provided.isEmpty {
metadata.merge(provided, uniquingKeysWith: { _, provided in provided })
}

if let explicit = explicit, !explicit.isEmpty {
metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
}

return metadata
}

private func prettify(_ metadata: Logger.Metadata) -> String? {
if metadata.isEmpty {
return nil
} else {
return metadata.lazy.sorted(by: { $0.key < $1.key }).map { "\($0)=\($1)" }.joined(separator: " ")
}
}

private func timestamp() -> String {
Date().formatted(date: .numeric, time: .complete)
}
}
64 changes: 64 additions & 0 deletions Sources/WebPush/WebPushManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,67 @@
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//

import AsyncHTTPClient
import Foundation
import Logging
import NIOCore
import ServiceLifecycle

actor WebPushManager: Service, Sendable {
public let vapidConfiguration: VAPID.Configuration

nonisolated let logger: Logger
let httpClient: HTTPClient

let vapidKeyLookup: [VAPID.Key.ID : VAPID.Key]
var vapidAuthorizationCache: [String : (authorization: String, expiration: Date)] = [:]

public init(
vapidConfiguration: VAPID.Configuration,
// TODO: Add networkConfiguration for proxy, number of simultaneous pushes, etc…
logger: Logger? = nil,
eventLoopGroupProvider: NIOEventLoopGroupProvider = .shared(.singletonNIOTSEventLoopGroup)
) {
self.vapidConfiguration = vapidConfiguration
let allKeys = vapidConfiguration.keys + Array(vapidConfiguration.deprecatedKeys ?? [])
self.vapidKeyLookup = Dictionary(
allKeys.map { ($0.id, $0) },
uniquingKeysWith: { first, _ in first }
)

self.logger = Logger(label: "WebPushManager", factory: { logger?.handler ?? PrintLogHandler(label: $0, metadataProvider: $1) })

var httpClientConfiguration = HTTPClient.Configuration()
httpClientConfiguration.httpVersion = .automatic

switch eventLoopGroupProvider {
case .shared(let eventLoopGroup):
self.httpClient = HTTPClient(
eventLoopGroupProvider: .shared(eventLoopGroup),
configuration: httpClientConfiguration,
backgroundActivityLogger: self.logger
)
case .createNew:
self.httpClient = HTTPClient(
configuration: httpClientConfiguration,
backgroundActivityLogger: self.logger
)
}
}

public func run() async throws {
logger.info("Starting up WebPushManager")
try await withTaskCancellationOrGracefulShutdownHandler {
try await gracefulShutdown()
} onCancelOrGracefulShutdown: { [self] in
logger.info("Shutting down WebPushManager")
do {
try httpClient.syncShutdown()
} catch {
logger.error("Graceful Shutdown Failed", metadata: [
"error": "\(error)"
])
}
}
}
}
19 changes: 19 additions & 0 deletions Tests/WebPushTests/VAPIDConfiguration+Testing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// VAPIDConfiguration+Testing.swift
// swift-webpush
//
// Created by Dimitri Bouniol on 2024-12-06.
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//

import Foundation
import WebPush

extension VAPID.Configuration {
static func makeTesting() -> VAPID.Configuration {
VAPID.Configuration(
key: VAPID.Key(),
contactInformation: .url(URL(string: "https://example.com/contact")!)
)
}
}
27 changes: 24 additions & 3 deletions Tests/WebPushTests/WebPushTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,31 @@
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//


import Logging
import ServiceLifecycle
import Testing
@testable import WebPush

@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
@Test func webPushManagerInitializesOnItsOwn() async throws {
let manager = WebPushManager(vapidConfiguration: .makeTesting())
await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await manager.run()
}
group.cancelAll()
}
}

@Test func webPushManagerInitializesAsService() async throws {
let logger = Logger(label: "ServiceLogger", factory: { PrintLogHandler(label: $0, metadataProvider: $1) })
let manager = WebPushManager(
vapidConfiguration: .makeTesting(),
logger: logger
)
await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await ServiceGroup(services: [manager], logger: logger).run()
}
group.cancelAll()
}
}

0 comments on commit a547dbb

Please sign in to comment.