Skip to content

Commit 07f1ebe

Browse files
committed
Parity: URLCache adoption for HTTP and FTP invocations.
- Store and provide the storage date for cached responses, for internal use only. - Implement protocol properties. - URLSessionConfiguration now configures the shared and ephemeral sessions with caches of appropriate size and storage setup. Each ephemeral session has a new in-memory-only cache, and the shared session used the shared URLCache. - The URLProtocolClient that drives URLSession tasks now asks its delegate for caching, and if a cacheable response is determined, uses the URLSession’s cache to store it. It will also fetch from the cache if appropriate, and will initialize the protocol with that response. - Add hooks to _NativeProtocol to affect whether the transfer can be satisfied by the cached response it was initialized with. If it can, the response will be replayed instead. - _HTTPURLProtocol implements those hooks to make the URLCache behave as a RFC 7234 private cache. Note that _FTPURLProtocol uses the default implementation of those hooks, which will cause related responses to be evicted from the cache and no response to be fulfilled from the cache. This is intentional.
1 parent 6df76d0 commit 07f1ebe

File tree

9 files changed

+508
-85
lines changed

9 files changed

+508
-85
lines changed

Foundation/NSURLRequest.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ open class NSMutableURLRequest : NSURLRequest {
564564
get { return super.httpShouldUsePipelining }
565565
set { super.httpShouldUsePipelining = newValue }
566566
}
567+
568+
// These properties are settable using URLProtocol's class methods.
569+
var protocolProperties: [String: Any] = [:]
567570
}
568571

569572
/// Returns an existing key-value pair inside the header fields if it exists.

Foundation/URLCache.swift

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,21 @@ class StoredCachedURLResponse: NSObject, NSSecureCoding {
5555
aCoder.encode(cachedURLResponse.data as NSData, forKey: "data")
5656
aCoder.encode(cachedURLResponse.storagePolicy.rawValue, forKey: "storagePolicy")
5757
aCoder.encode(cachedURLResponse.userInfo as NSDictionary?, forKey: "userInfo")
58+
aCoder.encode(cachedURLResponse.date as NSDate, forKey: "date")
5859
}
5960

