Skip to content

Commit 1f0f614

Browse files
authored
Add caching and additional http headers (#196)
* Add caching and additional http headers * Enable caching for all valid responses * Fix linux build * Don't use cache for User signup/login, save, delete, and other important * Add custom cache instead of using shared * Fix tests * remove test that can't run in parallel
1 parent bc4a793 commit 1f0f614

File tree

15 files changed

+297
-31
lines changed

15 files changed

+297
-31
lines changed

ParseSwift.xcodeproj/project.pbxproj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@
365365
89899DB526045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */; };
366366
89899DB626045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */; };
367367
89899DB726045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */; };
368+
9116F66F26A35D610082F6D6 /* URLCache+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9116F66E26A35D600082F6D6 /* URLCache+extensions.swift */; };
369+
9116F67026A35D610082F6D6 /* URLCache+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9116F66E26A35D600082F6D6 /* URLCache+extensions.swift */; };
370+
9116F67126A35D620082F6D6 /* URLCache+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9116F66E26A35D600082F6D6 /* URLCache+extensions.swift */; };
371+
9116F67226A35D620082F6D6 /* URLCache+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9116F66E26A35D600082F6D6 /* URLCache+extensions.swift */; };
368372
911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12B24C3F7720027F3C7 /* MockURLResponse.swift */; };
369373
911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */; };
370374
911DB13324C494390027F3C7 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13224C494390027F3C7 /* MockURLProtocol.swift */; };
@@ -717,6 +721,7 @@
717721
89899CF32603CE9D002E2043 /* ParseFacebookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFacebookTests.swift; sourceTree = "<group>"; };
718722
89899D9E26045998002E2043 /* ParseTwitterCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseTwitterCombineTests.swift; sourceTree = "<group>"; };
719723
89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFacebookCombineTests.swift; sourceTree = "<group>"; };
724+
9116F66E26A35D600082F6D6 /* URLCache+extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLCache+extensions.swift"; sourceTree = "<group>"; };
720725
911DB12B24C3F7720027F3C7 /* MockURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLResponse.swift; sourceTree = "<group>"; };
721726
911DB12D24C4837E0027F3C7 /* APICommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICommandTests.swift; sourceTree = "<group>"; };
722727
911DB13224C494390027F3C7 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = "<group>"; };
@@ -1250,9 +1255,10 @@
12501255
F97B462624D9C72700F4A88B /* API.swift */,
12511256
F97B462E24D9C74400F4A88B /* API+Commands.swift */,
12521257
F97B462B24D9C74400F4A88B /* BatchUtils.swift */,
1258+
7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */,
12531259
F97B462D24D9C74400F4A88B /* Responses.swift */,
1260+
9116F66E26A35D600082F6D6 /* URLCache+extensions.swift */,
12541261
F97B462C24D9C74400F4A88B /* URLSession+extensions.swift */,
1255-
7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */,
12561262
);
12571263
path = API;
12581264
sourceTree = "<group>";
@@ -1762,6 +1768,7 @@
17621768
7044C17525C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */,
17631769
F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */,
17641770
700396EA25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */,
1771+
9116F66F26A35D610082F6D6 /* URLCache+extensions.swift in Sources */,
17651772
70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
17661773
707A3C2025B14BD0000D215C /* ParseApple.swift in Sources */,
17671774
F97B462224D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */,
@@ -1923,6 +1930,7 @@
19231930
7044C17625C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */,
19241931
F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */,
19251932
700396EB25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */,
1933+
9116F67026A35D610082F6D6 /* URLCache+extensions.swift in Sources */,
19261934
70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
19271935
707A3C2125B14BD0000D215C /* ParseApple.swift in Sources */,
19281936
F97B462324D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */,
@@ -2159,6 +2167,7 @@
21592167
7044C17825C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */,
21602168
F97B45DD24D9C6F200F4A88B /* Extensions.swift in Sources */,
21612169
700396ED25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */,
2170+
9116F67226A35D620082F6D6 /* URLCache+extensions.swift in Sources */,
21622171
70572674259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
21632172
707A3C2325B14BD0000D215C /* ParseApple.swift in Sources */,
21642173
F97B462124D9C6F200F4A88B /* ParseStorage.swift in Sources */,
@@ -2254,6 +2263,7 @@
22542263
7044C17725C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */,
22552264
F97B45DC24D9C6F200F4A88B /* Extensions.swift in Sources */,
22562265
700396EC25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */,
2266+
9116F67126A35D620082F6D6 /* URLCache+extensions.swift in Sources */,
22572267
70572673259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
22582268
707A3C2225B14BD0000D215C /* ParseApple.swift in Sources */,
22592269
F97B462024D9C6F200F4A88B /* ParseStorage.swift in Sources */,

