Skip to content

Commit

Permalink
Merge pull request #3 from crontab/new-map
Browse files Browse the repository at this point in the history
Bring back MultiplexerMap
  • Loading branch information
crontab authored Jul 2, 2024
2 parents cc57b5f + 32c99d3 commit 8cfd549
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 111 deletions.
18 changes: 15 additions & 3 deletions AsyncMux/AsyncMux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
3635173029746D2700F9B045 /* AsyncMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3635172829746D2600F9B045 /* AsyncMedia.swift */; };
3635173129746D2700F9B045 /* MuxCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3635172929746D2600F9B045 /* MuxCacher.swift */; };
3635173229746D2700F9B045 /* MuxRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3635172A29746D2600F9B045 /* MuxRepository.swift */; };
36CD20042C340DFA000C31FF /* MultieplexerMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36CD20032C340DFA000C31FF /* MultieplexerMap.swift */; };
36E20F2F298215640022D151 /* Multiplexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E20F2E298215640022D151 /* Multiplexer.swift */; };
/* End PBXBuildFile section */

Expand All @@ -24,6 +25,7 @@
3635172829746D2600F9B045 /* AsyncMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncMedia.swift; sourceTree = "<group>"; };
3635172929746D2600F9B045 /* MuxCacher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuxCacher.swift; sourceTree = "<group>"; };
3635172A29746D2600F9B045 /* MuxRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuxRepository.swift; sourceTree = "<group>"; };
36CD20032C340DFA000C31FF /* MultieplexerMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultieplexerMap.swift; sourceTree = "<group>"; };
36E20F2E298215640022D151 /* Multiplexer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Multiplexer.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -59,6 +61,7 @@
isa = PBXGroup;
children = (
36E20F2E298215640022D151 /* Multiplexer.swift */,
36CD20032C340DFA000C31FF /* MultieplexerMap.swift */,
3635172829746D2600F9B045 /* AsyncMedia.swift */,
3635172A29746D2600F9B045 /* MuxRepository.swift */,
3635172929746D2600F9B045 /* MuxCacher.swift */,
Expand Down Expand Up @@ -108,7 +111,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastUpgradeCheck = 1420;
LastUpgradeCheck = 1540;
TargetAttributes = {
362CC58929746BE900059933 = {
CreatedOnToolsVersion = 14.2;
Expand Down Expand Up @@ -153,6 +156,7 @@
3635172F29746D2700F9B045 /* AsyncUtils.swift in Sources */,
3635172D29746D2700F9B045 /* AsyncError.swift in Sources */,
3635173129746D2700F9B045 /* MuxCacher.swift in Sources */,
36CD20042C340DFA000C31FF /* MultieplexerMap.swift in Sources */,
3635173029746D2700F9B045 /* AsyncMedia.swift in Sources */,
36E20F2F298215640022D151 /* Multiplexer.swift in Sources */,
);
Expand Down Expand Up @@ -198,6 +202,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand All @@ -212,7 +217,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -262,6 +267,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -270,7 +276,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
Expand All @@ -287,13 +293,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 639KEQWW6F;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
Expand All @@ -305,6 +313,7 @@
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20";
PRODUCT_BUNDLE_IDENTIFIER = com.melikyan.AsyncMux;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
Expand All @@ -322,13 +331,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 639KEQWW6F;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
Expand All @@ -340,6 +351,7 @@
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20";
PRODUCT_BUNDLE_IDENTIFIER = com.melikyan.AsyncMux;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
Expand Down
106 changes: 106 additions & 0 deletions AsyncMux/Sources/MultieplexerMap.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// MultieplexerMap.swift
// AsyncMux
//
// Created by Hovik Melikyan on 02.07.24.
// Copyright © 2023 Hovik Melikyan. All rights reserved.
//

import Foundation


public typealias MuxKey = LosslessStringConvertible & Hashable & Sendable


// MARK: - MultieplexerMap

///
/// `MultiplexerMap<K, T>` is similar to `Multiplexer<T>` in many ways except it maintains a dictionary of objects of the same type. One example would be e.g. user profile objects in your social app.
/// The `K` generic paramter should conform to `LosslessStringConvertible & Hashable & Sendable`. The string convertibility requirement is because it simplifies the disk cacher's job of storing objects on disk or a database.
/// See README.md for a more detailed discussion.
///
@MuxActor
public final class MultiplexerMap<K: MuxKey, T: Codable & Sendable>: MuxRepositoryProtocol {

public typealias OnFetch = @Sendable (K) async throws -> T

public let cacheKey: String

/// Instantiates a `MultiplexerMap<T>` object with a given `onFetch` block.
/// - parameter cacheKey (optional): a string to be used as a file name for the disk cache. If omitted, an automatic name is generated based on `T`'s description. NOTE: if you have several multiplexers whose `T` is the same, you *should* define unique non-conflicting `cacheKey` parameters for each.
/// - parameter onFetch: an async throwing block that should retrieve an object by a given key presumably in an asynchronous manner.
nonisolated
public init(cacheKey: String? = nil, onFetch: @escaping OnFetch) {
self.cacheKey = cacheKey ?? String(describing: T.self)
self.onFetch = onFetch
}

/// Performs a request either by calling the `onFetch` block supplied in the multiplexer's constructor, or by returning the previously cached object, if available. Multiple simultaneous calls to `request(key:)` are handled by the MultiplexerMap so that only one `onFetch` operation is invoked at a time for any given `key`, but all callers of `request(key:)` will eventually receive the result.
public func request(key: K) async throws -> T {
let mux = muxMap[key] ?? {
let onFetch = onFetch // avoid capture of `self`
let mux = Multiplexer {
try await onFetch(key)
}
muxMap[key] = mux
return mux
}()
return try await mux.request(domain: cacheKey, key: key)
}

/// "Soft" refresh: the next call to `request(key:)` will attempt to retrieve the object again, without discarding the caches in case of a failure. `refresh(key:)` does not have an immediate effect on any ongoing asynchronous requests. Can be chained with the subsequent `request(key:)`.
@discardableResult
public func refresh(_ flag: Bool = true, key: K) -> Self {
if flag {
muxMap[key]?.refreshFlag = true
}
return self
}

/// "Soft" refresh for all objects stored in this `MultiplexerMap`: the next call to `request(key:)` with any key will attempt to retrieve the object again, without discarding the caches in case of a failure. `refresh()` does not have an immediate effect on any ongoing asynchronous requests. Can be chained with the subsequent `request(key:)`.
@discardableResult
public func refresh(_ flag: Bool = true) -> Self {
if flag {
muxMap.values.forEach {
$0.refreshFlag = true
}
}
return self
}

/// Writes all previously cached objects in this `MultiplexerMap` to disk.
public func save() {
muxMap.forEach { key, mux in
if mux.isDirty, let storedValue = mux.storedValue {
MuxCacher.save(storedValue, domain: cacheKey, key: String(key))
mux.isDirty = false
}
}
}

public func clearMemory(key: K) {
muxMap.removeValue(forKey: key)
}

public func clearMemory() {
muxMap = [:]
}

/// Clears the memory and disk caches for an object with a given `key`. Will trigger a full fetch on the next `request(key:)` call.
public func clear(key: K) {
MuxCacher.delete(domain: cacheKey, key: String(key))
clearMemory(key: key)
}

/// Clears the memory and disk caches all objects in this `MultiplexerMap`. Will trigger a full fetch on the next `request(key:)` call.
public func clear() {
MuxCacher.deleteDomain(cacheKey)
clearMemory()
}


// Private part

private let onFetch: OnFetch
private var muxMap: [K: Multiplexer<T>] = [:]
}
21 changes: 13 additions & 8 deletions AsyncMux/Sources/Multiplexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import Foundation
private let defaultTTL: TimeInterval = 30 * 60
private let muxRootDomain = "_Root.Domain"

@globalActor
public actor MuxActor {
public static var shared = MuxActor()
}


// MARK: - Multiplexer

Expand All @@ -20,7 +25,8 @@ private let muxRootDomain = "_Root.Domain"
/// For each multiplexer singleton you define a block that implements asynchronous retrieval of the object, which in your app will likely be a network request, e.g. to your backend system.
/// See README.md for a more detailed discussion.
///
public actor Multiplexer<T: Codable & Sendable>: MuxRepositoryProtocol {
@MuxActor
public final class Multiplexer<T: Codable & Sendable>: MuxRepositoryProtocol {

public typealias OnFetch = @Sendable () async throws -> T

Expand All @@ -29,16 +35,15 @@ public actor Multiplexer<T: Codable & Sendable>: MuxRepositoryProtocol {
/// Instantiates a `Multiplexer<T>` object with a given `onFetch` block.
/// - parameter cacheKey (optional): a string to be used as a file name for the disk cache. If omitted, an automatic name is generated based on `T`'s description. NOTE: if you have several multiplexers whose `T` is the same, you *should* define unique non-conflicting `cacheKey` parameters for each.
/// - parameter onFetch: an async throwing block that should retrieve an object presumably in an asynchronous manner.
nonisolated
public init(cacheKey: String? = nil, onFetch: @escaping OnFetch) {
self.cacheKey = cacheKey ?? String(describing: T.self)
self.onFetch = onFetch
}

/// Performs a request either by calling the `onFetch` block supplied in the multiplexer's constructor, or by returning the previously cached object, if available. Multiple simultaneous calls to `request()` are handled by the Multiplexer so that only one `onFetch` operation can be invoked at a time, but all callers of `request()` will eventually receive the result.
public func request() async throws -> T {
return try await request(domain: muxRootDomain, key: cacheKey) { [self] in
try await onFetch()
}
return try await request(domain: muxRootDomain, key: cacheKey)
}

/// "Soft" refresh: the next call to `request()` will attempt to retrieve the object again, without discarding the caches in case of a failure. `refresh()` does not have an immediate effect on any ongoing asynchronous requests. Can be chained with the subsequent `request()`.
Expand Down Expand Up @@ -74,14 +79,14 @@ public actor Multiplexer<T: Codable & Sendable>: MuxRepositoryProtocol {

private let onFetch: OnFetch

private var storedValue: T?
private var isDirty: Bool = false
private var refreshFlag: Bool = false
internal var storedValue: T?
internal var isDirty: Bool = false
internal var refreshFlag: Bool = false

private var task: Task<T, Error>?
private var completionTime: TimeInterval = 0

private func request(domain: String, key: String, onFetch: @escaping OnFetch) async throws -> T {
internal func request(domain: String, key: LosslessStringConvertible) async throws -> T {
if !refreshFlag, !isExpired {
if let storedValue {
return storedValue
Expand Down
10 changes: 5 additions & 5 deletions AsyncMux/Sources/MuxCacher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@ import Foundation

public class MuxCacher {

public static func load<T: Decodable>(domain: String, key: String, type: T.Type) -> T? {
public static func load<T: Decodable>(domain: String, key: LosslessStringConvertible, type: T.Type) -> T? {
return try? JSONDecoder().decode(type, from: Data(contentsOf: cacheFileURL(domain: domain, key: key, create: false)))
}

public static func save<T: Encodable>(_ result: T, domain: String, key: String) {
public static func save<T: Encodable>(_ result: T, domain: String, key: LosslessStringConvertible) {
try! JSONEncoder().encode(result).write(to: cacheFileURL(domain: domain, key: key, create: true), options: .atomic)
}

public static func delete(domain: String, key: String) {
public static func delete(domain: String, key: LosslessStringConvertible) {
try? FileManager.default.removeItem(at: cacheFileURL(domain: domain, key: key, create: false))
}

public static func deleteDomain(_ domain: String) {
try? FileManager.default.removeItem(at: cacheDirURL(domain: domain, create: false))
}

private static func cacheFileURL(domain: String, key: String, create: Bool) -> URL {
return cacheDirURL(domain: domain, create: create).appendingPathComponent(key).appendingPathExtension("json")
private static func cacheFileURL(domain: String, key: LosslessStringConvertible, create: Bool) -> URL {
return cacheDirURL(domain: domain, create: create).appendingPathComponent(String(key)).appendingPathExtension("json")
}

private static func cacheDirURL(domain: String, create: Bool) -> URL {
Expand Down
4 changes: 2 additions & 2 deletions AsyncMuxDemo/AsyncMuxDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -331,7 +331,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
Expand Down
Loading

0 comments on commit 8cfd549

Please sign in to comment.