
Encapsulates shared mutable state, allowing transactional operations and safe reads without mutexes. Supports diff filtering for efficient, thread-safe management of large, frequently updated data.
Make mutable objects safe!
I don't like mutability. Whenever it's possible I use immutable objects and JVM is pretty damn good optimized to handle a lot of object allocations. But there are rare cases when you just can't copy objects to mutate them.
Here is my case:
This library allows me to encapsulate Shared Mutable State and work with it transactionally with safe reads. No Mutexes or Semaphores for you to manage.
Simple usage scenario looks like this:
suspend fun main() = coroutineScope {
val store = createStore(scope = this)
store.collect { counter ->
// This IS thread-safe, while collector is
// working it hangs all pending transactions
if (counter > 0) {
println(counter)
}
}
}
// Expose Read-only type
fun createStore(scope: CoroutineScope): Store<Counter> {
val store = MutableStore(MutableCounter(i = 0))
// launch a coroutine that modifies counter
scope.launch {
while (true) {
store.transaction { counter -> counter.i++ }
}
}
return store
}
interface Counter {
val i: Int
}
class MutableCounter(
override var i: Int
) : CounterBut this example will not compile. Usually when you have this usecase it's some enormous amount of data that is stored in memory. And it's a lot of different data as well. Modifications could happen anywhere, how to know – where? For that matter there is builtin support for diffs. Here is an updated example that will compile:
suspend fun main() = coroutineScope {
createStore(scope = this)
// batch or drop is supported
.batchUnstable()
// filter by diffs (generic filter function is also present)
.filterDiff<_, Counter.Diff.Increment>()
.collect { counter, _ ->
// This IS thread-safe, while collectors are
// working it hangs all pending transactions.
// Multiple collectors can run in parallel.
if (counter.i > 0) {
println(counter)
}
}
}
// Expose Read-only type
fun createStore(scope: CoroutineScope): Store<Counter, Counter.Diff> {
val store = MutableStore(MutableCounter(i = 0)).diff<Counter.Diff>()
// launch a coroutine that modifies counter
scope.launch {
while (true) {
store.transaction { counter ->
counter.i++
StoreDiff.of(Counter.Diff.Increment)
}
}
}
return store
}
interface Counter {
val i: Int
sealed interface Diff {
data object Increment : Diff
}
}
data class MutableCounter(
override var i: Int
) : CounterPlease remember that this library is not for simple cases. You still should prefer immutable objects whenever it's possible. But for that usecases where you will end up using this library the amount of boilerplate is acceptable.
Add this to your build.gradle to install libary:
implementation("me.y9san9.store:core:$version")Or this to your libs.versions.toml:
[versions]
y9san9-store = "$version"
[libraries]
y9san9-store = { module = "me.y9san9.store", version = "y9san9-store" }$version should be the last from GitHub Releases.
At this point of time I present the API of this library to public judge. It's implementation is still very scratchy and I know the exact moments where I could've done better. But it works for my very data-intensive case, so PRs are welcome.
Make mutable objects safe!
I don't like mutability. Whenever it's possible I use immutable objects and JVM is pretty damn good optimized to handle a lot of object allocations. But there are rare cases when you just can't copy objects to mutate them.
Here is my case:
This library allows me to encapsulate Shared Mutable State and work with it transactionally with safe reads. No Mutexes or Semaphores for you to manage.
Simple usage scenario looks like this:
suspend fun main() = coroutineScope {
val store = createStore(scope = this)
store.collect { counter ->
// This IS thread-safe, while collector is
// working it hangs all pending transactions
if (counter > 0) {
println(counter)
}
}
}
// Expose Read-only type
fun createStore(scope: CoroutineScope): Store<Counter> {
val store = MutableStore(MutableCounter(i = 0))
// launch a coroutine that modifies counter
scope.launch {
while (true) {
store.transaction { counter -> counter.i++ }
}
}
return store
}
interface Counter {
val i: Int
}
class MutableCounter(
override var i: Int
) : CounterBut this example will not compile. Usually when you have this usecase it's some enormous amount of data that is stored in memory. And it's a lot of different data as well. Modifications could happen anywhere, how to know – where? For that matter there is builtin support for diffs. Here is an updated example that will compile:
suspend fun main() = coroutineScope {
createStore(scope = this)
// batch or drop is supported
.batchUnstable()
// filter by diffs (generic filter function is also present)
.filterDiff<_, Counter.Diff.Increment>()
.collect { counter, _ ->
// This IS thread-safe, while collectors are
// working it hangs all pending transactions.
// Multiple collectors can run in parallel.
if (counter.i > 0) {
println(counter)
}
}
}
// Expose Read-only type
fun createStore(scope: CoroutineScope): Store<Counter, Counter.Diff> {
val store = MutableStore(MutableCounter(i = 0)).diff<Counter.Diff>()
// launch a coroutine that modifies counter
scope.launch {
while (true) {
store.transaction { counter ->
counter.i++
StoreDiff.of(Counter.Diff.Increment)
}
}
}
return store
}
interface Counter {
val i: Int
sealed interface Diff {
data object Increment : Diff
}
}
data class MutableCounter(
override var i: Int
) : CounterPlease remember that this library is not for simple cases. You still should prefer immutable objects whenever it's possible. But for that usecases where you will end up using this library the amount of boilerplate is acceptable.
Add this to your build.gradle to install libary:
implementation("me.y9san9.store:core:$version")Or this to your libs.versions.toml:
[versions]
y9san9-store = "$version"
[libraries]
y9san9-store = { module = "me.y9san9.store", version = "y9san9-store" }$version should be the last from GitHub Releases.
At this point of time I present the API of this library to public judge. It's implementation is still very scratchy and I know the exact moments where I could've done better. But it works for my very data-intensive case, so PRs are welcome.