diff --git a/README.md b/README.md index 3c6d584b..daa5633a 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,14 @@ Toolbox of utilities/helpers for Kotlin development. ![badge-jvm] ![badge-mac] -### [`SynchronizedMap`] +### `AtomicList`, `AtomicSet`, and `AtomicMap` -Provides a map where read and write operations are protected using a reentrant lock, allowing mutation from multiple -threads without fear of data corruption. +Implementations of `List`, `Set`, and `Map` with strong guarantees around mutability. Each of these collections can be `snapshot` to reference their current values without reflecting future changes. A `StateFlow` of `snapshots` is accessible for receiving hot notifications of mutations. Returned collections and iterators automatically reference the `snapshot` of when they were created. -Use `SynchronizedMap.synchronize` when multiple read and write operations need to happen atomically, such as when -performing a `getOrPut`. - -```kotlin -// Creating a synchronized map -val map = SynchronizedMap(mutableMapOf("key" to "value")) -// Synchronize across multiple operations -val value = map.synchronized { getOrPut("key") { "defaultValue" } } -``` +These collections do not implement the various mutable collection interfaces. To mutate these collections, you must use an explicit mutator function. These mutator functions use a lambda to modify the list, and if concurrent mutations occur these lambdas may be ran more than once. In this way each mutation is guaranteed _atomic_, but you must be careful with side effects. +- `mutate` updates the collection without returning a value. +- `snapshotAndMutate` updates the collection and returns the snapshot which was used in the successful mutation. +- `mutateAndSnapshot` updates the collection and returns the snapshot which results from the mutation. ### [`Map.toJsObject`] diff --git a/collections/api/collections.api b/collections/api/collections.api index e2b1c7a6..cad6fc5e 100644 --- a/collections/api/collections.api +++ b/collections/api/collections.api @@ -1,3 +1,48 @@ +public final class com/juul/tuulbox/collections/AtomicList : java/util/List, kotlin/jvm/internal/markers/KMappedMarker { + public fun (Lkotlinx/collections/immutable/PersistentList;)V + public fun add (ILjava/lang/Object;)V + public fun add (Ljava/lang/Object;)Z + public fun addAll (ILjava/util/Collection;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public fun contains (Ljava/lang/Object;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun equals (Ljava/lang/Object;)Z + public fun get (I)Ljava/lang/Object; + public fun getSize ()I + public final fun getSnapshot ()Lkotlinx/collections/immutable/ImmutableList; + public final fun getSnapshots ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getState ()Lkotlinx/coroutines/flow/MutableStateFlow; + public fun hashCode ()I + public fun indexOf (Ljava/lang/Object;)I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public fun lastIndexOf (Ljava/lang/Object;)I + public fun listIterator ()Ljava/util/ListIterator; + public fun listIterator (I)Ljava/util/ListIterator; + public final fun mutate (Lkotlin/jvm/functions/Function1;)V + public final fun mutateAndSnapshot (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableList; + public fun remove (I)Ljava/lang/Object; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun replaceAll (Ljava/util/function/UnaryOperator;)V + public fun retainAll (Ljava/util/Collection;)Z + public fun set (ILjava/lang/Object;)Ljava/lang/Object; + public final fun size ()I + public final fun snapshotAndMutate (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableList; + public fun sort (Ljava/util/Comparator;)V + public synthetic fun subList (II)Ljava/util/List; + public fun subList (II)Lkotlinx/collections/immutable/ImmutableList; + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class com/juul/tuulbox/collections/AtomicListKt { + public static final fun atomicListOf ()Lcom/juul/tuulbox/collections/AtomicList; + public static final fun atomicListOf ([Ljava/lang/Object;)Lcom/juul/tuulbox/collections/AtomicList; +} + public final class com/juul/tuulbox/collections/AtomicMap : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { public fun (Lkotlinx/collections/immutable/PersistentMap;)V public fun clear ()V @@ -40,11 +85,47 @@ public final class com/juul/tuulbox/collections/AtomicMap : java/util/Map, kotli } public final class com/juul/tuulbox/collections/AtomicMapKt { + public static final fun atomicHashMapOf ()Lcom/juul/tuulbox/collections/AtomicMap; public static final fun atomicHashMapOf ([Lkotlin/Pair;)Lcom/juul/tuulbox/collections/AtomicMap; + public static final fun atomicMapOf ()Lcom/juul/tuulbox/collections/AtomicMap; public static final fun atomicMapOf ([Lkotlin/Pair;)Lcom/juul/tuulbox/collections/AtomicMap; public static final fun getOrPut (Lcom/juul/tuulbox/collections/AtomicMap;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; } +public final class com/juul/tuulbox/collections/AtomicSet : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker { + public fun (Lkotlinx/collections/immutable/PersistentSet;)V + public fun add (Ljava/lang/Object;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public fun contains (Ljava/lang/Object;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun equals (Ljava/lang/Object;)Z + public fun getSize ()I + public final fun getSnapshot ()Lkotlinx/collections/immutable/ImmutableSet; + public final fun getSnapshots ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getState ()Lkotlinx/coroutines/flow/MutableStateFlow; + public fun hashCode ()I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public final fun mutate (Lkotlin/jvm/functions/Function1;)V + public final fun mutateAndSnapshot (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableSet; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun retainAll (Ljava/util/Collection;)Z + public final fun size ()I + public final fun snapshotAndMutate (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableSet; + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class com/juul/tuulbox/collections/AtomicSetKt { + public static final fun atomicHashSetOf ()Lcom/juul/tuulbox/collections/AtomicSet; + public static final fun atomicHashSetOf ([Ljava/lang/Object;)Lcom/juul/tuulbox/collections/AtomicSet; + public static final fun atomicSetOf ()Lcom/juul/tuulbox/collections/AtomicSet; + public static final fun atomicSetOf ([Ljava/lang/Object;)Lcom/juul/tuulbox/collections/AtomicSet; +} + public final class com/juul/tuulbox/collections/SynchronizedMap : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { public fun ()V public fun (I)V diff --git a/collections/src/commonMain/kotlin/AtomicList.kt b/collections/src/commonMain/kotlin/AtomicList.kt new file mode 100644 index 00000000..98cffdf6 --- /dev/null +++ b/collections/src/commonMain/kotlin/AtomicList.kt @@ -0,0 +1,89 @@ +package com.juul.tuulbox.collections + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet + +/** Returns an empty [AtomicList]. */ +public fun atomicListOf(): AtomicList = AtomicList(persistentListOf()) + +/** Returns an [AtomicList]. */ +public fun atomicListOf( + vararg elements: E, +): AtomicList = AtomicList(persistentListOf(*elements)) + +/** + * A [List] that allows for thread safe, atomic mutation. Returned collections such as [iterator] and + * [subList] reference a [snapshot] of when they were accessed, and are not mutated when the list is. + * + * Although mutable, this class intentionally does not implement [MutableList]. Mutation must use + * designated mutator functions ([mutate], [snapshotAndMutate], [mutateAndSnapshot]). + */ +public class AtomicList private constructor( + @PublishedApi + internal val state: MutableStateFlow>, +) : List { + + /** Construct an [AtomicList] with [initial] list. */ + public constructor(initial: PersistentList) : this(MutableStateFlow(initial)) + + /** Returns this list as a [StateFlow]. Each mutation will cause a new emission on this flow. */ + public val snapshots: StateFlow> = state.asStateFlow() + + /** + * Returns the current value of this List as an [immutable][ImmutableList] snapshot. + * + * This operation is non-copying and efficient. + */ + public val snapshot: ImmutableList + get() = snapshots.value + + override val size: Int + get() = snapshot.size + + override fun get(index: Int): E = snapshot[index] + + override fun isEmpty(): Boolean = snapshot.isEmpty() + + override fun iterator(): Iterator = snapshot.iterator() + + override fun listIterator(): ListIterator = snapshot.listIterator() + + override fun listIterator(index: Int): ListIterator = snapshot.listIterator(index) + + override fun subList(fromIndex: Int, toIndex: Int): ImmutableList = snapshot.subList(fromIndex, toIndex) + + override fun lastIndexOf(element: E): Int = snapshot.lastIndexOf(element) + + override fun indexOf(element: E): Int = snapshot.indexOf(element) + + override fun containsAll(elements: Collection): Boolean = snapshot.containsAll(elements) + + override fun contains(element: E): Boolean = snapshot.contains(element) + + override fun equals(other: Any?): Boolean = snapshot == other + + override fun hashCode(): Int = snapshot.hashCode() + + override fun toString(): String = snapshot.toString() + + /** Mutates this List atomically. [mutator] can be evaluated multiple times if a concurrent edit occurs. */ + public inline fun mutate(mutator: MutableList.() -> Unit) { + state.update { it.mutate(mutator) } + } + + /** Mutates this List atomically and returns the previous [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */ + public inline fun snapshotAndMutate(mutator: MutableList.() -> Unit): ImmutableList = + state.getAndUpdate { it.mutate(mutator) } + + /** Mutates this List atomically and returns the new [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */ + public inline fun mutateAndSnapshot(mutator: MutableList.() -> Unit): ImmutableList = + state.updateAndGet { it.mutate(mutator) } +} diff --git a/collections/src/commonMain/kotlin/AtomicMap.kt b/collections/src/commonMain/kotlin/AtomicMap.kt index 0ae76055..3728cd26 100644 --- a/collections/src/commonMain/kotlin/AtomicMap.kt +++ b/collections/src/commonMain/kotlin/AtomicMap.kt @@ -14,11 +14,17 @@ import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet +/** Returns an empty [AtomicMap] that guarantees preservation of iteration order, but may be slower. */ +public fun atomicMapOf(): AtomicMap = AtomicMap(persistentMapOf()) + /** Returns an [AtomicMap] that guarantees preservation of iteration order, but may be slower. */ public fun atomicMapOf( vararg pairs: Pair, ): AtomicMap = AtomicMap(persistentMapOf(*pairs)) +/** Returns an empty [AtomicMap] that does not guarantee preservation of iteration order, but may be faster. */ +public fun atomicHashMapOf(): AtomicMap = AtomicMap(persistentHashMapOf()) + /** Returns an [AtomicMap] that does not guarantee preservation of iteration order, but may be faster. */ public fun atomicHashMapOf( vararg pairs: Pair, diff --git a/collections/src/commonMain/kotlin/AtomicSet.kt b/collections/src/commonMain/kotlin/AtomicSet.kt new file mode 100644 index 00000000..b3c3011c --- /dev/null +++ b/collections/src/commonMain/kotlin/AtomicSet.kt @@ -0,0 +1,86 @@ +package com.juul.tuulbox.collections + +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentHashSetOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet + +/** Returns an empty [AtomicSet] that guarantees preservation of iteration order, but may be slower. */ +public fun atomicSetOf(): AtomicSet = AtomicSet(persistentSetOf()) + +/** Returns an [AtomicSet] that guarantees preservation of iteration order, but may be slower. */ +public fun atomicSetOf( + vararg elements: E, +): AtomicSet = AtomicSet(persistentSetOf(*elements)) + +/** Returns an empty [AtomicSet] that does not guarantee preservation of iteration order, but may be faster. */ +public fun atomicHashSetOf(): AtomicSet = AtomicSet(persistentHashSetOf()) + +/** Returns an [AtomicSet] that does not guarantee preservation of iteration order, but may be faster. */ +public fun atomicHashSetOf( + vararg elements: E, +): AtomicSet = AtomicSet(persistentHashSetOf(*elements)) + +/** + * A [Set] that allows for thread safe, atomic mutation. Returned [iterator] references a [snapshot] + * of when this was accessed, and is not mutated when the set is. + * + * Although mutable, this class intentionally does not implement [MutableSet]. Mutation must use + * designated mutator functions ([mutate], [snapshotAndMutate], [mutateAndSnapshot]). + */ +public class AtomicSet private constructor( + @PublishedApi + internal val state: MutableStateFlow>, +) : Set { + + /** Construct an [AtomicSet] with [initial] set. */ + public constructor(initial: PersistentSet) : this(MutableStateFlow(initial)) + + /** Returns this set as a [StateFlow]. Each mutation will cause a new emission on this flow. */ + public val snapshots: StateFlow> = state.asStateFlow() + + /** + * Returns the current value of this set as an [immutable][ImmutableSet] snapshot. + * + * This operation is non-copying and efficient. + */ + public val snapshot: ImmutableSet + get() = snapshots.value + + override val size: Int + get() = snapshot.size + + override fun containsAll(elements: Collection): Boolean = snapshot.containsAll(elements) + + override fun contains(element: E): Boolean = snapshot.contains(element) + + override fun isEmpty(): Boolean = snapshot.isEmpty() + + override fun iterator(): Iterator = snapshot.iterator() + + override fun equals(other: Any?): Boolean = snapshot == other + + override fun hashCode(): Int = snapshot.hashCode() + + override fun toString(): String = snapshot.toString() + + /** Mutates this set atomically. [mutator] can be evaluated multiple times if a concurrent edit occurs. */ + public inline fun mutate(mutator: MutableSet.() -> Unit) { + state.update { it.mutate(mutator) } + } + + /** Mutates this set atomically and returns the previous [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */ + public inline fun snapshotAndMutate(mutator: MutableSet.() -> Unit): ImmutableSet = + state.getAndUpdate { it.mutate(mutator) } + + /** Mutates this set atomically and returns the new [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */ + public inline fun mutateAndSnapshot(mutator: MutableSet.() -> Unit): ImmutableSet = + state.updateAndGet { it.mutate(mutator) } +}