Skip to content

Fixes handling of throwing batch load funcs #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ struct UserResolver {

class UserContext {
let database = ...
let userLoader = DataLoader<Int, User>() { [unowned self] keys in
let userLoader = DataLoader<Int, User>() { [weak self] keys in
guard let self = self else { throw ContextError }
return User.query(on: self.database).filter(\.$id ~~ keys).all().map { users in
keys.map { key in
users.first { $0.id == key }!
Expand Down
51 changes: 31 additions & 20 deletions Sources/DataLoader/DataLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ final public class DataLoader<Key: Hashable, Value> {
private var dispatchScheduled = false
private let lock = Lock()

public init(options: DataLoaderOptions<Key, Value> = DataLoaderOptions(), batchLoadFunction: @escaping BatchLoadFunction<Key, Value>) {
public init(
options: DataLoaderOptions<Key, Value> = DataLoaderOptions(),
batchLoadFunction: @escaping BatchLoadFunction<Key, Value>
) {
self.options = options
self.batchLoadFunction = batchLoadFunction
}
Expand All @@ -37,7 +40,7 @@ final public class DataLoader<Key: Hashable, Value> {
public func load(key: Key, on eventLoopGroup: EventLoopGroup) throws -> EventLoopFuture<Value> {
let cacheKey = options.cacheKeyFunction?(key) ?? key

return try lock.withLock {
return lock.withLock {
if options.cachingEnabled, let cachedFuture = cache[cacheKey] {
return cachedFuture
}
Expand All @@ -53,16 +56,20 @@ final public class DataLoader<Key: Hashable, Value> {
dispatchScheduled = true
}
} else {
_ = try batchLoadFunction([key]).map { results in
if results.isEmpty {
promise.fail(DataLoaderError.noValueForKey("Did not return value for key: \(key)"))
} else {
let result = results[0]
switch result {
case .success(let value): promise.succeed(value)
case .failure(let error): promise.fail(error)
do {
_ = try batchLoadFunction([key]).map { results in
if results.isEmpty {
promise.fail(DataLoaderError.noValueForKey("Did not return value for key: \(key)"))
} else {
let result = results[0]
switch result {
case .success(let value): promise.succeed(value)
case .failure(let error): promise.fail(error)
}
}
}
} catch {
promise.fail(error)
}
}

Expand Down Expand Up @@ -181,20 +188,24 @@ final public class DataLoader<Key: Hashable, Value> {

// Step through the values, resolving or rejecting each Promise in the
// loaded queue.
_ = try batchLoadFunction(keys).flatMapThrowing { values in
if values.count != keys.count {
throw DataLoaderError.typeError("The function did not return an array of the same length as the array of keys. \nKeys count: \(keys.count)\nValues count: \(values.count)")
}
do {
_ = try batchLoadFunction(keys).flatMapThrowing { values in
if values.count != keys.count {
throw DataLoaderError.typeError("The function did not return an array of the same length as the array of keys. \nKeys count: \(keys.count)\nValues count: \(values.count)")
}

for entry in batch.enumerated() {
let result = values[entry.offset]
for entry in batch.enumerated() {
let result = values[entry.offset]

switch result {
case .failure(let error): entry.element.promise.fail(error)
case .success(let value): entry.element.promise.succeed(value)
switch result {
case .failure(let error): entry.element.promise.fail(error)
case .success(let value): entry.element.promise.succeed(value)
}
}
}.recover { error in
self.failedExecution(batch: batch, error: error)
}
}.recover { error in
} catch {
self.failedExecution(batch: batch, error: error)
}
}
Expand Down
35 changes: 35 additions & 0 deletions Tests/DataLoaderTests/DataLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,39 @@ final class DataLoaderTests: XCTestCase {

XCTAssertNotNil(value)
}

func testErrorResult() throws {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
}

let loaderErrorMessage = "TEST"

// Test throwing loader without auto-executing
let throwLoader = DataLoader<Int, Int>(
options: DataLoaderOptions(executionPeriod: nil)
) { keys in
throw DataLoaderError.typeError(loaderErrorMessage)
}

let value = try throwLoader.load(key: 1, on: eventLoopGroup)
XCTAssertNoThrow(try throwLoader.execute())
XCTAssertThrowsError(
try value.wait(),
loaderErrorMessage
)

// Test throwing loader with auto-executing
let throwLoaderAutoExecute = DataLoader<Int, Int>(
options: DataLoaderOptions()
) { keys in
throw DataLoaderError.typeError(loaderErrorMessage)
}

XCTAssertThrowsError(
try throwLoaderAutoExecute.load(key: 1, on: eventLoopGroup).wait(),
loaderErrorMessage
)
}
}