diff --git a/.gitignore b/.gitignore index 6f00659..7674fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,17 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore -## Build generated +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ - -## Various settings +*.moved-aside *.pbxuser !default.pbxuser *.mode1v3 @@ -15,15 +21,11 @@ DerivedData/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ - -## Other -*.moved-aside -*.xccheckout -*.xcscmblueprint ## Obj-C/Swift specific *.hmap + +## App packaging *.ipa *.dSYM.zip *.dSYM @@ -38,11 +40,13 @@ playground.xcworkspace # Packages/ # Package.pins # Package.resolved -.DS_Store -/.build -/Packages -/*.xcodeproj +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm +.build/ # CocoaPods # @@ -51,18 +55,25 @@ playground.xcworkspace # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts -Carthage/Build +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ # fastlane # -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control @@ -70,3 +81,10 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ \ No newline at end of file diff --git a/Package.swift b/Package.swift index 4a4eb4c..b4b3781 100644 --- a/Package.swift +++ b/Package.swift @@ -4,16 +4,16 @@ import PackageDescription let package = Package( - name: "SwiftDataLoader", + name: "DataLoader", products: [ - .library(name: "SwiftDataLoader", targets: ["SwiftDataLoader"]), + .library(name: "DataLoader", targets: ["DataLoader"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), ], targets: [ - .target(name: "SwiftDataLoader", dependencies: ["NIO"]), - .testTarget(name: "SwiftDataLoaderTests", dependencies: ["SwiftDataLoader"]), + .target(name: "DataLoader", dependencies: ["NIO"]), + .testTarget(name: "DataLoaderTests", dependencies: ["DataLoader"]), ], swiftLanguageVersions: [.v5] ) diff --git a/README.md b/README.md index 9c7e769..f751d5b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# SwiftDataLoader -SwiftDataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching. +# DataLoader +DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching. This is a Swift version of the Facebook [DataLoader](https://github.com/facebook/dataloader). -[![CircleCI](https://circleci.com/gh/kimdv/SwiftDataLoader.svg?style=svg)](https://circleci.com/gh/kimdv/SwiftDataLoader) -[![codecov](https://codecov.io/gh/kimdv/SwiftDataLoader/branch/master/graph/badge.svg)](https://codecov.io/gh/kimdv/SwiftDataLoader) +[![Swift][swift-badge]][swift-url] +[![License][mit-badge]][mit-url] ## Installation 💻 Update your `Package.swift` file. ```swift -.Package(url: "https://github.com/kimdv/SwiftDataLoader.git", majorVersion: 1) +.Package(url: "https://github.com/GraphQLSwift/DataLoader.git", .upToNextMajor(from: "1.1.0")) ``` ## Gettings started 🚀 @@ -27,10 +27,10 @@ let userLoader = Dataloader(batchLoadFunction: { keys in }) ``` #### Load single key -``` -let future1 = try userLoader.load(key: 1, on: req) -let future2 = try userLoader.load(key: 2, on: req) -let future3 = try userLoader.load(key: 1, on: req) +```swift +let future1 = try userLoader.load(key: 1, on: eventLoopGroup) +let future2 = try userLoader.load(key: 2, on: eventLoopGroup) +let future3 = try userLoader.load(key: 1, on: eventLoopGroup) ``` Now there is only one thing left and that is to dispathc it `try userLoader.dispatchQueue(on: req.eventLoop)` @@ -38,16 +38,16 @@ Now there is only one thing left and that is to dispathc it `try userLoader.disp The example above will only fetch two users, because the user with key `1` is present twice in the list. #### Load multiple keys -There is also an API to load multiple keys at once -``` -try userLoader.loadMany(keys: [1, 2, 3], on: req.eventLoop) +There is also a method to load multiple keys at once +```swift +try userLoader.loadMany(keys: [1, 2, 3], on: eventLoopGroup) ``` -### Disable batching -It is also possible to disable batching `DataLoaderOptions(batchingEnabled: false)` -It will invoke `batchLoadFunction` with a single key +#### Disable batching +It is possible to disable batching `DataLoaderOptions(batchingEnabled: false)` +It will invoke `batchLoadFunction` immediately whenever any key is loaded -### Chaching +### Caching DataLoader provides a memoization cache for all loads which occur in a single request to your application. After `.load()` is called once with a given key, @@ -58,8 +58,8 @@ also creates fewer objects which may relieve memory pressure on your application ```swift let userLoader = DataLoader(...) -let future1 = userLoader.load(1) -let future2 = userLoader.load(1) +let future1 = userLoader.load(key: 1, on: eventLoopGroup) +let future2 = userLoader.load(key: 1, on: eventLoopGroup) assert(future1 === future2) ``` @@ -91,13 +91,13 @@ Here's a simple example using SQL UPDATE to illustrate. let userLoader = DataLoader(...) // And a value happens to be loaded (and cached). -userLoader.load(4) +userLoader.load(key: 4, on: eventLoopGroup) // A mutation occurs, invalidating what might be in cache. sqlRun('UPDATE users WHERE id=4 SET username="zuck"').then { userLoader.clear(4) } // Later the value load is loaded again so the mutated data appears. -userLoader.load(4) +userLoader.load(key: 4, on: eventLoopGroup) // Request completes. ``` @@ -112,11 +112,11 @@ be cached to avoid frequently loading the same `Error`. In some circumstances you may wish to clear the cache for these individual Errors: ```swift -userLoader.load(1).catch { error in { +userLoader.load(key: 1, on: eventLoopGroup).catch { error in { if (/* determine if should clear error */) { - userLoader.clear(1); - } - throw error + userLoader.clear(key: 1); + } + throw error } ``` @@ -124,7 +124,7 @@ userLoader.load(1).catch { error in { In certain uncommon cases, a DataLoader which *does not* cache may be desirable. Calling `DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction)` will ensure that every -call to `.load()` will produce a *new* Future, and requested keys will not be +call to `.load()` will produce a *new* Future, and previously requested keys will not be saved in memory. However, when the memoization cache is disabled, your batch function will @@ -135,13 +135,16 @@ for each instance of the requested key. For example: ```swift -let myLoader = DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: { keys in - self.someBatchLoader(keys: keys).map { DataLoaderFutureValue.success($0) } -}) +let myLoader = DataLoader( + options: DataLoaderOptions(cachingEnabled: false), + batchLoadFunction: { keys in + self.someBatchLoader(keys: keys).map { DataLoaderFutureValue.success($0) } + } +) -myLoader.load("A") -myLoader.load("B") -myLoader.load("A") +myLoader.load(key: "A", on: eventLoopGroup) +myLoader.load(key: "B", on: eventLoopGroup) +myLoader.load(key: "A", on: eventLoopGroup) // > [ "A", "B", "A" ] ``` @@ -171,3 +174,9 @@ When creating a pull request, please adhere to the current coding style where po This library is entirely a Swift version of Facebooks [DataLoader](https://github.com/facebook/dataloader). Developed by [Lee Byron](https://github.com/leebyron) and [Nicholas Schrock](https://github.com/schrockn) from [Facebook](https://www.facebook.com/). + +[swift-badge]: https://img.shields.io/badge/Swift-5.2-orange.svg?style=flat +[swift-url]: https://swift.org + +[mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat +[mit-url]: https://tldrlegal.com/license/mit-license diff --git a/Sources/SwiftDataLoader/DataLoader.swift b/Sources/DataLoader/DataLoader.swift similarity index 78% rename from Sources/SwiftDataLoader/DataLoader.swift rename to Sources/DataLoader/DataLoader.swift index 2785496..3496da4 100644 --- a/Sources/SwiftDataLoader/DataLoader.swift +++ b/Sources/DataLoader/DataLoader.swift @@ -1,9 +1,3 @@ -// -// DataLoader.swift -// App -// -// Created by Kim de Vos on 01/06/2018. -// import NIO public enum DataLoaderFutureValue { @@ -12,10 +6,16 @@ public enum DataLoaderFutureValue { } public typealias BatchLoadFunction = (_ keys: [Key]) throws -> EventLoopFuture<[DataLoaderFutureValue]> - -// Private private typealias LoaderQueue = Array<(key: Key, promise: EventLoopPromise)> +/// DataLoader creates a public API for loading data from a particular +/// data back-end with unique keys such as the id column of a SQL table +/// or document name in a MongoDB database, given a batch loading function. +/// +/// Each DataLoader instance contains a unique memoized cache. Use caution +/// when used in long-lived applications or those which serve many users +/// with different access permissions and consider creating a new instance +/// per data request. final public class DataLoader { private let batchLoadFunction: BatchLoadFunction @@ -29,8 +29,7 @@ final public class DataLoader { self.batchLoadFunction = batchLoadFunction } - - /// Loads a key, returning a `Promise` for the value represented by that key. + /// Loads a key, returning an `EventLoopFuture` for the value represented by that key. public func load(key: Key, on eventLoop: EventLoopGroup) throws -> EventLoopFuture { let cacheKey = options.cacheKeyFunction?(key) ?? key @@ -64,7 +63,21 @@ final public class DataLoader { return future } - + + /// Loads multiple keys, promising an array of values: + /// + /// ``` + /// let aAndB = myLoader.loadMany(keys: [ "a", "b" ], on: eventLoopGroup).wait() + /// ``` + /// + /// This is equivalent to the more verbose: + /// + /// ``` + /// let aAndB = [ + /// myLoader.load(key: "a", on: eventLoopGroup), + /// myLoader.load(key: "b", on: eventLoopGroup) + /// ].flatten(on: eventLoopGroup).wait() + /// ``` public func loadMany(keys: [Key], on eventLoop: EventLoopGroup) throws -> EventLoopFuture<[Value]> { guard !keys.isEmpty else { return eventLoop.next().makeSucceededFuture([]) } @@ -86,18 +99,28 @@ final public class DataLoader { return promise.futureResult } - + + /// Clears the value at `key` from the cache, if it exists. Returns itself for + /// method chaining. + @discardableResult func clear(key: Key) -> DataLoader { let cacheKey = options.cacheKeyFunction?(key) ?? key futureCache.removeValue(forKey: cacheKey) return self } - + + /// Clears the entire cache. To be used when some event results in unknown + /// invalidations across this particular `DataLoader`. Returns itself for + /// method chaining. + @discardableResult func clearAll() -> DataLoader { futureCache.removeAll() return self } + /// Adds the provied key and value to the cache. If the key already exists, no + /// change is made. Returns itself for method chaining. + @discardableResult func prime(key: Key, value: Value, on eventLoop: EventLoopGroup) -> DataLoader { let cacheKey = options.cacheKeyFunction?(key) ?? key @@ -111,7 +134,25 @@ final public class DataLoader { return self } - // MARK: - Private + public func dispatchQueue(on eventLoop: EventLoopGroup) throws { + // Take the current loader queue, replacing it with an empty queue. + let queue = self.queue + self.queue = [] + + // If a maxBatchSize was provided and the queue is longer, then segment the + // queue into multiple batches, otherwise treat the queue as a single batch. + if let maxBatchSize = options.maxBatchSize, maxBatchSize > 0 && maxBatchSize < queue.count { + for i in 0...(queue.count / maxBatchSize) { + let startIndex = i * maxBatchSize + let endIndex = (i + 1) * maxBatchSize + let slicedQueue = queue[startIndex.., on eventLoop: EventLoopGroup) throws { let keys = queue.map { $0.key } @@ -141,25 +182,6 @@ final public class DataLoader { } } - public func dispatchQueue(on eventLoop: EventLoopGroup) throws { - // Take the current loader queue, replacing it with an empty queue. - let queue = self.queue - self.queue = [] - - // If a maxBatchSize was provided and the queue is longer, then segment the - // queue into multiple batches, otherwise treat the queue as a single batch. - if let maxBatchSize = options.maxBatchSize, maxBatchSize > 0 && maxBatchSize < queue.count { - for i in 0...(queue.count / maxBatchSize) { - let startIndex = i * maxBatchSize - let endIndex = (i + 1) * maxBatchSize - let slicedQueue = queue[startIndex.., error: Error) { queue.forEach { (key, promise) in _ = clear(key: key) diff --git a/Sources/DataLoader/DataLoaderError.swift b/Sources/DataLoader/DataLoaderError.swift new file mode 100644 index 0000000..8366d04 --- /dev/null +++ b/Sources/DataLoader/DataLoaderError.swift @@ -0,0 +1,4 @@ +public enum DataLoaderError: Error { + case typeError(String) + case noValueForKey(String) +} diff --git a/Sources/SwiftDataLoader/DataLoaderOptions.swift b/Sources/DataLoader/DataLoaderOptions.swift similarity index 51% rename from Sources/SwiftDataLoader/DataLoaderOptions.swift rename to Sources/DataLoader/DataLoaderOptions.swift index 4a929bd..d9bac5a 100644 --- a/Sources/SwiftDataLoader/DataLoaderOptions.swift +++ b/Sources/DataLoader/DataLoaderOptions.swift @@ -1,15 +1,22 @@ -// -// DataLoaderOptions.swift -// CNIOAtomics -// -// Created by Kim de Vos on 02/06/2018. -// - public struct DataLoaderOptions { + /// Default `true`. Set to `false` to disable batching, invoking + /// `batchLoadFunction` with a single load key. This is + /// equivalent to setting `maxBatchSize` to `1`. public let batchingEnabled: Bool + + /// Default `nil`. Limits the number of items that get passed in to the + /// `batchLoadFn`. May be set to `1` to disable batching. + public let maxBatchSize: Int? + + /// Default `true`. Set to `false` to disable memoization caching, creating a + /// new `EventLoopFuture` and new key in the `batchLoadFunction` + /// for every load of the same key. public let cachingEnabled: Bool + public let cacheMap: [Key: Value] - public let maxBatchSize: Int? + + /// Default `nil`. Produces cache key for a given load key. Useful + /// when objects are keys and two objects should be considered equivalent. public let cacheKeyFunction: ((Key) -> Key)? public init(batchingEnabled: Bool = true, diff --git a/Sources/SwiftDataLoader/DataLoaderError.swift b/Sources/SwiftDataLoader/DataLoaderError.swift deleted file mode 100644 index f87e6c8..0000000 --- a/Sources/SwiftDataLoader/DataLoaderError.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// DataLoaderError.swift -// CNIOAtomics -// -// Created by Kim de Vos on 02/06/2018. -// - -import Foundation - -public enum DataLoaderError: Error { - case typeError(String) - case noValueForKey(String) -} diff --git a/Tests/SwiftDataLoaderTests/DataLoaderAbuseTests.swift b/Tests/DataLoaderTests/DataLoaderAbuseTests.swift similarity index 96% rename from Tests/SwiftDataLoaderTests/DataLoaderAbuseTests.swift rename to Tests/DataLoaderTests/DataLoaderAbuseTests.swift index 911e77b..318dbdf 100644 --- a/Tests/SwiftDataLoaderTests/DataLoaderAbuseTests.swift +++ b/Tests/DataLoaderTests/DataLoaderAbuseTests.swift @@ -1,14 +1,7 @@ -// -// DataLoaderAbuseTests.swift -// DataLoaderTests -// -// Created by Kim de Vos on 03/06/2018. -// - import XCTest import NIO -@testable import SwiftDataLoader +@testable import DataLoader /// Provides descriptive error messages for API abuse class DataLoaderAbuseTests: XCTestCase { diff --git a/Tests/SwiftDataLoaderTests/DataLoaderTests.swift b/Tests/DataLoaderTests/DataLoaderTests.swift similarity index 93% rename from Tests/SwiftDataLoaderTests/DataLoaderTests.swift rename to Tests/DataLoaderTests/DataLoaderTests.swift index 34ba047..456eafe 100644 --- a/Tests/SwiftDataLoaderTests/DataLoaderTests.swift +++ b/Tests/DataLoaderTests/DataLoaderTests.swift @@ -1,7 +1,7 @@ import XCTest import NIO -@testable import SwiftDataLoader +@testable import DataLoader /// Primary API final class DataLoaderTests: XCTestCase { @@ -361,18 +361,4 @@ final class DataLoaderTests: XCTestCase { XCTAssertTrue(loadCalls == [["B"]]) } - - static var allTests: [(String, (DataLoaderTests) -> () throws -> Void)] = [ - ("testRealyRealySimpleDataLoader", testReallyReallySimpleDataLoader), - ("testLoadingMultipleKeys", testLoadingMultipleKeys), - ("testMultipleRequests", testMultipleRequests), - ("testMultipleRequestsWithMaxBatchSize", testMultipleRequestsWithMaxBatchSize), - ("testCoalescesIdenticalRequests", testCoalescesIdenticalRequests), - ("testCachesRepeatedRequests", testCachesRepeatedRequests), - ("testClearSingleValueLoader", testClearSingleValueLoader), - ("testClearsAllValuesInLoader", testClearsAllValuesInLoader), - ("testAllowsPrimingTheCache", testAllowsPrimingTheCache), - ("testDoesNotPrimeKeysThatAlreadyExist", testDoesNotPrimeKeysThatAlreadyExist), - ("testAllowsForcefullyPrimingTheCache", testAllowsForcefullyPrimingTheCache) - ] } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 6d878df..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest - -@testable import SwiftDataLoaderTests - -XCTMain([ - testCase(DataLoaderAbuseTests.allTests), - testCase(DataLoaderTests.allTests) -]) diff --git a/Tests/SwiftDataLoaderTests/XCTestManifests.swift b/Tests/SwiftDataLoaderTests/XCTestManifests.swift deleted file mode 100644 index 441840e..0000000 --- a/Tests/SwiftDataLoaderTests/XCTestManifests.swift +++ /dev/null @@ -1,10 +0,0 @@ -import XCTest - -#if !os(macOS) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(DataLoaderTests.allTests), - testCase(DataLoaderAbuseTests.allTests) - ] -} -#endif diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 232f76d..21c759b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -30,7 +30,7 @@ jobs: displayName: xcodebuild -version - script: > set -o pipefail && - xcodebuild -project SwiftDataLoader.xcodeproj -scheme SwiftDataLoader-Package test | + xcodebuild -project DataLoader.xcodeproj -scheme DataLoader-Package test | xcpretty -r junit -o build/reports/xcodebuild.xml displayName: xcodebuild test - task: PublishTestResults@2 @@ -53,4 +53,4 @@ jobs: - script: > set -o pipefail && swift test --parallel - displayName: swift test \ No newline at end of file + displayName: swift test