WIP: prototyping new transaction structure

This commit is contained in:
John Estropia
2017-03-24 21:15:51 +09:00
parent 494965de23
commit cb6d5b015b
7 changed files with 212 additions and 51 deletions

View File

@@ -42,33 +42,32 @@ final class TransactionTests: BaseTestCase {
do {
let createExpectation = self.expectation(description: "create")
stack.beginSynchronous { (transaction) in
let hasChanges: Bool = stack.perform(
synchronous: { (transaction) in
XCTAssertEqual(transaction.context, transaction.internalContext())
XCTAssertTrue(transaction.context.isTransactionContext)
XCTAssertFalse(transaction.context.isDataStackContext)
let object = transaction.create(Into<TestEntity1>())
XCTAssertEqual(object.fetchSource()?.internalContext(), transaction.context)
XCTAssertEqual(object.querySource()?.internalContext(), transaction.context)
object.testEntityID = NSNumber(value: 1)
object.testString = "string1"
object.testNumber = 100
object.testDate = testDate
switch transaction.commitAndWait() {
defer {
createExpectation.fulfill()
}
XCTAssertEqual(transaction.context, transaction.internalContext())
XCTAssertTrue(transaction.context.isTransactionContext)
XCTAssertFalse(transaction.context.isDataStackContext)
case .success(let hasChanges):
XCTAssertTrue(hasChanges)
createExpectation.fulfill()
let object = transaction.create(Into<TestEntity1>())
XCTAssertEqual(object.fetchSource()?.internalContext(), transaction.context)
XCTAssertEqual(object.querySource()?.internalContext(), transaction.context)
default:
XCTFail()
}
}
object.testEntityID = NSNumber(value: 1)
object.testString = "string1"
object.testNumber = 100
object.testDate = testDate
return transaction.hasChanges
},
waitForObserverNotifications: true
)
self.checkExpectationsImmediately()
XCTAssertTrue(hasChanges)
XCTAssertEqual(stack.fetchCount(From<TestEntity1>()), 1)
@@ -85,28 +84,28 @@ final class TransactionTests: BaseTestCase {
do {
let updateExpectation = self.expectation(description: "update")
stack.beginSynchronous { (transaction) in
guard let object = transaction.fetchOne(From<TestEntity1>()) else {
let hasChanges: Bool = stack.perform(
synchronous: { (transaction) in
XCTFail()
return
}
object.testString = "string1_edit"
object.testNumber = 200
object.testDate = Date.distantFuture
switch transaction.commitAndWait() {
defer {
updateExpectation.fulfill()
}
guard let object = transaction.fetchOne(From<TestEntity1>()) else {
XCTFail()
return // TODO: convert fetch methods to throwing methods
}
object.testString = "string1_edit"
object.testNumber = 200
object.testDate = Date.distantFuture
case .success(let hasChanges):
XCTAssertTrue(hasChanges)
updateExpectation.fulfill()
default:
XCTFail()
}
}
return transaction.hasChanges
},
waitForObserverNotifications: true
)
self.checkExpectationsImmediately()
XCTAssertTrue(hasChanges)
XCTAssertEqual(stack.fetchCount(From<TestEntity1>()), 1)

View File

@@ -815,7 +815,7 @@ public protocol ImportableUniqueObject: ImportableObject {
static var uniqueIDKeyPath: String { get }
var uniqueIDValue: UniqueIDType { get set }
static func shouldInsert(from source: ImportSource, inn transaction: BaseDataTransaction) -> Bool
static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType?
func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws

View File

@@ -59,6 +59,16 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
*/
case internalError(NSError: NSError)
/**
The transaction was terminated by a user-thrown `Error`.
*/
case userError(error: Error)
/**
The transaction was cancelled by the user.
*/
case userCancelled
// MARK: CustomNSError
@@ -85,6 +95,12 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
case .internalError:
return CoreStoreErrorCode.internalError.rawValue
case .userError:
return CoreStoreErrorCode.userError.rawValue
case .userCancelled:
return CoreStoreErrorCode.userCancelled.rawValue
}
}
@@ -112,10 +128,18 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
"localStoreURL": localStoreURL
]
case .internalError(let NSError):
case .internalError(let nsError):
return [
"NSError": NSError
"NSError": nsError
]
case .userError(let error):
return [
"Error": error
]
case .userCancelled:
return [:]
}
}
@@ -139,7 +163,23 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
return url1 == url2
case (.internalError(let NSError1), .internalError(let NSError2)):
return NSError1 == NSError2
return NSError1.isEqual(NSError2)
case (.userError(let error1), .userError(let error2)):
switch (error1, error2) {
case (let error1 as AnyHashable, let error2 as AnyHashable):
return error1 == error2
case (let error1 as NSError, let error2 as NSError):
return error1.isEqual(error2)
default:
return false // shouldn't happen
}
case (.userCancelled, .userCancelled):
return true
default:
return false
@@ -166,8 +206,14 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
case .progressiveMigrationRequired(let localStoreURL):
return code.hashValue ^ localStoreURL.hashValue
case .internalError(let NSError):
return code.hashValue ^ NSError.hashValue
case .internalError(let nsError):
return code.hashValue ^ nsError.hashValue
case .userError(let error):
return code.hashValue ^ (error as NSError).hashValue
case .userCancelled:
return code.hashValue
}
}
@@ -221,6 +267,16 @@ public enum CoreStoreErrorCode: Int {
An internal SDK call failed with the specified "NSError" userInfo key.
*/
case internalError
/**
The transaction was terminated by a user-thrown `Error` specified by "Error" userInfo key.
*/
case userError
/**
The transaction was cancelled by the user.
*/
case userCancelled
}

View File

@@ -142,6 +142,13 @@ extension CoreStoreError: CustomDebugStringConvertible, CoreStoreDebugStringConv
case .internalError(let NSError):
firstLine = ".internalError"
info.append(("NSError", NSError))
case .userError(error: let error):
firstLine = ".userError"
info.append(("Error", error))
case .userCancelled:
firstLine = ".userCancelled"
}
return createFormattedString(

View File

@@ -137,6 +137,16 @@ public enum CSErrorCode: Int {
An internal SDK call failed with the specified "NSError" userInfo key.
*/
case internalError
/**
The transaction was terminated by a user-thrown error with the specified "Error" userInfo key.
*/
case userError
/**
The transaction was cancelled by the user.
*/
case userCancelled
}
@@ -209,12 +219,23 @@ extension CoreStoreError: CoreStoreSwiftType, _ObjectiveCBridgeableError {
self = .progressiveMigrationRequired(localStoreURL: localStoreURL)
case .internalError:
guard case let NSError as NSError = info["NSError"] else {
guard case let nsError as NSError = info["NSError"] else {
self = .unknown
return
}
self = .internalError(NSError: NSError)
self = .internalError(NSError: nsError)
case .userError:
guard case let error as Error = info["Error"] else {
self = .unknown
return
}
self = .userError(error: error)
case .userCancelled:
self = .userCancelled
}
}
}

View File

@@ -36,6 +36,12 @@ public /*abstract*/ class BaseDataTransaction {
// MARK: Object management
public func cancel() throws -> Never {
throw CoreStoreError.userCancelled
}
/**
Indicates if the transaction has pending changes
*/

View File

@@ -31,6 +31,79 @@ import CoreData
public extension DataStack {
public func perform<T>(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, success: @escaping (T) -> Void, failure: @escaping (CoreStoreError) -> Void) {
let transaction = AsynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue,
closure: { _ in }
)
transaction.transactionQueue.cs_async {
do {
let extraInfo = try task(transaction)
transaction.commit { (result) in
switch result {
case .success:
success(extraInfo)
case .failure(let error):
failure(error)
}
}
}
catch let error as CoreStoreError {
DispatchQueue.main.async { failure(error) }
}
catch let error {
DispatchQueue.main.async { failure(.userError(error: error)) }
}
}
}
public func perform<T>(synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), waitForObserverNotifications: Bool = true) throws -> T {
let transaction = SynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue,
closure: { _ in }
)
return try transaction.transactionQueue.cs_sync {
let extraInfo: T
do {
extraInfo = try task(transaction)
}
catch let error as CoreStoreError {
throw error
}
catch let error {
throw CoreStoreError.userError(error: error)
}
let result = waitForObserverNotifications
? transaction.commitAndWait()
: transaction.commit()
switch result {
case .success:
return extraInfo
case .failure(let error):
throw error
}
}
}
/**
Begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
@@ -83,7 +156,6 @@ public extension DataStack {
Thread.isMainThread,
"Attempted to refresh entities outside their designated queue."
)
self.mainContext.refreshAndMergeAllObjects()
}
}