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

View File

@@ -815,7 +815,7 @@ public protocol ImportableUniqueObject: ImportableObject {
static var uniqueIDKeyPath: String { get } static var uniqueIDKeyPath: String { get }
var uniqueIDValue: UniqueIDType { get set } 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 shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType?
func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws 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) 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 // MARK: CustomNSError
@@ -85,6 +95,12 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
case .internalError: case .internalError:
return CoreStoreErrorCode.internalError.rawValue 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 "localStoreURL": localStoreURL
] ]
case .internalError(let NSError): case .internalError(let nsError):
return [ 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 return url1 == url2
case (.internalError(let NSError1), .internalError(let NSError2)): 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: default:
return false return false
@@ -166,8 +206,14 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
case .progressiveMigrationRequired(let localStoreURL): case .progressiveMigrationRequired(let localStoreURL):
return code.hashValue ^ localStoreURL.hashValue return code.hashValue ^ localStoreURL.hashValue
case .internalError(let NSError): case .internalError(let nsError):
return code.hashValue ^ NSError.hashValue 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. An internal SDK call failed with the specified "NSError" userInfo key.
*/ */
case internalError 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): case .internalError(let NSError):
firstLine = ".internalError" firstLine = ".internalError"
info.append(("NSError", NSError)) info.append(("NSError", NSError))
case .userError(error: let error):
firstLine = ".userError"
info.append(("Error", error))
case .userCancelled:
firstLine = ".userCancelled"
} }
return createFormattedString( return createFormattedString(

View File

@@ -137,6 +137,16 @@ public enum CSErrorCode: Int {
An internal SDK call failed with the specified "NSError" userInfo key. An internal SDK call failed with the specified "NSError" userInfo key.
*/ */
case internalError 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) self = .progressiveMigrationRequired(localStoreURL: localStoreURL)
case .internalError: case .internalError:
guard case let NSError as NSError = info["NSError"] else { guard case let nsError as NSError = info["NSError"] else {
self = .unknown self = .unknown
return 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 // MARK: Object management
public func cancel() throws -> Never {
throw CoreStoreError.userCancelled
}
/** /**
Indicates if the transaction has pending changes Indicates if the transaction has pending changes
*/ */

View File

@@ -31,6 +31,79 @@ import CoreData
public extension DataStack { 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. Begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
@@ -83,7 +156,6 @@ public extension DataStack {
Thread.isMainThread, Thread.isMainThread,
"Attempted to refresh entities outside their designated queue." "Attempted to refresh entities outside their designated queue."
) )
self.mainContext.refreshAndMergeAllObjects() self.mainContext.refreshAndMergeAllObjects()
} }
} }