Skip to content

Commit

Permalink
Merge branch 'main' into twyatt/binary-compatibility-validator
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt authored Mar 1, 2024
2 parents c2306dc + f0ada12 commit 44d1304
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 42 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ jobs:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/wrapper-validation-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: assemble
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: assemble

- name: check
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: check

Expand All @@ -36,15 +36,15 @@ jobs:
retention-days: 5

- name: publishToMavenLocal
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: |
-PVERSION_NAME=unspecified
-PRELEASE_SIGNING_ENABLED=false
publishToMavenLocal
- name: jacocoTestReport
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: jacocoTestReport

Expand All @@ -53,4 +53,4 @@ jobs:
junit_files: '**/build/test-results/**/*.xml'
report_individual_runs: 'true'

- uses: codecov/codecov-action@v3
- uses: codecov/codecov-action@v4
4 changes: 2 additions & 2 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/wrapper-validation-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: dokkaHtmlMultiModule
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: dokkaHtmlMultiModule

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/wrapper-validation-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: check
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: check

- name: publish
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
arguments: |
-PVERSION_NAME=${{ github.ref_name }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`]

Expand Down
128 changes: 128 additions & 0 deletions collections/api/collections.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,131 @@
public final class com/juul/tuulbox/collections/AtomicList : java/util/List, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (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 <init> (Lkotlinx/collections/immutable/PersistentMap;)V
public fun clear ()V
public fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;
public fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun containsKey (Ljava/lang/Object;)Z
public fun containsValue (Ljava/lang/Object;)Z
public synthetic fun entrySet ()Ljava/util/Set;
public final fun entrySet ()Lkotlinx/collections/immutable/ImmutableSet;
public fun equals (Ljava/lang/Object;)Z
public fun get (Ljava/lang/Object;)Ljava/lang/Object;
public fun getEntries ()Lkotlinx/collections/immutable/ImmutableSet;
public fun getKeys ()Lkotlinx/collections/immutable/ImmutableSet;
public fun getSize ()I
public final fun getSnapshot ()Lkotlinx/collections/immutable/ImmutableMap;
public final fun getSnapshots ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getState ()Lkotlinx/coroutines/flow/MutableStateFlow;
public fun getValues ()Lkotlinx/collections/immutable/ImmutableCollection;
public fun hashCode ()I
public fun isEmpty ()Z
public synthetic fun keySet ()Ljava/util/Set;
public final fun keySet ()Lkotlinx/collections/immutable/ImmutableSet;
public fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public final fun mutate (Lkotlin/jvm/functions/Function1;)V
public final fun mutateAndSnapshot (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableMap;
public fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun putAll (Ljava/util/Map;)V
public fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z
public fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z
public fun replaceAll (Ljava/util/function/BiFunction;)V
public final fun size ()I
public final fun snapshotAndMutate (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableMap;
public fun toString ()Ljava/lang/String;
public synthetic fun values ()Ljava/util/Collection;
public final fun values ()Lkotlinx/collections/immutable/ImmutableCollection;
}

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 <init> (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 <init> ()V
public fun <init> (I)V
Expand Down
8 changes: 7 additions & 1 deletion collections/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ kotlin {
iosSimulatorArm64()

sourceSets {
val commonMain by getting { }
val commonMain by getting {
dependencies {
api(libs.kotlinx.collections.immutable)
api(libs.kotlinx.coroutines.core)
}
}

val commonTest by getting {
dependencies {
implementation(libs.kotlinx.coroutines.test)
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
Expand Down
89 changes: 89 additions & 0 deletions collections/src/commonMain/kotlin/AtomicList.kt
Original file line number Diff line number Diff line change
@@ -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 <E> atomicListOf(): AtomicList<E> = AtomicList(persistentListOf())

/** Returns an [AtomicList]. */
public fun <E> atomicListOf(
vararg elements: E,
): AtomicList<E> = 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<E> private constructor(
@PublishedApi
internal val state: MutableStateFlow<PersistentList<E>>,
) : List<E> {

/** Construct an [AtomicList] with [initial] list. */
public constructor(initial: PersistentList<E>) : this(MutableStateFlow(initial))

/** Returns this list as a [StateFlow]. Each mutation will cause a new emission on this flow. */
public val snapshots: StateFlow<ImmutableList<E>> = 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<E>
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<E> = snapshot.iterator()

override fun listIterator(): ListIterator<E> = snapshot.listIterator()

override fun listIterator(index: Int): ListIterator<E> = snapshot.listIterator(index)

override fun subList(fromIndex: Int, toIndex: Int): ImmutableList<E> = 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<E>): 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<E>.() -> 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<E>.() -> Unit): ImmutableList<E> =
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<E>.() -> Unit): ImmutableList<E> =
state.updateAndGet { it.mutate(mutator) }
}
Loading

0 comments on commit 44d1304

Please sign in to comment.