diff --git a/README.md b/README.md index 896c920..d3cb6db 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,16 @@ The showcase iOS application targets iOS 17.2. This value is the host applicatio | `rememberResultAsState` | Converts delivered results into read-only Compose state. | | `rememberSaveableResultAsState` | Converts delivered results into read-only saveable Compose state with automatic or custom saving. | +Read the contracts in order: [key identity](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultKey.kt) +→ [store lifetime](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultStore.kt) +and [presence](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/PendingResult.kt) +→ [consumer](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultConsumer.kt) +→ [effect](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultEffect.kt) +→ [state helpers](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/RememberResultAsState.kt). +The focused [store tests](gbeewa/src/commonTest/kotlin/com/quantipixels/gbeewa/ResultStoreTest.kt) +and [Compose runtime tests](gbeewa/src/commonTest/kotlin/com/quantipixels/gbeewa/ResultEffectTest.kt) +show these contracts in use. + ## Define a typed key Key equality defines the result lane. An object is suitable for one shared lane. A data class is suitable when an identifier is part of the lane. @@ -194,6 +204,20 @@ The key type controls the payload type at normal call sites. Create one `ResultStore` at the application or navigation root. Keep it for the communication lifetime that the app needs. Gbéewá does not require the store to be exposed to screen content. +### Transient handoffs and restored navigation + +`ResultStore` lives in memory and is not serialized or restored. Restoring a navigation destination +does not restore its pending result. If a destination requires a transient handoff, choose its +missing-value recovery before starting dependent work. For example, return to an earlier step or +close the destination when the handoff is missing. Check and consume the handoff in an event +handler, effect, or app-owned entry boundary, never during composition or in a `remember` initializer. +Save durable identifiers through the app's own navigation or storage contract. + +`rememberSaveableResultAsState` saves a receiver's UI value, not a pending handoff. Restoring that +value does not establish that a new result was delivered. + +### Store exposure + The app chooses how far each capability travels: - Keep `ResultStore` at the root and capture it in route-owned callbacks. @@ -318,6 +342,37 @@ Direct consumption removes the pending result before it returns. Do not call it because removal changes observable Compose state. Use `ResultEffect`, `rememberResultAsState`, or `rememberSaveableResultAsState` for composition-owned delivery. +### Cancel or abandon a request + +Use the existing `ResultConsumer.consume(key)` overload to remove a retired request's pending +value without invoking a result callback. Cleanup affects only that key, so other requests keep +their pending values. Run it on the UI or main context that drives Compose consumption, such as +the Cancel event handler: + +```kotlin +import com.quantipixels.gbeewa.ResultConsumer +import com.quantipixels.gbeewa.ResultKey + +data class EditNoteKey(val requestId: String) : ResultKey + +fun cancelNoteEdit( + consumer: ResultConsumer, + key: EditNoteKey, + dismiss: () -> Unit, +) { + consumer.consume(key) + dismiss() +} +``` + +The return value can be ignored for cleanup. If it matters, `PendingResult.Value(null)` means a +present nullable value was removed; `PendingResult.Missing` means no pending value existed. +Cleanup does not cancel the producer or reject later publications. The app must stop or ignore +late producer callbacks for a retired request and use distinct keys for overlapping requests. +No discard API, registry, session manager, or navigation policy is needed for this cleanup. + +### Receive as state + Use `rememberResultAsState` when the result directly updates local Compose state. The `AsState` suffix distinguishes the returned `State` from the root-owned `ResultStore`. Extra remember keys reset the initial value when their context changes: ```kotlin @@ -428,6 +483,13 @@ the key has no pending result. - The saveable helper can restore a receiver's delivered UI value. It does not restore pending results. - Any app layer can own the store or call `put`, including a `ViewModel` or navigation component. The call must execute on the UI or main context that drives Compose consumption. The library does not switch contexts or synchronize concurrent access. +### Same-key publication inside a callback + +Do not publish another value for the same key from inside its result callback. The replacement, +including an equal payload, can remain pending until the receiver leaves and re-enters composition. +Publishing twice retains only the latest pending value. There is no queue, automatic redelivery, +or retry guarantee. + ## Contributing Open an issue before a behavior change or new platform target. Keep navigation policy outside the library, add tests for contract changes, and update `CHANGELOG.md` for public behavior changes. Before a pull request, run: diff --git a/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultConsumer.kt b/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultConsumer.kt index 83b7aef..b25954d 100644 --- a/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultConsumer.kt +++ b/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultConsumer.kt @@ -27,6 +27,11 @@ public class ResultConsumer public constructor( * Direct consumption is intended for event handlers, effects, and non-Compose code. Do not * call it during composition because removal changes observable Compose state. * + * To clean up an abandoned request, call this overload and ignore its return value. It removes + * only this key's pending result and invokes no callback. It does not prevent a later producer + * from publishing another result. Consume on the UI or main context that drives Compose + * consumption; the store does not switch contexts or synchronize concurrent access. + * * @param key the typed result lane to consume. * @return the removed value or [PendingResult.Missing]. */ diff --git a/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultEffect.kt b/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultEffect.kt index b93f6a0..0052089 100644 --- a/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultEffect.kt +++ b/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultEffect.kt @@ -38,6 +38,8 @@ public val LocalResultConsumer: ProvidableCompositionLocal = * * Do not publish another value for [key] from inside [onResult]. The current keyed effect has * completed, so that value can remain pending until this effect leaves and re-enters composition. + * This includes a payload equal to the consumed value; callback republishing has no automatic + * redelivery or retry guarantee. * * @param key the typed result lane to observe. * @param onResult the latest callback that receives a pending result. diff --git a/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultStore.kt b/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultStore.kt index 5ff53c4..00534f9 100644 --- a/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultStore.kt +++ b/gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultStore.kt @@ -8,11 +8,13 @@ import androidx.compose.runtime.mutableStateMapOf * * The owner publishes results through [put]. The store retains each value until a * [ResultConsumer] consumes it, a later value replaces it, or the owner discards this store. - * The store does not address a screen or inspect navigation direction. A consumer backed by this - * store can consume an equal key on the current screen or on a screen reached after a pop, push, - * replacement, or other navigation change. Any application layer can call [put], but the call - * must execute on the UI or main context that drives Compose consumption. This store does not - * switch contexts or synchronize concurrent access. + * This store lives in memory and is not serialized or restored. Restoring navigation does not + * restore pending results. If a destination requires a transient handoff, the caller must choose + * how to recover from a missing result before starting work that depends on it. + * + * Shared store ownership, equal keys, and consumption timing select the receiver; navigation + * direction does not. Publish and consume on the UI or main context that drives Compose + * consumption. This store does not switch contexts or synchronize concurrent access. */ @Stable public class ResultStore public constructor() {