Created
June 25, 2024 23:38
-
-
Save zach-klippenstein/ca1679a35594be9c6b8ac3493764d031 to your computer and use it in GitHub Desktop.
Revisions
-
zach-klippenstein created this gist
Jun 25, 2024 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,109 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.SnapshotMutationPolicy import androidx.compose.runtime.structuralEqualityPolicy /** * Returns a [MutableState] object that only accepts writes that happen [debounceMillis] after the * previously-accepted write. Writes that happen less than [debounceMillis] after a previous write * are ignored. * * Write times are determined using [System.currentTimeMillis]. * * When the value is written in multiple concurrent snapshots and a conflict occurs when applying, * the timestamp of the merged result will be the timestamp of the _later_ write, unless the values * are the same, in which case it will be the timestamp of the earlier write. */ fun <T> throttlingMutableStateOf( initialValue: T, debounceMillis: Long = 500L, mutationPolicy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> = ThrottlingMutableState(initialValue, debounceMillis, mutationPolicy) private class ThrottlingMutableState<T>( initialValue: T, private val debounceMillis: Long, private val mutationPolicy: SnapshotMutationPolicy<T> ) : MutableState<T>, StateObject { private var record = ThrottledRecord(initialValue) override var value: T get() = record.readable(this).value set(value) { val now = System.currentTimeMillis() // withCurrent: Don't notify write observers unless we actually update something. record.withCurrent { if ( // Timestamp comparison is constant time, so do it first. now - it.lastWriteMillis > debounceMillis && !mutationPolicy.equivalent(value, it.value) ) { record.writable(this) { it.value = value it.lastWriteMillis = now } } } } override val firstStateRecord: StateRecord get() = record override fun prependStateRecord(value: StateRecord) { @Suppress("UNCHECKED_CAST") record = value as ThrottledRecord<T> } override fun component1(): T = value override fun component2(): (T) -> Unit = { value = it } @Suppress("UNCHECKED_CAST") override fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { val previousRecord = previous as ThrottledRecord<T> val currentRecord = current as ThrottledRecord<T> val appliedRecord = applied as ThrottledRecord<T> if (mutationPolicy.equivalent(currentRecord.value, appliedRecord.value)) { // Always resolve merge with the earlier timestamp, since if the state were updated // twice to the same value in the same snapshot, only the earlier timestamp would be // recorded. return (appliedRecord.create() as ThrottledRecord<T>).also { it.value = appliedRecord.value it.lastWriteMillis = minOf(currentRecord.lastWriteMillis, appliedRecord.lastWriteMillis) } } val merged = mutationPolicy.merge( previous = previousRecord.value, current = currentRecord.value, applied = appliedRecord.value ) ?: return null // The timestamp of the merge is that of the latest record to be written, since the new // value represents the latest write that explicitly occurred. It should not be the time of // the actual merge since that's just the time the snapshot was applied, which does not // correspond to any explicit write. return (appliedRecord.create() as ThrottledRecord<T>).also { it.value = merged it.lastWriteMillis = maxOf(currentRecord.lastWriteMillis, appliedRecord.lastWriteMillis) } } private class ThrottledRecord<T>(var value: T) : StateRecord() { var lastWriteMillis = -1L override fun create(): StateRecord = ThrottledRecord(value) override fun assign(value: StateRecord) { @Suppress("UNCHECKED_CAST") value as ThrottledRecord<T> this.value = value.value this.lastWriteMillis = value.lastWriteMillis } } }