6061
required init?(coder aDecoder: NSCoder) {
6162
guard let response = aDecoder.decodeObject(of: URLResponse.self, forKey: "response"),
6263
let data = aDecoder.decodeObject(of: NSData.self, forKey: "data"),
63-
let storagePolicy = URLCache.StoragePolicy(rawValue: UInt(aDecoder.decodeInt64(forKey: "storagePolicy"))) else {
64+
let storagePolicy = URLCache.StoragePolicy(rawValue: UInt(aDecoder.decodeInt64(forKey: "storagePolicy"))),
65+
let date = aDecoder.decodeObject(of: NSDate.self, forKey: "date") else {
6466
return nil
6567
}
6668

6769
let userInfo = aDecoder.decodeObject(of: NSDictionary.self, forKey: "userInfo") as? [AnyHashable: Any]
6870

6971
cachedURLResponse = CachedURLResponse(response: response, data: data as Data, userInfo: userInfo, storagePolicy: storagePolicy)
72+
cachedURLResponse.date = date as Date
7073
}
7174

7275
let cachedURLResponse: CachedURLResponse
@@ -178,6 +181,8 @@ open class CachedURLResponse : NSObject, NSCopying {
178181
self.data == other.data &&
179182
self.storagePolicy == other.storagePolicy
180183
}
184+
185+
internal fileprivate(set) var date: Date = Date()
181186

182187
open override var hash: Int {
183188
var hasher = Hasher()
@@ -212,12 +217,14 @@ open class URLCache : NSObject {
212217
*/
213218
open class var shared: URLCache {
214219
get {
215-
sharedLock.lock(); defer { sharedLock.unlock() }
216-
return _shared
220+
return sharedLock.performLocked {
221+
return _shared
222+
}
217223
}
218224
set {
219-
sharedLock.lock(); defer { sharedLock.unlock() }
220-
_shared = newValue
225+
sharedLock.performLocked {
226+
_shared = newValue
227+
}
221228
}
222229
}
223230

@@ -348,8 +355,22 @@ open class URLCache : NSObject {
348355
}
349356

350357
private func identifier(for request: URLRequest) -> String? {
351-
guard let url = request.url?.absoluteString else { return nil }
352-
return Data(url.utf8).base64EncodedString()
358+
guard let url = request.url else { return nil }
359+
360+
if let host = url.host {
361+
var data = Data()
362+
data.append(Data(host.lowercased(with: NSLocale.system).utf8))
363+
data.append(0)
364+
let port = url.port ?? -1
365+
data.append(Data("\(port)".utf8))
366+
data.append(0)
367+
data.append(Data(url.path.utf8))
368+
data.append(0)
369+
370+
return data.base64EncodedString()
371+
} else {
372+
return nil
373+
}
353374
}
354375

355376
private struct DiskEntry {

Foundation/URLProtocol.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ public protocol URLProtocolClient : NSObjectProtocol {
148148
func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge)
149149
}
150150

151-
internal class _ProtocolClient : NSObject { }
151+
internal class _ProtocolClient : NSObject {
152+
var cachePolicy: URLCache.StoragePolicy = .notAllowed
153+
var cacheableData: [Data]?
154+
var cacheableResponse: URLResponse?
155+
}
152156

153157
/*!
154158
@class NSURLProtocol
@@ -308,7 +312,9 @@ open class URLProtocol : NSObject {
308312
@result The property stored with the given key, or nil if no property
309313
had previously been stored with the given key in the given request.
310314
*/
311-
open class func property(forKey key: String, in request: URLRequest) -> Any? { NSUnimplemented() }
315+
open class func property(forKey key: String, in request: URLRequest) -> Any? {
316+
return request.protocolProperties[key]
317+
}
312318

313319
/*!
314320
@method setProperty:forKey:inRequest:
@@ -321,7 +327,9 @@ open class URLProtocol : NSObject {
321327
@param key The string to use for the property storage.
322328
@param request The request in which to store the property.
323329
*/
324-
open class func setProperty(_ value: Any, forKey key: String, in request: NSMutableURLRequest) { NSUnimplemented() }
330+
open class func setProperty(_ value: Any, forKey key: String, in request: NSMutableURLRequest) {
331+
request.protocolProperties[key] = value
332+
}
325333

326334
/*!
327335
@method removePropertyForKey:inRequest:
@@ -332,7 +340,9 @@ open class URLProtocol : NSObject {
332340
@param key The key whose value should be removed
333341
@param request The request to be modified
334342
*/
335-
open class func removeProperty(forKey key: String, in request: NSMutableURLRequest) { NSUnimplemented() }
343+
open class func removeProperty(forKey key: String, in request: NSMutableURLRequest) {
344+
request.protocolProperties.removeValue(forKey: key)
345+
}
336346

337347
/*!
338348
@method registerClass:
@@ -408,7 +418,10 @@ open class URLProtocol : NSObject {
408418
_classesLock.unlock()
409419
}
410420

411-
open class func canInit(with task: URLSessionTask) -> Bool { NSUnimplemented() }
421+
open class func canInit(with task: URLSessionTask) -> Bool {
422+
guard let request = task.currentRequest else { return false }
423+
return canInit(with: request)
424+
}
412425
public required convenience init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
413426
let urlRequest = task.originalRequest
414427
self.init(request: urlRequest!, cachedResponse: cachedResponse, client: client)

Foundation/URLRequest.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ public struct URLRequest : ReferenceConvertible, Equatable, Hashable {
240240
public static func ==(lhs: URLRequest, rhs: URLRequest) -> Bool {
241241
return lhs._handle._uncopiedReference().isEqual(rhs._handle._uncopiedReference())
242242
}
243+
244+
var protocolProperties: [String: Any] {
245+
get {
246+
return _handle.map { $0.protocolProperties }
247+
}
248+
set {
249+
_applyMutation { $0.protocolProperties = newValue }
250+
}
251+
}
243252
}
244253

245254
extension URLRequest : CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable {

Foundation/URLSession/NativeProtocol.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,44 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate {
382382
guard let r = task?.originalRequest else {
383383
fatalError("Task has no original request.")
384384
}
385-
startNewTransfer(with: r)
385+
386+
// Check if the cached response is good to use:
387+
if let cachedResponse = cachedResponse, canRespondFromCache(using: cachedResponse) {
388+
self.internalState = .fulfillingFromCache(cachedResponse)
389+
task?.workQueue.async {
390+
self.client?.urlProtocol(self, cachedResponseIsValid: cachedResponse)
391+
self.client?.urlProtocol(self, didReceive: cachedResponse.response, cacheStoragePolicy: .notAllowed)
392+
if !cachedResponse.data.isEmpty {
393+
self.client?.urlProtocol(self, didLoad: cachedResponse.data)
394+
}
395+
396+
self.client?.urlProtocolDidFinishLoading(self)
397+
398+
self.internalState = .taskCompleted
399+
}
400+
401+
} else {
402+
startNewTransfer(with: r)
403+
}
386404
}
387405

388406
if case .transferReady(let transferState) = self.internalState {
389407
self.internalState = .transferInProgress(transferState)
390408
}
391409
}
410+
411+
func canCache(_ response: CachedURLResponse) -> Bool {
412+
return false
413+
}
414+
415+
/// Allows a native protocol to process a cached response. If `true` is returned, the protocol will replay the cached response instead of starting a new transfer. The default implementation invalidates the response in the cache and returns `false`.
416+
func canRespondFromCache(using response: CachedURLResponse) -> Bool {
417+
// By default, native protocols do not cache. Aggressively remove unexpected cached responses.
418+
if let cache = task?.session.configuration.urlCache, let task = task as? URLSessionDataTask {
419+
cache.removeCachedResponse(for: task)
420+
}
421+
return false
422+
}
392423

393424
func suspend() {
394425
if case .transferInProgress(let transferState) = self.internalState {
@@ -492,6 +523,8 @@ extension _NativeProtocol {
492523
enum _InternalState {
493524
/// Task has been created, but nothing has been done, yet
494525
case initial
526+
/// The task is being fulfilled from the cache rather than the network.
527+
case fulfillingFromCache(CachedURLResponse)
495528
/// The easy handle has been fully configured. But it is not added to
496529
/// the multi handle.
497530
case transferReady(_TransferState)
@@ -531,6 +564,7 @@ extension _NativeProtocol._InternalState {
531564
var isEasyHandleAddedToMultiHandle: Bool {
532565
switch self {
533566
case .initial: return false
567+
case .fulfillingFromCache: return false
534568
case .transferReady: return false
535569
case .transferInProgress: return true
536570
case .transferCompleted: return false
@@ -544,6 +578,7 @@ extension _NativeProtocol._InternalState {
544578
var isEasyHandlePaused: Bool {
545579
switch self {
546580
case .initial: return false
581+
case .fulfillingFromCache: return false
547582
case .transferReady: return false
548583
case .transferInProgress: return false
549584
case .transferCompleted: return false

Foundation/URLSession/URLSession.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,8 @@ internal protocol URLSessionProtocol: class {
629629
func add(handle: _EasyHandle)
630630
func remove(handle: _EasyHandle)
631631
func behaviour(for: URLSessionTask) -> URLSession._TaskBehaviour
632+
var configuration: URLSessionConfiguration { get }
633+
var delegate: URLSessionDelegate? { get }
632634
}
633635
extension URLSession: URLSessionProtocol {
634636
func add(handle: _EasyHandle) {
@@ -642,6 +644,12 @@ extension URLSession: URLSessionProtocol {
642644
///
643645
/// - SeeAlso: URLSessionTask.init()
644646
final internal class _MissingURLSession: URLSessionProtocol {
647+
var delegate: URLSessionDelegate? {
648+
fatalError()
649+
}
650+
var configuration: URLSessionConfiguration {
651+
fatalError()
652+
}
645653
func add(handle: _EasyHandle) {
646654
fatalError()
647655
}

Foundation/URLSession/URLSessionConfiguration.swift

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,21 @@ open class URLSessionConfiguration : NSObject, NSCopying {
3939
// -init is silently incorrect in URLSessionCofiguration on the desktop. Ensure code that relied on swift-corelibs-foundation's init() being functional is redirected to the appropriate cross-platform class property.
4040
@available(*, deprecated, message: "Use .default instead.", renamed: "URLSessionConfiguration.default")
4141
public override init() {
42-
self.requestCachePolicy = .useProtocolCachePolicy
43-
self.timeoutIntervalForRequest = 60
44-
self.timeoutIntervalForResource = 604800
45-
self.networkServiceType = .default
46-
self.allowsCellularAccess = true
47-
self.isDiscretionary = false
48-
self.httpShouldUsePipelining = false
49-
self.httpShouldSetCookies = true
50-
self.httpCookieAcceptPolicy = .onlyFromMainDocumentDomain
51-
self.httpMaximumConnectionsPerHost = 6
52-
self.httpCookieStorage = HTTPCookieStorage.shared
53-
self.urlCredentialStorage = nil
54-
self.urlCache = nil
55-
self.shouldUseExtendedBackgroundIdleMode = false
56-
self.protocolClasses = [_HTTPURLProtocol.self, _FTPURLProtocol.self]
42+
self.requestCachePolicy = URLSessionConfiguration.default.requestCachePolicy
43+
self.timeoutIntervalForRequest = URLSessionConfiguration.default.timeoutIntervalForRequest
44+
self.timeoutIntervalForResource = URLSessionConfiguration.default.timeoutIntervalForResource
45+
self.networkServiceType = URLSessionConfiguration.default.networkServiceType
46+
self.allowsCellularAccess = URLSessionConfiguration.default.allowsCellularAccess
47+
self.isDiscretionary = URLSessionConfiguration.default.isDiscretionary
48+
self.httpShouldUsePipelining = URLSessionConfiguration.default.httpShouldUsePipelining
49+
self.httpShouldSetCookies = URLSessionConfiguration.default.httpShouldSetCookies
50+
self.httpCookieAcceptPolicy = URLSessionConfiguration.default.httpCookieAcceptPolicy
51+
self.httpMaximumConnectionsPerHost = URLSessionConfiguration.default.httpMaximumConnectionsPerHost
52+
self.httpCookieStorage = URLSessionConfiguration.default.httpCookieStorage
53+
self.urlCredentialStorage = URLSessionConfiguration.default.urlCredentialStorage
54+
self.urlCache = URLSessionConfiguration.default.urlCache
55+
self.shouldUseExtendedBackgroundIdleMode = URLSessionConfiguration.default.shouldUseExtendedBackgroundIdleMode
56+
self.protocolClasses = URLSessionConfiguration.default.protocolClasses
5757
super.init()
5858
}
5959

@@ -73,7 +73,7 @@ open class URLSessionConfiguration : NSObject, NSCopying {
7373
httpMaximumConnectionsPerHost: 6,
7474
httpCookieStorage: .shared,
7575
urlCredentialStorage: nil, // Should be .shared once implemented.
76-
urlCache: nil,
76+
urlCache: .shared,
7777
shouldUseExtendedBackgroundIdleMode: false,
7878
protocolClasses: [_HTTPURLProtocol.self, _FTPURLProtocol.self])
7979
}
@@ -149,10 +149,11 @@ open class URLSessionConfiguration : NSObject, NSCopying {
149149

150150
open class var ephemeral: URLSessionConfiguration {
151151
// Return a new ephemeral URLSessionConfiguration every time this property is invoked
152-
// TODO: urlCache and urlCredentialStorage should also be ephemeral/in-memory
153-
// URLCache and URLCredentialStorage are still unimplemented
152+
// TODO: urlCredentialStorage should also be ephemeral/in-memory
153+
// URLCredentialStorage is still unimplemented
154154
let ephemeralConfiguration = URLSessionConfiguration.default.copy() as! URLSessionConfiguration
155155
ephemeralConfiguration.httpCookieStorage = .ephemeralStorage()
156+
ephemeralConfiguration.urlCache = URLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 0, diskPath: nil)
156157
return ephemeralConfiguration
157158
}
158159

0 commit comments

Comments
 (0)