Last active
October 28, 2019 07:05
-
-
Save daniel-hall/422775084a05779b0157caf68955e09d to your computer and use it in GitHub Desktop.
SingleWriter wraps any struct in a mechanism that only allows modifications through an optional-typed writer object. Only one writer object can exist at a time, so while one code site is referencing the writer or using it to change a property on the struct, any other code that attempts to get the writer to make changes will get a nil value.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The SingleWriter generic type only works with structs | |
// | |
// SingleWriter wraps any struct in a mechanism that only allows modifications through an optional-typed writer object. | |
// Only one writer object can exist at a time, so while one code site is referencing the writer or using it to change | |
// a property on the struct, any other code that attempts to get the writer to make changes will get a nil value. | |
// | |
// This is a tool to work with global shared mutable state, that allows write access to be controlled across threads or | |
// call sites. Any code using the writer interface will cause any other code / threads to not have access to the writer | |
// interface. | |
// | |
// Like more traditional locking, the SingleWriter type also has a writeNext method that allows client code to add a | |
// write operation to a queue that will run as soon as any existing references to the writer interface are released | |
class SingleWriter<T> { | |
private var value:T | |
private var nextWriters = [(WriterProxy<T>)->()]() | |
private weak var proxy: WriterProxy<T>? | |
/// Returns a read-only copy of the wrapped struct | |
var read: T { return value } | |
/// Returns a writer interface that allows changes to the wrapped struct. Only one interface instance can exist at a time | |
var writer: WriterProxy<T>? { | |
get { | |
if self.proxy != nil { return nil } | |
let proxy = WriterProxy<T>(value:value, updateClosure: { self.value = $0 }){ | |
if let next = self.nextWriters.popLast(), let writer = self.writer { | |
next(writer) | |
} | |
} | |
self.proxy = proxy | |
return proxy | |
} | |
} | |
/// Init with any struct instance to provide the single writer interface for | |
init(value:T) { | |
self.value = value | |
} | |
/// Provide a closure to write modifications to the wrapped struct whenever the writer interface is next available | |
func writeNext(closure:@escaping (WriterProxy<T>)->()) { | |
if let writer = writer { | |
closure(writer) | |
} else { | |
nextWriters.insert(closure, at: 0) | |
} | |
} | |
} | |
class WriterProxy<T> { | |
var write:T { | |
didSet { | |
updateClosure(write) | |
} | |
} | |
private let updateClosure:(T)->() | |
private let completedClosure:()->() | |
fileprivate init(value:T, updateClosure:@escaping (T)->(), completedClosure:@escaping ()->()) { | |
self.write = value | |
self.updateClosure = updateClosure | |
self.completedClosure = completedClosure | |
} | |
deinit { | |
completedClosure() | |
} | |
} | |
// Example usage: | |
// A simple struct type | |
struct User { | |
var name:String | |
var id:Int | |
} | |
// Wrap an instance of a User with initial values in a SingleWriter instance | |
let currentUser = SingleWriter(value: User(name: "Default", id: 0)) | |
print(currentUser.read.name) // prints "Default" | |
// hold a reference to the "writer" interface for this User instance | |
var writerReference = currentUser.writer | |
// modify the name via the reference to the "writer" interface | |
writerReference?.write.name = "Joe" | |
// try to also modify the name through a different call to the "writer" interface. Because writerReference already has a | |
// "lock" on the "writer" interface, this attempt to get the "writer" interface returns nil and the write operation | |
// doesn't take effect | |
currentUser.writer?.write.name = "Sally" | |
// notice that only the first modification to the name, via the writerReference worked | |
print(currentUser.read.name) // prints "Joe" | |
// queue up a modification for when the writer interface is next released | |
currentUser.writeNext{ $0.write.name = "Bobo" } | |
// because there is still a reference to the "writer" interface being retained, the "Bobo" name change hasn't happened yet | |
print(currentUser.read.name) // prints "Joe" | |
// release the retained reference to the single "writer" interface | |
writerReference = nil | |
// as soon as the reference to the "writer" interface was released, the queued name change to "Bobo" was applied | |
print(currentUser.read.name) // prints "Bobo" | |
// and now other code can access the single "writer" interface again as well and this will work | |
currentUser.writer?.write.name = "Sally" | |
currentUser.writer?.write.id = 10 | |
print(currentUser.read) // prints User(name: "Sally", id: 10) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment