new auto-commit transaction methods

This commit is contained in:
John Estropia
2017-03-31 19:44:18 +09:00
parent d72d1afe8b
commit 97d7a276fe
39 changed files with 3739 additions and 3232 deletions

View File

@@ -34,63 +34,51 @@ import CoreData
*/
public final class AsynchronousDataTransaction: BaseDataTransaction {
// MARK: - Result
public enum Result<T> {
case success(userInfo: T)
case failure(error: CoreStoreError)
public var boolValue: Bool {
switch self {
case .success: return true
case .failure: return false
}
}
// MARK: Internal
internal init(userInfo: T) {
self = .success(userInfo: userInfo)
}
internal init(error: CoreStoreError) {
self = .failure(error: error)
}
}
// MARK: -
/**
Cancels a transaction by throwing `CoreStoreError.userCancelled`.
```
try transaction.cancel()
```
- Important: Never use `try?` or `try!` on a `cancel()` call. Always use `try`. Using `try?` will swallow the cancellation and the transaction will proceed to commit as normal. Using `try!` will crash the app as `cancel()` will *always* throw an error.
*/
public func cancel() throws -> Never {
throw CoreStoreError.userCancelled
}
/**
Saves the transaction changes. This method should not be used after the `commit()` method was already called once.
- parameter completion: the block executed after the save completes. Success or failure is reported by the `SaveResult` argument of the block.
*/
public func commit(_ completion: @escaping (_ result: SaveResult) -> Void = { _ in }) {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to commit a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to commit a \(cs_typeName(self)) more than once."
)
self.isCommitted = true
let group = DispatchGroup()
group.enter()
self.context.saveAsynchronouslyWithCompletion { (result) -> Void in
self.result = result
completion(result)
group.leave()
}
group.wait()
}
/**
Begins a child transaction synchronously where NSManagedObject creates, updates, and deletes can be made. This method should not be used after the `commit()` method was already called once.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@discardableResult
public func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to begin a child transaction from a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to begin a child transaction from an already committed \(cs_typeName(self))."
)
return SynchronousDataTransaction(
mainContext: self.context,
queue: self.childTransactionQueue,
closure: closure).performAndWait()
}
// MARK: BaseDataTransaction
@@ -193,47 +181,93 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
// MARK: Internal
internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue, closure: @escaping (_ transaction: AsynchronousDataTransaction) -> Void) {
self.closure = closure
internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue) {
super.init(mainContext: mainContext, queue: queue, supportsUndo: false, bypassesQueueing: false)
}
internal func perform() {
internal func autoCommit(_ completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void) {
self.transactionQueue.async {
self.isCommitted = true
let group = DispatchGroup()
group.enter()
self.context.saveAsynchronouslyWithCompletion { (result) -> Void in
self.closure(self)
if !self.isCommitted && self.hasChanges {
completion(result.0, result.1)
self.result = result
group.leave()
}
group.wait()
}
// MARK: Deprecated
/**
Saves the transaction changes. This method should not be used after the `commit()` method was already called once.
- parameter completion: the block executed after the save completes. Success or failure is reported by the `SaveResult` argument of the block.
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commiting methods `DataStack.perform(asynchronous:completion:)` or `DataStack.perform(asynchronous:success:failure:)`. Please read the documentation on the behavior of the new methods.")
public func commit(_ completion: @escaping (_ result: SaveResult) -> Void = { _ in }) {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to commit a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to commit a \(cs_typeName(self)) more than once."
)
self.autoCommit { (result) in
switch result {
CoreStore.log(
.warning,
message: "The closure for the \(cs_typeName(self)) completed without being committed. All changes made within the transaction were discarded."
)
case (let hasChanges, nil): completion(SaveResult(hasChanges: hasChanges))
case (_, let error?): completion(SaveResult(error))
}
}
}
internal func performAndWait() -> SaveResult? {
/**
Begins a child transaction synchronously where NSManagedObject creates, updates, and deletes can be made. This method should not be used after the `commit()` method was already called once.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@available(*, deprecated: 4.0.0, message: "Secondary tasks spawned from AsynchronousDataTransactions and SynchronousDataTransactions are no longer supported. ")
@discardableResult
public func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
self.transactionQueue.sync {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to begin a child transaction from a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to begin a child transaction from an already committed \(cs_typeName(self))."
)
let childTransaction = SynchronousDataTransaction(
mainContext: self.context,
queue: self.childTransactionQueue
)
childTransaction.transactionQueue.cs_sync {
self.closure(self)
closure(childTransaction)
if !self.isCommitted && self.hasChanges {
if !childTransaction.isCommitted && childTransaction.hasChanges {
CoreStore.log(
.warning,
message: "The closure for the \(cs_typeName(self)) completed without being committed. All changes made within the transaction were discarded."
message: "The closure for the \(cs_typeName(childTransaction)) completed without being committed. All changes made within the transaction were discarded."
)
}
}
return self.result
switch childTransaction.result {
case nil: return nil
case (let hasChanges, nil)?: return SaveResult(hasChanges: hasChanges)
case (_, let error?)?: return SaveResult(error)
}
}
// MARK: Private
private let closure: (_ transaction: AsynchronousDataTransaction) -> Void
}

View File

@@ -455,7 +455,7 @@ public /*abstract*/ class BaseDataTransaction {
internal let supportsUndo: Bool
internal let bypassesQueueing: Bool
internal var isCommitted = false
internal var result: SaveResult?
internal var result: (hasChanges: Bool, error: CoreStoreError?)?
internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue, supportsUndo: Bool, bypassesQueueing: Bool) {

View File

@@ -31,25 +31,39 @@ import Foundation
public extension CoreStore {
/**
Using the `defaultStack`, begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
Using the `defaultStack`, performs a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the wrapped as `TransactionResult.success(userInfo: T)` in the `completion`'s `TransactionResult<T>`. Any errors thrown from inside the `task` will be reported as `TransactionResult.failure(error: Error)`. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter completion: the closure executed after the save completes. The `TransactionResult` argument of the closure will either wrap the return value of `task`, or any uncaught errors thrown from within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
*/
public static func beginAsynchronous(_ closure: @escaping (_ transaction: AsynchronousDataTransaction) -> Void) {
public static func perform<T>(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, completion: @escaping (AsynchronousDataTransaction.Result<T>) -> Void) {
self.defaultStack.beginAsynchronous(closure)
self.defaultStack.perform(asynchronous: task, completion: completion)
}
/**
Using the `defaultStack`, begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
Using the `defaultStack`, performs a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the argument of the `success` closure. Any errors thrown from inside the `task` will be wrapped in a `CoreStoreError` and reported in the `failure` closure. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
- parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter success: the closure executed after the save succeeds. The `T` argument of the closure will be the value returned from `task`.
- parameter failure: the closure executed if the save fails or if any errors are thrown within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
*/
@discardableResult
public static func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
public static func perform<T>(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, success: @escaping (T) -> Void, failure: @escaping (CoreStoreError) -> Void) {
return self.defaultStack.beginSynchronous(closure)
self.defaultStack.perform(asynchronous: task, success: success, failure: failure)
}
/**
Using the `defaultStack`, performs a transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the return value of `perform(synchronous:)`. Any errors thrown from inside the `task` will be rethrown from `perform(synchronous:)`. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
- parameter task: the synchronous non-escaping closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter waitForAllObservers: When `true`, this method waits for all observers to be notified of the changes before returning. This results in more predictable data update order, but may risk triggering deadlocks. When `false`, this method does not wait for observers to be notified of the changes before returning. This results in lower risk for deadlocks, but the updated data may not have been propagated to the `DataStack` after returning. Defaults to `true`.
- throws: a `CoreStoreError` value indicating the failure. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
- returns: the value returned from `task`
*/
public static func perform<T>(synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), waitForAllObservers: Bool = true) throws -> T {
return try self.defaultStack.perform(synchronous: task, waitForAllObservers: waitForAllObservers)
}
/**
@@ -70,4 +84,31 @@ public extension CoreStore {
self.defaultStack.refreshAndMergeAllObjects()
}
// MARK: Deprecated
/**
Using the `defaultStack`, begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commiting methods `perform(asynchronous:completion:)` or `perform(asynchronous:success:failure:)`. Please read the documentation on the behavior of the new methods.")
public static func beginAsynchronous(_ closure: @escaping (_ transaction: AsynchronousDataTransaction) -> Void) {
self.defaultStack.beginAsynchronous(closure)
}
/**
Using the `defaultStack`, begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commiting method `perform(synchronous:)`. Please read the documentation on the behavior of the new methods.")
@discardableResult
public static func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
return self.defaultStack.beginSynchronous(closure)
}
}

View File

@@ -31,22 +31,33 @@ import CoreData
public extension DataStack {
public func perform<T>(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, completion: @escaping (TransactionResult<T>) -> Void) {
/**
Performs a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the wrapped as `TransactionResult.success(userInfo: T)` in the `completion`'s `TransactionResult<T>`. Any errors thrown from inside the `task` will be reported as `TransactionResult.failure(error: Error)`. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
- parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter completion: the closure executed after the save completes. The `TransactionResult` argument of the closure will either wrap the return value of `task`, or any uncaught errors thrown from within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
*/
public func perform<T>(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, completion: @escaping (AsynchronousDataTransaction.Result<T>) -> Void) {
self.perform(
asynchronous: task,
success: { completion(TransactionResult(userInfo: $0)) },
failure: { completion(TransactionResult(error: $0)) }
success: { completion(.init(userInfo: $0)) },
failure: { completion(.init(error: $0)) }
)
}
/**
Performs a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the argument of the `success` closure. Any errors thrown from inside the `task` will be wrapped in a `CoreStoreError` and reported in the `failure` closure. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
- parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter success: the closure executed after the save succeeds. The `T` argument of the closure will be the value returned from `task`.
- parameter failure: the closure executed if the save fails or if any errors are thrown within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
*/
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 }
queue: self.childTransactionQueue
)
transaction.transactionQueue.cs_async {
@@ -65,30 +76,40 @@ public extension DataStack {
DispatchQueue.main.async { failure(.userError(error: error)) }
return
}
transaction.commit { (result) in
transaction.autoCommit { (_, error) in
switch result {
if let error = error {
case .success: success(userInfo)
case .failure(let error): failure(error)
failure(error)
}
else {
success(userInfo)
}
}
}
}
public func perform<T>(synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), waitForObserverNotifications: Bool = true) throws -> T {
/**
Performs a transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the return value of `perform(synchronous:)`. Any errors thrown from inside the `task` will be rethrown from `perform(synchronous:)`. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
- parameter task: the synchronous non-escaping closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- parameter waitForAllObservers: When `true`, this method waits for all observers to be notified of the changes before returning. This results in more predictable data update order, but may risk triggering deadlocks. When `false`, this method does not wait for observers to be notified of the changes before returning. This results in lower risk for deadlocks, but the updated data may not have been propagated to the `DataStack` after returning. Defaults to `true`.
- throws: a `CoreStoreError` value indicating the failure. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
- returns: the value returned from `task`
*/
public func perform<T>(synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), waitForAllObservers: Bool = true) throws -> T {
let transaction = SynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue,
closure: { _ in }
queue: self.childTransactionQueue
)
return try transaction.transactionQueue.cs_sync {
let userInfo: T
do {
userInfo = try task(transaction)
userInfo = try withoutActuallyEscaping(task, do: { try $0(transaction) })
}
catch let error as CoreStoreError {
@@ -98,46 +119,17 @@ public extension DataStack {
throw CoreStoreError.userError(error: error)
}
let result = waitForObserverNotifications
? transaction.commitAndWait()
: transaction.commit()
switch result {
if case (_, let error?) = transaction.autoCommit(waitForMerge: waitForAllObservers) {
case .success: return userInfo
case .failure(let error): throw error
throw error
}
else {
return userInfo
}
}
}
/**
Begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
*/
public func beginAsynchronous(_ closure: @escaping (_ transaction: AsynchronousDataTransaction) -> Void) {
AsynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue,
closure: closure).perform()
}
/**
Begins a transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@discardableResult
public func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
return SynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue,
closure: closure).performAndWait()
}
/**
Begins a non-contiguous transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms.
@@ -164,4 +156,67 @@ public extension DataStack {
)
self.mainContext.refreshAndMergeAllObjects()
}
// MARK: Deprecated
/**
Begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commiting methods `perform(asynchronous:completion:)` or `perform(asynchronous:success:failure:)`. Please read the documentation on the behavior of the new methods.")
public func beginAsynchronous(_ closure: @escaping (_ transaction: AsynchronousDataTransaction) -> Void) {
let transaction = AsynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue
)
transaction.transactionQueue.cs_async {
closure(transaction)
if !transaction.isCommitted && transaction.hasChanges {
CoreStore.log(
.warning,
message: "The closure for the \(cs_typeName(transaction)) completed without being committed. All changes made within the transaction were discarded."
)
}
}
}
/**
Begins a transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commiting method `perform(synchronous:)`. Please read the documentation on the behavior of the new methods.")
@discardableResult
public func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
let transaction = SynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue
)
transaction.transactionQueue.cs_sync {
closure(transaction)
if !transaction.isCommitted && transaction.hasChanges {
CoreStore.log(
.warning,
message: "The closure for the \(cs_typeName(transaction)) completed without being committed. All changes made within the transaction were discarded."
)
}
}
switch transaction.result {
case nil: return nil
case (let hasChanges, nil)?: return SaveResult(hasChanges: hasChanges)
case (_, let error?)?: return SaveResult(error)
}
}
}

View File

@@ -57,6 +57,7 @@ import Foundation
}
```
*/
@available(*, deprecated: 4.0.0, message: "Use the new DataStack.perform(asynchronous:...) and DataStack.perform(synchronous:...) family of APIs")
public enum SaveResult: Hashable {
/**

View File

@@ -34,83 +34,18 @@ import CoreData
*/
public final class SynchronousDataTransaction: BaseDataTransaction {
/**
Cancels a transaction by throwing `CoreStoreError.userCancelled`.
```
try transaction.cancel()
```
- Important: Never use `try?` or `try!` on a `cancel()` call. Always use `try`. Using `try?` will swallow the cancellation and the transaction will proceed to commit as normal. Using `try!` will crash the app as `cancel()` will *always* throw an error.
*/
public func cancel() throws -> Never {
throw CoreStoreError.userCancelled
}
/**
Saves the transaction changes and waits for completion synchronously. This method should not be used after the `commit()` or `commitAndWait()` method was already called once.
- Important: Unlike `SynchronousDataTransaction.commit()`, this method waits for all observers to be notified of the changes before returning. This results in more predictable data update order, but may risk triggering deadlocks.
- returns: a `SaveResult` containing the success or failure information
*/
public func commitAndWait() -> SaveResult {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to commit a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to commit a \(cs_typeName(self)) more than once."
)
self.isCommitted = true
let result = self.context.saveSynchronously(waitForMerge: true)
self.result = result
return result
}
/**
Saves the transaction changes and waits for completion synchronously. This method should not be used after the `commit()` or `commitAndWait()` method was already called once.
- Important: Unlike `SynchronousDataTransaction.commitAndWait()`, this method does not wait for observers to be notified of the changes before returning. This results in lower risk for deadlocks, but the updated data may not have been propagated to the `DataStack` after returning.
- returns: a `SaveResult` containing the success or failure information
*/
public func commit() -> SaveResult {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to commit a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to commit a \(cs_typeName(self)) more than once."
)
self.isCommitted = true
let result = self.context.saveSynchronously(waitForMerge: false)
self.result = result
return result
}
/**
Begins a child transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made. This method should not be used after the `commit()` method was already called once.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@discardableResult
public func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to begin a child transaction from a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to begin a child transaction from an already committed \(cs_typeName(self))."
)
return SynchronousDataTransaction(
mainContext: self.context,
queue: self.childTransactionQueue,
closure: closure).performAndWait()
}
// MARK: BaseDataTransaction
@@ -213,32 +148,109 @@ public final class SynchronousDataTransaction: BaseDataTransaction {
// MARK: Internal
internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue, closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) {
self.closure = closure
internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue) {
super.init(mainContext: mainContext, queue: queue, supportsUndo: false, bypassesQueueing: false)
}
internal func performAndWait() -> SaveResult? {
internal func autoCommit(waitForMerge: Bool) -> (hasChanges: Bool, error: CoreStoreError?) {
self.transactionQueue.sync {
self.closure(self)
if !self.isCommitted && self.hasChanges {
CoreStore.log(
.warning,
message: "The closure for the \(cs_typeName(self)) completed without being committed. All changes made within the transaction were discarded."
)
}
}
return self.result
self.isCommitted = true
let result = self.context.saveSynchronously(waitForMerge: waitForMerge)
self.result = result
return result
}
// MARK: Private
// MARK: Deprecated
private let closure: (_ transaction: SynchronousDataTransaction) -> Void
/**
Saves the transaction changes and waits for completion synchronously. This method should not be used after the `commit()` or `commitAndWait()` method was already called once.
- Important: Unlike `SynchronousDataTransaction.commit()`, this method waits for all observers to be notified of the changes before returning. This results in more predictable data update order, but may risk triggering deadlocks.
- returns: a `SaveResult` containing the success or failure information
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commit method DataStack.perform(synchronous:waitForAllObservers:)")
public func commitAndWait() -> SaveResult {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to commit a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to commit a \(cs_typeName(self)) more than once."
)
switch self.autoCommit(waitForMerge: true) {
case (let hasChanges, nil): return SaveResult(hasChanges: hasChanges)
case (_, let error?): return SaveResult(error)
}
}
/**
Saves the transaction changes and waits for completion synchronously. This method should not be used after the `commit()` or `commitAndWait()` method was already called once.
- Important: Unlike `SynchronousDataTransaction.commitAndWait()`, this method does not wait for observers to be notified of the changes before returning. This results in lower risk for deadlocks, but the updated data may not have been propagated to the `DataStack` after returning.
- returns: a `SaveResult` containing the success or failure information
*/
@available(*, deprecated: 4.0.0, message: "Use the new auto-commit method DataStack.perform(synchronous:waitForAllObservers:)")
public func commit() -> SaveResult {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to commit a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to commit a \(cs_typeName(self)) more than once."
)
switch self.autoCommit(waitForMerge: false) {
case (let hasChanges, nil): return SaveResult(hasChanges: hasChanges)
case (_, let error?): return SaveResult(error)
}
}
/**
Begins a child transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made. This method should not be used after the `commit()` method was already called once.
- parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: a `SaveResult` value indicating success or failure, or `nil` if the transaction was not comitted synchronously
*/
@available(*, deprecated: 4.0.0, message: "Secondary tasks spawned from AsynchronousDataTransactions and SynchronousDataTransactions are no longer supported. ")
@discardableResult
public func beginSynchronous(_ closure: @escaping (_ transaction: SynchronousDataTransaction) -> Void) -> SaveResult? {
CoreStore.assert(
self.transactionQueue.cs_isCurrentExecutionContext(),
"Attempted to begin a child transaction from a \(cs_typeName(self)) outside its designated queue."
)
CoreStore.assert(
!self.isCommitted,
"Attempted to begin a child transaction from an already committed \(cs_typeName(self))."
)
let childTransaction = SynchronousDataTransaction(
mainContext: self.context,
queue: self.childTransactionQueue
)
childTransaction.transactionQueue.cs_sync {
closure(childTransaction)
if !childTransaction.isCommitted && childTransaction.hasChanges {
CoreStore.log(
.warning,
message: "The closure for the \(cs_typeName(childTransaction)) completed without being committed. All changes made within the transaction were discarded."
)
}
}
switch childTransaction.result {
case nil: return nil
case (let hasChanges, nil)?: return SaveResult(hasChanges: hasChanges)
case (_, let error?)?: return SaveResult(error)
}
}
}

View File

@@ -1,58 +0,0 @@
//
// TransactionResult.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
// MARK: - TransactionResult
public enum TransactionResult<T> {
case success(T)
case failure(CoreStoreError)
public var boolValue: Bool {
switch self {
case .success: return true
case .failure: return false
}
}
// MARK: Internal
internal init(userInfo: T) {
self = .success(userInfo)
}
internal init(error: CoreStoreError) {
self = .failure(error)
}
}

View File

@@ -34,30 +34,33 @@ import CoreData
*/
public final class UnsafeDataTransaction: BaseDataTransaction {
// MARK: -
/**
Saves the transaction changes asynchronously. For an `UnsafeDataTransaction`, multiple commits are allowed, although it is the developer's responsibility to ensure a reasonable leeway to prevent blocking the main thread.
- parameter completion: the block executed after the save completes. Success or failure is reported by the `SaveResult` argument of the block.
- parameter completion: the block executed after the save completes. Success or failure is reported by the optional `error` argument of the block.
*/
public func commit(_ completion: @escaping (_ result: SaveResult) -> Void) {
public func commit(_ completion: @escaping (_ error: CoreStoreError?) -> Void) {
self.context.saveAsynchronouslyWithCompletion { (result) -> Void in
self.context.saveAsynchronouslyWithCompletion { (_, error) in
self.result = result
completion(result)
completion(error)
withExtendedLifetime(self, {})
}
}
/**
Saves the transaction changes and waits for completion synchronously. For an `UnsafeDataTransaction`, multiple commits are allowed, although it is the developer's responsibility to ensure a reasonable leeway to prevent blocking the main thread.
- returns: a `SaveResult` containing the success or failure information
- throws: a `CoreStoreError` value indicating the failure.
*/
public func commitAndWait() -> SaveResult {
public func commitAndWait() throws {
let result = self.context.saveSynchronously(waitForMerge: true)
self.result = result
return result
if case (_, let error?) = self.context.saveSynchronously(waitForMerge: true) {
throw error
}
}
/**
@@ -142,13 +145,4 @@ public final class UnsafeDataTransaction: BaseDataTransaction {
super.init(mainContext: mainContext, queue: queue, supportsUndo: supportsUndo, bypassesQueueing: true)
}
// MARK: Obsolete
@available(*, obsoleted: 3.0.0, message: "Transaction contexts are now exposed through the FetchableSource and QueryableSource protocols.", renamed: "internalContext()")
public var internalContext: NSManagedObjectContext {
fatalError()
}
}