Skip to content

Commit

Permalink
Add weak provider subscriber and unregistering functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
NichtStudioCode committed Jul 31, 2024
1 parent ea07e14 commit 9e7d679
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import java.util.*
*/
abstract class AbstractProvider<T> : MutableProvider<T> {

protected var children: MutableSet<Provider<*>>? = null
private var subscribers: MutableList<(T) -> Unit>? = null
protected open var children: MutableSet<Provider<*>>? = null
protected open var subscribers: MutableList<(T) -> Unit>? = null
protected open var weakSubscribers: MutableMap<Any, MutableList<(Any, T) -> Unit>>? = null

private var isInitialized = false
protected var value: T? = null
protected open var isInitialized = false
protected open var value: T? = null

/**
* Retrieves the value of this [Provider].
Expand All @@ -31,7 +32,7 @@ abstract class AbstractProvider<T> : MutableProvider<T> {
override fun update() {
// lazy approach: only load value if it is actually needed (i.e. subscribers are present),
// otherwise just unset the value and load on next get() call
if (subscribers.isNullOrEmpty()) {
if (subscribers.isNullOrEmpty() && weakSubscribers.isNullOrEmpty()) {
isInitialized = false
value = null
children?.forEach { it.update() }
Expand All @@ -51,7 +52,7 @@ abstract class AbstractProvider<T> : MutableProvider<T> {
* @param callSubscribers Whether the update handlers of this [Provider] should be called.
* @param ignoredChildren A set of children that should not be updated.
*/
protected open fun set(
private fun set(
value: T,
updateChildren: Boolean = true,
callSubscribers: Boolean = true,
Expand All @@ -70,8 +71,10 @@ abstract class AbstractProvider<T> : MutableProvider<T> {
}
}

if (callSubscribers)
if (callSubscribers) {
subscribers?.forEach { it.invoke(value) }
weakSubscribers?.forEach { (owner, subs) -> subs.forEach { it(owner, value) } }
}
}

override fun addChild(provider: Provider<*>) {
Expand All @@ -88,6 +91,40 @@ abstract class AbstractProvider<T> : MutableProvider<T> {
subscribers!!.add(action)
}

@Suppress("UNCHECKED_CAST")
override fun <R : Any> subscribeWeak(owner: R, action: (R, T) -> Unit) {
if (weakSubscribers == null)
weakSubscribers = WeakHashMap(1)

weakSubscribers!!
.getOrPut(owner) { ArrayList(1) }
.add(action as (Any, T) -> Unit)
}

override fun removeChild(provider: Provider<*>) {
children?.remove(provider)
}

override fun unsubscribe(action: Function1<T, Unit>) {
subscribers?.remove(action)
}

override fun <R : Any> unsubscribeWeak(owner: R, action: Function2<R, T, Unit>) {
val weakSubscribers = weakSubscribers
?: return
val list = weakSubscribers[owner]
?: return

list.remove(action)

if (list.isEmpty())
weakSubscribers.remove(owner)
}

override fun <R : Any> unsubscribeWeak(owner: R) {
weakSubscribers?.remove(owner)
}

protected abstract fun loadValue(): T

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,40 @@ interface Provider<out T> : Supplier<@UnsafeVariance T> {

/**
* Registers a child [Provider] that will be updated via [Provider.update] when the value of this [Provider] changes.
* Children are stored as weak references and will be automatically removed when they are garbage collected.
*/
fun addChild(provider: Provider<*>)

/**
* Registers a subscriber that will be called when the value of this [Provider] changes.
*/
fun subscribe(action: (T) -> Unit)
fun subscribe(action: (value: T) -> Unit)

/**
* Registers a weak subscriber that will be called when the value of this [Provider] changes.
* The subscriber will be automatically removed when the [owner] is garbage collected.
*/
fun <R : Any> subscribeWeak(owner: R, action: (owner: R, value: T) -> Unit)

/**
* Removes a previously registered child [Provider].
*/
fun removeChild(provider: Provider<*>)

/**
* Removes a previously registered subscriber.
*/
fun unsubscribe(action: Function1<T, Unit>)

/**
* Removes a previously registered weak subscriber.
*/
fun <R : Any> unsubscribeWeak(owner: R, action: Function2<R, T, Unit>)

/**
* Removes all weak subscribers under the given [owner].
*/
fun <R : Any> unsubscribeWeak(owner: R)

operator fun <X> getValue(thisRef: X?, property: KProperty<*>?): T = get()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ fun <T> mutableProvider(initialValue: T): MutableProvider<T> =
fun <T> mutableProvider(loadValue: () -> T, setValue: (T) -> Unit = {}): MutableProvider<T> =
object : AbstractProvider<T>() {
override fun loadValue(): T = loadValue()
override fun set(value: T, updateChildren: Boolean, callSubscribers: Boolean, ignoredChildren: Set<Provider<*>>) {
super.set(value, updateChildren, callSubscribers, ignoredChildren)
override fun set(value: T, ignoredChildren: Set<Provider<*>>) {
super.set(value, ignoredChildren)
setValue(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ private class MutableFallbackValueProvider<T>(
return provider.get() ?: fallback
}

override fun set(value: T, updateChildren: Boolean, callSubscribers: Boolean, ignoredChildren: Set<Provider<*>>) {
super.set(value, updateChildren, callSubscribers, ignoredChildren)
override fun set(value: T, ignoredChildren: Set<Provider<*>>) {
super.set(value, ignoredChildren)
provider.set((if (value == fallback) null else value) as T, setOf(this))
}

Expand All @@ -82,8 +82,8 @@ private class MutableFallbackProviderProvider<T : Any>(
return provider.get() ?: fallback.get()
}

override fun set(value: T, updateChildren: Boolean, callSubscribers: Boolean, ignoredChildren: Set<Provider<*>>) {
super.set(value, updateChildren, callSubscribers, ignoredChildren)
override fun set(value: T, ignoredChildren: Set<Provider<*>>) {
super.set(value, ignoredChildren)
provider.set(if (value == fallback.get()) null else value, setOf(this))
}

Expand All @@ -98,8 +98,8 @@ private class MutableNullableFallbackProviderProvider<T>(
return provider.get() ?: fallback.get()
}

override fun set(value: T?, updateChildren: Boolean, callSubscribers: Boolean, ignoredChildren: Set<Provider<*>>) {
super.set(value, updateChildren, callSubscribers, ignoredChildren)
override fun set(value: T?, ignoredChildren: Set<Provider<*>>) {
super.set(value, ignoredChildren)

val fallbackValue = fallback.get()
when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ private class MutableMappingProvider<T, R>(
return transform(parent.get())
}

override fun set(value: R, updateChildren: Boolean, callSubscribers: Boolean, ignoredChildren: Set<Provider<*>>) {
super.set(value, updateChildren, callSubscribers, ignoredChildren)
override fun set(value: R, ignoredChildren: Set<Provider<*>>) {
super.set(value, ignoredChildren)
parent.set(untransform(value), setOf(this))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package xyz.xenondevs.commons.provider

import org.junit.jupiter.api.Test
import xyz.xenondevs.commons.provider.immutable.map
import xyz.xenondevs.commons.provider.immutable.provider
import kotlin.test.assertEquals

class ProviderTest {
Expand All @@ -10,9 +11,7 @@ class ProviderTest {
fun testProviderPropagate() {
var value = 1

val top: Provider<Int> = object : AbstractProvider<Int>() {
override fun loadValue(): Int = value
}
val top: Provider<Int> = provider { value }

val branchA = top.map { it * 10 }
val branchB = top.map { it * 100 }
Expand All @@ -36,28 +35,92 @@ class ProviderTest {
assertEquals(leafB.get(), 201)
}

@Test
fun testProviderRemoveChild() {
var value = 1
val top: Provider<Int> = provider { value }
val middle = top.map { it + 1 }
val bottom = middle.map { it + 1 }

assertEquals(3, bottom.get())

value = 2
top.update()

assertEquals(4, bottom.get())

top.removeChild(middle)
value = 3
top.update()

assertEquals(4, bottom.get())

middle.update()

assertEquals(5, bottom.get())
}

@Test
fun testProviderSubscriber() {
var value = 0
var invoked = false
var invokedWeak = false

val provider: Provider<Int> = object : AbstractProvider<Int>() {
override fun loadValue(): Int = value
}
val provider: Provider<Int> = provider { value }
provider.subscribe { invoked = true }
provider.subscribeWeak(this) { _, _ -> invokedWeak = true }

// initializing should not call update handler
provider.get()
assert(!invoked)
assert(!invokedWeak)

// updating without changes to value should not call update handler
provider.update()
assert(!invoked)
assert(!invokedWeak)

// updating with change to value should call update handler
value = 1
provider.update()
assert(invoked)
assert(invokedWeak)
}

@Test
fun testProviderRemoveSubscriber() {
var value = 0
var mirror1 = -1
var mirror2 = -1
var mirror3 = -1

val provider: Provider<Int> = provider { value }
val subscriber1: (Int) -> Unit = { mirror1 = it }
val subscriber2: (Any, Int) -> Unit = { _, v -> mirror2 = v }
val subscriber3: (Any, Int) -> Unit = { _, v -> mirror3 = v }
provider.subscribe(subscriber1)
provider.subscribeWeak(this, subscriber2)
provider.subscribeWeak(this, subscriber3)

provider.update()
assertEquals(0, mirror1)
assertEquals(0, mirror2)
assertEquals(0, mirror3)

value = 1
provider.unsubscribe(subscriber1)
provider.unsubscribeWeak(this, subscriber2)
provider.update()
assertEquals(0, mirror1)
assertEquals(0, mirror2)
assertEquals(1, mirror3)

value = 2
provider.unsubscribeWeak(this)
provider.update()
assertEquals(0, mirror1)
assertEquals(0, mirror2)
assertEquals(1, mirror3)
}

}

0 comments on commit 9e7d679

Please sign in to comment.