From 55d82ada8430bbae8289ed6d79b2eae6f820ba18 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 20:13:18 -0500 Subject: [PATCH 01/25] EntityCache now allows specific ContentType instead of Any --- Source/Siesta/EntityCache.swift | 10 ++++++++-- Source/Siesta/Pipeline/PipelineProcessing.swift | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Source/Siesta/EntityCache.swift b/Source/Siesta/EntityCache.swift index 4e043edd..4d835a98 100644 --- a/Source/Siesta/EntityCache.swift +++ b/Source/Siesta/EntityCache.swift @@ -46,6 +46,12 @@ public protocol EntityCache */ associatedtype Key + /** + The type of payload this cache knows how to store and retrieve. If the response data configured at a particular + point in the cache does not match this content type, Siesta will log a warning and bypass the cache. + */ + associatedtype ContentType + /** Provides the key appropriate to this cache for the given resource. @@ -70,7 +76,7 @@ public protocol EntityCache - Warning: This method may be called on a background thread. Make sure your implementation is threadsafe. */ - func readEntity(forKey key: Key) -> Entity? + func readEntity(forKey key: Key) -> Entity? /** Store the given entity in the cache, associated with the given key. The key’s format is arbitrary, and internal @@ -88,7 +94,7 @@ public protocol EntityCache - Warning: The method may be called on a background thread. Make sure your implementation is threadsafe. */ - func writeEntity(_ entity: Entity, forKey key: Key) + func writeEntity(_ entity: Entity, forKey key: Key) /** Update the timestamp of the entity for the given key. If there is no such cache entry, do nothing. diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index a1eb394e..717202a1 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -199,13 +199,19 @@ private struct CacheEntry: CacheEntryProtocol func read() -> Entity? { cache.workQueue.sync - { self.cache.readEntity(forKey: self.key) } + { self.cache.readEntity(forKey: self.key)?.withContentRetyped() } } func write(_ entity: Entity) { + guard let cacheableEntity = entity.withContentRetyped() as Entity? else + { + SiestaLog.log(.cache, ["WARNING: Unable to cache entity:", Cache.self, "expects", Cache.ContentType.self, "but content at this stage of the pipeline is", type(of: entity.content)]) + return + } + cache.workQueue.async - { self.cache.writeEntity(entity, forKey: self.key) } + { self.cache.writeEntity(cacheableEntity, forKey: self.key) } } func updateTimestamp(_ timestamp: TimeInterval) From fcdb017833e7d3ccab2004a28be20da43353efe4 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sun, 26 Nov 2017 20:54:01 -0600 Subject: [PATCH 02/25] Corrections to the EntityCache API docs --- Source/Siesta/EntityCache.swift | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Source/Siesta/EntityCache.swift b/Source/Siesta/EntityCache.swift index 4d835a98..63269801 100644 --- a/Source/Siesta/EntityCache.swift +++ b/Source/Siesta/EntityCache.swift @@ -15,8 +15,10 @@ import Foundation - recover from low memory situations with fewer reissued network requests, and - work offline. - Siesta uses any HTTP request caching provided by the networking layer (e.g. `URLCache`). Why another type of - caching, then? Because `URLCache` has a subtle but significant mismatch with the use cases above: + Siesta can aldo use whatever HTTP request the networking layer provides (e.g. `URLCache`). Why another type of + caching, then? Because `URLCache` has a several subtle but significant mismatches with the use cases above. + + The big one: * The purpose of HTTP caching is to _prevent_ network requests, but what we need is a way to show old data _while issuing new requests_. This is the real deal-killer. @@ -29,7 +31,7 @@ import Foundation exhibit the behavior we want; the logic involved is far more tangled and brittle than implementing a separate cache. * Precisely because of the complexity of these rules, APIs frequently disable all caching via headers. * HTTP caching does not preserve Siesta’s timestamps, which thwarts the staleness logic. - * HTTP caching stores raw responses; storing parsed responses offers the opportunity for faster app launch. + * HTTP caching stores raw responses. Apps may wish instead to cache responses in an app-specific parsed form. Siesta currently does not include any implementations of `EntityCache`, but a future version will. @@ -59,8 +61,11 @@ public protocol EntityCache This method is called for both cache writes _and_ for cache reads. The `resource` therefore may not have any content. Implementations will almost always examine `resource.url`. (Cache keys should be _at least_ as unique - as URLs except in very unusual circumstances.) Implementations may also want to examine `resource.configuration`, - for example to take authentication into account. + as URLs except in very unusual circumstances.) + + - Warning: When working with an authenticated API, caches must take care not to accidentally mix cached responses + for different users. The usual solution to this is to make `Key` vary with some sort of user ID as + well as the URL. - Note: This method is always called on the **main thread**. However, the key it returns will be passed repeatedly across threads. Siesta therefore strongly recommends making `Key` a value type, i.e. a struct. @@ -70,24 +75,21 @@ public protocol EntityCache /** Return the entity associated with the given key, or nil if it is not in the cache. - If this method returns an entity, it does _not_ pass through the transformer pipeline. Implementations should - return the entity as if already fully parsed and transformed — with the same type of `entity.content` that was - originally sent to `writeEntity(...)`. + If this method returns an entity, it passes through the portion of the transformer pipeline _after_ this cache. - Warning: This method may be called on a background thread. Make sure your implementation is threadsafe. */ func readEntity(forKey key: Key) -> Entity? /** - Store the given entity in the cache, associated with the given key. The key’s format is arbitrary, and internal - to Siesta. (OK, it’s just the resource’s URL, but you should pretend you don’t know that in your implementation. - Cache implementations should treat the `forKey` parameter as an opaque value.) + Store the given entity in the cache, associated with the given key. - This method receives entities _after_ they have been through the transformer pipeline. The `entity.content` will - be a parsed object, not raw data. + This method receives entities _after_ they have been through the stage of the transformer pipeline for which this + cache is configured. Implementations are under no obligation to actually perform the write. This method can — and should — examine the - type of the entity’s `content` and/or its header values, and ignore it if it is not encodable. + type of the entity’s `content` and/or its header values, and ignore it if it is unencodable or otherwise + unsuitable for caching. Note that this method does not receive a URL as input; if you need to limit caching to specific resources, use Siesta’s configuration mechanism to control which resources are cacheable. From 2760c113504dc7e38bfa26f11efd095959fdc5f3 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 20:26:03 -0500 Subject: [PATCH 03/25] New SiestaTools subproject --- Siesta.podspec | 6 + Siesta.xcodeproj/project.pbxproj | 390 ++++++++++++++++++ .../xcschemes/SiestaTools iOS.xcscheme | 82 ++++ .../xcschemes/SiestaTools macOS.xcscheme | 67 +++ .../xcschemes/SiestaTools tvOS.xcscheme | 67 +++ Source/Siesta/Support/SiestaTools.h | 16 + 6 files changed, 628 insertions(+) create mode 100644 Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools iOS.xcscheme create mode 100644 Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools macOS.xcscheme create mode 100644 Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools tvOS.xcscheme create mode 100644 Source/Siesta/Support/SiestaTools.h diff --git a/Siesta.podspec b/Siesta.podspec index a721af78..1c9e2a2b 100644 --- a/Siesta.podspec +++ b/Siesta.podspec @@ -79,6 +79,12 @@ Pod::Spec.new do |s| s.exclude_files = "**/Info*.plist" end + s.subspec "Tools" do |s| + s.source_files = "Source/SiestaTools/**/*" + s.exclude_files = "**/Info*.plist" + s.dependency "Siesta/Core" + end + s.subspec "UI" do |s| s.ios.source_files = "Source/SiestaUI/**/*.{swift,m,h}" s.dependency "Siesta/Core" diff --git a/Siesta.xcodeproj/project.pbxproj b/Siesta.xcodeproj/project.pbxproj index e800c8c1..dab6e0e4 100644 --- a/Siesta.xcodeproj/project.pbxproj +++ b/Siesta.xcodeproj/project.pbxproj @@ -188,6 +188,9 @@ DA788A8F1D6AC1590085C820 /* ObjcCompatibilitySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = DA4D61971B751FEE00F6BB9C /* ObjcCompatibilitySpec.m */; }; DA7D05C41D57C2B500431980 /* PipelineProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7D05C31D57C2B500431980 /* PipelineProcessing.swift */; }; DA7D05C51D57C30400431980 /* PipelineProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7D05C31D57C2B500431980 /* PipelineProcessing.swift */; }; + DA8B5B5522DEAC93008E47B0 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; }; + DA8B5B6422DEADDB008E47B0 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; }; + DA8C83581FC6096100C947F9 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; }; DA8EF6D11BC20AFE002175EB /* ProgressSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8EF6CF1BC1A917002175EB /* ProgressSpec.swift */; }; DA99B5C91B38C8E6009C6937 /* String+Siesta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA99B5C81B38C8E6009C6937 /* String+Siesta.swift */; }; DA9AA8151CCAFDD20016DB18 /* ConfigurationPatternConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9AA8141CCAFDD20016DB18 /* ConfigurationPatternConvertible.swift */; }; @@ -293,6 +296,48 @@ remoteGlobalIDString = DA5E4B3922DCE9670059ED10; remoteInfo = "SiestaUI tvOS"; }; + DA8B5B5C22DEACA4008E47B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA336E461B2E6DDB006F702A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9F2FB51D1C28645E0068DFFA; + remoteInfo = "Siesta macOS"; + }; + DA8B5B6B22DEAE72008E47B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA336E461B2E6DDB006F702A /* Project object */; + proxyType = 1; + remoteGlobalIDString = CDCCAF711E6A31D900860D18; + remoteInfo = "Siesta tvOS"; + }; + DA8B5B6D22DEAE97008E47B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA336E461B2E6DDB006F702A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DA8B5B4F22DEAC93008E47B0; + remoteInfo = "SiestaTools macOS"; + }; + DA8B5B6F22DEAEA2008E47B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA336E461B2E6DDB006F702A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DA8B5B5E22DEADDB008E47B0; + remoteInfo = "SiestaTools tvOS"; + }; + DA8C83421FC600A900C947F9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA336E461B2E6DDB006F702A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DA336E4E1B2E6DDB006F702A; + remoteInfo = "SiestaTools iOS"; + }; + DA8C83591FC60A2900C947F9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA336E461B2E6DDB006F702A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DA8C83401FC600A900C947F9; + remoteInfo = "SiestaTools iOS"; + }; DAC4CFCA1DAC10EC00EECEDE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DA336E461B2E6DDB006F702A /* Project object */; @@ -408,6 +453,10 @@ DA60F5FB1B30B2F800D76DC6 /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; DA62C97C2428589B00398674 /* NetworkStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStub.swift; sourceTree = ""; }; DA7D05C31D57C2B500431980 /* PipelineProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PipelineProcessing.swift; sourceTree = ""; }; + DA8B5B5A22DEAC93008E47B0 /* SiestaTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SiestaTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DA8B5B6922DEADDB008E47B0 /* SiestaTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SiestaTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DA8C83521FC600A900C947F9 /* SiestaTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SiestaTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DA8C83571FC6096100C947F9 /* SiestaTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SiestaTools.h; sourceTree = ""; }; DA8EF6CF1BC1A917002175EB /* ProgressSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressSpec.swift; sourceTree = ""; }; DA99B5C81B38C8E6009C6937 /* String+Siesta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Siesta.swift"; sourceTree = ""; }; DA9AA8141CCAFDD20016DB18 /* ConfigurationPatternConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationPatternConvertible.swift; sourceTree = ""; }; @@ -530,6 +579,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DA8B5B5322DEAC93008E47B0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8B5B6222DEADDB008E47B0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8C834A1FC600A900C947F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DAC0B3A21D651CB500D25C44 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -617,6 +687,9 @@ CDCCAF721E6A31D900860D18 /* Siesta.framework */, CDCCAF7A1E6A31DA00860D18 /* SiestaTests.xctest */, DA5E4B4B22DCE9670059ED10 /* SiestaUI.framework */, + DA8C83521FC600A900C947F9 /* SiestaTools.framework */, + DA8B5B5A22DEAC93008E47B0 /* SiestaTools.framework */, + DA8B5B6922DEADDB008E47B0 /* SiestaTools.framework */, ); name = Products; sourceTree = ""; @@ -625,6 +698,7 @@ isa = PBXGroup; children = ( DAC0B37B1D651A4600D25C44 /* Core */, + DA8C833F1FC5FFDA00C947F9 /* Tools */, DAE490981B4E51AA004D97D6 /* UI (iOS) */, ); path = Source; @@ -651,6 +725,14 @@ path = Functional; sourceTree = ""; }; + DA8C833F1FC5FFDA00C947F9 /* Tools */ = { + isa = PBXGroup; + children = ( + ); + name = Tools; + path = SiestaTools; + sourceTree = ""; + }; DA99B5C61B38C356009C6937 /* Support */ = { isa = PBXGroup; children = ( @@ -659,6 +741,7 @@ CDCCAF681E6A313800860D18 /* Info-watchOS.plist */, CDCCAF751E6A31D900860D18 /* Info-tvOS.plist */, DA336E521B2E6DDB006F702A /* Siesta.h */, + DA8C83571FC6096100C947F9 /* SiestaTools.h */, DA6022801D65590800FB5673 /* SiestaUI.h */, DA3FBD9A1B55917600161A25 /* Siesta-ObjC.swift */, DA9F4BDB1B3DFE3700E8966F /* WeakCache.swift */, @@ -811,6 +894,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DA8B5B5422DEAC93008E47B0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DA8B5B5522DEAC93008E47B0 /* SiestaTools.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8B5B6322DEADDB008E47B0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DA8B5B6422DEADDB008E47B0 /* SiestaTools.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8C834B1FC600A900C947F9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DA8C83581FC6096100C947F9 /* SiestaTools.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DAC0B3A31D651CB500D25C44 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -844,6 +951,7 @@ ); dependencies = ( 9F0FAFAF1D033FE800CE1B61 /* PBXTargetDependency */, + DA8B5B6E22DEAE97008E47B0 /* PBXTargetDependency */, DAC4CFCD1DAC10F500EECEDE /* PBXTargetDependency */, ); name = "SiestaTests macOS"; @@ -926,6 +1034,7 @@ ); dependencies = ( CDCCAF7D1E6A31DA00860D18 /* PBXTargetDependency */, + DA8B5B7022DEAEA2008E47B0 /* PBXTargetDependency */, DA5E4B5122DCEFCA0059ED10 /* PBXTargetDependency */, ); name = "SiestaTests tvOS"; @@ -991,6 +1100,7 @@ ); dependencies = ( DA336E5C1B2E6DDB006F702A /* PBXTargetDependency */, + DA8C835A1FC60A2900C947F9 /* PBXTargetDependency */, DAC4CFCB1DAC10EC00EECEDE /* PBXTargetDependency */, ); name = "SiestaTests iOS"; @@ -1022,6 +1132,63 @@ productReference = DA5E4B4B22DCE9670059ED10 /* SiestaUI.framework */; productType = "com.apple.product-type.framework"; }; + DA8B5B4F22DEAC93008E47B0 /* SiestaTools macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DA8B5B5722DEAC93008E47B0 /* Build configuration list for PBXNativeTarget "SiestaTools macOS" */; + buildPhases = ( + DA8B5B5222DEAC93008E47B0 /* Sources */, + DA8B5B5322DEAC93008E47B0 /* Frameworks */, + DA8B5B5422DEAC93008E47B0 /* Headers */, + DA8B5B5622DEAC93008E47B0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DA8B5B5D22DEACA4008E47B0 /* PBXTargetDependency */, + ); + name = "SiestaTools macOS"; + productName = Siesta; + productReference = DA8B5B5A22DEAC93008E47B0 /* SiestaTools.framework */; + productType = "com.apple.product-type.framework"; + }; + DA8B5B5E22DEADDB008E47B0 /* SiestaTools tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DA8B5B6622DEADDB008E47B0 /* Build configuration list for PBXNativeTarget "SiestaTools tvOS" */; + buildPhases = ( + DA8B5B6122DEADDB008E47B0 /* Sources */, + DA8B5B6222DEADDB008E47B0 /* Frameworks */, + DA8B5B6322DEADDB008E47B0 /* Headers */, + DA8B5B6522DEADDB008E47B0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DA8B5B6C22DEAE72008E47B0 /* PBXTargetDependency */, + ); + name = "SiestaTools tvOS"; + productName = Siesta; + productReference = DA8B5B6922DEADDB008E47B0 /* SiestaTools.framework */; + productType = "com.apple.product-type.framework"; + }; + DA8C83401FC600A900C947F9 /* SiestaTools iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DA8C834F1FC600A900C947F9 /* Build configuration list for PBXNativeTarget "SiestaTools iOS" */; + buildPhases = ( + DA8C83431FC600A900C947F9 /* Sources */, + DA8C834A1FC600A900C947F9 /* Frameworks */, + DA8C834B1FC600A900C947F9 /* Headers */, + DA8C834D1FC600A900C947F9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DA8C83411FC600A900C947F9 /* PBXTargetDependency */, + ); + name = "SiestaTools iOS"; + productName = Siesta; + productReference = DA8C83521FC600A900C947F9 /* SiestaTools.framework */; + productType = "com.apple.product-type.framework"; + }; DAC0B37D1D651CB500D25C44 /* SiestaUI iOS */ = { isa = PBXNativeTarget; buildConfigurationList = DAC0B3A71D651CB500D25C44 /* Build configuration list for PBXNativeTarget "SiestaUI iOS" */; @@ -1135,6 +1302,9 @@ 9F2FB51D1C28645E0068DFFA /* Siesta macOS */, CDCCAF641E6A313800860D18 /* Siesta watchOS */, CDCCAF711E6A31D900860D18 /* Siesta tvOS */, + DA8C83401FC600A900C947F9 /* SiestaTools iOS */, + DA8B5B4F22DEAC93008E47B0 /* SiestaTools macOS */, + DA8B5B5E22DEADDB008E47B0 /* SiestaTools tvOS */, DAC0B37D1D651CB500D25C44 /* SiestaUI iOS */, DAC0B3AC1D651CC700D25C44 /* SiestaUI macOS */, DA5E4B3922DCE9670059ED10 /* SiestaUI tvOS */, @@ -1210,6 +1380,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DA8B5B5622DEAC93008E47B0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8B5B6522DEADDB008E47B0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8C834D1FC600A900C947F9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DAC0B3A51D651CB500D25C44 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1559,6 +1750,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DA8B5B5222DEAC93008E47B0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8B5B6122DEADDB008E47B0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA8C83431FC600A900C947F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DAC0B37E1D651CB500D25C44 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1612,6 +1824,36 @@ target = DA5E4B3922DCE9670059ED10 /* SiestaUI tvOS */; targetProxy = DA5E4B5022DCEFCA0059ED10 /* PBXContainerItemProxy */; }; + DA8B5B5D22DEACA4008E47B0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9F2FB51D1C28645E0068DFFA /* Siesta macOS */; + targetProxy = DA8B5B5C22DEACA4008E47B0 /* PBXContainerItemProxy */; + }; + DA8B5B6C22DEAE72008E47B0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CDCCAF711E6A31D900860D18 /* Siesta tvOS */; + targetProxy = DA8B5B6B22DEAE72008E47B0 /* PBXContainerItemProxy */; + }; + DA8B5B6E22DEAE97008E47B0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DA8B5B4F22DEAC93008E47B0 /* SiestaTools macOS */; + targetProxy = DA8B5B6D22DEAE97008E47B0 /* PBXContainerItemProxy */; + }; + DA8B5B7022DEAEA2008E47B0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DA8B5B5E22DEADDB008E47B0 /* SiestaTools tvOS */; + targetProxy = DA8B5B6F22DEAEA2008E47B0 /* PBXContainerItemProxy */; + }; + DA8C83411FC600A900C947F9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DA336E4E1B2E6DDB006F702A /* Siesta iOS */; + targetProxy = DA8C83421FC600A900C947F9 /* PBXContainerItemProxy */; + }; + DA8C835A1FC60A2900C947F9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DA8C83401FC600A900C947F9 /* SiestaTools iOS */; + targetProxy = DA8C83591FC60A2900C947F9 /* PBXContainerItemProxy */; + }; DAC4CFCB1DAC10EC00EECEDE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DAC0B37D1D651CB500D25C44 /* SiestaUI iOS */; @@ -2144,6 +2386,127 @@ }; name = Release; }; + DA8B5B5822DEAC93008E47B0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info-macOS.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.SiestaTools; + PRODUCT_NAME = SiestaTools; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DA8B5B5922DEAC93008E47B0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info-macOS.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.SiestaTools; + PRODUCT_NAME = SiestaTools; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + DA8B5B6722DEADDB008E47B0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info-tvOS.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.SiestaTools; + PRODUCT_NAME = SiestaTools; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DA8B5B6822DEADDB008E47B0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info-tvOS.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.SiestaTools; + PRODUCT_NAME = SiestaTools; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + }; + name = Release; + }; + DA8C83501FC600A900C947F9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info-iOS.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.SiestaTools; + PRODUCT_NAME = SiestaTools; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DA8C83511FC600A900C947F9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info-iOS.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.SiestaTools; + PRODUCT_NAME = SiestaTools; + SKIP_INSTALL = YES; + }; + name = Release; + }; DAC0B3A81D651CB500D25C44 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2318,6 +2681,33 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DA8B5B5722DEAC93008E47B0 /* Build configuration list for PBXNativeTarget "SiestaTools macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DA8B5B5822DEAC93008E47B0 /* Debug */, + DA8B5B5922DEAC93008E47B0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DA8B5B6622DEADDB008E47B0 /* Build configuration list for PBXNativeTarget "SiestaTools tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DA8B5B6722DEADDB008E47B0 /* Debug */, + DA8B5B6822DEADDB008E47B0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DA8C834F1FC600A900C947F9 /* Build configuration list for PBXNativeTarget "SiestaTools iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DA8C83501FC600A900C947F9 /* Debug */, + DA8C83511FC600A900C947F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DAC0B3A71D651CB500D25C44 /* Build configuration list for PBXNativeTarget "SiestaUI iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools iOS.xcscheme b/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools iOS.xcscheme new file mode 100644 index 00000000..c6d389c8 --- /dev/null +++ b/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools iOS.xcscheme @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools macOS.xcscheme b/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools macOS.xcscheme new file mode 100644 index 00000000..cb3d95ba --- /dev/null +++ b/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools macOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools tvOS.xcscheme b/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools tvOS.xcscheme new file mode 100644 index 00000000..20720125 --- /dev/null +++ b/Siesta.xcodeproj/xcshareddata/xcschemes/SiestaTools tvOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Siesta/Support/SiestaTools.h b/Source/Siesta/Support/SiestaTools.h new file mode 100644 index 00000000..a984896b --- /dev/null +++ b/Source/Siesta/Support/SiestaTools.h @@ -0,0 +1,16 @@ +// +// SiestaTools.h +// Siesta +// +// Created by Paul on 2017/11/22. +// Copyright © 2016 Bust Out Solutions. All rights reserved. +// + +#import + +//! Project version number for SiestaTools. +FOUNDATION_EXPORT double SiestaToolsVersionNumber; + +//! Project version string for SiestaTools. +FOUNDATION_EXPORT const unsigned char SiestaToolsVersionString[]; + From 2c6687b5aa4f337b6ce94f485483b2e41d93529d Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 20:27:34 -0500 Subject: [PATCH 04/25] Initial FileCache implementation --- Siesta.xcodeproj/project.pbxproj | 21 +++- Source/SiestaTools/FileCache.swift | 157 +++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 Source/SiestaTools/FileCache.swift diff --git a/Siesta.xcodeproj/project.pbxproj b/Siesta.xcodeproj/project.pbxproj index dab6e0e4..dd1a1b65 100644 --- a/Siesta.xcodeproj/project.pbxproj +++ b/Siesta.xcodeproj/project.pbxproj @@ -188,9 +188,12 @@ DA788A8F1D6AC1590085C820 /* ObjcCompatibilitySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = DA4D61971B751FEE00F6BB9C /* ObjcCompatibilitySpec.m */; }; DA7D05C41D57C2B500431980 /* PipelineProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7D05C31D57C2B500431980 /* PipelineProcessing.swift */; }; DA7D05C51D57C30400431980 /* PipelineProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7D05C31D57C2B500431980 /* PipelineProcessing.swift */; }; - DA8B5B5522DEAC93008E47B0 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; }; - DA8B5B6422DEADDB008E47B0 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; }; - DA8C83581FC6096100C947F9 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; }; + DA8B5B5522DEAC93008E47B0 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DA8B5B6422DEADDB008E47B0 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DA8B5B7122DEB0DF008E47B0 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8DEB491FC6763300555D92 /* FileCache.swift */; }; + DA8B5B7222DEB0DF008E47B0 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8DEB491FC6763300555D92 /* FileCache.swift */; }; + DA8C83581FC6096100C947F9 /* SiestaTools.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8C83571FC6096100C947F9 /* SiestaTools.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DA8DEB4A1FC6763300555D92 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8DEB491FC6763300555D92 /* FileCache.swift */; }; DA8EF6D11BC20AFE002175EB /* ProgressSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8EF6CF1BC1A917002175EB /* ProgressSpec.swift */; }; DA99B5C91B38C8E6009C6937 /* String+Siesta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA99B5C81B38C8E6009C6937 /* String+Siesta.swift */; }; DA9AA8151CCAFDD20016DB18 /* ConfigurationPatternConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9AA8141CCAFDD20016DB18 /* ConfigurationPatternConvertible.swift */; }; @@ -457,6 +460,7 @@ DA8B5B6922DEADDB008E47B0 /* SiestaTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SiestaTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DA8C83521FC600A900C947F9 /* SiestaTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SiestaTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DA8C83571FC6096100C947F9 /* SiestaTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SiestaTools.h; sourceTree = ""; }; + DA8DEB491FC6763300555D92 /* FileCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = ""; }; DA8EF6CF1BC1A917002175EB /* ProgressSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressSpec.swift; sourceTree = ""; }; DA99B5C81B38C8E6009C6937 /* String+Siesta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Siesta.swift"; sourceTree = ""; }; DA9AA8141CCAFDD20016DB18 /* ConfigurationPatternConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationPatternConvertible.swift; sourceTree = ""; }; @@ -728,6 +732,7 @@ DA8C833F1FC5FFDA00C947F9 /* Tools */ = { isa = PBXGroup; children = ( + DA8DEB491FC6763300555D92 /* FileCache.swift */, ); name = Tools; path = SiestaTools; @@ -1272,6 +1277,9 @@ DA5E4B3922DCE9670059ED10 = { ProvisioningStyle = Manual; }; + DA8C83401FC600A900C947F9 = { + LastSwiftMigration = 0910; + }; DAC0B37D1D651CB500D25C44 = { LastSwiftMigration = 1000; }; @@ -1754,6 +1762,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DA8B5B7122DEB0DF008E47B0 /* FileCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1761,6 +1770,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DA8B5B7222DEB0DF008E47B0 /* FileCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1768,6 +1778,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DA8DEB4A1FC6763300555D92 /* FileCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1880,6 +1891,7 @@ 9F0FAFAA1D033FCD00CE1B61 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = "$(SRCROOT)/Tests/Info-macOS.plist"; @@ -1897,6 +1909,7 @@ 9F0FAFAB1D033FCD00CE1B61 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = "$(SRCROOT)/Tests/Info-macOS.plist"; @@ -2310,6 +2323,7 @@ DA336E671B2E6DDB006F702A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; INFOPLIST_FILE = "$(SRCROOT)/Tests/Info-iOS.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -2325,6 +2339,7 @@ DA336E681B2E6DDB006F702A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; INFOPLIST_FILE = "$(SRCROOT)/Tests/Info-iOS.plist"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift new file mode 100644 index 00000000..5a6c0ebf --- /dev/null +++ b/Source/SiestaTools/FileCache.swift @@ -0,0 +1,157 @@ +// +// FileCache.swift +// Siesta +// +// Created by Paul on 2017/11/22. +// Copyright © 2017 Bust Out Solutions. All rights reserved. +// + +#if !COCOAPODS + import Siesta +#endif +import CommonCrypto + +private typealias File = URL + +private let fileCacheFormatVersion: [UInt8] = [0] + +public struct FileCache: EntityCache + where ContentType: Codable + { + private let keyPrefix: Data + private let cacheDir: File + + private let encoder = PropertyListEncoder() + private let decoder = PropertyListDecoder() + + public init(poolName: String = "Default", userIdentity: T?) throws + where T: Encodable + { + encoder.outputFormat = .binary + + self.keyPrefix = try + fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format + + encoder.encode(userIdentity) // prevents one user from seeing another’s cached requests + + [0] // separator for URL + + cacheDir = try + FileManager.default.url( + for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + .appendingPathComponent(Bundle.main.bundleIdentifier ?? "") + .appendingPathComponent("Siesta") + .appendingPathComponent(poolName) + try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) + } + + // MARK: - Keys and filenames + + public func key(for resource: Resource) -> Key? + { Key(resource: resource, prefix: keyPrefix) } + + public struct Key: CustomStringConvertible + { + fileprivate var url, hash: String + + fileprivate init(resource: Resource, prefix: Data) + { + url = resource.url.absoluteString + hash = Data(prefix + url.utf8) + .sha256 + .urlSafeBase64EncodedString + } + + public var description: String + { "FileCache.Key(\(url))" } + } + + private func file(for key: Key) -> File + { cacheDir.appendingPathComponent(key.hash + ".plist") } + + // MARK: - Reading and writing + + public func readEntity(forKey key: Key) -> Entity? + { + do { + return try + decoder.decode(EncodableEntity.self, + from: Data(contentsOf: file(for: key))) + .entity + } + catch CocoaError.fileReadNoSuchFile + { } // a cache miss is just fine + catch + { SiestaLog.log(.cache, ["WARNING: FileCache unable to read cached entity for", key, ":", error]) } + return nil + } + + public func writeEntity(_ entity: Entity, forKey key: Key) + { + #if os(macOS) + let options: Data.WritingOptions = [.atomic] + #else + let options: Data.WritingOptions = [.atomic, .completeFileProtection] + #endif + + do { + try encoder.encode(EncodableEntity(entity)) + .write(to: file(for: key), options: options) + } + catch + { SiestaLog.log(.cache, ["WARNING: FileCache unable to write entity for", key, ":", error]) } + } + + public func removeEntity(forKey key: Key) + { + do { + try FileManager.default.removeItem(at: file(for: key)) + } + catch + { SiestaLog.log(.cache, ["WARNING: FileCache unable to clear cache entity for", key, ":", error]) } + } + } + +/// Ideally, Entity itself would be codable when its ContentType is codable. To do this, Swift would need to: +/// +/// 1. allow conditional conformance, and +/// 2. allow extensions to synthesize encode/decode. +/// +/// This struct is a stopgap until the language can do all that. +/// +private struct EncodableEntity: Codable + where ContentType: Codable + { + var timestamp: TimeInterval + var headers: [String:String] + var charset: String? + var content: ContentType + + init(_ entity: Entity) + { + timestamp = entity.timestamp + headers = entity.headers + charset = entity.charset + content = entity.content + } + + var entity: Entity + { Entity(content: content, charset: charset, headers: headers, timestamp: timestamp) } + } + +extension Data + { + var sha256: Data + { + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + _ = withUnsafeBytes + { CC_SHA256($0.baseAddress, CC_LONG(count), &hash) } + return Data(hash) + } + + var urlSafeBase64EncodedString: String + { + base64EncodedString() + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "=", with: "") + } + } From 33a8bda5355474ec8bf0a74821f8fcb11f60756e Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Mon, 18 Jun 2018 11:01:01 -0500 Subject: [PATCH 05/25] Clarified log message --- Source/Siesta/Pipeline/PipelineConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Siesta/Pipeline/PipelineConfiguration.swift b/Source/Siesta/Pipeline/PipelineConfiguration.swift index ae758741..e8c633b5 100644 --- a/Source/Siesta/Pipeline/PipelineConfiguration.swift +++ b/Source/Siesta/Pipeline/PipelineConfiguration.swift @@ -69,7 +69,7 @@ public struct Pipeline .map(\.key) let missingStages = Set(nonEmptyStages).subtracting(newValue) if !missingStages.isEmpty - { SiestaLog.log(.pipeline, ["WARNING: Stages", missingStages, "configured but not present in custom pipeline order, will be ignored:", newValue]) } + { SiestaLog.log(.pipeline, ["WARNING: Stages", missingStages, "are configured but not present in custom pipeline order", newValue, "and will be ignored"]) } } } From 1ea6ed021255f4ded7a753f15b68b272a1b65eb5 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 20:34:05 -0500 Subject: [PATCH 06/25] EntityCache methods can now throw --- Source/Siesta/EntityCache.swift | 14 ++++----- .../Pipeline/PipelineConfiguration.swift | 15 +++++++-- .../Siesta/Pipeline/PipelineProcessing.swift | 31 ++++++++++++++++--- Source/SiestaTools/FileCache.swift | 24 +++++--------- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/Source/Siesta/EntityCache.swift b/Source/Siesta/EntityCache.swift index 63269801..9a2c055c 100644 --- a/Source/Siesta/EntityCache.swift +++ b/Source/Siesta/EntityCache.swift @@ -79,7 +79,7 @@ public protocol EntityCache - Warning: This method may be called on a background thread. Make sure your implementation is threadsafe. */ - func readEntity(forKey key: Key) -> Entity? + func readEntity(forKey key: Key) throws -> Entity? /** Store the given entity in the cache, associated with the given key. @@ -96,18 +96,18 @@ public protocol EntityCache - Warning: The method may be called on a background thread. Make sure your implementation is threadsafe. */ - func writeEntity(_ entity: Entity, forKey key: Key) + func writeEntity(_ entity: Entity, forKey key: Key) throws /** Update the timestamp of the entity for the given key. If there is no such cache entry, do nothing. */ - func updateEntityTimestamp(_ timestamp: TimeInterval, forKey key: Key) + func updateEntityTimestamp(_ timestamp: TimeInterval, forKey key: Key) throws /** Remove any entities cached for the given key. After a call to `removeEntity(forKey:)`, subsequent calls to `readEntity(forKey:)` for the same key **must** return nil until the next call to `writeEntity(_:forKey:)`. */ - func removeEntity(forKey key: Key) + func removeEntity(forKey key: Key) throws /** Returns the GCD queue on which this cache implementation will do its work. @@ -133,11 +133,11 @@ extension EntityCache While this default implementation always gives the correct behavior, cache implementations may choose to override it for performance reasons. */ - public func updateEntityTimestamp(_ timestamp: TimeInterval, forKey key: Key) + public func updateEntityTimestamp(_ timestamp: TimeInterval, forKey key: Key) throws { - guard var entity = readEntity(forKey: key) else + guard var entity = try readEntity(forKey: key) else { return } entity.timestamp = timestamp - writeEntity(entity, forKey: key) + try writeEntity(entity, forKey: key) } } diff --git a/Source/Siesta/Pipeline/PipelineConfiguration.swift b/Source/Siesta/Pipeline/PipelineConfiguration.swift index e8c633b5..bfb1db37 100644 --- a/Source/Siesta/Pipeline/PipelineConfiguration.swift +++ b/Source/Siesta/Pipeline/PipelineConfiguration.swift @@ -183,7 +183,8 @@ public struct PipelineStage } /** - An optional persistent cache for this stage. + Configures a persistent cache for responses after they pass this stage. Passing nil removes any previously + configured caching. When processing a response, the cache will receive the resulting entity after this stage’s transformers have run. @@ -194,8 +195,16 @@ public struct PipelineStage - Note: Siesta may ask your cache for content before any load requests run. This means that your observer may initially see an empty resources and then get a `newData(Cache)` event — even if you never call `load()`. */ - public mutating func cacheUsing(_ cache: T) - { cacheBox = CacheBox(cache: cache) } + public mutating func cacheUsing(_ cache: @autoclosure () throws -> T) + { + do + { cacheBox = CacheBox(cache: try cache()) } + catch + { + SiestaLog.log(.cache, ["Error while attempting to create persistent cache for", self, "; caching disabled at this stage:", error]) + doNotCache() + } + } /** Removes any caching that had been configured at this stage. diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index 717202a1..cb4b5772 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -199,7 +199,10 @@ private struct CacheEntry: CacheEntryProtocol func read() -> Entity? { cache.workQueue.sync - { self.cache.readEntity(forKey: self.key)?.withContentRetyped() } + { + catchAndLogErrors(attemptingTo: "read cached entity") + { try self.cache.readEntity(forKey: self.key)?.withContentRetyped() } + } } func write(_ entity: Entity) @@ -211,18 +214,38 @@ private struct CacheEntry: CacheEntryProtocol } cache.workQueue.async - { self.cache.writeEntity(cacheableEntity, forKey: self.key) } + { + self.catchAndLogErrors(attemptingTo: "write cached entity") + { try self.cache.writeEntity(cacheableEntity, forKey: self.key) } + } } func updateTimestamp(_ timestamp: TimeInterval) { cache.workQueue.async - { self.cache.updateEntityTimestamp(timestamp, forKey: self.key) } + { + self.catchAndLogErrors(attemptingTo: "update entity timestamp") + { try self.cache.updateEntityTimestamp(timestamp, forKey: self.key) } + } } func remove() { cache.workQueue.async - { self.cache.removeEntity(forKey: self.key) } + { + self.catchAndLogErrors(attemptingTo: "remove entity from cache") + { try self.cache.removeEntity(forKey: self.key) } + } + } + + private func catchAndLogErrors(attemptingTo actionName: String, action: () throws -> T?) -> T? + { + do + { return try action() } + catch + { + SiestaLog.log(.cache, ["WARNING:", cache, "unable to", actionName, "for", key, ":", error]) + return nil + } } } diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift index 5a6c0ebf..b78655ae 100644 --- a/Source/SiestaTools/FileCache.swift +++ b/Source/SiestaTools/FileCache.swift @@ -69,7 +69,7 @@ public struct FileCache: EntityCache // MARK: - Reading and writing - public func readEntity(forKey key: Key) -> Entity? + public func readEntity(forKey key: Key) throws -> Entity? { do { return try @@ -78,13 +78,11 @@ public struct FileCache: EntityCache .entity } catch CocoaError.fileReadNoSuchFile - { } // a cache miss is just fine - catch - { SiestaLog.log(.cache, ["WARNING: FileCache unable to read cached entity for", key, ":", error]) } + { } // a cache miss is just fine; don't log it return nil } - public func writeEntity(_ entity: Entity, forKey key: Key) + public func writeEntity(_ entity: Entity, forKey key: Key) throws { #if os(macOS) let options: Data.WritingOptions = [.atomic] @@ -92,21 +90,13 @@ public struct FileCache: EntityCache let options: Data.WritingOptions = [.atomic, .completeFileProtection] #endif - do { - try encoder.encode(EncodableEntity(entity)) - .write(to: file(for: key), options: options) - } - catch - { SiestaLog.log(.cache, ["WARNING: FileCache unable to write entity for", key, ":", error]) } + try encoder.encode(EncodableEntity(entity)) + .write(to: file(for: key), options: options) } - public func removeEntity(forKey key: Key) + public func removeEntity(forKey key: Key) throws { - do { - try FileManager.default.removeItem(at: file(for: key)) - } - catch - { SiestaLog.log(.cache, ["WARNING: FileCache unable to clear cache entity for", key, ":", error]) } + try FileManager.default.removeItem(at: file(for: key)) } } From f1aa9239e2501cb78178c04642d29cd29cdf13bc Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 20:53:46 -0500 Subject: [PATCH 07/25] FileCache data isolation --- Source/SiestaTools/FileCache.swift | 96 ++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift index b78655ae..9edc223c 100644 --- a/Source/SiestaTools/FileCache.swift +++ b/Source/SiestaTools/FileCache.swift @@ -15,47 +15,52 @@ private typealias File = URL private let fileCacheFormatVersion: [UInt8] = [0] +private let decoder = PropertyListDecoder() +private let encoder: PropertyListEncoder = + { + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + return encoder + }() + public struct FileCache: EntityCache where ContentType: Codable { - private let keyPrefix: Data + private let isolationStrategy: DataIsolationStrategy private let cacheDir: File - private let encoder = PropertyListEncoder() - private let decoder = PropertyListDecoder() - - public init(poolName: String = "Default", userIdentity: T?) throws - where T: Encodable + public init(poolName: String = "Default", dataIsolation isolationStrategy: DataIsolationStrategy) throws { - encoder.outputFormat = .binary - - self.keyPrefix = try - fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format - + encoder.encode(userIdentity) // prevents one user from seeing another’s cached requests - + [0] // separator for URL - - cacheDir = try - FileManager.default.url( - for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - .appendingPathComponent(Bundle.main.bundleIdentifier ?? "") + let cacheDir = try FileManager.default + .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + .appendingPathComponent(Bundle.main.bundleIdentifier ?? "") // no bundle → directly inside cache dir .appendingPathComponent("Siesta") .appendingPathComponent(poolName) try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) + + self.init(inDirectory: cacheDir, dataIsolation: isolationStrategy) + } + + public init(inDirectory cacheDir: URL, dataIsolation isolationStrategy: DataIsolationStrategy) + { + self.cacheDir = cacheDir + self.isolationStrategy = isolationStrategy } // MARK: - Keys and filenames public func key(for resource: Resource) -> Key? - { Key(resource: resource, prefix: keyPrefix) } + { Key(resource: resource, isolatedUsing: isolationStrategy) } public struct Key: CustomStringConvertible { - fileprivate var url, hash: String + fileprivate var hash: String + private var url: URL - fileprivate init(resource: Resource, prefix: Data) + fileprivate init(resource: Resource, isolatedUsing isolationStrategy: DataIsolationStrategy) { - url = resource.url.absoluteString - hash = Data(prefix + url.utf8) + url = resource.url + hash = isolationStrategy.keyData(for: url) .sha256 .urlSafeBase64EncodedString } @@ -65,7 +70,7 @@ public struct FileCache: EntityCache } private func file(for key: Key) -> File - { cacheDir.appendingPathComponent(key.hash + ".plist") } + { cacheDir.appendingPathComponent(key.hash + ".cache") } // MARK: - Reading and writing @@ -73,7 +78,8 @@ public struct FileCache: EntityCache { do { return try - decoder.decode(EncodableEntity.self, + decoder.decode( + EncodableEntity.self, from: Data(contentsOf: file(for: key))) .entity } @@ -100,6 +106,37 @@ public struct FileCache: EntityCache } } +extension FileCache + { + public struct DataIsolationStrategy + { + private let keyPrefix: Data + + private init(keyIsolator: Data) + { + keyPrefix = + fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format + + keyIsolator // prevents one user from seeing another’s cached requests + + [0] // separator for URL + } + + fileprivate func keyData(for url: URL) -> Data + { + Data(keyPrefix + url.absoluteString.utf8) + } + + public static var sharedByAllUsers: DataIsolationStrategy + { DataIsolationStrategy(keyIsolator: Data()) } + + public static func perUser(identifiedBy partitionID: T) throws -> DataIsolationStrategy + where T: Codable + { + DataIsolationStrategy( + keyIsolator: try encoder.encode([partitionID])) + } + } + } + /// Ideally, Entity itself would be codable when its ContentType is codable. To do this, Swift would need to: /// /// 1. allow conditional conformance, and @@ -127,9 +164,11 @@ private struct EncodableEntity: Codable { Entity(content: content, charset: charset, headers: headers, timestamp: timestamp) } } +// MARK: - Encryption helpers + extension Data { - var sha256: Data + fileprivate var sha256: Data { var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) _ = withUnsafeBytes @@ -137,7 +176,12 @@ extension Data return Data(hash) } - var urlSafeBase64EncodedString: String + fileprivate var shortenWithSHA256: Data + { + count > 32 ? sha256 : self + } + + fileprivate var urlSafeBase64EncodedString: String { base64EncodedString() .replacingOccurrences(of: "/", with: "_") From 2f26b6f3f6f9c14c513fb86846c956bcb3996b4a Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sun, 18 Mar 2018 00:54:21 -0500 Subject: [PATCH 08/25] Including FileCache.ContentType in keys --- Source/SiestaTools/FileCache.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift index 9edc223c..50d47fd1 100644 --- a/Source/SiestaTools/FileCache.swift +++ b/Source/SiestaTools/FileCache.swift @@ -115,9 +115,11 @@ extension FileCache private init(keyIsolator: Data) { keyPrefix = - fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format - + keyIsolator // prevents one user from seeing another’s cached requests - + [0] // separator for URL + fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format + + "\(ContentType.self)".utf8 // prevent data collision when caching at multiple pipeline stages + + [0] // null-terminate ContentType to prevent bleed into username + + keyIsolator // prevents one user from seeing another’s cached requests + + [0] // separator for URL } fileprivate func keyData(for url: URL) -> Data From 48bb9ebb767e7869f3461017b4f9e464a0a26d3f Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 20:58:02 -0500 Subject: [PATCH 09/25] More legible cache logging --- Source/Siesta/Pipeline/PipelineProcessing.swift | 7 +++++-- Source/SiestaTools/FileCache.swift | 16 +++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index cb4b5772..d6c75f5d 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -55,7 +55,7 @@ extension Pipeline if case .success(let entity) = output, let cacheEntry = cacheEntry { - SiestaLog.log(.cache, [" ├╴Caching entity with", type(of: entity.content), "content in", cacheEntry]) + SiestaLog.log(.cache, [" ├╴Caching entity with", type(of: entity.content), "content for", cacheEntry]) cacheEntry.write(entity) } @@ -180,7 +180,7 @@ private protocol CacheEntryProtocol func remove() } -private struct CacheEntry: CacheEntryProtocol +private struct CacheEntry: CacheEntryProtocol, CustomStringConvertible where Cache: EntityCache, Cache.Key == Key { let cache: Cache @@ -248,4 +248,7 @@ private struct CacheEntry: CacheEntryProtocol return nil } } + + var description: String + { "\(key) in \(cache)" } } diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift index 50d47fd1..58db327b 100644 --- a/Source/SiestaTools/FileCache.swift +++ b/Source/SiestaTools/FileCache.swift @@ -23,12 +23,14 @@ private let encoder: PropertyListEncoder = return encoder }() -public struct FileCache: EntityCache +public struct FileCache: EntityCache, CustomStringConvertible where ContentType: Codable { private let isolationStrategy: DataIsolationStrategy private let cacheDir: File + public let description: String + public init(poolName: String = "Default", dataIsolation isolationStrategy: DataIsolationStrategy) throws { let cacheDir = try FileManager.default @@ -38,26 +40,30 @@ public struct FileCache: EntityCache .appendingPathComponent(poolName) try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) - self.init(inDirectory: cacheDir, dataIsolation: isolationStrategy) + self.init(inDirectory: cacheDir, dataIsolation: isolationStrategy, cacheName: "poolName: " + poolName) } - public init(inDirectory cacheDir: URL, dataIsolation isolationStrategy: DataIsolationStrategy) + public init( + inDirectory cacheDir: URL, + dataIsolation isolationStrategy: DataIsolationStrategy, + cacheName: String? = nil) { self.cacheDir = cacheDir self.isolationStrategy = isolationStrategy + self.description = "\(type(of: self))(\(cacheName ?? cacheDir.path))" } // MARK: - Keys and filenames public func key(for resource: Resource) -> Key? - { Key(resource: resource, isolatedUsing: isolationStrategy) } + { Key(resource: resource, isolationStrategy: isolationStrategy) } public struct Key: CustomStringConvertible { fileprivate var hash: String private var url: URL - fileprivate init(resource: Resource, isolatedUsing isolationStrategy: DataIsolationStrategy) + fileprivate init(resource: Resource, isolationStrategy: DataIsolationStrategy) { url = resource.url hash = isolationStrategy.keyData(for: url) From 87906bdc77fc94b3442d6b8e06d1120ff924ba35 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sun, 31 Mar 2019 15:11:53 -0500 Subject: [PATCH 10/25] Make Entity conditionally codable --- Source/Siesta/Entity.swift | 6 ++++++ Source/SiestaTools/FileCache.swift | 33 +++--------------------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/Source/Siesta/Entity.swift b/Source/Siesta/Entity.swift index 13e210ca..4fa1b8cf 100644 --- a/Source/Siesta/Entity.swift +++ b/Source/Siesta/Entity.swift @@ -156,6 +156,12 @@ public struct Entity } +extension Entity: Codable where ContentType: Codable + { + // codability synthesized + } + + /** Mixin that provides convenience accessors for the content of an optional contained entity. diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift index 58db327b..49b80208 100644 --- a/Source/SiestaTools/FileCache.swift +++ b/Source/SiestaTools/FileCache.swift @@ -85,9 +85,8 @@ public struct FileCache: EntityCache, CustomStringConvertible do { return try decoder.decode( - EncodableEntity.self, + Entity.self, from: Data(contentsOf: file(for: key))) - .entity } catch CocoaError.fileReadNoSuchFile { } // a cache miss is just fine; don't log it @@ -102,7 +101,7 @@ public struct FileCache: EntityCache, CustomStringConvertible let options: Data.WritingOptions = [.atomic, .completeFileProtection] #endif - try encoder.encode(EncodableEntity(entity)) + try encoder.encode(entity) .write(to: file(for: key), options: options) } @@ -122,6 +121,7 @@ extension FileCache { keyPrefix = fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format + // TODO: include pipeline stage name here + "\(ContentType.self)".utf8 // prevent data collision when caching at multiple pipeline stages + [0] // null-terminate ContentType to prevent bleed into username + keyIsolator // prevents one user from seeing another’s cached requests @@ -145,33 +145,6 @@ extension FileCache } } -/// Ideally, Entity itself would be codable when its ContentType is codable. To do this, Swift would need to: -/// -/// 1. allow conditional conformance, and -/// 2. allow extensions to synthesize encode/decode. -/// -/// This struct is a stopgap until the language can do all that. -/// -private struct EncodableEntity: Codable - where ContentType: Codable - { - var timestamp: TimeInterval - var headers: [String:String] - var charset: String? - var content: ContentType - - init(_ entity: Entity) - { - timestamp = entity.timestamp - headers = entity.headers - charset = entity.charset - content = entity.content - } - - var entity: Entity - { Entity(content: content, charset: charset, headers: headers, timestamp: timestamp) } - } - // MARK: - Encryption helpers extension Data From 06d7f7ed01914050a9aa867eff6d4662cc84f01a Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Thu, 18 Apr 2019 07:55:25 -0500 Subject: [PATCH 11/25] Removed deprecated isCompleted from comments/spec names --- Source/Siesta/Request/Request.swift | 2 +- Source/Siesta/Request/RequestCallbacks.swift | 2 +- Tests/Functional/RequestSpec.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Siesta/Request/Request.swift b/Source/Siesta/Request/Request.swift index 7a6a8a09..0e818c03 100644 --- a/Source/Siesta/Request/Request.swift +++ b/Source/Siesta/Request/Request.swift @@ -118,7 +118,7 @@ public protocol Request: AnyObject The property will always be 1 if a request is completed. Note that the converse is not true: a value of 1 does not necessarily mean the request is completed; it means only that we estimate the request _should_ be completed - by now. Use the `isCompleted` property to test for actual completion. + by now. Use the `state` property to test for actual completion. */ var progress: Double { get } diff --git a/Source/Siesta/Request/RequestCallbacks.swift b/Source/Siesta/Request/RequestCallbacks.swift index 03c8b516..48e8ffb3 100644 --- a/Source/Siesta/Request/RequestCallbacks.swift +++ b/Source/Siesta/Request/RequestCallbacks.swift @@ -90,7 +90,7 @@ internal struct CallbackGroup completedValue = arguments // We need to let this mutating method finish before calling the callbacks. Some of them inspect - // completeValue (via isCompleted), which causes a simultaneous access error at runtime. + // completeValue (via request.state), which causes a simultaneous access error at runtime. // See https://github.com/apple/swift-evolution/blob/master/proposals/0176-enforce-exclusive-access-to-memory.md let snapshot = self diff --git a/Tests/Functional/RequestSpec.swift b/Tests/Functional/RequestSpec.swift index d2d89bda..f7bf5624 100644 --- a/Tests/Functional/RequestSpec.swift +++ b/Tests/Functional/RequestSpec.swift @@ -670,7 +670,7 @@ class RequestSpec: ResourceSpecBase expectResult("yoyo", for: chainedReq) } - it("isCompleted is false until a “use” action") + it("state is inProgress until callback returns a “use response” action") { let reqStub = stubText("yo").delay() let req = resource().request(.get).chained From 99fc17218077d8d83f46a33059ff73f1f3e6bdf9 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Mon, 22 Apr 2019 11:56:24 -0500 Subject: [PATCH 12/25] Lint warning for disabled specs --- Tests/.swiftlint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml index 70f5c2d6..8fbcc6c8 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -8,5 +8,5 @@ disabled_rules: custom_rules: focused_spec: - name: "Focused spec in effect" - regex: '(fit|fdescribe)\s*\(' + name: "Remember not to commit any focused or disabled specs!" + regex: '\b[fx](it|describe)\s*\(' From 8219e44325309e2414f58291c288873583a64138 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Wed, 10 Apr 2019 20:07:35 -0500 Subject: [PATCH 13/25] Mopping up cache API changes --- Source/Siesta/Pipeline/PipelineConfiguration.swift | 13 ++++++++++++- Source/Siesta/Support/Logging.swift | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineConfiguration.swift b/Source/Siesta/Pipeline/PipelineConfiguration.swift index bfb1db37..c9f52106 100644 --- a/Source/Siesta/Pipeline/PipelineConfiguration.swift +++ b/Source/Siesta/Pipeline/PipelineConfiguration.swift @@ -195,7 +195,18 @@ public struct PipelineStage - Note: Siesta may ask your cache for content before any load requests run. This means that your observer may initially see an empty resources and then get a `newData(Cache)` event — even if you never call `load()`. */ - public mutating func cacheUsing(_ cache: @autoclosure () throws -> T) + public mutating func cacheUsing(_ cache: T) + { + cacheBox = CacheBox(cache: cache) + } + + /** + Convenience for `cacheUsing(_:)` that takes a failable closure, for situations where caching is optional. + + Configures a persistent cache at this stage if the given closure succeeds. Disables caching at this stage if the + closure throws an error. + */ + public mutating func cacheUsing(_ cache: () throws -> T) { do { cacheBox = CacheBox(cache: try cache()) } diff --git a/Source/Siesta/Support/Logging.swift b/Source/Siesta/Support/Logging.swift index a52be73f..d70b3e31 100644 --- a/Source/Siesta/Support/Logging.swift +++ b/Source/Siesta/Support/Logging.swift @@ -77,7 +77,7 @@ public enum SiestaLog .forceUnwrapped(because: "Modulus always maps thread IDs to valid unicode scalars"))) threadID /= 0x55 } - threadName += "]" + threadName += "] " } let prefix = "Siesta:\(paddedCategory) │ \(threadName)" let indentedMessage = $1.replacingOccurrences(of: "\n", with: "\n" + prefix) From d4f06d8ac57dffb88f2127d15ab10138964c58a0 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 21:03:40 -0500 Subject: [PATCH 14/25] Experimenting with FileCache in GHBrowser --- .../GithubBrowser/Source/API/GithubAPI.swift | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/Examples/GithubBrowser/Source/API/GithubAPI.swift b/Examples/GithubBrowser/Source/API/GithubAPI.swift index d7c2ae09..ad47d05f 100644 --- a/Examples/GithubBrowser/Source/API/GithubAPI.swift +++ b/Examples/GithubBrowser/Source/API/GithubAPI.swift @@ -17,7 +17,7 @@ class _GitHubAPI { fileprivate init() { #if DEBUG // Bare-bones logging of which network calls Siesta makes: - SiestaLog.Category.enabled = [.network] + SiestaLog.Category.enabled = [.network, .cache] // For more info about how Siesta decides whether to make a network call, // and which state updates it broadcasts to the app: @@ -44,8 +44,40 @@ class _GitHubAPI { $0.pipeline[.cleanup].add( GitHubErrorMessageExtractor(jsonDecoder: jsonDecoder)) + + // Cache API results for fast launch & offline access: + + $0.pipeline[.rawData].cacheUsing { + try FileCache( + poolName: "api.github.com", + dataIsolation: .perUser(identifiedBy: self.username)) // Show each user their own data + } + + // Using the closure form of cacheUsing above signals that if we encounter an error trying create a cache + // directory or generate a cache isolation key from the username, we should simply proceed silently without + // having a persistent cache. + + // Note that the dataIsolation uses only username. This means that users will not _see_ other users’ data; + // however, it does not _secure_ one user’s data from another. A user with permission to see the cache + // directory could in principle see all the cached data. + // + // To fully secure one user’s data from another, the application would need to generate some long-lived + // secret that is unique to each user. A password can work, though it will essentially empty the user’s + // cache if the password changes. The server could also send some kind of high-entropy per-user token in + // the authentication response. + } + + RemoteImageView.defaultImageService.configure { + // We can cache images offline too: + + $0.pipeline[.rawData].cacheUsing { + try FileCache( + poolName: "images", + dataIsolation: .sharedByAllUsers) // images aren't secret, so no need to isolate them + } } + // –––––– Resource-specific configuration –––––– service.configure("/search/**") { @@ -116,12 +148,14 @@ class _GitHubAPI { // MARK: - Authentication func logIn(username: String, password: String) { - if let auth = "\(username):\(password)".data(using: String.Encoding.utf8) { + self.username = username + if let auth = "\(username):\(password)".data(using: .utf8) { basicAuthHeader = "Basic \(auth.base64EncodedString())" } } func logOut() { + username = nil basicAuthHeader = nil } @@ -129,6 +163,8 @@ class _GitHubAPI { return basicAuthHeader != nil } + private var username: String? + private var basicAuthHeader: String? { didSet { // These two calls are almost always necessary when you have changing auth for your API: From 8aae16e7c5b6f3671b43f9bbfa0bea142e1b8084 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 21:08:13 -0500 Subject: [PATCH 15/25] Tuck cache actions inside ResponseInfo, let Resource control whether they happen --- .../Siesta/Pipeline/PipelineProcessing.swift | 34 +++++++++---------- Source/Siesta/Request/NetworkRequest.swift | 15 +++++--- Source/Siesta/Request/Request.swift | 3 ++ Source/Siesta/Resource/Resource.swift | 6 ++++ 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index d6c75f5d..4870714b 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -21,7 +21,7 @@ extension Pipeline { stage in (stage, stage.cacheBox?.buildEntry(resource)) } } - internal func makeProcessor(_ rawResponse: Response, resource: Resource) -> () -> Response + internal func makeProcessor(_ rawResponse: Response, resource: Resource) -> () -> ResponseInfo { // Generate cache keys on main thread (because this touches Resource) let stagesAndEntries = self.stagesAndEntries(for: resource) @@ -29,37 +29,37 @@ extension Pipeline // Return deferred processor to run on background queue return { - let result = Pipeline.processAndCache(rawResponse, using: stagesAndEntries) + let result = Pipeline.process(rawResponse, using: stagesAndEntries) - SiestaLog.log(.pipeline, [" └╴Response after pipeline:", result.summary()]) - SiestaLog.log(.networkDetails, [" Details:", result.dump(" ")]) + SiestaLog.log(.pipeline, [" └╴Response after pipeline:", result.response.summary()]) + SiestaLog.log(.networkDetails, [" Details:", result.response.dump(" ")]) return result } } // Runs on a background queue - private static func processAndCache( + private static func process( _ rawResponse: Response, using stagesAndEntries: StagesAndEntries) - -> Response + -> ResponseInfo where StagesAndEntries.Iterator.Element == StageAndEntry { - stagesAndEntries.reduce(rawResponse) + stagesAndEntries.reduce(into: ResponseInfo(response: rawResponse)) { - let input = $0, - (stage, cacheEntry) = $1 + let (stage, cacheEntry) = $1 - let output = stage.process(input) + $0.response = stage.process($0.response) - if case .success(let entity) = output, + if case .success(let entity) = $0.response, let cacheEntry = cacheEntry { - SiestaLog.log(.cache, [" ├╴Caching entity with", type(of: entity.content), "content for", cacheEntry]) - cacheEntry.write(entity) + $0.cacheActions.append( + { + SiestaLog.log(.cache, ["Caching entity with", type(of: entity.content), "content for", cacheEntry]) + cacheEntry.write(entity) + }) } - - return output } } @@ -136,11 +136,11 @@ extension Pipeline { SiestaLog.log(.cache, ["Cache hit for", cacheEntry]) - let processed = Pipeline.processAndCache( + let processed = Pipeline.process( .success(result), using: stagesAndEntries.suffix(from: index + 1)) - switch processed + switch processed.response { case .failure: SiestaLog.log(.cache, ["Error processing cached entity; will ignore cached value. Error:", processed]) diff --git a/Source/Siesta/Request/NetworkRequest.swift b/Source/Siesta/Request/NetworkRequest.swift index 2e094d87..da4564da 100644 --- a/Source/Siesta/Request/NetworkRequest.swift +++ b/Source/Siesta/Request/NetworkRequest.swift @@ -148,12 +148,17 @@ internal final class NetworkRequestDelegate: RequestDelegate { let processor = config.pipeline.makeProcessor(rawInfo.response, resource: resource) - DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async + let processingQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated) + processingQueue.async { - let processedInfo = - rawInfo.isNew - ? ResponseInfo(response: processor(), isNew: true) - : rawInfo + var processedInfo: ResponseInfo + if rawInfo.isNew + { + processedInfo = processor() + processedInfo.isNew = true + } + else + { processedInfo = rawInfo } // result from a 304 is already transformed, cached, etc. DispatchQueue.main.async { afterTransformation(processedInfo) } diff --git a/Source/Siesta/Request/Request.swift b/Source/Siesta/Request/Request.swift index 0e818c03..f633e57d 100644 --- a/Source/Siesta/Request/Request.swift +++ b/Source/Siesta/Request/Request.swift @@ -252,6 +252,9 @@ public struct ResponseInfo /// Used to distinguish `ResourceEvent.newData` from `ResourceEvent.notModified`. public var isNew: Bool + /// Callbacks to cache this response according to the pipeline config originally used to process it + var cacheActions: [() -> Void] = [] + /// Creates new responseInfo, with `isNew` true by default. public init(response: Response, isNew: Bool = true) { diff --git a/Source/Siesta/Resource/Resource.swift b/Source/Siesta/Resource/Resource.swift index 94e94b02..d59000e2 100644 --- a/Source/Siesta/Resource/Resource.swift +++ b/Source/Siesta/Resource/Resource.swift @@ -474,6 +474,12 @@ public final class Resource: NSObject req.onProgress(notifyObservers(ofProgress:)) + req.onCompletion + { + for action in $0.cacheActions + { action() } + } + req.onNewData(receiveNewDataFromNetwork) req.onNotModified(receiveDataNotModified) req.onFailure(receiveError) From 16188d8787c0f412d35b41020d7d8772acda5561 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 21:09:58 -0500 Subject: [PATCH 16/25] Caching finally limited to GETs on same resource --- Source/Siesta/Request/NetworkRequest.swift | 7 ++- Source/Siesta/Request/Request.swift | 3 ++ Source/Siesta/Resource/Resource.swift | 22 ++++---- Tests/Functional/EntityCacheSpec.swift | 58 +++++++++++++++++++--- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/Source/Siesta/Request/NetworkRequest.swift b/Source/Siesta/Request/NetworkRequest.swift index da4564da..7f43e04b 100644 --- a/Source/Siesta/Request/NetworkRequest.swift +++ b/Source/Siesta/Request/NetworkRequest.swift @@ -12,8 +12,9 @@ internal final class NetworkRequestDelegate: RequestDelegate { // Basic metadata private let resource: Resource + private let method: RequestMethod internal var config: Configuration - { resource.configuration(for: underlyingRequest) } + { resource.configuration(for: method) } internal let requestDescription: String // Networking @@ -32,6 +33,9 @@ internal final class NetworkRequestDelegate: RequestDelegate self.requestBuilder = requestBuilder underlyingRequest = requestBuilder() + method = RequestMethod(rawValue: underlyingRequest.httpMethod?.lowercased() ?? "") + ?? .get // All unrecognized methods default to .get + requestDescription = SiestaLog.Category.enabled.contains(.network) || SiestaLog.Category.enabled.contains(.networkDetails) ? debugStr([underlyingRequest.httpMethod, underlyingRequest.url]) @@ -156,6 +160,7 @@ internal final class NetworkRequestDelegate: RequestDelegate { processedInfo = processor() processedInfo.isNew = true + processedInfo.configurationSource = (self.method, self.resource) } else { processedInfo = rawInfo } // result from a 304 is already transformed, cached, etc. diff --git a/Source/Siesta/Request/Request.swift b/Source/Siesta/Request/Request.swift index f633e57d..c8c9cc14 100644 --- a/Source/Siesta/Request/Request.swift +++ b/Source/Siesta/Request/Request.swift @@ -252,6 +252,9 @@ public struct ResponseInfo /// Used to distinguish `ResourceEvent.newData` from `ResourceEvent.notModified`. public var isNew: Bool + /// Used to determine whether the response is suitable for caching when loaded by a particular resource + var configurationSource: (method: RequestMethod, resource: Resource)? + /// Callbacks to cache this response according to the pipeline config originally used to process it var cacheActions: [() -> Void] = [] diff --git a/Source/Siesta/Resource/Resource.swift b/Source/Siesta/Resource/Resource.swift index d59000e2..3917ca69 100644 --- a/Source/Siesta/Resource/Resource.swift +++ b/Source/Siesta/Resource/Resource.swift @@ -68,13 +68,6 @@ public final class Resource: NSObject { service.configuration(forResource: self, requestMethod: method) } } - internal func configuration(for request: URLRequest) -> Configuration - { - configuration(for: - RequestMethod(rawValue: request.httpMethod?.lowercased() ?? "") - ?? .get) // All unrecognized methods default to .get - } - private var cachedConfig: [RequestMethod:Configuration] = [:] private var configVersion: UInt64 = 0 @@ -476,8 +469,19 @@ public final class Resource: NSObject req.onCompletion { - for action in $0.cacheActions - { action() } + // TODO: explain this + if let configurationSource = $0.configurationSource + { + if configurationSource == (.get, self) + { + for action in $0.cacheActions + { action() } + } + else + { + SiestaLog.log(.cache, ["Resource.load(using:) will not cache the results of this request because it is not a GET and/or is for a different resource:", configurationSource.method, configurationSource.resource]) + } + } } req.onNewData(receiveNewDataFromNetwork) diff --git a/Tests/Functional/EntityCacheSpec.swift b/Tests/Functional/EntityCacheSpec.swift index da3c3d06..0647b373 100644 --- a/Tests/Functional/EntityCacheSpec.swift +++ b/Tests/Functional/EntityCacheSpec.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Bust Out Solutions. All rights reserved. // -import Siesta +@testable import Siesta import Foundation import Quick @@ -16,9 +16,12 @@ class EntityCacheSpec: ResourceSpecBase { override func resourceSpec(_ service: @escaping () -> Service, _ resource: @escaping () -> Resource) { - func configureCache(_ cache: C, at stageKey: PipelineStageKey) + func configureCache( + _ cache: C, + for pattern: ConfigurationPatternConvertible = "**", + at stageKey: PipelineStageKey) { - service().configure + service().configure(pattern) { $0.pipeline[stageKey].cacheUsing(cache) } } @@ -168,6 +171,15 @@ class EntityCacheSpec: ResourceSpecBase describe("write") { + @discardableResult + func stubAndAwaitRequestWithoutLoading(for resource: Resource, method: RequestMethod) -> Request + { + NetworkStub.add(method, { resource }) + let req = resource.request(method) + awaitNewData(req, initialState: .inProgress) + return req + } + func expectCacheWrite(to cache: TestCache, content: String) { waitForCacheWrite(cache) @@ -175,7 +187,7 @@ class EntityCacheSpec: ResourceSpecBase expect(cache.entries.values.first?.typedContent()) == content } - it("caches new data on success") + it("caches new data on a successful load()") { let testCache = TestCache("new data") configureCache(testCache, at: .cleanup) @@ -231,6 +243,40 @@ class EntityCacheSpec: ResourceSpecBase expect(testCache.entries).toEventually(beEmpty()) } + + it("does not cache anything for call to Resource.request() without load()") + { + configureCache(UnwritableCache(), at: .cleanup) + stubAndAwaitRequestWithoutLoading(for: resource(), method: .get) + } + + it("caches new data for a GET on the same resource passed to load(using:)") + { + let testCache = TestCache("new data from load(using:)") + configureCache(testCache, at: .cleanup) + let req = stubAndAwaitRequestWithoutLoading(for: resource(), method: .get) + resource().load(using: req) + expectCacheWrite(to: testCache, content: "decparmodcle") + } + + it("does not cache anything for a non-GET request, even if passed to load(using:)") + { + configureCache(UnwritableCache(), at: .cleanup) + for method in RequestMethod.allCases + where method != .get + { + let req = stubAndAwaitRequestWithoutLoading(for: resource(), method: method) + resource().load(using: req) + } + } + + it("does not cache anything for a GET request for a different resource, even if passed to load(using:)") + { + let otherResource = service().resource("/otherResource") + configureCache(UnwritableCache(), at: .cleanup) + let req = stubAndAwaitRequestWithoutLoading(for: otherResource, method: .get) + resource().load(using: req) + } } func exerciseCache() @@ -376,10 +422,10 @@ private struct UnwritableCache: EntityCache { nil } func writeEntity(_ entity: Entity, forKey key: URL) - { fatalError("cache should never be written to") } + { fail("cache should never be written to") } func removeEntity(forKey key: URL) - { fatalError("cache should never be written to") } + { fail("cache should never be written to") } } private class ObserverEventRecorder: ResourceObserver From 95594a708ad9572f0816977da21fe073b3064897 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 4 Apr 2020 11:05:20 -0500 Subject: [PATCH 17/25] Made cache requests behave correctly if passed to load(using:) Also mopped up some cache request cleanup issues --- .../Siesta/Pipeline/PipelineProcessing.swift | 58 +++++++++---- Source/Siesta/Request/NetworkRequest.swift | 2 +- Source/Siesta/Request/Request.swift | 8 +- Source/Siesta/Resource/Resource.swift | 14 +++- Tests/Functional/EntityCacheSpec.swift | 83 ++++++++++++++++++- 5 files changed, 139 insertions(+), 26 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index 4870714b..5c5d5508 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -55,14 +55,27 @@ extension Pipeline let cacheEntry = cacheEntry { $0.cacheActions.append( - { - SiestaLog.log(.cache, ["Caching entity with", type(of: entity.content), "content for", cacheEntry]) - cacheEntry.write(entity) - }) + cacheAction(writing: entity, into: cacheEntry)) } } } + // swiftlint:disable implicit_return + + fileprivate static func cacheAction( + writing entity: Entity, + into cacheEntry: CacheEntryProtocol) + -> () -> Void + { + return + { + SiestaLog.log(.cache, ["Caching entity with", type(of: entity.content), "content for", cacheEntry]) + cacheEntry.write(entity) + } + } + + // swiftlint:enable implicit_return + internal func checkCache(for resource: Resource) -> Request { Resource @@ -92,11 +105,13 @@ extension Pipeline private struct CacheRequestDelegate: RequestDelegate { let requestDescription: String + private weak var resource: Resource? private let stagesAndEntries: [StageAndEntry] init(for resource: Resource, searching stagesAndEntries: [StageAndEntry]) { requestDescription = "Cache request for \(resource)" + self.resource = resource self.stagesAndEntries = stagesAndEntries } @@ -104,19 +119,18 @@ extension Pipeline { defaultEntityCacheWorkQueue.async { - let response: Response - if let entity = self.performCacheLookup() - { response = .success(entity) } - else - { - response = .failure(RequestError( - userMessage: NSLocalizedString("Cache miss", comment: "userMessage"), - cause: RequestError.Cause.CacheMiss())) - } + var result = self.performCacheLookup() + ?? ResponseInfo( + response: .failure(RequestError( + userMessage: NSLocalizedString("Cache miss", comment: "userMessage"), + cause: RequestError.Cause.CacheMiss()))) + + if let resource = self.resource + { result.configurationSource = .init(method: .get, resource: resource) } DispatchQueue.main.async { - completionHandler.broadcastResponse(ResponseInfo(response: response)) + completionHandler.broadcastResponse(result) } } } @@ -128,7 +142,7 @@ extension Pipeline { self } // Runs on a background queue - private func performCacheLookup() -> Entity? + private func performCacheLookup() -> ResponseInfo? { for (index, (_, cacheEntry)) in stagesAndEntries.enumerated().reversed() { @@ -136,17 +150,25 @@ extension Pipeline { SiestaLog.log(.cache, ["Cache hit for", cacheEntry]) - let processed = Pipeline.process( + var processed = Pipeline.process( .success(result), using: stagesAndEntries.suffix(from: index + 1)) + // TODO: explain this + if let cacheEntry = cacheEntry + { + processed.cacheActions.insert( + Pipeline.cacheAction(writing: result, into: cacheEntry), + at: 0) + } + switch processed.response { case .failure: SiestaLog.log(.cache, ["Error processing cached entity; will ignore cached value. Error:", processed]) - case .success(let entity): - return entity + case .success: + return processed } } } diff --git a/Source/Siesta/Request/NetworkRequest.swift b/Source/Siesta/Request/NetworkRequest.swift index 7f43e04b..e411cd09 100644 --- a/Source/Siesta/Request/NetworkRequest.swift +++ b/Source/Siesta/Request/NetworkRequest.swift @@ -160,7 +160,7 @@ internal final class NetworkRequestDelegate: RequestDelegate { processedInfo = processor() processedInfo.isNew = true - processedInfo.configurationSource = (self.method, self.resource) + processedInfo.configurationSource = .init(method: self.method, resource: self.resource) } else { processedInfo = rawInfo } // result from a 304 is already transformed, cached, etc. diff --git a/Source/Siesta/Request/Request.swift b/Source/Siesta/Request/Request.swift index c8c9cc14..e681bdfa 100644 --- a/Source/Siesta/Request/Request.swift +++ b/Source/Siesta/Request/Request.swift @@ -253,7 +253,7 @@ public struct ResponseInfo public var isNew: Bool /// Used to determine whether the response is suitable for caching when loaded by a particular resource - var configurationSource: (method: RequestMethod, resource: Resource)? + var configurationSource: ConfigurationSource? /// Callbacks to cache this response according to the pipeline config originally used to process it var cacheActions: [() -> Void] = [] @@ -270,4 +270,10 @@ public struct ResponseInfo response: .failure(RequestError( userMessage: NSLocalizedString("Request cancelled", comment: "userMessage"), cause: RequestError.Cause.RequestCancelled(networkError: nil)))) + + struct ConfigurationSource: Equatable + { + var method: RequestMethod + weak var resource: Resource? + } } diff --git a/Source/Siesta/Resource/Resource.swift b/Source/Siesta/Resource/Resource.swift index 3917ca69..26846cbd 100644 --- a/Source/Siesta/Resource/Resource.swift +++ b/Source/Siesta/Resource/Resource.swift @@ -472,7 +472,7 @@ public final class Resource: NSObject // TODO: explain this if let configurationSource = $0.configurationSource { - if configurationSource == (.get, self) + if configurationSource == .init(method: .get, resource: self) { for action in $0.cacheActions { action() } @@ -720,19 +720,25 @@ public final class Resource: NSObject { if case .notStarted = cacheCheckStatus { + if _latestData != nil + { + cacheCheckStatus = .completed + return + } + cacheCheckStatus = .inProgress( configuration.pipeline.checkCache(for: self) .onCompletion { [weak self] result in - guard let resource = self, resource.latestData == nil else + self?.cacheCheckStatus = .completed + + guard let resource = self, resource._latestData == nil else { SiestaLog.log(.cache, ["Ignoring cache hit for", self, " because it is either deallocated or already has data"]) return } - resource.cacheCheckStatus = .completed - if case .success(let entity) = result.response { resource.receiveNewData(entity, source: .cache) } } diff --git a/Tests/Functional/EntityCacheSpec.swift b/Tests/Functional/EntityCacheSpec.swift index 0647b373..0a5637a8 100644 --- a/Tests/Functional/EntityCacheSpec.swift +++ b/Tests/Functional/EntityCacheSpec.swift @@ -26,10 +26,16 @@ class EntityCacheSpec: ResourceSpecBase } func waitForCacheRead(_ cache: TestCache) - { expect(cache.receivedCacheRead).toEventually(beTrue()) } + { + expect(cache.receivedCacheRead).toEventually(beTrue()) + cache.receivedCacheRead = false + } func waitForCacheWrite(_ cache: TestCache) - { expect(cache.receivedCacheWrite).toEventually(beTrue()) } + { + expect(cache.receivedCacheWrite).toEventually(beTrue()) + cache.receivedCacheWrite = false + } beforeEach { @@ -244,6 +250,19 @@ class EntityCacheSpec: ResourceSpecBase expect(testCache.entries).toEventually(beEmpty()) } + it("does not write previously cached data back to the cache when reading it") + { + let testCache = TestCache("does not write previously cached") + configureCache(testCache, at: .parsing) + configureCache(UnwritableCache(), at: .model) + configureCache(UnwritableCache(), at: .cleanup) + + testCache.entries[TestCacheKey(forTestResourceIn: testCache)] = + Entity(content: "🌮", contentType: "text/plain") + awaitNewData(resource().loadIfNeeded()!, initialState: .inProgress) + expect(resource().typedContent()) == "🌮modcle" + } + it("does not cache anything for call to Resource.request() without load()") { configureCache(UnwritableCache(), at: .cleanup) @@ -277,6 +296,66 @@ class EntityCacheSpec: ResourceSpecBase let req = stubAndAwaitRequestWithoutLoading(for: otherResource, method: .get) resource().load(using: req) } + + func stubText(_ text: String) + { + NetworkStub.add( + .get, resource, + returning: HTTPResponse( + headers: ["content-type": "text/plain; charset=utf-8"], + body: text)) + } + + it("will restore cache state to an older request if passed to load(using:)") + { + let testCache = TestCache("restore cache state") + configureCache(testCache, at: .model) + service().configure + { + $0.pipeline[.decoding].removeTransformers() + $0.pipeline[.decoding].add(TextResponseTransformer()) + } + + stubText("🌮") + let originalReq = resource().load() + awaitNewData(originalReq, initialState: .inProgress) + expectCacheWrite(to: testCache, content: "🌮parmod") + + stubText("🧇") + awaitNewData(resource().load(), initialState: .inProgress) + expectCacheWrite(to: testCache, content: "🧇parmod") + + resource().load(using: originalReq) + expectCacheWrite(to: testCache, content: "🌮parmod") + } + + it("will restore cache state to original state if original cache request is passed to load(using:)") + { + let testCacheDec = TestCache("restore cache state - dec") + let testCacheCle = TestCache("restore cache state - cle") + configureCache(testCacheDec, at: .model) + configureCache(testCacheCle, at: .cleanup) + service().configure + { + $0.pipeline[.decoding].removeTransformers() + $0.pipeline[.decoding].add(TextResponseTransformer()) + } + + testCacheDec.entries[TestCacheKey(forTestResourceIn: testCacheDec)] = + Entity(content: "🌮", contentType: "text/plain") + let originalReq = resource().loadIfNeeded()! + awaitNewData(originalReq, initialState: .inProgress) + expect(resource().typedContent()) == "🌮cle" + + stubText("🧇") + awaitNewData(resource().load(), initialState: .inProgress) + expectCacheWrite(to: testCacheDec, content: "🧇parmod") + expectCacheWrite(to: testCacheCle, content: "🧇parmodcle") + + resource().load(using: originalReq) + expectCacheWrite(to: testCacheDec, content: "🌮") + expectCacheWrite(to: testCacheCle, content: "🌮cle") + } } func exerciseCache() From 17c743613fa34e50abe8b3023efdfa9f2af5f953 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Wed, 28 Oct 2020 21:22:46 -0500 Subject: [PATCH 18/25] Removed some unnecessary self. refs --- .../Siesta/Pipeline/PipelineProcessing.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index 5c5d5508..75855891 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -119,13 +119,13 @@ extension Pipeline { defaultEntityCacheWorkQueue.async { - var result = self.performCacheLookup() + var result = performCacheLookup() ?? ResponseInfo( response: .failure(RequestError( userMessage: NSLocalizedString("Cache miss", comment: "userMessage"), cause: RequestError.Cause.CacheMiss()))) - if let resource = self.resource + if let resource = resource { result.configurationSource = .init(method: .get, resource: resource) } DispatchQueue.main.async @@ -223,7 +223,7 @@ private struct CacheEntry: CacheEntryProtocol, CustomStringConvertib cache.workQueue.sync { catchAndLogErrors(attemptingTo: "read cached entity") - { try self.cache.readEntity(forKey: self.key)?.withContentRetyped() } + { try cache.readEntity(forKey: key)?.withContentRetyped() } } } @@ -237,8 +237,8 @@ private struct CacheEntry: CacheEntryProtocol, CustomStringConvertib cache.workQueue.async { - self.catchAndLogErrors(attemptingTo: "write cached entity") - { try self.cache.writeEntity(cacheableEntity, forKey: self.key) } + catchAndLogErrors(attemptingTo: "write cached entity") + { try cache.writeEntity(cacheableEntity, forKey: key) } } } @@ -246,8 +246,8 @@ private struct CacheEntry: CacheEntryProtocol, CustomStringConvertib { cache.workQueue.async { - self.catchAndLogErrors(attemptingTo: "update entity timestamp") - { try self.cache.updateEntityTimestamp(timestamp, forKey: self.key) } + catchAndLogErrors(attemptingTo: "update entity timestamp") + { try cache.updateEntityTimestamp(timestamp, forKey: key) } } } @@ -255,8 +255,8 @@ private struct CacheEntry: CacheEntryProtocol, CustomStringConvertib { cache.workQueue.async { - self.catchAndLogErrors(attemptingTo: "remove entity from cache") - { try self.cache.removeEntity(forKey: self.key) } + catchAndLogErrors(attemptingTo: "remove entity from cache") + { try cache.removeEntity(forKey: key) } } } From c07b7256ce26f5582400a6855c45f78104e69684 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 21:17:15 -0500 Subject: [PATCH 19/25] Fixed cache timestamp handling --- Source/Siesta/Resource/Resource.swift | 10 ++++----- Tests/Functional/EntityCacheSpec.swift | 29 ++++++++++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Source/Siesta/Resource/Resource.swift b/Source/Siesta/Resource/Resource.swift index 26846cbd..bc21c893 100644 --- a/Source/Siesta/Resource/Resource.swift +++ b/Source/Siesta/Resource/Resource.swift @@ -369,8 +369,8 @@ public final class Resource: NSObject if case .inProgress(let cacheRequest) = cacheCheckStatus { // isLoading needs to be: - // - false at first, - // - true after loadIfNeeded() while the cache check is in progress, but + // - false at first, even if a cache check has already started, + // - true after loadIfNeeded() while the cache check is still in progress, but // - false again before observers receive a cache hit. // // To make this happen, we need to add the chained cacheThenNetwork below @@ -390,10 +390,10 @@ public final class Resource: NSObject { _ in // We don’t need the result of the cache request here; resource state is already updated - if self.isUpToDate // If cached data is up to date... + if self.isUpToDate // If cached data is up to date... { - self.receiveDataNotModified() // ...tell observers isLoading is false... - return .useThisResponse // ...and no need to make a network call! + self.notifyObservers(.notModified) // ...tell observers isLoading is false... + return .useThisResponse // ...and no need to make a network call! } else { diff --git a/Tests/Functional/EntityCacheSpec.swift b/Tests/Functional/EntityCacheSpec.swift index 0a5637a8..61c1d4da 100644 --- a/Tests/Functional/EntityCacheSpec.swift +++ b/Tests/Functional/EntityCacheSpec.swift @@ -237,6 +237,17 @@ class EntityCacheSpec: ResourceSpecBase .toEventually(equal(2000)) } + it("preserves the timestamp of cached data") + { + let testCache = UnwritableCache(cachedValue: + Entity(content: "hi", charset: nil, headers: [:], timestamp: 2001)) + configureCache(testCache, at: .cleanup) + + setResourceTime(2010) + awaitNewData(resource().loadIfNeeded()!) + expect(resource().latestData?.timestamp) == 2001 + } + it("clears cached data on local override") { let testCache = TestCache("local override") @@ -331,9 +342,10 @@ class EntityCacheSpec: ResourceSpecBase it("will restore cache state to original state if original cache request is passed to load(using:)") { - let testCacheDec = TestCache("restore cache state - dec") + let testCacheMod = TestCache("restore cache state - mod") let testCacheCle = TestCache("restore cache state - cle") - configureCache(testCacheDec, at: .model) + configureCache(testCacheMod, at: .model) + configureCache(testCacheMod, at: .model) configureCache(testCacheCle, at: .cleanup) service().configure { @@ -341,7 +353,7 @@ class EntityCacheSpec: ResourceSpecBase $0.pipeline[.decoding].add(TextResponseTransformer()) } - testCacheDec.entries[TestCacheKey(forTestResourceIn: testCacheDec)] = + testCacheMod.entries[TestCacheKey(forTestResourceIn: testCacheMod)] = Entity(content: "🌮", contentType: "text/plain") let originalReq = resource().loadIfNeeded()! awaitNewData(originalReq, initialState: .inProgress) @@ -349,11 +361,11 @@ class EntityCacheSpec: ResourceSpecBase stubText("🧇") awaitNewData(resource().load(), initialState: .inProgress) - expectCacheWrite(to: testCacheDec, content: "🧇parmod") + expectCacheWrite(to: testCacheMod, content: "🧇parmod") expectCacheWrite(to: testCacheCle, content: "🧇parmodcle") resource().load(using: originalReq) - expectCacheWrite(to: testCacheDec, content: "🌮") + expectCacheWrite(to: testCacheMod, content: "🌮") expectCacheWrite(to: testCacheCle, content: "🌮cle") } } @@ -494,11 +506,16 @@ private class KeylessCache: EntityCache private struct UnwritableCache: EntityCache { + let cachedValue: Entity? + + init(cachedValue: Entity? = nil) + { self.cachedValue = cachedValue } + func key(for resource: Resource) -> URL? { resource.url } func readEntity(forKey key: URL) -> Entity? - { nil } + { cachedValue } func writeEntity(_ entity: Entity, forKey key: URL) { fail("cache should never be written to") } From 066baacc07c2536a2913f5afb03b22a36e25c25a Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sun, 5 Apr 2020 14:40:27 -0500 Subject: [PATCH 20/25] Rerunning a cache request needs to clear earlier cache stages --- Source/Siesta/Pipeline/PipelineProcessing.swift | 10 +++++++++- Tests/Functional/EntityCacheSpec.swift | 14 +++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index 75855891..e0a4f98b 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -13,7 +13,7 @@ extension Pipeline private var stagesInOrder: [PipelineStage] { order.compactMap { self[$0] } } - private typealias StageAndEntry = (PipelineStage, CacheEntryProtocol?) + private typealias StageAndEntry = (stage: PipelineStage, cacheEntry: CacheEntryProtocol?) private func stagesAndEntries(for resource: Resource) -> [StageAndEntry] { @@ -155,6 +155,7 @@ extension Pipeline using: stagesAndEntries.suffix(from: index + 1)) // TODO: explain this + if let cacheEntry = cacheEntry { processed.cacheActions.insert( @@ -162,6 +163,10 @@ extension Pipeline at: 0) } + processed.cacheActions.append(contentsOf: + stagesAndEntries.prefix(upTo: index) + .compactMap { $0.cacheEntry?.remove }) // Can't use keypath due to https://bugs.swift.org/browse/SR-12519 + switch processed.response { case .failure: @@ -202,6 +207,9 @@ private protocol CacheEntryProtocol func remove() } + +// MARK: Cache Entry + private struct CacheEntry: CacheEntryProtocol, CustomStringConvertible where Cache: EntityCache, Cache.Key == Key { diff --git a/Tests/Functional/EntityCacheSpec.swift b/Tests/Functional/EntityCacheSpec.swift index 61c1d4da..6487e832 100644 --- a/Tests/Functional/EntityCacheSpec.swift +++ b/Tests/Functional/EntityCacheSpec.swift @@ -342,9 +342,10 @@ class EntityCacheSpec: ResourceSpecBase it("will restore cache state to original state if original cache request is passed to load(using:)") { + let testCacheDec = TestCache("restore cache state - dec") let testCacheMod = TestCache("restore cache state - mod") let testCacheCle = TestCache("restore cache state - cle") - configureCache(testCacheMod, at: .model) + configureCache(testCacheDec, at: .decoding) configureCache(testCacheMod, at: .model) configureCache(testCacheCle, at: .cleanup) service().configure @@ -361,12 +362,16 @@ class EntityCacheSpec: ResourceSpecBase stubText("🧇") awaitNewData(resource().load(), initialState: .inProgress) + expectCacheWrite(to: testCacheDec, content: "🧇") expectCacheWrite(to: testCacheMod, content: "🧇parmod") expectCacheWrite(to: testCacheCle, content: "🧇parmodcle") resource().load(using: originalReq) expectCacheWrite(to: testCacheMod, content: "🌮") expectCacheWrite(to: testCacheCle, content: "🌮cle") + waitForCacheWrite(testCacheDec) + expect(testCacheDec.entries[TestCacheKey(forTestResourceIn: testCacheDec)]) + .toEventually(beNil()) } } @@ -437,8 +442,11 @@ private class TestCache: EntityCache func removeEntity(forKey key: TestCacheKey) { - _ = DispatchQueue.main.sync - { entries.removeValue(forKey: key) } + DispatchQueue.main.sync + { + entries.removeValue(forKey: key) + self.receivedCacheWrite = true + } } } From 1ada1be3edea8d3ea0229c613ded257a144bb9eb Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sun, 5 Apr 2020 22:22:51 -0500 Subject: [PATCH 21/25] Show stale cached data during load in example project --- Examples/GithubBrowser/Source/API/GithubAPI.swift | 3 +++ .../GithubBrowser/Source/UI/RepositoryListViewController.swift | 2 ++ .../GithubBrowser/Source/UI/RepositoryViewController.swift | 1 + Examples/GithubBrowser/Source/UI/UserViewController.swift | 2 ++ 4 files changed, 8 insertions(+) diff --git a/Examples/GithubBrowser/Source/API/GithubAPI.swift b/Examples/GithubBrowser/Source/API/GithubAPI.swift index ad47d05f..99bc94d9 100644 --- a/Examples/GithubBrowser/Source/API/GithubAPI.swift +++ b/Examples/GithubBrowser/Source/API/GithubAPI.swift @@ -83,6 +83,9 @@ class _GitHubAPI { service.configure("/search/**") { // Refresh search results after 10 seconds (Siesta default is 30) $0.expirationTime = 10 + + // Don't cache search results between runs, so we don't see stale results on launch + $0.pipeline.removeAllCaches() } // –––––– Auth configuration –––––– diff --git a/Examples/GithubBrowser/Source/UI/RepositoryListViewController.swift b/Examples/GithubBrowser/Source/UI/RepositoryListViewController.swift index a5fc1fd8..805ee237 100644 --- a/Examples/GithubBrowser/Source/UI/RepositoryListViewController.swift +++ b/Examples/GithubBrowser/Source/UI/RepositoryListViewController.swift @@ -38,7 +38,9 @@ class RepositoryListViewController: UITableViewController, ResourceObserver { super.viewDidLoad() view.backgroundColor = SiestaTheme.darkColor + statusOverlay.embed(in: self) + statusOverlay.displayPriority = [.anyData, .loading, .error] } override func viewDidLayoutSubviews() { diff --git a/Examples/GithubBrowser/Source/UI/RepositoryViewController.swift b/Examples/GithubBrowser/Source/UI/RepositoryViewController.swift index 64e56103..acfbd3db 100644 --- a/Examples/GithubBrowser/Source/UI/RepositoryViewController.swift +++ b/Examples/GithubBrowser/Source/UI/RepositoryViewController.swift @@ -76,6 +76,7 @@ class RepositoryViewController: UIViewController, ResourceObserver { super.viewDidLoad() view.backgroundColor = SiestaTheme.darkColor + statusOverlay.embed(in: self) statusOverlay.displayPriority = [.anyData, .loading, .error] // Prioritize partial data over loading indicator diff --git a/Examples/GithubBrowser/Source/UI/UserViewController.swift b/Examples/GithubBrowser/Source/UI/UserViewController.swift index 1a938185..ef7bf60f 100644 --- a/Examples/GithubBrowser/Source/UI/UserViewController.swift +++ b/Examples/GithubBrowser/Source/UI/UserViewController.swift @@ -53,6 +53,8 @@ class UserViewController: UIViewController, UISearchBarDelegate, ResourceObserve view.backgroundColor = SiestaTheme.darkColor statusOverlay.embed(in: self) + statusOverlay.displayPriority = [.anyData, .loading, .error] + showUser(nil) searchBar.becomeFirstResponder() From ff47faced913ff7ce15452f6994d9ef92905c4d8 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 21:19:59 -0500 Subject: [PATCH 22/25] Tidied up cache-related logging --- .../Siesta/Pipeline/PipelineProcessing.swift | 5 ++++- Source/Siesta/Request/LiveRequest.swift | 21 +++++++++++++++++-- Source/Siesta/Request/NetworkRequest.swift | 2 +- Source/Siesta/Request/RequestChaining.swift | 6 ++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Source/Siesta/Pipeline/PipelineProcessing.swift b/Source/Siesta/Pipeline/PipelineProcessing.swift index e0a4f98b..2aece5b9 100644 --- a/Source/Siesta/Pipeline/PipelineProcessing.swift +++ b/Source/Siesta/Pipeline/PipelineProcessing.swift @@ -110,7 +110,7 @@ extension Pipeline init(for resource: Resource, searching stagesAndEntries: [StageAndEntry]) { - requestDescription = "Cache request for \(resource)" + requestDescription = "Cache check for \(resource)" self.resource = resource self.stagesAndEntries = stagesAndEntries } @@ -179,6 +179,9 @@ extension Pipeline } return nil } + + var logCategory: SiestaLog.Category? + { .cache } } } diff --git a/Source/Siesta/Request/LiveRequest.swift b/Source/Siesta/Request/LiveRequest.swift index f562f345..3ab779ab 100644 --- a/Source/Siesta/Request/LiveRequest.swift +++ b/Source/Siesta/Request/LiveRequest.swift @@ -92,6 +92,11 @@ public protocol RequestDelegate A description of the underlying operation suitable for logging and debugging. */ var requestDescription: String { get } + + /** + Indicates where information about requests using this delegate should be logged. + */ + var logCategory: SiestaLog.Category? { get } } extension RequestDelegate @@ -107,6 +112,12 @@ extension RequestDelegate */ public var progressReportingInterval: TimeInterval { 0.05 } + + /** + Log info to `SiestaLog.Category.network`. + */ + public var logCategory: SiestaLog.Category? + { .network } } /** @@ -164,7 +175,7 @@ private final class LiveRequest: Request, RequestCompletionHandler, CustomDebugS return self } - SiestaLog.log(.network, [delegate.requestDescription]) + logDelegateStateChange([delegate.requestDescription]) underlyingOperationStarted = true delegate.startUnderlyingOperation(passingResponseTo: self) @@ -186,7 +197,7 @@ private final class LiveRequest: Request, RequestCompletionHandler, CustomDebugS return } - SiestaLog.log(.network, ["Cancelled", delegate.requestDescription]) + logDelegateStateChange(["Cancelled", delegate.requestDescription]) delegate.cancelUnderlyingOperation() @@ -274,6 +285,12 @@ private final class LiveRequest: Request, RequestCompletionHandler, CustomDebugS // MARK: Debug + func logDelegateStateChange(_ messageParts: @autoclosure () -> [Any?]) + { + if let category = delegate.logCategory + { SiestaLog.log(category, messageParts()) } + } + final var debugDescription: String { "Request:" diff --git a/Source/Siesta/Request/NetworkRequest.swift b/Source/Siesta/Request/NetworkRequest.swift index e411cd09..ba355483 100644 --- a/Source/Siesta/Request/NetworkRequest.swift +++ b/Source/Siesta/Request/NetworkRequest.swift @@ -94,7 +94,7 @@ internal final class NetworkRequestDelegate: RequestDelegate { DispatchQueue.mainThreadPrecondition() - SiestaLog.log(.network, ["Response: ", underlyingResponse?.statusCode ?? error, "←", requestDescription]) + SiestaLog.log(.network, ["Response:", underlyingResponse?.statusCode ?? error, "←", requestDescription]) SiestaLog.log(.networkDetails, ["Raw response headers:", underlyingResponse?.allHeaderFields]) SiestaLog.log(.networkDetails, ["Raw response body:", body?.count ?? 0, "bytes"]) diff --git a/Source/Siesta/Request/RequestChaining.swift b/Source/Siesta/Request/RequestChaining.swift index c9558b3d..0da605b4 100644 --- a/Source/Siesta/Request/RequestChaining.swift +++ b/Source/Siesta/Request/RequestChaining.swift @@ -135,4 +135,10 @@ internal struct RequestChainDelgate: RequestDelegate { RequestChainDelgate(wrapping: wrappedRequest.repeated(), whenCompleted: determineAction) } + + /** + Chain requests are silent since their underlying requests are logged already. + */ + var logCategory: SiestaLog.Category? + { nil } } From 8e2d8ba550e630b5f1039d65ec0cd62b36a79da0 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Tue, 7 Apr 2020 22:09:32 -0500 Subject: [PATCH 23/25] Oops, where did example project's scheme go? --- .../xcschemes/GithubBrowser.xcscheme | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Examples/GithubBrowser/GithubBrowser.xcodeproj/xcshareddata/xcschemes/GithubBrowser.xcscheme diff --git a/Examples/GithubBrowser/GithubBrowser.xcodeproj/xcshareddata/xcschemes/GithubBrowser.xcscheme b/Examples/GithubBrowser/GithubBrowser.xcodeproj/xcshareddata/xcschemes/GithubBrowser.xcscheme new file mode 100644 index 00000000..aca962d4 --- /dev/null +++ b/Examples/GithubBrowser/GithubBrowser.xcodeproj/xcshareddata/xcschemes/GithubBrowser.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e4f8ab4fc0d21c89a13aee37383fdd3c7aa4b0ab Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Tue, 7 Apr 2020 23:09:51 -0500 Subject: [PATCH 24/25] Added SiestaTools to SwiftPM manifest & set up for testing --- Package.swift | 6 +++++- Siesta.xcodeproj/project.pbxproj | 14 ++++++++++++++ Source/SiestaTools/FileCache.swift | 1 + Tests/Functional/FileCacheSpec.swift | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Tests/Functional/FileCacheSpec.swift diff --git a/Package.swift b/Package.swift index 912c5df0..42036fe1 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,10 @@ let package = Package( .target( name: "Siesta" ), + .target( + name: "SiestaTools", + dependencies: ["Siesta"] + ), .target( name: "SiestaUI", dependencies: ["Siesta"], @@ -42,7 +46,7 @@ let package = Package( ), .testTarget( name: "SiestaTests", - dependencies: ["SiestaUI", "Siesta_Alamofire", "Quick", "Nimble"], + dependencies: ["SiestaUI", "SiestaTools", "Siesta_Alamofire", "Quick", "Nimble"], path: "Tests/Functional", exclude: ["ObjcCompatibilitySpec.m"] // SwiftPM currently only supports Swift ), diff --git a/Siesta.xcodeproj/project.pbxproj b/Siesta.xcodeproj/project.pbxproj index dd1a1b65..7fd6db1c 100644 --- a/Siesta.xcodeproj/project.pbxproj +++ b/Siesta.xcodeproj/project.pbxproj @@ -144,6 +144,12 @@ DA34E2C9222BAAE70025A77A /* Optional+Siesta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA34E2C7222BA8650025A77A /* Optional+Siesta.swift */; }; DA34E2CA222BAAEA0025A77A /* Optional+Siesta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA34E2C7222BA8650025A77A /* Optional+Siesta.swift */; }; DA34E2CB222BAAEB0025A77A /* Optional+Siesta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA34E2C7222BA8650025A77A /* Optional+Siesta.swift */; }; + DA3E165F243C24A3001F7CCA /* FileCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3E165E243C24A3001F7CCA /* FileCacheSpec.swift */; }; + DA3E1660243C24A3001F7CCA /* FileCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3E165E243C24A3001F7CCA /* FileCacheSpec.swift */; }; + DA3E1661243C24A3001F7CCA /* FileCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3E165E243C24A3001F7CCA /* FileCacheSpec.swift */; }; + DA3E1662243D6D00001F7CCA /* SiestaTools.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA8B5B5A22DEAC93008E47B0 /* SiestaTools.framework */; }; + DA3E1663243D6D0E001F7CCA /* SiestaTools.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA8C83521FC600A900C947F9 /* SiestaTools.framework */; }; + DA3E1664243D6D18001F7CCA /* SiestaTools.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA8B5B6922DEADDB008E47B0 /* SiestaTools.framework */; }; DA3FBD9B1B55917600161A25 /* Siesta-ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3FBD9A1B55917600161A25 /* Siesta-ObjC.swift */; }; DA3FBDB11B55BF0E00161A25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3FBDB01B55BF0E00161A25 /* Logging.swift */; }; DA3FD66C1D0DF5DE00C75742 /* PipelineConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DACE0BAD1D0201F800607F3E /* PipelineConfiguration.swift */; }; @@ -439,6 +445,7 @@ DA336E601B2E6DDB006F702A /* Info-iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; DA336E701B2F6659006F702A /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; DA34E2C7222BA8650025A77A /* Optional+Siesta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Siesta.swift"; sourceTree = ""; }; + DA3E165E243C24A3001F7CCA /* FileCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCacheSpec.swift; sourceTree = ""; }; DA3FBD9A1B55917600161A25 /* Siesta-ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Siesta-ObjC.swift"; sourceTree = ""; }; DA3FBDB01B55BF0E00161A25 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; DA4353511B6AD63C00543843 /* Networking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; @@ -509,6 +516,7 @@ buildActionMask = 2147483647; files = ( 9F0FAFB01D033FF400CE1B61 /* Siesta.framework in Frameworks */, + DA3E1662243D6D00001F7CCA /* SiestaTools.framework in Frameworks */, DAC4CFCE1DAC10FD00EECEDE /* SiestaUI.framework in Frameworks */, DAF2B7A827189B5A00E5B31A /* Quick in Frameworks */, DAF2B7A627189B5A00E5B31A /* Alamofire in Frameworks */, @@ -542,6 +550,7 @@ buildActionMask = 2147483647; files = ( CDCCAF7B1E6A31DA00860D18 /* Siesta.framework in Frameworks */, + DA3E1664243D6D18001F7CCA /* SiestaTools.framework in Frameworks */, DA5E4B5B22DCF0BB0059ED10 /* SiestaUI.framework in Frameworks */, DAA9102427239BDE00B2211D /* Nimble in Frameworks */, DAA9102027239BDE00B2211D /* Alamofire in Frameworks */, @@ -569,6 +578,7 @@ buildActionMask = 2147483647; files = ( DA336E5A1B2E6DDB006F702A /* Siesta.framework in Frameworks */, + DA3E1663243D6D0E001F7CCA /* SiestaTools.framework in Frameworks */, DAC4CFCF1DAC110500EECEDE /* SiestaUI.framework in Frameworks */, DAA9101E27239BD700B2211D /* Nimble in Frameworks */, DAA9101A27239BD700B2211D /* Alamofire in Frameworks */, @@ -725,6 +735,7 @@ DA9F4BDD1B3F8E2E00E8966F /* WeakCacheSpec.swift */, DA4D61971B751FEE00F6BB9C /* ObjcCompatibilitySpec.m */, DA99B5C71B38C36A009C6937 /* Support */, + DA3E165E243C24A3001F7CCA /* FileCacheSpec.swift */, ); path = Functional; sourceTree = ""; @@ -1501,6 +1512,7 @@ DA788A8F1D6AC1590085C820 /* ObjcCompatibilitySpec.m in Sources */, 9F0FAF991D033FCD00CE1B61 /* TestService.swift in Sources */, 9F0FAF9A1D033FCD00CE1B61 /* SiestaSpec.swift in Sources */, + DA3E1660243C24A3001F7CCA /* FileCacheSpec.swift in Sources */, DA5A90312054E0C500309D8B /* EntityCacheSpec.swift in Sources */, 9F0FAF9B1D033FCD00CE1B61 /* ResourcePathsSpec.swift in Sources */, DA7285E71D0DF46600132CD9 /* PipelineSpec.swift in Sources */, @@ -1650,6 +1662,7 @@ CD5651F71E6F306B009F224A /* ResponseDataHandlingSpec.swift in Sources */, CD5651F21E6F306B009F224A /* ResourcePathsSpec.swift in Sources */, CD5651F81E6F306B009F224A /* ProgressSpec.swift in Sources */, + DA3E1661243C24A3001F7CCA /* FileCacheSpec.swift in Sources */, DA5E4B5C22DCF1260059ED10 /* RemoteImageViewSpec.swift in Sources */, DA62C9862428670500398674 /* NetworkStub.swift in Sources */, CD5651F11E6F306B009F224A /* ResourceSpecBase.swift in Sources */, @@ -1728,6 +1741,7 @@ DA2FF1061C0F97F600C98FF1 /* Networking-Alamofire.swift in Sources */, DA8EF6D11BC20AFE002175EB /* ProgressSpec.swift in Sources */, DAC0CE6D1B4A3ADA004FBB4B /* ResourceObserversSpec.swift in Sources */, + DA3E165F243C24A3001F7CCA /* FileCacheSpec.swift in Sources */, DA49EA801B36174700AE1B8F /* SpecHelpers.swift in Sources */, DA62C9842428670400398674 /* NetworkStub.swift in Sources */, DA9F4BDE1B3F8E2E00E8966F /* WeakCacheSpec.swift in Sources */, diff --git a/Source/SiestaTools/FileCache.swift b/Source/SiestaTools/FileCache.swift index 49b80208..9cef7d15 100644 --- a/Source/SiestaTools/FileCache.swift +++ b/Source/SiestaTools/FileCache.swift @@ -9,6 +9,7 @@ #if !COCOAPODS import Siesta #endif +import Foundation import CommonCrypto private typealias File = URL diff --git a/Tests/Functional/FileCacheSpec.swift b/Tests/Functional/FileCacheSpec.swift new file mode 100644 index 00000000..f98f117a --- /dev/null +++ b/Tests/Functional/FileCacheSpec.swift @@ -0,0 +1,24 @@ +// +// FileCacheSpec.swift +// Siesta +// +// Created by Paul on 2020/4/6. +// Copyright © 2020 Bust Out Solutions. All rights reserved. +// + +import Siesta +import SiestaTools + +import Foundation +import Quick +import Nimble + +class FileCacheSpec: ResourceSpecBase + { + override func resourceSpec(_ service: @escaping () -> Service, _ resource: @escaping () -> Resource) + { + it("needs testing") + { + } + } + } From 63baa8e22b441ae19fd187190228314cb0f39ff7 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Sat, 23 Oct 2021 21:46:45 -0500 Subject: [PATCH 25/25] Consistent rules for which branches CI runs on --- .github/workflows/swiftpm.yml | 4 ++-- .github/workflows/xcode.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/swiftpm.yml b/.github/workflows/swiftpm.yml index f8272297..26515a14 100644 --- a/.github/workflows/swiftpm.yml +++ b/.github/workflows/swiftpm.yml @@ -2,9 +2,9 @@ name: SwiftPM regression tests on: push: - branches: [ master ] + branches: [ main, ci-experiments ] pull_request: - branches: [ master ] + branches: [ main ] jobs: test: diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml index 3760d146..dcf58c56 100644 --- a/.github/workflows/xcode.yml +++ b/.github/workflows/xcode.yml @@ -2,9 +2,9 @@ name: Xcode regression tests on: push: - branches: [ $default-branch, main, master, ci-experiments ] + branches: [ main, ci-experiments ] pull_request: - branches: [ $default-branch, main, master ] + branches: [ main ] jobs: test: