Skip to content

Commit 919d41a

Browse files
Fix memory issues (#26) by @grahamburgsma
* Use Vapor client instead to get helpers and reduce lower level work * Update all other uses of client * Generalize response error handling
1 parent 9998bdc commit 919d41a

8 files changed

+118
-162
lines changed

Sources/FCM/FCM.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import JWT
77
public struct FCM {
88
let application: Application
99

10-
let client: HTTPClient
10+
let client: Client
1111

1212
let scope = "https://www.googleapis.com/auth/cloud-platform"
1313
let audience = "https://www.googleapis.com/oauth2/v4/token"
@@ -38,7 +38,7 @@ public struct FCM {
3838
if !application.http.client.configuration.ignoreUncleanSSLShutdown {
3939
application.http.client.configuration.ignoreUncleanSSLShutdown = true
4040
}
41-
self.client = application.http.client.shared
41+
self.client = application.client
4242
}
4343
}
4444

Sources/FCM/FCMError.swift

+18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Vapor
2+
13
public struct GoogleError: Error, Decodable {
24
public let code: Int
35
public let message: String
@@ -39,3 +41,19 @@ public struct FCMError: Error, Decodable {
3941
case `internal` = "INTERNAL"
4042
}
4143
}
44+
45+
extension EventLoopFuture where Value == ClientResponse {
46+
func validate() -> EventLoopFuture<ClientResponse> {
47+
return flatMapThrowing { (response) in
48+
guard 200 ..< 300 ~= response.status.code else {
49+
if let error = try? response.content.decode(GoogleError.self) {
50+
throw error
51+
}
52+
let body = response.body.map(String.init) ?? ""
53+
throw Abort(.internalServerError, reason: "FCM: Unexpected error '\(body)'")
54+
}
55+
56+
return response
57+
}
58+
}
59+
}

Sources/FCM/Helpers/FCM+AccessToken.swift

+14-28
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,22 @@ extension FCM {
77
fatalError("FCM gAuth can't be nil")
88
}
99
if !gAuth.hasExpired, let token = accessToken {
10-
return client.eventLoopGroup.future(token)
10+
return client.eventLoop.future(token)
1111
}
12-
return application.eventLoopGroup.future(()).flatMapThrowing { _ throws -> Data in
13-
var payload: [String: String] = [:]
14-
payload["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer"
15-
payload["assertion"] = try self.getJWT()
16-
return try JSONEncoder().encode(payload)
17-
}.flatMapThrowing { data -> HTTPClient.Request in
18-
var headers = HTTPHeaders()
19-
headers.add(name: "Content-Type", value: "application/json")
20-
return try HTTPClient.Request(url: self.audience, method: .POST, headers: headers, body: .data(data))
21-
}.flatMap { request in
22-
return self.client.execute(request: request).flatMapThrowing { res throws -> String in
23-
guard let body = res.body, let data = body.getData(at: body.readerIndex, length: body.readableBytes) else {
24-
throw Abort(.notFound, reason: "Data not found")
25-
}
26-
if res.status.code != 200 {
27-
let code = "Code: \(res.status.code)"
28-
let message = "Message: \(String(data: data, encoding: .utf8) ?? "n/a"))"
29-
let reason = "[FCM] Unable to refresh access token. \(code) \(message)"
30-
throw Abort(.internalServerError, reason: reason)
31-
}
32-
struct Result: Codable {
33-
var access_token: String
34-
}
35-
guard let result = try? JSONDecoder().decode(Result.self, from: data) else {
36-
throw Abort(.notFound, reason: "Data not found")
37-
}
38-
return result.access_token
12+
13+
return client.post(URI(string: audience)) { (req) in
14+
try req.content.encode([
15+
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
16+
"assertion": try self.getJWT(),
17+
])
18+
}
19+
.validate()
20+
.flatMapThrowing { res -> String in
21+
struct Result: Codable {
22+
var access_token: String
3923
}
24+
let result = try res.content.decode(Result.self)
25+
return result.access_token
4026
}
4127
}
4228
}

Sources/FCM/Helpers/FCM+BatchSend.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ extension FCM {
55
public func batchSend(_ message: FCMMessageDefault, tokens: String...) -> EventLoopFuture<[String]> {
66
_send(message, tokens: tokens)
77
}
8-
8+
99
public func batchSend(_ message: FCMMessageDefault, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<[String]> {
1010
_send(message, tokens: tokens).hop(to: eventLoop)
1111
}
12-
12+
1313
public func batchSend(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> {
1414
_send(message, tokens: tokens)
1515
}
16-
16+
1717
public func batchSend(_ message: FCMMessageDefault, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<[String]> {
1818
_send(message, tokens: tokens).hop(to: eventLoop)
1919
}
20-
20+
2121
private func _send(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> {
2222
if message.apns == nil,
2323
let apnsDefaultConfig = apnsDefaultConfig {

Sources/FCM/Helpers/FCM+CreateTopic.swift

+21-33
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ extension FCM {
55
public func createTopic(_ name: String? = nil, tokens: String...) -> EventLoopFuture<String> {
66
createTopic(name, tokens: tokens)
77
}
8-
8+
99
public func createTopic(_ name: String? = nil, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<String> {
1010
createTopic(name, tokens: tokens).hop(to: eventLoop)
1111
}
12-
12+
1313
public func createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture<String> {
1414
_createTopic(name, tokens: tokens)
1515
}
16-
16+
1717
public func createTopic(_ name: String? = nil, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<String> {
1818
_createTopic(name, tokens: tokens).hop(to: eventLoop)
1919
}
20-
20+
2121
private func _createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture<String> {
2222
guard let configuration = self.configuration else {
2323
fatalError("FCM not configured. Use app.fcm.configuration = ...")
@@ -27,39 +27,27 @@ extension FCM {
2727
}
2828
let url = self.iidURL + "batchAdd"
2929
let name = name ?? UUID().uuidString
30-
return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in
31-
struct Payload: Codable {
32-
let to: String
33-
let registration_tokens: [String]
34-
35-
init (to: String, registration_tokens: [String]) {
36-
self.to = "/topics/\(to)"
37-
self.registration_tokens = registration_tokens
38-
}
39-
}
40-
let payload = Payload(to: name, registration_tokens: tokens)
41-
let payloadData = try JSONEncoder().encode(payload)
30+
return getAccessToken().flatMap { accessToken -> EventLoopFuture<ClientResponse> in
4231
var headers = HTTPHeaders()
43-
headers.add(name: "Authorization", value: "key=\(serverKey)")
44-
headers.add(name: "Content-Type", value: "application/json")
45-
return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData))
46-
}.flatMap { request in
47-
return self.client.execute(request: request).flatMapThrowing { res in
48-
guard 200 ..< 300 ~= res.status.code else {
49-
if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) {
50-
throw googleError
51-
} else {
52-
guard
53-
let bb = res.body,
54-
let bytes = bb.getBytes(at: 0, length: bb.readableBytes),
55-
let reason = String(bytes: bytes, encoding: .utf8) else {
56-
throw Abort(.internalServerError, reason: "FCM: CreateTopic: unable to decode error response")
57-
}
58-
throw Abort(.internalServerError, reason: reason)
32+
headers.add(name: .authorization, value: "key=\(serverKey)")
33+
34+
return self.client.post(URI(string: url), headers: headers) { (req) in
35+
struct Payload: Content {
36+
let to: String
37+
let registration_tokens: [String]
38+
39+
init(to: String, registration_tokens: [String]) {
40+
self.to = "/topics/\(to)"
41+
self.registration_tokens = registration_tokens
5942
}
6043
}
61-
return name
44+
let payload = Payload(to: name, registration_tokens: tokens)
45+
try req.content.encode(payload)
6246
}
6347
}
48+
.validate()
49+
.map { _ in
50+
return name
51+
}
6452
}
6553
}

Sources/FCM/Helpers/FCM+DeleteTopic.swift

+19-32
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ extension FCM {
55
public func deleteTopic(_ name: String, tokens: String...) -> EventLoopFuture<Void> {
66
deleteTopic(name, tokens: tokens)
77
}
8-
8+
99
public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<Void> {
1010
deleteTopic(name, tokens: tokens).hop(to: eventLoop)
1111
}
12-
12+
1313
public func deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture<Void> {
1414
_deleteTopic(name, tokens: tokens)
1515
}
16-
16+
1717
public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<Void> {
1818
_deleteTopic(name, tokens: tokens).hop(to: eventLoop)
1919
}
20-
20+
2121
private func _deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture<Void> {
2222
guard let configuration = self.configuration else {
2323
fatalError("FCM not configured. Use app.fcm.configuration = ...")
@@ -26,38 +26,25 @@ extension FCM {
2626
fatalError("FCM: DeleteTopic: Server Key is missing.")
2727
}
2828
let url = self.iidURL + "batchRemove"
29-
return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in
30-
struct Payload: Codable {
31-
let to: String
32-
let registration_tokens: [String]
33-
34-
init (to: String, registration_tokens: [String]) {
35-
self.to = "/topics/\(to)"
36-
self.registration_tokens = registration_tokens
37-
}
38-
}
39-
let payload = Payload(to: name, registration_tokens: tokens)
40-
let payloadData = try JSONEncoder().encode(payload)
29+
return getAccessToken().flatMap { accessToken -> EventLoopFuture<ClientResponse> in
4130
var headers = HTTPHeaders()
42-
headers.add(name: "Authorization", value: "key=\(serverKey)")
43-
headers.add(name: "Content-Type", value: "application/json")
44-
return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData))
45-
}.flatMap { request in
46-
return self.client.execute(request: request).flatMapThrowing { res in
47-
guard 200 ..< 300 ~= res.status.code else {
48-
if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) {
49-
throw googleError
50-
} else {
51-
guard
52-
let bb = res.body,
53-
let bytes = bb.getBytes(at: 0, length: bb.readableBytes),
54-
let reason = String(bytes: bytes, encoding: .utf8) else {
55-
throw Abort(.internalServerError, reason: "FCM: DeleteTopic: unable to decode error response")
56-
}
57-
throw Abort(.internalServerError, reason: reason)
31+
headers.add(name: .authorization, value: "key=\(serverKey)")
32+
33+
return self.client.post(URI(string: url), headers: headers) { (req) in
34+
struct Payload: Content {
35+
let to: String
36+
let registration_tokens: [String]
37+
38+
init(to: String, registration_tokens: [String]) {
39+
self.to = "/topics/\(to)"
40+
self.registration_tokens = registration_tokens
5841
}
5942
}
43+
let payload = Payload(to: name, registration_tokens: tokens)
44+
try req.content.encode(payload)
6045
}
6146
}
47+
.validate()
48+
.map { _ in () }
6249
}
6350
}

Sources/FCM/Helpers/FCM+RegisterAPNS.swift

+22-35
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public struct RegisterAPNSID {
55
let appBundleId: String
66
let serverKey: String?
77
let sandbox: Bool
8-
8+
99
public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) {
1010
self.appBundleId = appBundleId
1111
self.serverKey = serverKey
@@ -37,7 +37,7 @@ public struct APNSToFirebaseToken {
3737
extension FCM {
3838
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
3939
/// and returns firebase tokens for each APNS token
40-
///
40+
///
4141
/// Convenient way
4242
///
4343
/// Declare `RegisterAPNSID` via extension
@@ -53,7 +53,7 @@ extension FCM {
5353
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
5454
registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop)
5555
}
56-
56+
5757
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
5858
/// and returns firebase tokens for each APNS token
5959
///
@@ -72,7 +72,7 @@ extension FCM {
7272
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
7373
registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop)
7474
}
75-
75+
7676
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
7777
/// and returns firebase tokens for each APNS token
7878
public func registerAPNS(
@@ -83,7 +83,7 @@ extension FCM {
8383
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
8484
registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop)
8585
}
86-
86+
8787
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
8888
/// and returns firebase tokens for each APNS token
8989
public func registerAPNS(
@@ -110,43 +110,30 @@ extension FCM {
110110
fatalError("FCM: Register APNS: Server Key is missing.")
111111
}
112112
let url = iidURL + "batchImport"
113-
return eventLoop.future().flatMapThrowing { accessToken throws -> HTTPClient.Request in
114-
struct Payload: Codable {
113+
114+
var headers = HTTPHeaders()
115+
headers.add(name: .authorization, value: "key=\(serverKey)")
116+
117+
return self.client.post(URI(string: url), headers: headers) { (req) in
118+
struct Payload: Content {
115119
let application: String
116120
let sandbox: Bool
117121
let apns_tokens: [String]
118122
}
119123
let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens)
120-
let payloadData = try JSONEncoder().encode(payload)
121-
122-
var headers = HTTPHeaders()
123-
headers.add(name: "Authorization", value: "key=\(serverKey)")
124-
headers.add(name: "Content-Type", value: "application/json")
125-
126-
return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData))
127-
}.flatMap { request in
128-
return self.client.execute(request: request).flatMapThrowing { res in
129-
guard 200 ..< 300 ~= res.status.code else {
130-
guard
131-
let bb = res.body,
132-
let bytes = bb.getBytes(at: 0, length: bb.readableBytes),
133-
let reason = String(bytes: bytes, encoding: .utf8) else {
134-
throw Abort(.internalServerError, reason: "FCM: Register APNS: unable to decode error response")
135-
}
136-
throw Abort(.internalServerError, reason: reason)
137-
}
124+
try req.content.encode(payload)
125+
}
126+
.validate()
127+
.flatMapThrowing { res in
128+
struct Result: Codable {
138129
struct Result: Codable {
139-
struct Result: Codable {
140-
let registration_token, apns_token, status: String
141-
}
142-
var results: [Result]
143-
}
144-
guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else {
145-
throw Abort(.notFound, reason: "FCM: Register APNS: empty response")
146-
}
147-
return result.results.map {
148-
.init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK")
130+
let registration_token, apns_token, status: String
149131
}
132+
let results: [Result]
133+
}
134+
let result = try res.content.decode(Result.self)
135+
return result.results.map {
136+
.init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK")
150137
}
151138
}
152139
}

0 commit comments

Comments
 (0)