[go: nahoru, domu]

blob: 0785ddc9617853c5e79c5bfa0d81fe01463a449d [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
package androidx.compose.runtime.snapshots
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.ThreadLocal
/**
* Take a snapshot of the current value of all state objects. The values are preserved until
* [Snapshot.dispose] is called on the result.
*
* The [readObserver] parameter can be used to track when all state objects are read when in
* [Snapshot.enter]. A [SnapshotApplyObserver] can be registered using
* [Snapshot.registerApplyObserver] to observe modification of state objects.
*
* An active snapshot (after it is created but before [Snapshot.dispose] is called) requires
* resources to track the values in the snapshot. Once a snapshot is no longer needed it should
* disposed by calling [Snapshot.dispose].
*
* Leaving a snapshot active could cause hard to diagnose memory leaks values as are maintained
* by state objects for these unneeded snapshots. Take care to always call [Snapshot.dispose] on all
* snapshots when they are no longer needed.
*
* Composition uses both of these to implicitly subscribe to changes to state object and
* automatically update the composition when state objects read during composition change.
*
* A nested snapshot can be taken of a snapshot which is an independent read-only copy of the
* snapshot and can be disposed independently. This is used by [takeSnapshot] when in a read-only
* snapshot for API consistency allowing the result of [takeSnapshot] to be disposed leaving the
* parent snapshot active.
*
* @param readObserver called when any state object is read in the lambda passed to
* [Snapshot.enter] or in the [Snapshot.enter] of any nested snapshot.
*
* @see Snapshot
* @see Snapshot.registerApplyObserver
* @see SnapshotApplyObserver
*/
fun takeSnapshot(
readObserver: SnapshotReadObserver? = null
): Snapshot = currentSnapshot().takeNestedSnapshot(readObserver)
/**
* Take a snapshot of the current value of all state objects that also allows the state to be
* changed and later atomically applied when [MutableSnapshot.apply] is called. The values are
* preserved until [Snapshot.dispose] is called on the result. The global state will either see
* all the changes made as one atomic change, when [MutableSnapshot.apply] is called, or none of
* the changes if the mutable state object is disposed before being applied.
*
* The values in a snapshot can be modified by calling [Snapshot.enter] and then, in its lambda,
* modify any state object. The new values of the state objects will only become visible to the
* global state when [MutableSnapshot.apply] is called.
*
* An active snapshot (after it is created but before [Snapshot.dispose] is called) requires
* resources to track the values in the snapshot. Once a snapshot is no longer needed it should
* disposed by calling [Snapshot.dispose].
*
* Leaving a snapshot active could cause hard to diagnose memory leaks as values are maintained
* by state objects for these unneeded snapshots. Take care to always call [Snapshot.dispose] on all
* snapshots when they are no longer needed.
*
* A nested snapshot can be taken by calling [Snapshot.takeNestedSnapshot], for a read-only
* snapshot, or [MutableSnapshot.takeNestedMutableSnapshot] for a snapshot that can be changed.
* Nested mutable snapshots are applied to the this, the parent snapshot, when their
* [MutableSnapshot.apply] is called. Their applied changes will be visible to in this snapshot
* but will not be visible other snapshots (including other nested snapshots) or the global state
* until this snapshot is applied by calling [MutableSnapshot.apply].
*
* Once [MutableSnapshot.apply] is called on this, the parent snapshot, all calls to
* [MutableSnapshot.apply] on an active nested snapshot will fail.
*
* Changes to a mutable snapshot are isolated, using snapshot isolation, from all other snapshots.
* Their changes are only visible as global state or to new snapshots once [MutableSnapshot.apply]
* is called.
*
* Applying a snapshot can fail if currently visible changes to the state object conflicts with a
* change made in the snapshot.
*
* When in a mutable snapshot, [takeMutableSnapshot] creates a nested snapshot of the current mutable
* snapshot. If the current snapshot is read-only, an exception is thrown. The current snapshot
* is the result of calling [currentSnapshot] which is updated by calling [Snapshot.enter] which
* makes the [Snapshot] the current snapshot while in its lambda.
*
* Composition uses mutable snapshots to allow changes made in a [Composable] functions to be
* temporarily isolated from the global state and is later applied to the global state when the
* composition is applied. If [MutableSnapshot.apply] fails applying this snapshot, the snapshot
* and the changes calculated during composition are disposed and a new composition is scheduled
* to be calculated again.
*
* @param readObserver called when any state object is read in the lambda passed to
* [Snapshot.enter] or in the [Snapshot.enter] of any nested snapshots.
*
* Composition, layout and draw use [readObserver] to implicitly subscribe to changes to state
* objects to know when to update.
*
* @param writeObserver called when a state object is created or just before it is written to the
* first time in the snapshot or a nested mutable snapshot. This might be called several times
* for the same object if nested mutable snapshots are created.
*
* Composition uses [writeObserver] to track when a state object is modified during composition
* in order to invalidate the reads that have not yet occurred. This allows a single pass of
* composition for state objects that are written to before they are read (such as modifying the
* value of a dynamic ambient provider).
*
* @see takeSnapshot
* @see Snapshot
* @see MutableSnapshot
*/
@ExperimentalComposeApi
fun takeMutableSnapshot(
readObserver: SnapshotReadObserver? = null,
writeObserver: SnapshotWriteObserver? = null
): MutableSnapshot =
(currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(readObserver, writeObserver)
?: error("Cannot create a mutable snapshot of an read-only snapshot")
/**
* A snapshot of the values return by mutable states and other state objects. All state object
* will have the same value in the snapshot as they had when the snapshot was created unless they
* are explicitly changed in the snapshot.
*
* To enter a snapshot call [enter]. The snapshot is the current snapshot as returned by
* [currentSnapshot] until the control returns from the lambda (or until a nested [enter] is
* called). All state objects will return the values associated with this snapshot, locally in the
* thread, until [enter] returns. All other threads are unaffected.
*
* Snapshots can be nested by calling [takeNestedSnapshot].
*
* @see takeSnapshot
* @see takeMutableSnapshot
* @see androidx.compose.runtime.mutableStateOf
* @see androidx.compose.runtime.mutableStateListOf
* @see androidx.compose.runtime.mutableStateMapOf
*/
@ExperimentalComposeApi
sealed class Snapshot(
id: Int,
invalid: SnapshotIdSet
) {
/**
* The snapshot id of the snapshot. This is a unique number from a monotonically increasing
* value for each snapshot taken.
*/
open var id: Int = id
internal set
/**
* The root snapshot for this snapshot. For non-nested snapshots this is always `this`. For
* nested snapshot it is the parent's [root].
*/
abstract val root: Snapshot
/**
* True if any change to a state object in this snapshot will throw.
*/
abstract val readonly: Boolean
/**
* Dispose the snapshot. Neglecting to dispose a snapshot will result in difficult to
* diagnose memory leaks as it indirectly causes all state objects to maintain its value for
* the un-disposed snapshot.
*/
open fun dispose() {
disposed = true
}
/**
* Take a snapshot of the state values in this snapshot. The resulting [Snapshot] is read-only.
* All nested snapshots need to be disposed by calling [dispose] before resources associated
* with this snapshot can be collected. Nested snapshots are still valid after the parent has
* been disposed.
*/
abstract fun takeNestedSnapshot(readObserver: SnapshotReadObserver? = null): Snapshot
/**
* Whether there are any pending changes in this snapshot. These changes are not visible
* until the snapshot is applied.
*/
abstract fun hasPendingChanges(): Boolean
/**
* Enter the snapshot. In [block] all state objects have the value associated with this
* snapshot. The value of [currentSnapshot] will be this snapshot until this [block] returns
* or a nested call to [enter] is called. When [block] returns, the previous current snapshot
* is restored if there was one.
*
* All changes to state object inside [block] are isolated to this snapshot and are not
* visible to other snapshot or as global state. If this is a [readonly] snapshot, any
* changes to state objects will throw an [IllegalStateException].
*
* For a [MutableSnapshot], changes made to a snapshot inside [block] can be applied
* atomically to the global state (or to its parent snapshot if it is a nested snapshot) by
* calling [MutableSnapshot.apply].
*
* @see androidx.compose.runtime.mutableStateOf
* @see androidx.compose.runtime.mutableStateListOf
* @see androidx.compose.runtime.mutableStateMapOf
*/
inline fun <T> enter(block: () -> T): T {
val previous = makeCurrent()
try {
return block()
} finally {
restoreCurrent(previous)
}
}
@PublishedApi
internal open fun makeCurrent(): Snapshot? {
val previous = threadSnapshot.get()
threadSnapshot.set(this)
return previous
}
@PublishedApi
internal open fun restoreCurrent(snapshot: Snapshot?) {
threadSnapshot.set(snapshot)
}
internal var disposed = false
/*
* The read observer for the snapshot if there is one.
*/
internal abstract val readObserver: SnapshotReadObserver?
/**
* The write observer for the snapshot if there is one.
*/
internal abstract val writeObserver: SnapshotWriteObserver?
/**
* A set of all the snapshots that should be treated as invalid.
*/
internal open var invalid: SnapshotIdSet = invalid
internal set
/**
* Called when a nested snapshot of this snapshot is activated
*/
internal abstract fun nestedActivated(snapshot: Snapshot)
/**
* Called when a nested snapshot of this snapshot is deactivated
*/
internal abstract fun nestedDeactivated(snapshot: Snapshot)
/**
* Record that state was modified in the snapshot.
*/
internal abstract fun recordModified(state: StateObject)
/**
* The set of state objects that have been modified in this snapshot.
*/
internal abstract val modified: MutableSet<StateObject>?
/**
* Notify the snapshot that all objects created in this snapshot to this point should be
* considered initialized. If any state object is are modified passed this point it will
* appear as modified in the snapshot and any applicable [SnapshotWriteObserver] will be
* called for the object and the object will be part of the a set of mutated objects sent to
* any applicable [SnapshotApplyObserver].
*
* Unless [notifyObjectsInitialized] is called, state objects created in a snapshot are not
* considered modified by the snapshot even if they are modified after construction.
*/
internal abstract fun notifyObjectsInitialized()
/**
* Closes the snapshot by removing the snapshot id (an any previous id's) from the list of
* open snapshots.
*/
internal open fun close() {
sync {
openSnapshots = openSnapshots.clear(id)
}
}
internal fun validateNotDisposed() {
require(!disposed) { "Cannot use a disposed snapshot" }
}
companion object {
/**
* Return the thread's active snapshot. If no thread snapshot is active then the current
* global snapshot is used.
*/
val current get() = currentSnapshot()
/**
* Escape the current snapshot, if there is one. All state objects will have the value
* associated with the global while the [block] lambda is executing.
*
* @return the result of [block]
*/
inline fun <T> global(block: () -> T): T {
val previous = removeCurrent()
return block().also { restoreCurrent(previous) }
}
/**
* Observe reads and or write of state objects in the current thread.
*
* This only affects the current snapshot (if any) and any new snapshots create from
* [takeSnapshot] and [takeMutableSnapshot]. It will not affect any snapshots previous
* created even if [Snapshot.enter] is called in [block].
*
* @param readObserver called when any state object is read.
* @param writeObserver called when a state object is created or just before it is
* written to the first time in the snapshot or a nested mutable snapshot. This might be
* called several times for the same object if nested mutable snapshots are created.
*/
fun <T> observe(
readObserver: SnapshotReadObserver? = null,
writeObserver: SnapshotWriteObserver? = null,
block: () -> T
): T {
if (readObserver != null || writeObserver != null) {
val currentSnapshot = threadSnapshot.get()
val snapshot =
if (currentSnapshot == null || currentSnapshot is MutableSnapshot)
TransparentObserverMutableSnapshot(
currentSnapshot as? MutableSnapshot,
readObserver,
writeObserver
)
else if (readObserver == null) return block()
else currentSnapshot.takeNestedSnapshot(readObserver)
try {
return snapshot.enter(block)
} finally {
snapshot.dispose()
}
} else return block()
}
/**
* Register an apply listener that is called back when snapshots are applied to the
* global state.
*
* @return a lambda that, when called, unregisters [observer].
*/
fun registerApplyObserver(observer: SnapshotApplyObserver): () -> Unit {
// Ensure observer does not see changes before this call.
advanceGlobalSnapshot(emptyLambda)
sync {
applyObservers.add(observer)
}
return {
sync {
applyObservers.remove(observer)
}
}
}
/**
* Register an observer of the first write to the global state of a global state object
* since the last call to [sendApplyNotifications].
*
* Composition uses this to schedule a new composition whenever a state object that
* was read in composition is modified.
*
* State objects can be sent to the apply observer that have not been sent to global write
* observers. This happens for state objects inside [MutableSnapshot] that is later
* applied by calling [MutableSnapshot.apply].
*
* This should only be used to determine if a call to [sendApplyNotifications] should be
* scheduled to be called.
*
* @return a lambda that, when called, unregisters [observer].
*/
@ExperimentalComposeApi
fun registerGlobalWriteObserver(observer: SnapshotWriteObserver): () -> Unit {
sync {
globalWriteObservers.add(observer)
}
advanceGlobalSnapshot()
return {
sync {
globalWriteObservers.remove(observer)
}
advanceGlobalSnapshot()
}
}
/**
* Notify the snapshot that all objects created in this snapshot to this point should be
* considered initialized. If any state object is are modified passed this point it will
* appear as modified in the snapshot and any applicable [SnapshotWriteObserver] will be
* called for the object and the object will be part of the a set of mutated objects sent to
* any applicable [SnapshotApplyObserver].
*
* Unless [notifyObjectsInitialized] is called, state objects created in a snapshot are not
* considered modified by the snapshot even if they are modified after construction.
*
* Compose uses this between phases of composition to allow observing changes to state
* objects create in a previous phase.
*/
@ExperimentalComposeApi
fun notifyObjectsInitialized() = currentSnapshot().notifyObjectsInitialized()
/**
* Send any pending apply notifications for state objects changed outside a snapshot.
*
* Apply notifications for state objects modified outside snapshot are deferred until method
* is called. This method is implicitly called whenever a non-nested [MutableSnapshot]
* is applied making its changes visible to all new, non-nested snapshots.
*
* Composition schedules this to be called after changes to state objects are
* detected an observer registered with [registerGlobalWriteObserver].
*/
@ExperimentalComposeApi
fun sendApplyNotifications() {
val changes = sync {
currentGlobalSnapshot.modified?.isNotEmpty() == true
}
if (changes)
advanceGlobalSnapshot()
}
@InternalComposeApi
fun openSnapshotCount() = openSnapshots.toList().size
@PublishedApi
internal fun removeCurrent(): Snapshot? {
val previous = threadSnapshot.get()
if (previous != null) threadSnapshot.set(null)
return previous
}
@PublishedApi
internal fun restoreCurrent(previous: Snapshot?) {
if (previous != null) threadSnapshot.set(previous)
}
}
}
/**
* A snapshot of the values return by mutable states and other state objects. All state object
* will have the same value in the snapshot as they had when the snapshot was created unless they
* are explicitly changed in the snapshot.
* To enter a snapshot call [enter]. The snapshot is the current snapshot as returned by
* [currentSnapshot] until the control returns from the lambda (or until a nested [enter] is
* called. All state objects will return the values associated with this snapshot, locally in the
* thread, until [enter] returns. All other threads are unaffected.
*
* All changes made in a [MutableSnapshot] are snapshot isolated from all other snapshots and
* their changes can only be seen globally, or by new shots, after [MutableSnapshot.apply] as been
* called.
*
* Snapshots can be nested by calling [takeNestedSnapshot] or
* [MutableSnapshot.takeNestedMutableSnapshot].
*
* @see takeMutableSnapshot
* @see androidx.compose.runtime.mutableStateOf
* @see androidx.compose.runtime.mutableStateListOf
* @see androidx.compose.runtime.mutableStateMapOf
*/
@ExperimentalComposeApi
open class MutableSnapshot internal constructor(
id: Int,
invalid: SnapshotIdSet,
override val readObserver: SnapshotReadObserver?,
override val writeObserver: SnapshotWriteObserver?
) : Snapshot(id, invalid) {
/**
* Whether there are any pending changes in this snapshot. These changes are not visible
* until the snapshot is applied.
*/
override fun hasPendingChanges(): Boolean = modified?.isNotEmpty() == true
/**
* Take a mutable snapshot of the state values in this snapshot. Entering this snapshot by
* calling [enter] allows state objects to be modified that are not visible to the this, the
* parent snapshot, until the [apply] is called.
*
* Applying a nested snapshot, by calling [apply], applies its change to, this, the parent
* snapshot. For a change to be visible globally, all the parent snapshots need to be applied
* until the root snapshot is applied to the global state.
*
* All nested snapshots need to be disposed by calling [dispose] before resources associated
* with this snapshot can be collected. Nested active snapshots are still valid after the parent
* has been disposed but calling [apply] will fail.
*/
open fun takeNestedMutableSnapshot(
readObserver: SnapshotReadObserver? = null,
writeObserver: SnapshotWriteObserver? = null
): MutableSnapshot {
validateNotDisposed()
validateNotApplied()
return advance {
sync {
val newId = nextSnapshotId++
openSnapshots = openSnapshots.set(newId)
val invalid = invalid
this.invalid = invalid.set(newId)
NestedMutableSnapshot(
newId,
invalid,
mergedReadObserver(readObserver, this.readObserver),
mergedWriteObserver(writeObserver, this.writeObserver),
this
)
}
}
}
/**
* Apply the changes made to state objects in this snapshot to the global state, or to the
* parent snapshot if this is a nested mutable snapshot.
*
* Once this method returns all changes made to this snapshot are atomically visible as the
* global state of the state object or to the parent snapshot.
*
* While a snapshot is active (after it is created but before [apply] or [dispose] is called)
* requires resources to track the values in the snapshot. Once a snapshot is no longer
* needed it should be either applied by calling [apply] or disposed by calling [dispose]. A
* snapshot that has been had is [apply] called can also have [dispose] called on it. However,
* calling [apply] after calling [dispose] will throw an exception.
*
* Leaving a snapshot active could cause hard to diagnose memory leaks values are maintained
* by state objects for unneeded snapshots. Take care to always call [dispose] on any snapshot.
*/
open fun apply(): SnapshotApplyResult {
// NOTE: the this algorithm is currently does not guarantee serializable snapshots as it
// doesn't prevent crossing writes as described here https://arxiv.org/pdf/1412.2324.pdf
// Just removing the snapshot from the active snapshot set is enough to make it part of the
// next snapshot, however, this should only be done after first determining that there are no
// colliding writes are being applied.
// A write is considered colliding if any write occurred in a state object in a snapshot
// applied since the snapshot was taken.
val modified = modified
val optimisticMerges = if (modified != null) optimisticMerges(
currentGlobalSnapshot,
this,
openSnapshots.clear(currentGlobalSnapshot.id)
) else null
val (observers, globalModified) = sync {
validateOpen(this)
if (modified == null || modified.size == 0) {
close()
val previousGlobalSnapshot = currentGlobalSnapshot
takeNewGlobalSnapshot(previousGlobalSnapshot, emptyLambda)
val globalModified = previousGlobalSnapshot.modified
if (globalModified != null && globalModified.isNotEmpty())
applyObservers.toList() to globalModified
else
emptyList<SnapshotApplyObserver>() to null
} else {
val previousGlobalSnapshot = currentGlobalSnapshot
val result = innerApply(
nextSnapshotId,
optimisticMerges,
openSnapshots.clear(previousGlobalSnapshot.id)
)
if (result != SnapshotApplyResult.Success) return result
// Close this snapshot
close()
// Take a new global snapshot that includes this one.
takeNewGlobalSnapshot(previousGlobalSnapshot, emptyLambda)
val globalModified = previousGlobalSnapshot.modified
this.modified = null
previousGlobalSnapshot.modified = null
applyObservers.toList() to globalModified
}
}
// Mark as applied
applied = true
// Notify any apply observers that changes applied were seen
if (globalModified != null && globalModified.isNotEmpty()) {
observers.fastForEach {
it(globalModified, this)
}
}
if (modified != null && modified.isNotEmpty()) {
observers.fastForEach {
it(modified, this)
}
}
return SnapshotApplyResult.Success
}
override val readonly: Boolean get() = false
override val root: Snapshot get() = this
override fun dispose() {
if (!disposed) {
super.dispose()
nestedDeactivated(this)
}
}
override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot {
validateNotDisposed()
validateNotApplied()
return advance {
sync {
val readonlyId = nextSnapshotId++
openSnapshots = openSnapshots.set(readonlyId)
NestedReadonlySnapshot(
readonlyId,
invalid,
readObserver,
this
)
}
}
}
override fun nestedActivated(snapshot: Snapshot) { snapshots++ }
override fun nestedDeactivated(snapshot: Snapshot) {
require(snapshots > 0)
if (--snapshots == 0) {
if (!applied) {
abandon()
}
}
}
override fun notifyObjectsInitialized() {
advance()
}
override fun close() {
sync {
// Remove itself and previous ids from the open set.
openSnapshots = openSnapshots.clear(id).andNot(previousIds)
}
}
internal fun validateNotApplied() {
require(!applied) {
"Unsupported operation on a snapshot that has been applied"
}
}
/**
* Abandon the snapshot.
*/
internal fun abandon() {
val modified = modified
if (modified != null) {
validateNotApplied()
// Mark all state records created in this snapshot as invalid. This allows the snapshot
// id to be forgotten as no state records will refer to it.
this.modified = null
val id = id
for (state in modified) {
var current: StateRecord? = state.firstStateRecord
while (current != null) {
if (current.snapshotId == id || current.snapshotId in previousIds) {
current.snapshotId = INVALID_SNAPSHOT
}
current = current.next
}
}
}
// The snapshot can now be closed.
close()
}
internal fun innerApply(
snapshotId: Int,
optimisticMerges: Map<StateRecord, StateRecord>?,
invalidSnapshots: SnapshotIdSet
): SnapshotApplyResult {
// This must be called in a synchronized block
// If there are modifications we need to ensure none of the modifications have
// collisions.
// A record is guaranteed not collide if no other write was performed to the record
// by an applied snapshot since this snapshot was taken. No writes to a state object
// occurred if, ignoring this snapshot, the readable records for the snapshots are
// the same. If they are different then there is a potential collision and the state
// object is asked if it can resolve the collision. If it can the updated state record
// is for the apply.
var mergedRecords: MutableList<Pair<StateObject, StateRecord>>? = null
val start = this.invalid.set(id).or(this.previousIds)
val modified = modified!!
var statesToRemove: MutableList<StateObject>? = null
for (state in modified) {
val first = state.firstStateRecord
// If either current or previous cannot be calculated the object was created
// in a nested snapshot that was committed then changed.
val current = readable(first, snapshotId, invalidSnapshots) ?: continue
val previous = readable(first, id, start) ?: continue
if (current != previous) {
val applied = readable(first, id, this.invalid) ?: readError()
val merged = optimisticMerges?.get(current) ?: run {
state.mergeRecords(previous, current, applied)
}
when (merged) {
null -> return SnapshotApplyResult.Failure(this)
applied -> {
// Nothing to do the merge policy says that the current changes
// obscure the current value so ignore the conflict
}
current -> {
(mergedRecords ?: mutableListOf<Pair<StateObject, StateRecord>>().also {
mergedRecords = it
}).add(state to current.create())
// If we revert to current then the state is no longer modified.
(statesToRemove ?: mutableListOf<StateObject>().also {
statesToRemove = it
}).add(state)
}
else -> {
(mergedRecords ?: mutableListOf<Pair<StateObject, StateRecord>>().also {
mergedRecords = it
}).add(
if (merged != previous) state to merged
else state to previous.create()
)
}
}
}
}
mergedRecords?.let {
// Ensure we have a new snapshot id
advance()
// Update all the merged records to have the new id.
it.fastForEach { merged ->
val (state, stateRecord) = merged
stateRecord.snapshotId = id
sync {
stateRecord.next = state.firstStateRecord
state.prependStateRecord(stateRecord)
}
}
}
statesToRemove?.let {
// Remove from modified any state objects that have reverted to the parent value.
modified.removeAll(it)
}
return SnapshotApplyResult.Success
}
internal inline fun <T> advance(block: () -> T): T {
recordPrevious(id)
return block().also {
sync {
id = nextSnapshotId++
openSnapshots = openSnapshots.set(id)
}
}
}
internal fun advance(): Unit = advance { }
internal fun recordPrevious(id: Int) {
sync {
previousIds = previousIds.set(id)
}
}
internal fun recordPreviousList(snapshots: SnapshotIdSet) {
sync {
previousIds = previousIds.or(snapshots)
}
}
override fun recordModified(state: StateObject) {
(modified ?: HashSet<StateObject>().also { modified = it }).add(state)
}
override var modified: MutableSet<StateObject>? = null
/**
* A set of the id's previously associated with this snapshot. When this snapshot closes
* then these ids must be removed from the global as well.
*/
internal var previousIds: SnapshotIdSet = SnapshotIdSet.EMPTY
/**
* The number of pending nested snapshots of this snapshot. To simplifythe code, this
* snapshot it, itself, counted as its own nested snapshot.
*/
private var snapshots = 1
/**
* Tracks whether the snapshot has been applied.
*/
internal var applied = false
}
/**
* The result of a applying a mutable snapshot. [Success] indicates that the snapshot was
* successfully applied and is not visible as the global state of the state object (or visible
* in the parent snapshot for a nested snapshot). [Failure] indicates one or more state objects
* were modified by both this snapshot and in the global (or parent) snapshot.
*/
sealed class SnapshotApplyResult {
/**
* Check the result of an apply. If the result is [Success] then this does does nothing. If
* the result is [Failure] then a [SnapshotApplyConflictException] exception is thrown. Once
* [check] as been called the snapshot is disposed.
*/
abstract fun check()
/**
* True if the result is [Success].
*/
abstract val succeeded: Boolean
object Success : SnapshotApplyResult() {
/**
* Check the result of a snapshot apply. Calling [check] on a [Success] result is a noop.
*/
override fun check() { }
override val succeeded: Boolean get() = true
}
class Failure(val snapshot: Snapshot) : SnapshotApplyResult() {
/**
* Check the result of a snapshot apply. Calling [check] on a [Failure] result throws a
* [SnapshotApplyConflictException] exception.
*/
override fun check() {
snapshot.dispose()
throw SnapshotApplyConflictException(snapshot)
}
override val succeeded: Boolean get() = false
}
}
/**
* Callback type for observing reads of state objects in a snapshot.
*
* @see takeSnapshot
* @see takeMutableSnapshot
*/
typealias SnapshotReadObserver = (state: Any) -> Unit
/**
* Callback type for observing writes to state objects in a snapshot.
*
* @see Snapshot.registerGlobalWriteObserver
*/
typealias SnapshotWriteObserver = (state: Any) -> Unit
/**
* Callback type for observing when a non-nested mutable snapshot is applied by calling
* [MutableSnapshot.apply], modifying the visible global state of state objects. Changes to a
* state object outside of a snapshot are also sent to the snapshot apply observer whenever
* [Snapshot.sendApplyNotifications] is called.
*
* The committed parameter is a collection of state objects modified in the snapshot.
* The snapshot parameter is the snapshot that was applied.
*
* @see takeMutableSnapshot
* @see MutableSnapshot.apply
* @see Snapshot
* @see Snapshot.registerApplyObserver
* @see Snapshot.sendApplyNotifications
*/
typealias SnapshotApplyObserver = (changed: Set<Any>, snapshot: Snapshot) -> Unit
/**
* Return the thread's active snapshot. If no thread snapshot is active then the current global
* snapshot is used.
*/
internal fun currentSnapshot(): Snapshot =
threadSnapshot.get() ?: sync { currentGlobalSnapshot }
/**
* An exception that is thrown when [SnapshotApplyResult.check] is called on a result of a
* [MutableSnapshot.apply] that fails to apply.
*/
@ExperimentalComposeApi
class SnapshotApplyConflictException(
@Suppress("unused") val snapshot: Snapshot
) : Exception()
/**
* Snapshot local value of a state object.
*/
@ExperimentalComposeApi
abstract class StateRecord {
/**
* The snapshot id of the snapshot in which the record was created.
*/
internal var snapshotId: Int = currentSnapshot().id
/**
* Reference of the next state record. State records are stored in a linked list.
*
* Changes to [next] must preserve all existing records to all threads even during
* intermediately changes. For example, it is safe to add the beginning or end of the list
* but adding to the middle requires care. First the new record must have its [next] updated
* then the [next] of its new predecessor can then be set to point to it. This implies that
* records that are already in the list cannot be moved in the list as this the change must
* be atomic to all threads that cannot happen without a lock which this list cannot afford.
*
* It is unsafe to remove a record as it might be in the process of being reused (see [used]).
* If a record is removed care must be taken to ensure that it is not being claimed by some
* other thread. This would require changes to [used].
*/
internal var next: StateRecord? = null
/**
* Copy the value into this state record from another for the same state object.
*/
abstract fun assign(value: StateRecord)
/**
* Create a new state record for the same state object.
*/
abstract fun create(): StateRecord
}
/**
* Interface implemented by all snapshot aware state objects. Used by this module to maintain the
* state records of a state object.
*/
@ExperimentalComposeApi
interface StateObject {
/**
* The first state record in a linked list of state records.
*/
val firstStateRecord: StateRecord
/**
* Add a new state record to the beginning of a list. After this call [firstStateRecord] should
* be [value].
*/
fun prependStateRecord(value: StateRecord)
/**
* Produce a merged state based on the conflicting state changes.
*
* This method must not modify any of the records received and should treat the state records
* as immutable, even the [applied] record.
*
* @param previous the state record that was used to create the [applied] record and is a state
* that also (though indirectly) produced the [current] record.
*
* @param current the state record of the parent snapshot or global state.
*
* @param applied the state record that is being applied of the parent snapshot or global
* state.
*
* @return the modified state or `null` if the values cannot be merged. If the states cannot
* be merged the current apply will fail. Any of the parameters can be returned as a result.
* If it is not one of the parameter values then it *must* be a new value that is created by
* calling [StateRecord.create] on one of the records passed and then can be modified
* to have the merged value before being returned. If a new record is returned
* [MutableSnapshot.apply] will update the internal snapshot id and call
* [prependStateRecord] if the record is used.
*/
fun mergeRecords(
previous: StateRecord,
current: StateRecord,
applied: StateRecord
): StateRecord? = null
}
/**
* A snapshot whose state objects cannot be modified. If a state object is modified when in a
* read-only snapshot a [IllegalStateException] is thrown.
*/
internal class ReadonlySnapshot internal constructor(
id: Int,
invalid: SnapshotIdSet,
override val readObserver: SnapshotReadObserver?
) : Snapshot(id, invalid) {
/**
* The number of nested snapshots that are active. To simplify the code, this snapshot counts
* itself as a nested snapshot.
*/
private var snapshots = 1
override val readonly: Boolean get() = true
override val root: Snapshot get() = this
override fun hasPendingChanges(): Boolean = false
override val writeObserver: SnapshotWriteObserver? get() = null
override var modified: HashSet<StateObject>?
get() = null
@Suppress("UNUSED_PARAMETER")
set(value) = unsupported()
override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot {
validateOpen(this)
return NestedReadonlySnapshot(id, invalid, readObserver, this)
}
override fun notifyObjectsInitialized() {
// Nothing to do for read-only snapshots
}
override fun dispose() {
if (!disposed) {
nestedDeactivated(this)
super.dispose()
}
}
override fun nestedActivated(snapshot: Snapshot) { snapshots++ }
override fun nestedDeactivated(snapshot: Snapshot) {
if (--snapshots == 0) {
// A read-only snapshot can be just be closed as it has no modifications.
close()
}
}
override fun recordModified(state: StateObject) {
error("Cannot modify a state object in a read-only snapshot")
}
}
internal class NestedReadonlySnapshot(
id: Int,
invalid: SnapshotIdSet,
readObserver: SnapshotReadObserver?,
val parent: Snapshot
) : Snapshot(id, invalid) {
init { parent.nestedActivated(this) }
override val readonly get() = true
override val root: Snapshot get() = parent.root
@OptIn(ExperimentalComposeApi::class)
override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?) =
parent.takeNestedSnapshot(readObserver)
override fun notifyObjectsInitialized() {
// Nothing to do for read-only snapshots
}
override fun hasPendingChanges(): Boolean = false
override val readObserver: SnapshotReadObserver? =
// Merge the read observers if necessary
readObserver?.let {
parent.readObserver?.let {
{ state: Any ->
readObserver(state)
it(state)
}
} ?: readObserver
} ?: parent.readObserver
override fun dispose() {
if (!disposed) {
if (id != parent.id) {
close()
}
parent.nestedDeactivated(this)
super.dispose()
}
}
override val modified: HashSet<StateObject>? get() = null
override val writeObserver: SnapshotWriteObserver? get() = null
@OptIn(ExperimentalComposeApi::class)
override fun recordModified(state: StateObject) = parent.recordModified(state)
override fun nestedDeactivated(snapshot: Snapshot) = unsupported()
override fun nestedActivated(snapshot: Snapshot) = unsupported()
}
private val emptyLambda: (invalid: SnapshotIdSet) -> Unit = { }
/**
* A snapshot object that simplifies the code by treating the global state as a mutable snapshot.
*/
internal class GlobalSnapshot(id: Int, invalid: SnapshotIdSet) :
MutableSnapshot(id, invalid, null, sync {
// Take a defensive copy of the globalWriteObservers list. This then avoids having to
// synchronized access to writerObserver in places it is called and allows the list to
// change while notifications are being dispatched. Changes to globalWriteObservers force
// a new global snapshot to be created.
(if (globalWriteObservers.isNotEmpty()) {
globalWriteObservers.toMutableList()
} else null)?.let {
it.firstOrNull() ?: { state: Any ->
it.forEach { it(state) }
}
}
}) {
override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot =
takeNewSnapshot { invalid ->
ReadonlySnapshot(
id = sync { nextSnapshotId++ },
invalid = invalid,
readObserver = readObserver
)
}
override fun takeNestedMutableSnapshot(
readObserver: SnapshotReadObserver?,
writeObserver: SnapshotWriteObserver?
): MutableSnapshot = takeNewSnapshot { invalid ->
MutableSnapshot(
id = sync { nextSnapshotId++ },
invalid = invalid,
// It is intentional that the global read observers are not merged with mutable
// snapshots read observers.
readObserver = readObserver,
// It is intentional that global write observers are not merged with mutable
// snapshots write observers.
writeObserver = writeObserver
)
}
override fun notifyObjectsInitialized() {
advanceGlobalSnapshot()
}
override fun nestedDeactivated(snapshot: Snapshot) = unsupported()
override fun nestedActivated(snapshot: Snapshot) = unsupported()
override fun apply(): SnapshotApplyResult =
error("Cannot apply the global snapshot directly. Call Snapshot.advanceGlobalSnapshot")
override fun dispose() {
// Disposing the global snapshot is a no-op.
// The dispose behavior is preformed by advancing the global snapshot. This method is
// squelched so calling it from `currentSnapshot` doesn't cause incorrect behavior
}
}
/**
* A nested mutable snapshot created by [MutableSnapshot.takeNestedMutableSnapshot].
*/
internal class NestedMutableSnapshot(
id: Int,
invalid: SnapshotIdSet,
readObserver: SnapshotReadObserver?,
writeObserver: SnapshotWriteObserver?,
val parent: MutableSnapshot
) : MutableSnapshot(id, invalid, readObserver, writeObserver) {
init { parent.nestedActivated(this) }
override val root: Snapshot get() = parent.root
override fun dispose() {
if (!disposed) {
super.dispose()
parent.nestedDeactivated(this)
}
}
override fun apply(): SnapshotApplyResult {
if (parent.applied || parent.disposed) return SnapshotApplyResult.Failure(this)
// Applying a nested mutable snapshot applies its changes to the parent snapshot.
// See MutableSnapshot.apply() for implantation notes.
// The apply observer notification are for applying to the global scope so it is elided
// here making this code a bit simpler than MutableSnapshot.apply.
val modified = modified
val id = id
val optimisticMerges = if (modified != null)
optimisticMerges(parent, this, parent.invalid)
else null
sync {
validateOpen(this)
if (modified == null || modified.size == 0) {
close()
} else {
val result = innerApply(parent.id, optimisticMerges, parent.invalid)
if (result != SnapshotApplyResult.Success) return result
// Add all modified objects in this set to the parent
(parent.modified ?: HashSet<StateObject>().also {
parent.modified = it
}).addAll(modified)
}
// Make the snapshot visible in the parent snapshot
parent.invalid = parent.invalid.clear(id).andNot(previousIds)
// Ensure the ids associated with this snapshot are also applied by the parent.
parent.recordPrevious(id)
parent.recordPreviousList(previousIds)
}
applied = true
return SnapshotApplyResult.Success
}
}
/**
* A pseudo snapshot that doesn't introduce isolation but does introduce observers.
*/
internal class TransparentObserverMutableSnapshot(
private val previousSnapshot: MutableSnapshot?,
internal val specifiedReadObserver: SnapshotReadObserver?,
internal val specifiedWriteObserver: SnapshotWriteObserver?
) : MutableSnapshot(
INVALID_SNAPSHOT,
SnapshotIdSet.EMPTY,
mergedReadObserver(
specifiedReadObserver,
previousSnapshot?.readObserver ?: currentGlobalSnapshot.readObserver
),
mergedWriteObserver(
specifiedWriteObserver,
previousSnapshot?.writeObserver ?: currentGlobalSnapshot.writeObserver
)
) {
private val currentSnapshot: MutableSnapshot
get() = previousSnapshot ?: currentGlobalSnapshot
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
}
override var id: Int
get() = currentSnapshot.id
@Suppress("UNUSED_PARAMETER")
set(value) { unsupported() }
@OptIn(ExperimentalComposeApi::class)
override var invalid get() = currentSnapshot.invalid
@Suppress("UNUSED_PARAMETER")
set(value) = unsupported()
override fun hasPendingChanges(): Boolean = currentSnapshot.hasPendingChanges()
override var modified: MutableSet<StateObject>?
get() = currentSnapshot.modified
@Suppress("UNUSED_PARAMETER")
set(value) = unsupported()
override val readonly: Boolean
get() = currentSnapshot.readonly
override fun apply(): SnapshotApplyResult =
currentSnapshot.apply()
@OptIn(ExperimentalComposeApi::class)
override fun recordModified(state: StateObject) =
currentSnapshot.recordModified(state)
override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot =
currentSnapshot.takeNestedSnapshot(mergedReadObserver(readObserver, this.readObserver))
override fun takeNestedMutableSnapshot(
readObserver: SnapshotReadObserver?,
writeObserver: SnapshotWriteObserver?
): MutableSnapshot = currentSnapshot.takeNestedMutableSnapshot(
mergedReadObserver(readObserver, this.readObserver),
mergedWriteObserver(writeObserver, this.writeObserver)
)
@OptIn(ExperimentalComposeApi::class)
override fun notifyObjectsInitialized() = currentSnapshot.notifyObjectsInitialized()
// The following should never be called.
override fun nestedActivated(snapshot: Snapshot) = unsupported()
override fun nestedDeactivated(snapshot: Snapshot) = unsupported()
}
private fun mergedReadObserver(
readObserver: SnapshotReadObserver?,
parentObserver: SnapshotReadObserver?
): SnapshotReadObserver? =
if (readObserver != null && parentObserver != null && readObserver != parentObserver) {
{ state: Any ->
readObserver(state)
parentObserver(state)
}
} else readObserver ?: parentObserver
private fun mergedWriteObserver(
writeObserver: SnapshotWriteObserver?,
parentObserver: SnapshotWriteObserver?
): SnapshotWriteObserver? =
if (writeObserver != null && parentObserver != null && writeObserver != parentObserver) {
{ state: Any ->
writeObserver(state)
parentObserver(state)
}
} else writeObserver ?: parentObserver
/**
* Snapshot id of `0` is reserved as invalid and no state record with snapshot `0` is considered
* valid.
*
* The value `0` was chosen as it is the default value of the Int snapshot id type and records
* initially created will naturally have a snapshot id of 0. If this wasn't considered invalid
* adding such a record to a state object will make the state record immediately visible to the
* snapshots instead of being born invalid. Using `0` ensures all state records are created invalid
* and must be explicitly marked as valid in to be visible in a snapshot.
*/
private const val INVALID_SNAPSHOT = 0
private fun validateNotInSnapshot() {
threadSnapshot.get().let {
if (it != null && it !is TransparentObserverMutableSnapshot)
error("In an existing snapshot")
}
}
private val threadSnapshot = ThreadLocal<Snapshot>()
// A global synchronization object. This synchronization object should be taken before modifying any
// of the fields below.
@PublishedApi
internal val lock = Any()
@PublishedApi
@Suppress("DEPRECATION_ERROR")
internal inline fun <T> sync(block: () -> T): T = synchronized(lock, block)
// The following variables should only be written when sync is taken
// A set of snapshots that are currently open and should be considered invalid for new snapshots.
private var openSnapshots = SnapshotIdSet.EMPTY
// The first snapshot created must be at least on more than the INVALID_SNAPSHOT
private var nextSnapshotId = INVALID_SNAPSHOT + 1
// A list of apply observers
private val applyObservers = mutableListOf<SnapshotApplyObserver>()
// A list of observers of writes to the global state.
private val globalWriteObservers = mutableListOf<SnapshotWriteObserver>()
private var currentGlobalSnapshot = GlobalSnapshot(
id = nextSnapshotId++,
invalid = SnapshotIdSet.EMPTY
).also {
openSnapshots = openSnapshots.set(it.id)
}
private fun <T> takeNewGlobalSnapshot(
previousGlobalSnapshot: Snapshot,
block: (invalid: SnapshotIdSet) -> T
): T {
// Deactivate global snapshot. It is safe to just deactivate it because it cannot have
// any conflicting writes as it is always closed before another snapshot is taken.
val result = block(openSnapshots.clear(previousGlobalSnapshot.id))
sync {
val globalId = nextSnapshotId++
openSnapshots = openSnapshots.clear(previousGlobalSnapshot.id)
currentGlobalSnapshot = GlobalSnapshot(
id = globalId,
invalid = openSnapshots
)
openSnapshots = openSnapshots.set(globalId)
}
return result
}
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
val previousGlobalSnapshot = currentGlobalSnapshot
val result = sync {
takeNewGlobalSnapshot(previousGlobalSnapshot, block)
}
// If the previous global snapshot had any modified states then notify the registered apply
// observers.
val modified = previousGlobalSnapshot.modified
if (modified != null) {
val observers = sync { applyObservers.toList() }
for (observer in observers) {
observer(modified, previousGlobalSnapshot)
}
}
// Update the transparent snapshot if necessary
// This doesn't need to take the sync because it is updating thread local state.
(threadSnapshot.get() as? TransparentObserverMutableSnapshot)?.let {
threadSnapshot.set(TransparentObserverMutableSnapshot(
currentGlobalSnapshot,
it.specifiedReadObserver,
it.specifiedWriteObserver
))
it.dispose()
}
return result
}
private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { }
private fun <T : Snapshot> takeNewSnapshot(block: (invalid: SnapshotIdSet) -> T): T =
advanceGlobalSnapshot { invalid ->
val result = block(invalid)
sync {
openSnapshots = openSnapshots.set(result.id)
}
result
}
private fun validateOpen(snapshot: Snapshot) {
if (!openSnapshots.get(snapshot.id)) error("Snapshot is not open")
}
private fun valid(currentSnapshot: Int, candidateSnapshot: Int, invalid: SnapshotIdSet): Boolean {
// A candidate snapshot is valid if the it is less than or equal to the current snapshot
// and it wasn't specifically marked as invalid when the snapshot started.
//
// All snapshot active at when the snapshot was taken considered invalid for the snapshot
// (they have not been applied and therefore are considered invalid).
//
// All snapshots taken after the current snapshot are considered invalid since they where taken
// after the current snapshot was taken.
//
// INVALID_SNAPSHOT is reserved as an invalid snapshot id.
return candidateSnapshot != INVALID_SNAPSHOT && candidateSnapshot <= currentSnapshot &&
!invalid.get(candidateSnapshot)
}
// Determine if the given data is valid for the snapshot.
private fun valid(data: StateRecord, snapshot: Int, invalid: SnapshotIdSet): Boolean {
return valid(snapshot, data.snapshotId, invalid)
}
private fun <T : StateRecord> readable(r: T, id: Int, invalid: SnapshotIdSet): T? {
// The readable record is the valid record with the highest snapshotId
var current: StateRecord? = r
var candidate: StateRecord? = null
while (current != null) {
if (valid(current, id, invalid)) {
candidate = if (candidate == null) current
else if (candidate.snapshotId < current.snapshotId) current else candidate
}
current = current.next
}
if (candidate != null) {
@Suppress("UNCHECKED_CAST")
return candidate as T
}
return null
}
/**
* Return the current readable state record for the current snapshot. It is assumed that [this]
* is the first record of [state]
*/
fun <T : StateRecord> T.readable(state: StateObject): T =
readable(state, currentSnapshot())
/**
* Return the current readable state record for the [snapshot]. It is assumed that [this]
* is the first record of [state]
*/
fun <T : StateRecord> T.readable(state: StateObject, snapshot: Snapshot): T {
// invoke the observer associated with the current snapshot.
snapshot.readObserver?.invoke(state)
return readable(this, snapshot.id, snapshot.invalid) ?: readError()
}
private fun readError(): Nothing {
error("Reading a state that was created after the snapshot was taken or in a snapshot that " +
"has not yet been applied")
}
/**
* A record can be reused if no other snapshot will see it as valid. This is always true for a
* record created in an abandoned snapshot. It is also true if the record is valid in the
* previous snapshot and is obscured by another record also valid in the previous state record.
*/
private fun used(state: StateObject, id: Int, invalid: SnapshotIdSet): StateRecord? {
var current: StateRecord? = state.firstStateRecord
var validRecord: StateRecord? = null
while (current != null) {
val currentId = current.snapshotId
if (currentId == INVALID_SNAPSHOT) {
// Any records that were marked invalid by an abandoned snapshot can be used
// immediately.
return current
}
if (valid(current, id - 1, invalid)) {
if (validRecord == null) {
validRecord = current
} else {
// If we have two valid records one must obscure the other. Return the
// record with the lowest id
return if (current.snapshotId < validRecord.snapshotId) current else validRecord
}
}
current = current.next
}
return null
}
@PublishedApi
internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T {
if (snapshot.readonly) {
// If the snapshot is read-only, use the snapshot recordModified to report it.
snapshot.recordModified(state)
}
val id = snapshot.id
val readData = readable<T>(this, id, snapshot.invalid) ?: readError()
// If the readable data was born in this snapshot, it is writable.
if (readData.snapshotId == snapshot.id) return readData
// Otherwise, make a copy of the readable data and mark it as born in this snapshot, making it
// writable.
// Calling used() on a state object might return the same record for each thread calling
// used() therefore selecting the record to reuse should be guarded.
// Note: setting the snapshotId to Int.MAX_VALUE will make it invalid for all snapshots.
// This means the lock can be released as used() will no longer select it. Using id could
// also be used but it puts the object into a state where the reused value appears to be
// the current valid value for the snapshot. This is not an issue if the snapshot is only
// being read from a single thread but using Int.MAX_VALUE allows multiple readers,
// single writer, of a snapshot. Note that threads reading a mutating snapshot should not
// cache the result of readable() as the mutating thread calls to writable() can change the
// result of readable().
@Suppress("UNCHECKED_CAST")
val newData = (used(state, id, openSnapshots) as T?)?.apply {
snapshotId = Int.MAX_VALUE
} ?: readData.create().apply {
snapshotId = Int.MAX_VALUE
this.next = state.firstStateRecord
state.prependStateRecord(this as T)
} as T
newData.assign(readData)
newData.snapshotId = id
snapshot.recordModified(state)
return newData
}
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
/**
* Call [block] with a writable state record for [snapshot] of the given record. It is
* assumed that this is called for the first state record in a state object. If the snapshot is
* read-only calling this will throw.
*/
inline fun <T : StateRecord, R> T.writable(
state: StateObject,
snapshot: Snapshot,
block: T.() -> R
): R {
// A writable record will always be the readable record (as all newer records are invalid it
// must be the newest valid record). This means that if the readable record is not from the
// current snapshot, a new record must be created. To create a new writable record, a record
// can be reused, if possible, and the readable record is applied to it. If a record cannot
// be reused, a new record is created and the readable record is applied to it. Once the
// values are correct the record is made live by giving it the current snapshot id.
// Writes need to be in a `sync` block as all writes in flight must be completed before a new
// snapshot is take. Writing in a sync block ensures this is the case because new snapshots
// are also in a sync block.
return sync {
this.writableRecord(state, snapshot).block()
}.also { notifyWrite(snapshot, state) }
}
/**
* Call [block] with a writable state record for the given record. It is assumed that this is
* called for the first state record in a state object. A record is writable if it was created in
* the current mutable snapshot.
*/
inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R =
this.writable(state, Snapshot.current, block)
/**
* Produce a set of optimistic merges of the state records, this is performed outside the
* a synchronization block to reduce the amount of time taken in the synchronization block
* reducing the thread contention of merging state values.
*/
private fun optimisticMerges(
currentSnapshot: MutableSnapshot,
applyingSnapshot: MutableSnapshot,
invalidSnapshots: SnapshotIdSet
): Map<StateRecord, StateRecord>? {
val modified = applyingSnapshot.modified
val id = currentSnapshot.id
if (modified == null) return null
val start = applyingSnapshot.invalid.set(applyingSnapshot.id).or(applyingSnapshot.previousIds)
var result: MutableMap<StateRecord, StateRecord>? = null
for (state in modified) {
val first = state.firstStateRecord
val current = readable(first, id, invalidSnapshots) ?: continue
val previous = readable(first, id, start) ?: continue
if (current != previous) {
// Try to produce a merged state record
val applied = readable(first, applyingSnapshot.id, applyingSnapshot.invalid)
?: readError()
val merged = state.mergeRecords(previous, current, applied)
if (merged != null) {
(result ?: hashMapOf<StateRecord, StateRecord>().also {
result = it
})[current] = merged
} else {
// If one fails don't bother calculating the others as they are likely not going
// to be used. There is an unlikely case that a optimistic merge cannot be
// produced but the snapshot will apply because, once the synchronization is taken,
// the current state can be merge. This routine errors on the side of reduced
// overall work by not performing work that is likely to be ignored.
return null
}
}
}
return result
}
/**
* Returns the current record without notifying any read observers.
*/
@PublishedApi
internal fun <T : StateRecord> current(r: T, snapshot: Snapshot) =
readable(r, snapshot.id, snapshot.invalid) ?: readError()
/**
* Provides a [block] with the current record, without notifying any read observers.
*
* @see readable
*/
@ExperimentalComposeApi
inline fun <T : StateRecord, R> T.withCurrent(block: (r: T) -> R): R =
block(current(this, Snapshot.current))