Skip to content

Commit

Permalink
Updates ServiceCache to be an actor
Browse files Browse the repository at this point in the history
  • Loading branch information
atljeremy committed Aug 7, 2024
1 parent 10697d3 commit 34b9ba3
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 40 deletions.
80 changes: 65 additions & 15 deletions HTTPService.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -375,8 +375,9 @@
3F43765B1AD1A14100FFC40C /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1030;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1540;
ORGANIZATIONNAME = "Jeremy Fox";
TargetAttributes = {
1988FC6222FA0AE6000F70B3 = {
Expand Down Expand Up @@ -537,18 +538,25 @@
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
FRAMEWORK_VERSION = A;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "HTTPService macOS/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.HTTPService-macOS";
Expand All @@ -573,18 +581,25 @@
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
FRAMEWORK_VERSION = A;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "HTTPService macOS/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.HTTPService-macOS";
PRODUCT_NAME = HTTPService;
Expand All @@ -608,12 +623,17 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ERTW5K73R4;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "HTTPService macOSTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.14;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.HTTPService-macOSTests";
Expand All @@ -637,12 +657,17 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ERTW5K73R4;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "HTTPService macOSTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.14;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.HTTPService-macOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -685,6 +710,7 @@
CURRENT_PROJECT_VERSION = 1;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand Down Expand Up @@ -745,6 +771,7 @@
CURRENT_PROJECT_VERSION = 1;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -756,7 +783,8 @@
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
Expand All @@ -767,16 +795,22 @@
3F43767B1AD1A14100FFC40C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
CODE_SIGN_IDENTITY = "";
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = HTTPService/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -787,16 +821,22 @@
3F43767C1AD1A14100FFC40C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
CODE_SIGN_IDENTITY = "";
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = HTTPService/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -814,7 +854,12 @@
"$(inherited)",
);
INFOPLIST_FILE = HTTPServiceTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "HTTPServiceTests/HTTPServiceTests-Bridging-Header.h";
Expand All @@ -829,7 +874,12 @@
CLANG_ENABLE_MODULES = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = HTTPServiceTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "HTTPServiceTests/HTTPServiceTests-Bridging-Header.h";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1540"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1540"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
7 changes: 7 additions & 0 deletions HTTPService/HTTPService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,11 @@ extension HTTPService {
return .failure(.requestFailed(error.localizedDescription))
}
}

@discardableResult
public func executeWithCancelation<T>(request: T) -> Task<HTTPResult<T.ResultType>, Never> where T : HTTPUploadRequest {
return Task {
await execute(request: request)
}
}
}
16 changes: 8 additions & 8 deletions HTTPService/ServiceBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,40 @@
import Foundation

public class ServiceBuilder<T: HTTPService> {
public static func purgeCache() {
public static func purgeCache() async {
let key = String(describing: T.self)
ServiceCache.shared.delete(key: key)
await ServiceCache.shared.delete(key: key)
}

public static func build(ignoringCache ignoreCache: Bool = false) -> T? {
public static func build(ignoringCache ignoreCache: Bool = false) async -> T? {
let key = String(describing: T.self)
let cachedService: T? = ServiceCache.shared.get(key: key)
let cachedService: T? = await ServiceCache.shared.get(key: key)
guard ignoreCache || cachedService == nil else {
return cachedService!
}

let service = T.Builder.build()
if let service = service {
ServiceCache.shared.set(service: service, for: key)
await ServiceCache.shared.set(service: service, for: key)
}

return service as? T
}
}

fileprivate struct ServiceCache {
fileprivate actor ServiceCache {
static var shared = ServiceCache()
private var cache = [String: Any]()

func get<T: HTTPService>(key: String) -> T? {
return cache[key] as? T
}

mutating func set<T: HTTPService>(service: T, for key: String) {
func set<T: HTTPService>(service: T, for key: String) {
cache[key] = service
}

mutating func delete(key: String) {
func delete(key: String) {
cache.removeValue(forKey: key)
}
}
10 changes: 8 additions & 2 deletions HTTPServiceTests/HTTPServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ import HTTPService
class HTTPServiceTests: XCTestCase {

func testServiceExecutesRequestAndReturnsParsedResultType() {
let service = ServiceBuilder<GitHubService>.build()
service?.execute(request: GitHubGetPullRequest(id: "123")) { (result) in
let expectation = expectation(description: "Async function completes")

Task {
let service = await ServiceBuilder<GitHubService>.build()!
let result = await service.execute(request: GitHubGetPullRequest(id: "123"))
switch result {
case let .success(pr):
XCTAssertTrue(pr?.name == "PR Name")
case .failure(_):
XCTFail()
}
expectation.fulfill()
}

waitForExpectations(timeout: 5)
}

}
44 changes: 36 additions & 8 deletions HTTPServiceTests/ServiceBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,47 @@ import HTTPService
class ServiceBuilderTests: XCTestCase {

func testReturnsCachedService() {
let ghService1 = ServiceBuilder<GitHubService>.build()
let ghService2 = ServiceBuilder<GitHubService>.build()
XCTAssertEqual(ghService1, ghService2)
let expectation = expectation(description: "Async")

Task {
let ghService1 = await ServiceBuilder<GitHubService>.build()
let ghService2 = await ServiceBuilder<GitHubService>.build()
XCTAssertEqual(ghService1, ghService2)
expectation.fulfill()
}

waitForExpectations(timeout: 5)
}

func testIgnoreingCacheDoesntReturnCachedService() {
let expectation = expectation(description: "Async")

Task {
let ghService1 = await ServiceBuilder<GitHubService>.build()
let ghService2 = await ServiceBuilder<GitHubService>.build(ignoringCache: true)
XCTAssertNotEqual(ghService1, ghService2)
expectation.fulfill()
}

waitForExpectations(timeout: 5)
}

func testPurgeRemovesCachedService() {
let ghService1 = ServiceBuilder<GitHubService>.build()
XCTAssertNotNil(ghService1)
let expectation = expectation(description: "Async")

ServiceBuilder<GitHubService>.purgeCache()
Task {
let ghService1 = await ServiceBuilder<GitHubService>.build()
XCTAssertNotNil(ghService1)

await ServiceBuilder<GitHubService>.purgeCache()

let ghService2 = await ServiceBuilder<GitHubService>.build()
XCTAssertNotEqual(ghService1, ghService2)

expectation.fulfill()
}

let ghService2 = ServiceBuilder<GitHubService>.build()
XCTAssertNotEqual(ghService1, ghService2)
waitForExpectations(timeout: 5)
}

}
8 changes: 3 additions & 5 deletions HTTPServiceTests/Test Service/GitHubService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,14 @@ extension GitHubService: HTTPServiceBuildable {

extension GitHubService {
@discardableResult
func execute<T>(request: T, handler: @escaping (HTTPResult<T.ResultType>) -> Void) -> URLSessionTask where T : HTTPRequest {
func execute<T>(request: T) async -> HTTPResult<T.ResultType> where T : HTTPRequest {
do {
let data = try JSONSerialization.data(withJSONObject: ["id": 123, "name": "PR Name"], options: .init(rawValue: 0))
let pr = try JSONDecoder().decode(T.ResultType.self, from: data)
handler(.success(pr))
return .success(pr)
} catch _ {
handler(.failure(.emptyResponseData("")))
return .failure(.emptyResponseData(""))
}

return URLSessionDataTask()
}
}

Expand Down

0 comments on commit 34b9ba3

Please sign in to comment.