Sources/ParseSwift/API/API+Commands.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ internal extension API {
200200
}
201201
} else if let otherURL = self.otherURL {
202202
//Non-parse servers don't receive any parse dedicated request info
203-
URLSession.parse.downloadTask(with: otherURL, mapper: mapper) { result in
203+
var request = URLRequest(url: otherURL)
204+
request.cachePolicy = requestCachePolicy(options: options)
205+
URLSession.parse.downloadTask(with: request, mapper: mapper) { result in
204206
switch result {
205207

206208
case .success(let decoded):
@@ -262,13 +264,24 @@ internal extension API {
262264
}
263265
}
264266
urlRequest.httpMethod = method.rawValue
267+
urlRequest.cachePolicy = requestCachePolicy(options: options)
265268
return .success(urlRequest)
266269
}
267270

268271
enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting
269272
case method, body, path
270273
}
271274
}
275+
276+
static func requestCachePolicy(options: API.Options) -> URLRequest.CachePolicy {
277+
var policy: URLRequest.CachePolicy = ParseSwift.configuration.requestCachePolicy
278+
options.forEach { option in
279+
if case .cachePolicy(let updatedPolicy) = option {
280+
policy = updatedPolicy
281+
}
282+
}
283+
return policy
284+
}
272285
}
273286

274287
internal extension API.Command {
@@ -668,7 +681,7 @@ internal extension API {
668681
urlRequest.httpBody = bodyData
669682
}
670683
urlRequest.httpMethod = method.rawValue
671-
684+
urlRequest.cachePolicy = requestCachePolicy(options: options)
672685
return .success(urlRequest)
673686
}
674687

Sources/ParseSwift/API/API.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
//
88

99
import Foundation
10+
#if canImport(FoundationNetworking)
11+
import FoundationNetworking
12+
#endif
13+
14+
// swiftlint:disable line_length
1015

1116
/// The REST API for communicating with a Parse Server.
1217
public struct API {
@@ -127,6 +132,11 @@ public struct API {
127132
/// Add context.
128133
/// - warning: Requires Parse Server > 4.5.0.
129134
case context(Encodable)
135+
/// The caching policy to use for a specific http request. Determines when to
136+
/// return a response from the cache. See Apple's
137+
/// [documentation](https://developer.apple.com/documentation/foundation/url_loading_system/accessing_cached_data)
138+
/// for more info.
139+
case cachePolicy(URLRequest.CachePolicy)
130140

131141
public func hash(into hasher: inout Hasher) {
132142
switch self {
@@ -148,6 +158,8 @@ public struct API {
148158
hasher.combine(8)
149159
case .context:
150160
hasher.combine(9)
161+
case .cachePolicy:
162+
hasher.combine(10)
151163
}
152164
}
153165

@@ -203,6 +215,8 @@ public struct API {
203215
let encodedString = String(data: encoded, encoding: .utf8) {
204216
headers["X-Parse-Cloud-Context"] = encodedString
205217
}
218+
default:
219+
break
206220
}
207221
}
208222

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// URLCache+extensions.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 7/17/21.
6+
// Copyright © 2021 Parse Community. All rights reserved.
7+
//
8+
9+
import Foundation
10+
#if canImport(FoundationNetworking)
11+
import FoundationNetworking
12+
#endif
13+
14+
extension URLCache {
15+
static let parse: URLCache = {
16+
guard let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
17+
return URLCache(memoryCapacity: ParseSwift.configuration.cacheMemoryCapacity,
18+
diskCapacity: ParseSwift.configuration.cacheDiskCapacity,
19+
diskPath: "/")
20+
}
21+
let diskURL = cacheURL.appendingPathComponent("ParseCache/")
22+
return .init(memoryCapacity: ParseSwift.configuration.cacheMemoryCapacity,
23+
diskCapacity: ParseSwift.configuration.cacheDiskCapacity,
24+
diskPath: diskURL.absoluteString)
25+
}()
26+
}

Sources/ParseSwift/API/URLSession+extensions.swift

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,32 @@ import FoundationNetworking
1515
extension URLSession {
1616
static let parse: URLSession = {
1717
if !ParseSwift.configuration.isTestingSDK {
18-
return URLSession(configuration: .default,
18+
let configuration = URLSessionConfiguration.default
19+
configuration.urlCache = URLCache.parse
20+
configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
21+
configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
22+
return URLSession(configuration: configuration,
1923
delegate: ParseSwift.sessionDelegate,
2024
delegateQueue: nil)
2125
} else {
26+
let session = URLSession.shared
27+
session.configuration.urlCache = URLCache.parse
2228
return URLSession.shared
2329
}
2430
}()
2531

26-
internal func makeResult<U>(responseData: Data?,
32+
internal func makeResult<U>(request: URLRequest,
33+
responseData: Data?,
2734
urlResponse: URLResponse?,
2835
responseError: Error?,
2936
mapper: @escaping (Data) throws -> U) -> Result<U, ParseError> {
37+
guard let response = urlResponse else {
38+
guard let parseError = responseError as? ParseError else {
39+
return .failure(ParseError(code: .unknownError,
40+
message: "No response from server"))
41+
}
42+
return .failure(parseError)
43+
}
3044
if let responseError = responseError {
3145
guard let parseError = responseError as? ParseError else {
3246
return .failure(ParseError(code: .unknownError,
@@ -37,6 +51,11 @@ extension URLSession {
3751

3852
if let responseData = responseData {
3953
do {
54+
if URLSession.parse.configuration.urlCache?.cachedResponse(for: request) == nil {
55+
URLSession.parse.configuration.urlCache?.storeCachedResponse(.init(response: response,
56+
data: responseData),
57+
for: request)
58+
}
4059
return try .success(mapper(responseData))
4160
} catch {
4261
if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: responseData) {
@@ -49,11 +68,11 @@ extension URLSession {
4968
options: .prettyPrinted) else {
5069
return .failure(ParseError(code: .unknownError,
5170
// swiftlint:disable:next line_length
52-
message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription) Format: \(String(describing: String(data: responseData, encoding: .utf8)))"))
71+
message: "Error decoding parse-server response: \(response) with error: \(error.localizedDescription) Format: \(String(describing: String(data: responseData, encoding: .utf8)))"))
5372
}
5473
return .failure(ParseError(code: .unknownError,
5574
// swiftlint:disable:next line_length
56-
message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription) Format: \(String(describing: String(data: json, encoding: .utf8)))"))
75+
message: "Error decoding parse-server response: \(response) with error: \(error.localizedDescription) Format: \(String(describing: String(data: json, encoding: .utf8)))"))
5776
}
5877
return .failure(parseError)
5978
}
@@ -63,10 +82,18 @@ extension URLSession {
6382
message: "Unable to sync with parse-server: \(String(describing: urlResponse))."))
6483
}
6584

66-
internal func makeResult<U>(location: URL?,
85+
internal func makeResult<U>(request: URLRequest,
86+
location: URL?,
6787
urlResponse: URLResponse?,
6888
responseError: Error?,
6989
mapper: @escaping (Data) throws -> U) -> Result<U, ParseError> {
90+
guard let response = urlResponse else {
91+
guard let parseError = responseError as? ParseError else {
92+
return .failure(ParseError(code: .unknownError,
93+
message: "No response from server"))
94+
}
95+
return .failure(parseError)
96+
}
7097
if let responseError = responseError {
7198
guard let parseError = responseError as? ParseError else {
7299
return .failure(ParseError(code: .unknownError,
@@ -78,19 +105,24 @@ extension URLSession {
78105
if let location = location {
79106
do {
80107
let data = try ParseCoding.jsonEncoder().encode(location)
108+
if URLSession.parse.configuration.urlCache?.cachedResponse(for: request) == nil {
109+
URLSession.parse.configuration.urlCache?.storeCachedResponse(.init(response: response,
110+
data: data),
111+
for: request)
112+
}
81113
return try .success(mapper(data))
82114
} catch {
83115
guard let parseError = error as? ParseError else {
84116
return .failure(ParseError(code: .unknownError,
85117
// swiftlint:disable:next line_length
86-
message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription)"))
118+
message: "Error decoding parse-server response: \(response) with error: \(error.localizedDescription)"))
87119
}
88120
return .failure(parseError)
89121
}
90122
}
91123

92124
return .failure(ParseError(code: .unknownError,
93-
message: "Unable to sync with parse-server: \(String(describing: urlResponse))."))
125+
message: "Unable to sync with parse-server: \(response)."))
94126
}
95127

96128
internal func dataTask<U>(
@@ -100,9 +132,10 @@ extension URLSession {
100132
) {
101133

102134
dataTask(with: request) { (responseData, urlResponse, responseError) in
103-
completion(self.makeResult(responseData: responseData,
104-
urlResponse: urlResponse,
105-
responseError: responseError, mapper: mapper))
135+
completion(self.makeResult(request: request,
136+
responseData: responseData,
137+
urlResponse: urlResponse,
138+
responseError: responseError, mapper: mapper))
106139
}.resume()
107140
}
108141
}
@@ -120,15 +153,17 @@ extension URLSession {
120153
var task: URLSessionTask?
121154
if let data = data {
122155
task = uploadTask(with: request, from: data) { (responseData, urlResponse, responseError) in
123-
completion(self.makeResult(responseData: responseData,
124-
urlResponse: urlResponse,
125-
responseError: responseError, mapper: mapper))
156+
completion(self.makeResult(request: request,
157+
responseData: responseData,
158+
urlResponse: urlResponse,
159+
responseError: responseError, mapper: mapper))
126160
}
127161
} else if let file = file {
128162
task = uploadTask(with: request, fromFile: file) { (responseData, urlResponse, responseError) in
129-
completion(self.makeResult(responseData: responseData,
130-
urlResponse: urlResponse,
131-
responseError: responseError, mapper: mapper))
163+
completion(self.makeResult(request: request,
164+
responseData: responseData,
165+
urlResponse: urlResponse,
166+
responseError: responseError, mapper: mapper))
132167
}
133168
} else {
134169
completion(.failure(ParseError(code: .unknownError, message: "data and file both can't be nil")))
@@ -148,23 +183,23 @@ extension URLSession {
148183
completion: @escaping(Result<U, ParseError>) -> Void
149184
) {
150185
let task = downloadTask(with: request) { (location, urlResponse, responseError) in
151-
completion(self.makeResult(location: location,
152-
urlResponse: urlResponse,
153-
responseError: responseError, mapper: mapper))
186+
completion(self.makeResult(request: request,
187+
location: location,
188+
urlResponse: urlResponse,
189+
responseError: responseError, mapper: mapper))
154190
}
155191
ParseSwift.sessionDelegate.downloadDelegates[task] = progress
156192
ParseSwift.sessionDelegate.taskCallbackQueues[task] = callbackQueue
157193
task.resume()
158194
}
159195

160196
internal func downloadTask<U>(
161-
with url: URL,
197+
with request: URLRequest,
162198
mapper: @escaping (Data) throws -> U,
163199
completion: @escaping(Result<U, ParseError>) -> Void
164200
) {
165-
166-
downloadTask(with: url) { (location, urlResponse, responseError) in
167-
completion(self.makeResult(location: location,
201+
downloadTask(with: request) { (location, urlResponse, responseError) in
202+
completion(self.makeResult(request: request, location: location,
168203
urlResponse: urlResponse,
169204
responseError: responseError,
170205
mapper: mapper))

0 commit comments

Comments
 (0)