Created
November 18, 2024 19:01
-
-
Save alexvanyo/a2f9f779cd0e28ea4bba7d40602627ae to your computer and use it in GitHub Desktop.
SessionValue utilities
This file contains 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 characters
/* | |
* Copyright 2024 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 | |
* | |
* https://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. | |
*/ | |
package com.alexvanyo.composelife.sessionvalue | |
import kotlin.contracts.ExperimentalContracts | |
import kotlin.contracts.contract | |
import kotlin.uuid.Uuid | |
/** | |
* Information about a local session in a [SessionValueHolder]. | |
*/ | |
sealed interface LocalSessionInfo { | |
/** | |
* The local session is active, meaning that the session value is running ahead of the upstream value. | |
*/ | |
data class Active( | |
/** | |
* The current local session id. | |
*/ | |
val currentLocalSessionId: Uuid, | |
/** | |
* If true, then the upstream value has caught up to the local session value. | |
*/ | |
val isUpstreamSessionValueUpToDate: Boolean, | |
/** | |
* The previous upstream session id. This was the session id that was replaced by this active | |
* [currentLocalSessionId]. | |
*/ | |
val previousUpstreamSessionId: Uuid, | |
) : LocalSessionInfo | |
/** | |
* The local session is inactive, meaning that the session value is just matching the upstream value. | |
*/ | |
data class Inactive( | |
/** | |
* The current upstream session id. | |
*/ | |
val currentUpstreamSessionId: Uuid, | |
/** | |
* [nextLocalSessionId] will be the local session id used when [SessionValueHolder.setValue] is next called | |
* if the upstream does not change. This will be cycled whenever the upstream session changes id or value. | |
*/ | |
val nextLocalSessionId: Uuid, | |
) : LocalSessionInfo | |
} | |
/** | |
* The local session id that will remain constant when upgrading from [LocalSessionInfo.Inactive] to an | |
* [LocalSessionInfo.Active]. | |
* | |
* Use this as a key to fork an editing session off of a specific session and value. | |
*/ | |
val LocalSessionInfo.localSessionId: Uuid | |
get() = | |
when (this) { | |
is LocalSessionInfo.Active -> currentLocalSessionId | |
is LocalSessionInfo.Inactive -> nextLocalSessionId | |
} | |
/** | |
* The previous upstream session id that will remain constant when upgrading from [LocalSessionInfo.Inactive] to an | |
* [LocalSessionInfo.Active]. | |
* | |
* Use this as a key for tracking a previous session. | |
*/ | |
val LocalSessionInfo.preLocalSessionId: Uuid | |
get() = | |
when (this) { | |
is LocalSessionInfo.Active -> previousUpstreamSessionId | |
is LocalSessionInfo.Inactive -> currentUpstreamSessionId | |
} | |
/** | |
* Returns `true` if the [LocalSessionInfo] is [LocalSessionInfo.Active], and `false` if the [LocalSessionInfo] | |
* is [LocalSessionInfo.Inactive]. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
fun LocalSessionInfo.isLocalSessionActive(): Boolean { | |
contract { | |
returns(true) implies (this@isLocalSessionActive is LocalSessionInfo.Active) | |
returns(false) implies (this@isLocalSessionActive is LocalSessionInfo.Inactive) | |
} | |
return when (this) { | |
is LocalSessionInfo.Active -> true | |
is LocalSessionInfo.Inactive -> false | |
} | |
} |
This file contains 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 characters
/* | |
* Copyright 2024 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 | |
* | |
* https://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. | |
*/ | |
package com.alexvanyo.composelife.sessionvalue | |
import androidx.compose.runtime.saveable.Saver | |
import androidx.compose.runtime.saveable.listSaver | |
import kotlin.uuid.Uuid | |
/** | |
* An object representing a specific session for [value]. | |
* | |
* This [value] is from the given [sessionId], and has the associated [valueId]. | |
* | |
* Session values can be managed with a [SessionValueHolder] created with [rememberSessionValueHolder]. | |
*/ | |
data class SessionValue<out T>( | |
val sessionId: Uuid, | |
val valueId: Uuid, | |
val value: T, | |
) { | |
companion object { | |
fun <T, R : Any> Saver( | |
valueSaver: Saver<T, R>, | |
): Saver<SessionValue<T>, Any> = listSaver( | |
save = { | |
listOf( | |
with(uuidSaver) { save(it.sessionId) }, | |
with(uuidSaver) { save(it.valueId) }, | |
with(valueSaver) { save(it.value) }, | |
) | |
}, | |
restore = { | |
@Suppress("UNCHECKED_CAST") | |
SessionValue( | |
sessionId = uuidSaver.restore(it[0]!! as String)!!, | |
valueId = uuidSaver.restore(it[1]!! as String)!!, | |
value = valueSaver.restore(it[2]!! as R)!!, | |
) | |
}, | |
) | |
} | |
} | |
/** | |
* A [Saver] for a [Uuid]. | |
*/ | |
internal val uuidSaver: Saver<Uuid, String> = Saver( | |
save = { it.toString() }, | |
restore = Uuid::parse, | |
) |
This file contains 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 characters
/* | |
* Copyright 2024 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 | |
* | |
* https://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. | |
*/ | |
package com.alexvanyo.composelife.sessionvalue | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.saveable.Saver | |
import androidx.compose.runtime.saveable.autoSaver | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import kotlin.uuid.Uuid | |
/** | |
* A multiplexer for a [SessionValue] that can maintain the state for a local session that runs ahead of the | |
* upstream [SessionValue]. | |
* | |
* The [sessionValue] will match the upstream session value passed to [rememberSessionValueHolder], until | |
* [setValue] is called. | |
* | |
* Once [setValue] is called, the [sessionValue] will run ahead of the upstream [SessionValue]. | |
* | |
* [info] contains additional status information about the local session. | |
*/ | |
sealed interface SessionValueHolder<T> { | |
val sessionValue: SessionValue<T> | |
val info: LocalSessionInfo | |
/** | |
* Sets the value upstream via the [SessionValueHolder], and begins (or continues) a local session. | |
*/ | |
fun setValue( | |
value: T, | |
valueId: Uuid = Uuid.random(), | |
) | |
} | |
/** | |
* A multiplexer for a [SessionValue] that can maintain the state for a local session that runs ahead of the | |
* upstream [SessionValue]. | |
* | |
* The [SessionValueHolder.sessionValue] will match the [upstreamSessionValue] until [SessionValueHolder.setValue] | |
* is called. | |
* | |
* Once [SessionValueHolder.setValue] is called, the [SessionValueHolder.sessionValue] will be representing a local | |
* session that may be ahead of what the upstream value shows. [setUpstreamSessionValue] will be called from | |
* [SessionValueHolder.setValue], and begin updating the upstream value in tandem with keeping a local state in | |
* [SessionValueHolder.sessionValue]. | |
* | |
* [SessionValueHolder.info] returns information about the current local session, if any. | |
* In particular [LocalSessionInfo.localSessionId] will returns the session id that will be used to represent | |
* the local session when [SessionValueHolder.setValue] is called. | |
* | |
* [SessionValueHolder] supports cases where the [upstreamSessionValue] changes independently from the local session. In | |
* those cases, the internal state will be reset to match the [upstreamSessionValue]. | |
*/ | |
@Composable | |
fun <T> rememberSessionValueHolder( | |
/** | |
* The upstream [SessionValue]. | |
*/ | |
upstreamSessionValue: SessionValue<T>, | |
/** | |
* Sets the upstream [SessionValue] to the given [SessionValue]. | |
* The provided upstream session id is the known previous id, for a compare-and-set updating of the [SessionValue]. | |
*/ | |
setUpstreamSessionValue: (expected: SessionValue<T>, newValue: SessionValue<T>) -> Unit, | |
valueSaver: Saver<T, *> = autoSaver(), | |
): SessionValueHolder<T> = | |
rememberSaveable( | |
saver = SessionValueHolderImpl.Saver( | |
initialSetUpstreamSessionValue = setUpstreamSessionValue, | |
valueSaver = valueSaver, | |
), | |
) { | |
SessionValueHolderImpl( | |
initialUpstreamSessionValue = upstreamSessionValue, | |
initialSetUpstreamSessionValue = setUpstreamSessionValue, | |
initialLocalSessionId = Uuid.random(), | |
initialLocalSessionValue = null, | |
initialUpstreamSessionIdBeforeLocalSession = upstreamSessionValue.sessionId, | |
) | |
} | |
.apply { | |
setValueFromUpstream(upstreamSessionValue) | |
this.setUpstreamSessionValue = setUpstreamSessionValue | |
} | |
private class SessionValueHolderImpl<T>( | |
initialUpstreamSessionIdBeforeLocalSession: Uuid, | |
initialUpstreamSessionValue: SessionValue<T>, | |
initialSetUpstreamSessionValue: (expected: SessionValue<T>, newValue: SessionValue<T>) -> Unit, | |
initialLocalSessionId: Uuid, | |
initialLocalSessionValue: SessionValue<T>?, | |
) : SessionValueHolder<T> { | |
/** | |
* The upstream session id known prior to the current local session (if any). | |
*/ | |
var upstreamSessionIdBeforeLocalSession by mutableStateOf(initialUpstreamSessionIdBeforeLocalSession) | |
/** | |
* The current known upstream session value. | |
*/ | |
var upstreamSessionValue by mutableStateOf(initialUpstreamSessionValue) | |
/** | |
* The local session id for the current local session (if any), otherwise the local session id that will be used | |
* for the next local session. | |
*/ | |
var localSessionId: Uuid by mutableStateOf(initialLocalSessionId) | |
/** | |
* If non-null, the local session value that is set and is running ahead of the [upstreamSessionValue]. | |
*/ | |
var localSessionValue: SessionValue<T>? by mutableStateOf(initialLocalSessionValue) | |
var setUpstreamSessionValue by mutableStateOf(initialSetUpstreamSessionValue) | |
override val sessionValue: SessionValue<T> | |
get() = localSessionValue ?: upstreamSessionValue | |
override val info: LocalSessionInfo | |
get() { | |
val currentLocalSessionValue = localSessionValue | |
return if (currentLocalSessionValue == null) { | |
check(upstreamSessionIdBeforeLocalSession == upstreamSessionValue.sessionId) | |
LocalSessionInfo.Inactive( | |
currentUpstreamSessionId = upstreamSessionValue.sessionId, | |
nextLocalSessionId = localSessionId, | |
) | |
} else { | |
LocalSessionInfo.Active( | |
currentLocalSessionId = localSessionId, | |
isUpstreamSessionValueUpToDate = | |
upstreamSessionValue.sessionId == currentLocalSessionValue.sessionId && | |
upstreamSessionValue.valueId == currentLocalSessionValue.valueId, | |
previousUpstreamSessionId = upstreamSessionIdBeforeLocalSession, | |
) | |
} | |
} | |
override fun setValue( | |
value: T, | |
valueId: Uuid, | |
) { | |
val expected = sessionValue | |
localSessionValue = SessionValue( | |
sessionId = localSessionId, | |
valueId = valueId, | |
value = value, | |
) | |
setUpstreamSessionValue(expected, sessionValue) | |
} | |
/** | |
* Synchronizes the internal state from the upstream [SessionValue]. | |
*/ | |
fun setValueFromUpstream( | |
newUpstreamSessionValue: SessionValue<T>, | |
) { | |
val hasSessionValueChanged = | |
newUpstreamSessionValue.sessionId != upstreamSessionValue.sessionId || | |
newUpstreamSessionValue.valueId != upstreamSessionValue.valueId | |
// If our most recent upstream session value still matches this new one, we have nothing to do | |
if (hasSessionValueChanged) { | |
// Otherwise, we've seen a new upstream session value | |
if (newUpstreamSessionValue.sessionId != localSessionValue?.sessionId) { | |
// The upstream session has become something different than the local session (if any) and the session | |
// value before our local session. Clear the local session, to revert back to the new upstream session. | |
localSessionId = Uuid.random() | |
localSessionValue = null | |
} | |
// Update the previous upstream session id in all cases except when we are see the update to our local | |
// session id | |
if (localSessionId != newUpstreamSessionValue.sessionId) { | |
upstreamSessionIdBeforeLocalSession = newUpstreamSessionValue.sessionId | |
} | |
upstreamSessionValue = newUpstreamSessionValue | |
} | |
} | |
companion object { | |
fun <T> Saver( | |
initialSetUpstreamSessionValue: (expected: SessionValue<T>, newValue: SessionValue<T>) -> Unit, | |
valueSaver: Saver<T, *>, | |
): Saver<SessionValueHolderImpl<T>, *> { | |
val sessionValueSaver = SessionValue.Saver(valueSaver) | |
return Saver( | |
save = { | |
listOf( | |
with(uuidSaver) { | |
save(it.localSessionId) | |
}, | |
with(sessionValueSaver) { | |
it.localSessionValue?.let { localSessionValue -> save(localSessionValue) } | |
}, | |
with(sessionValueSaver) { | |
save(it.upstreamSessionValue) | |
}, | |
with(uuidSaver) { | |
save(it.upstreamSessionIdBeforeLocalSession) | |
}, | |
) | |
}, | |
restore = { | |
SessionValueHolderImpl( | |
initialUpstreamSessionIdBeforeLocalSession = uuidSaver.restore(it[3] as String)!!, | |
initialUpstreamSessionValue = sessionValueSaver.restore(it[2]!!)!!, | |
initialSetUpstreamSessionValue = initialSetUpstreamSessionValue, | |
initialLocalSessionId = uuidSaver.restore(it[0] as String)!!, | |
initialLocalSessionValue = it[1]?.let(sessionValueSaver::restore), | |
) | |
}, | |
) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment